001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.maven.scm.provider.git.jgit.command;
020
021import java.io.File;
022import java.io.IOException;
023import java.util.ArrayList;
024import java.util.Collection;
025import java.util.Date;
026import java.util.HashSet;
027import java.util.List;
028import java.util.Optional;
029import java.util.Set;
030
031import org.apache.commons.lang3.StringUtils;
032import org.apache.maven.scm.ScmFile;
033import org.apache.maven.scm.ScmFileSet;
034import org.apache.maven.scm.ScmFileStatus;
035import org.apache.maven.scm.provider.git.repository.GitScmProviderRepository;
036import org.apache.maven.scm.util.FilenameUtils;
037import org.eclipse.jgit.api.AddCommand;
038import org.eclipse.jgit.api.Git;
039import org.eclipse.jgit.api.PushCommand;
040import org.eclipse.jgit.api.RmCommand;
041import org.eclipse.jgit.api.Status;
042import org.eclipse.jgit.api.TransportConfigCallback;
043import org.eclipse.jgit.api.errors.GitAPIException;
044import org.eclipse.jgit.diff.DiffEntry;
045import org.eclipse.jgit.diff.DiffEntry.ChangeType;
046import org.eclipse.jgit.diff.DiffFormatter;
047import org.eclipse.jgit.diff.RawTextComparator;
048import org.eclipse.jgit.errors.CorruptObjectException;
049import org.eclipse.jgit.errors.IncorrectObjectTypeException;
050import org.eclipse.jgit.errors.MissingObjectException;
051import org.eclipse.jgit.errors.StopWalkException;
052import org.eclipse.jgit.lib.Constants;
053import org.eclipse.jgit.lib.ObjectId;
054import org.eclipse.jgit.lib.ProgressMonitor;
055import org.eclipse.jgit.lib.Ref;
056import org.eclipse.jgit.lib.Repository;
057import org.eclipse.jgit.lib.RepositoryBuilder;
058import org.eclipse.jgit.lib.StoredConfig;
059import org.eclipse.jgit.lib.TextProgressMonitor;
060import org.eclipse.jgit.revwalk.RevCommit;
061import org.eclipse.jgit.revwalk.RevFlag;
062import org.eclipse.jgit.revwalk.RevSort;
063import org.eclipse.jgit.revwalk.RevWalk;
064import org.eclipse.jgit.revwalk.filter.CommitTimeRevFilter;
065import org.eclipse.jgit.revwalk.filter.RevFilter;
066import org.eclipse.jgit.transport.CredentialsProvider;
067import org.eclipse.jgit.transport.PushResult;
068import org.eclipse.jgit.transport.RefSpec;
069import org.eclipse.jgit.transport.RemoteRefUpdate;
070import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
071import org.eclipse.jgit.util.io.DisabledOutputStream;
072import org.slf4j.Logger;
073import org.slf4j.LoggerFactory;
074
075import static org.eclipse.jgit.lib.Constants.R_TAGS;
076
077/**
078 * JGit utility functions.
079 *
080 * @author <a href="mailto:struberg@yahoo.de">Mark Struberg</a>
081 * @author Dominik Bartholdi (imod)
082 * @since 1.9
083 */
084public class JGitUtils {
085    private static final Logger LOGGER = LoggerFactory.getLogger(JGitUtils.class);
086
087    private JGitUtils() {
088        // no op
089    }
090
091    /**
092     * Opens a JGit repository in the current directory or a parent directory.
093     *
094     * @param basedir the directory to start with
095     * @throws IOException if the repository cannot be opened
096     */
097    public static Git openRepo(File basedir) throws IOException {
098        return new Git(new RepositoryBuilder()
099                .readEnvironment()
100                .findGitDir(basedir)
101                .setMustExist(true)
102                .build());
103    }
104
105    /**
106     * Closes the repository wrapped by the passed git object.
107     *
108     * @param git
109     */
110    public static void closeRepo(Git git) {
111        if (git != null && git.getRepository() != null) {
112            git.getRepository().close();
113        }
114    }
115
116    /**
117     * Construct a logging ProgressMonitor for all JGit operations.
118     *
119     * @return a ProgressMonitor for use
120     */
121    public static ProgressMonitor getMonitor() {
122        // X TODO write an own ProgressMonitor which logs to ScmLogger!
123        return new TextProgressMonitor();
124    }
125
126    /**
127     * Prepares the in memory configuration of git to connect to the configured
128     * repository. It configures the following settings in memory: <br>
129     * <ul><li>push url</li> <li>fetch url</li></ul>
130     * <p>
131     *
132     * @param git        the instance to configure (only in memory, not saved)
133     * @param repository the repo config to be used
134     * @return {@link CredentialsProvider} in case credential information is configured in the repository
135     */
136    public static CredentialsProvider prepareSession(Git git, GitScmProviderRepository repository) {
137        StoredConfig config = git.getRepository().getConfig();
138        config.setString("remote", "origin", "url", repository.getFetchUrl());
139        config.setString("remote", "origin", "pushURL", repository.getPushUrl());
140
141        // make sure we do not log any passwords to the output
142        LOGGER.info("fetch url: " + repository.getFetchUrlWithMaskedPassword());
143        LOGGER.info("push url: " + repository.getPushUrlWithMaskedPassword());
144        return getCredentials(repository);
145    }
146
147    /**
148     * Creates a credentials provider from the information passed in the
149     * repository. Current implementation supports: <br>
150     * <ul><li>UserName/Password</li></ul>
151     * <p>
152     *
153     * @param repository the config to get the details from
154     * @return <code>null</code> if there is not enough info to create a
155     *         provider with
156     */
157    public static CredentialsProvider getCredentials(GitScmProviderRepository repository) {
158        if (StringUtils.isNotBlank(repository.getUser()) && StringUtils.isNotBlank(repository.getPassword())) {
159            return new UsernamePasswordCredentialsProvider(
160                    repository.getUser().trim(), repository.getPassword().trim());
161        }
162
163        return null;
164    }
165
166    public static Iterable<PushResult> push(
167            Git git,
168            GitScmProviderRepository repo,
169            RefSpec refSpec,
170            Set<RemoteRefUpdate.Status> successfulStatuses,
171            Optional<TransportConfigCallback> transportConfigCallback)
172            throws PushException {
173        CredentialsProvider credentials = prepareSession(git, repo);
174        PushCommand command = git.push().setRefSpecs(refSpec).setCredentialsProvider(credentials);
175        transportConfigCallback.ifPresent(command::setTransportConfigCallback);
176
177        Iterable<PushResult> pushResultList;
178        try {
179            pushResultList = command.call();
180        } catch (GitAPIException e) {
181            throw new PushException(repo.getPushUrlWithMaskedPassword(), e);
182        }
183        for (PushResult pushResult : pushResultList) {
184            Collection<RemoteRefUpdate> ru = pushResult.getRemoteUpdates();
185            for (RemoteRefUpdate remoteRefUpdate : ru) {
186                if (!successfulStatuses.contains(remoteRefUpdate.getStatus())) {
187                    throw new PushException(repo.getPushUrlWithMaskedPassword(), remoteRefUpdate);
188                }
189                LOGGER.debug("Push succeeded {}", remoteRefUpdate);
190            }
191        }
192        return pushResultList;
193    }
194
195    /**
196     * Does the Repository have any commits?
197     *
198     * @param repo
199     * @return false if there are no commits
200     */
201    public static boolean hasCommits(Repository repo) {
202        if (repo != null && repo.getDirectory().exists()) {
203            return (new File(repo.getDirectory(), "objects").list().length > 2)
204                    || (new File(repo.getDirectory(), "objects/pack").list().length > 0);
205        }
206        return false;
207    }
208
209    /**
210     * Get a list of all files in the given commit.
211     *
212     * @param repository the repo
213     * @param commit     the commit to get the files from
214     * @return a list of files included in the commit
215     * @throws CorruptObjectException
216     * @throws IncorrectObjectTypeException
217     * @throws IOException
218     * @throws MissingObjectException
219     */
220    public static List<ScmFile> getFilesInCommit(Repository repository, RevCommit commit)
221            throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException {
222        return getFilesInCommit(repository, commit, null);
223    }
224
225    /**
226     * Get a list of all files in the given commit.
227     *
228     * @param repository the repo
229     * @param commit     the commit to get the files from
230     * @param baseDir    the directory to which the returned files should be relative.
231     *                   May be {@code null} in case they should be relative to the working directory root.
232     * @return a list of files included in the commit
233     * @throws CorruptObjectException
234     * @throws IncorrectObjectTypeException
235     * @throws IOException
236     * @throws MissingObjectException
237     */
238    public static List<ScmFile> getFilesInCommit(Repository repository, RevCommit commit, File baseDir)
239            throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException {
240        List<ScmFile> list = new ArrayList<>();
241        if (JGitUtils.hasCommits(repository)) {
242
243            try (RevWalk rw = new RevWalk(repository);
244                    DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
245                RevCommit realParent = commit.getParentCount() > 0 ? commit.getParent(0) : commit;
246                RevCommit parent = rw.parseCommit(realParent.getId());
247                df.setRepository(repository);
248                df.setDiffComparator(RawTextComparator.DEFAULT);
249                df.setDetectRenames(true);
250                List<DiffEntry> diffs = df.scan(parent.getTree(), commit.getTree());
251                for (DiffEntry diff : diffs) {
252                    final String path;
253                    if (baseDir != null) {
254                        path = relativize(baseDir, new File(repository.getWorkTree(), diff.getNewPath()))
255                                .getPath();
256                    } else {
257                        path = diff.getNewPath();
258                    }
259                    list.add(new ScmFile(path, ScmFileStatus.CHECKED_IN));
260                }
261            }
262        }
263        return list;
264    }
265
266    /**
267     * Translate a {@code FileStatus} in the matching {@code ScmFileStatus}.
268     *
269     * @param changeType
270     * @return the matching ScmFileStatus
271     */
272    public static ScmFileStatus getScmFileStatus(ChangeType changeType) {
273        switch (changeType) {
274            case ADD:
275                return ScmFileStatus.ADDED;
276            case MODIFY:
277                return ScmFileStatus.MODIFIED;
278            case DELETE:
279                return ScmFileStatus.DELETED;
280            case RENAME:
281                return ScmFileStatus.RENAMED;
282            case COPY:
283                return ScmFileStatus.COPIED;
284            default:
285                return ScmFileStatus.UNKNOWN;
286        }
287    }
288
289    /**
290     * Adds all files in the given fileSet to the repository.
291     *
292     * @param git     the repo to add the files to
293     * @param fileSet the set of files within the workspace, the files are added
294     *                relative to the basedir of this fileset
295     * @return a list of added files
296     * @throws GitAPIException
297     */
298    public static List<ScmFile> addAllFiles(Git git, ScmFileSet fileSet) throws GitAPIException {
299        File workingCopyRootDirectory = git.getRepository().getWorkTree();
300        AddCommand add = git.add();
301        getWorkingCopyRelativePaths(workingCopyRootDirectory, fileSet).stream()
302                .forEach(f -> add.addFilepattern(toNormalizedFilePath(f)));
303        add.call();
304
305        Status status = git.status().call();
306
307        Set<String> allInIndex = new HashSet<>();
308        allInIndex.addAll(status.getAdded());
309        allInIndex.addAll(status.getChanged());
310        return getScmFilesForAllFileSetFilesContainedInRepoPath(
311                workingCopyRootDirectory, fileSet, allInIndex, ScmFileStatus.ADDED);
312    }
313
314    /**
315     * Remove all files in the given fileSet from the repository.
316     *
317     * @param git     the repo to remove the files from
318     * @param fileSet the set of files within the workspace, the files are removed
319     *                relative to the basedir of this fileset
320     * @return a list of removed files
321     * @throws GitAPIException
322     */
323    public static List<ScmFile> removeAllFiles(Git git, ScmFileSet fileSet) throws GitAPIException {
324        File workingCopyRootDirectory = git.getRepository().getWorkTree();
325        RmCommand remove = git.rm();
326        getWorkingCopyRelativePaths(workingCopyRootDirectory, fileSet).stream()
327                .forEach(f -> remove.addFilepattern(toNormalizedFilePath(f)));
328        remove.call();
329
330        Status status = git.status().call();
331
332        Set<String> allInIndex = new HashSet<>(status.getRemoved());
333        return getScmFilesForAllFileSetFilesContainedInRepoPath(
334                workingCopyRootDirectory, fileSet, allInIndex, ScmFileStatus.DELETED);
335    }
336
337    /**
338     * Convert each file in the {@code fileSet} to their relative file path to workingCopyDirectory
339     * and return them in a list.
340     *
341     * @param workingCopyDirectory the working copy root directory
342     * @param fileSet the file set to convert
343     */
344    public static List<File> getWorkingCopyRelativePaths(File workingCopyDirectory, ScmFileSet fileSet) {
345        List<File> repositoryRelativePaths = new ArrayList<>();
346        for (File path : fileSet.getFileList()) {
347            if (!path.isAbsolute()) {
348                path = new File(fileSet.getBasedir().getPath(), path.getPath());
349            }
350            File repositoryRelativePath = relativize(workingCopyDirectory, path);
351            repositoryRelativePaths.add(repositoryRelativePath);
352        }
353        return repositoryRelativePaths;
354    }
355
356    /**
357     * Converts the given file to a string only containing forward slashes.
358     *
359     * @param file
360     * @return the normalized file path
361     */
362    public static String toNormalizedFilePath(File file) {
363        return FilenameUtils.normalizeFilename(file);
364    }
365
366    private static List<ScmFile> getScmFilesForAllFileSetFilesContainedInRepoPath(
367            File workingCopyDirectory, ScmFileSet fileSet, Set<String> repoFilePaths, ScmFileStatus fileStatus) {
368        List<ScmFile> files = new ArrayList<>(repoFilePaths.size());
369        getWorkingCopyRelativePaths(workingCopyDirectory, fileSet).stream().forEach((relativeFile) -> {
370            // check if repo relative path is contained
371            if (repoFilePaths.contains(toNormalizedFilePath(relativeFile))) {
372                // returned ScmFiles should be relative to given fileset's basedir
373                ScmFile scmfile = new ScmFile(
374                        relativize(fileSet.getBasedir(), new File(workingCopyDirectory, relativeFile.getPath()))
375                                .getPath(),
376                        fileStatus);
377                files.add(scmfile);
378            }
379        });
380        return files;
381    }
382
383    private static File relativize(File baseDir, File file) {
384        if (file.isAbsolute()) {
385            return baseDir.toPath().relativize(file.toPath()).toFile();
386        }
387        return file;
388    }
389
390    /**
391     * Get a list of commits between two revisions.
392     *
393     * @param repo     the repository to work on
394     * @param sortings sorting
395     * @param fromRev  start revision
396     * @param toRev    if null, falls back to head
397     * @param fromDate from which date on
398     * @param toDate   until which date
399     * @param maxLines max number of lines
400     * @return a list of commits, might be empty, but never <code>null</code>
401     * @throws IncorrectObjectTypeException
402     * @throws IOException
403     * @throws MissingObjectException
404     */
405    public static List<RevCommit> getRevCommits(
406            Repository repo,
407            RevSort[] sortings,
408            String fromRev,
409            String toRev,
410            final Date fromDate,
411            final Date toDate,
412            int maxLines)
413            throws IOException, MissingObjectException, IncorrectObjectTypeException {
414
415        List<RevCommit> revs = new ArrayList<>();
416
417        ObjectId fromRevId = fromRev != null ? repo.resolve(fromRev) : null;
418        ObjectId toRevId = toRev != null ? repo.resolve(toRev) : null;
419
420        if (sortings == null || sortings.length == 0) {
421            sortings = new RevSort[] {RevSort.TOPO, RevSort.COMMIT_TIME_DESC};
422        }
423
424        try (RevWalk walk = new RevWalk(repo)) {
425            for (final RevSort s : sortings) {
426                walk.sort(s, true);
427            }
428
429            if (fromDate != null && toDate != null) {
430                // walk.setRevFilter( CommitTimeRevFilter.between( fromDate, toDate ) );
431                walk.setRevFilter(new RevFilter() {
432                    @Override
433                    public boolean include(RevWalk walker, RevCommit cmit)
434                            throws StopWalkException, MissingObjectException, IncorrectObjectTypeException,
435                                    IOException {
436                        int cmtTime = cmit.getCommitTime();
437
438                        return (cmtTime >= (fromDate.getTime() / 1000)) && (cmtTime <= (toDate.getTime() / 1000));
439                    }
440
441                    @Override
442                    public RevFilter clone() {
443                        return this;
444                    }
445                });
446            } else {
447                if (fromDate != null) {
448                    walk.setRevFilter(CommitTimeRevFilter.after(fromDate));
449                }
450                if (toDate != null) {
451                    walk.setRevFilter(CommitTimeRevFilter.before(toDate));
452                }
453            }
454
455            if (fromRevId != null) {
456                RevCommit c = walk.parseCommit(fromRevId);
457                c.add(RevFlag.UNINTERESTING);
458                RevCommit real = walk.parseCommit(c);
459                walk.markUninteresting(real);
460            }
461
462            if (toRevId != null) {
463                RevCommit c = walk.parseCommit(toRevId);
464                c.remove(RevFlag.UNINTERESTING);
465                RevCommit real = walk.parseCommit(c);
466                walk.markStart(real);
467            } else {
468                final ObjectId head = repo.resolve(Constants.HEAD);
469                if (head == null) {
470                    throw new IOException("Cannot resolve " + Constants.HEAD);
471                }
472                RevCommit real = walk.parseCommit(head);
473                walk.markStart(real);
474            }
475
476            int n = 0;
477            for (final RevCommit c : walk) {
478                n++;
479                if (maxLines != -1 && n > maxLines) {
480                    break;
481                }
482
483                revs.add(c);
484            }
485            return revs;
486        }
487    }
488
489    /**
490     * Get a list of tags that has been set in the specified commit.
491     *
492     * @param repo the repository to work on
493     * @param commit the commit for which we want the tags
494     * @return a list of tags, might be empty, and never <code>null</code>
495     */
496    public static List<String> getTags(Repository repo, RevCommit commit) throws IOException {
497        List<Ref> refList = repo.getRefDatabase().getRefsByPrefix(R_TAGS);
498
499        try (RevWalk revWalk = new RevWalk(repo)) {
500            ObjectId commitId = commit.getId();
501            List<String> result = new ArrayList<>();
502
503            for (Ref ref : refList) {
504                ObjectId tagId = ref.getObjectId();
505                RevCommit tagCommit = revWalk.parseCommit(tagId);
506                if (commitId.equals(tagCommit.getId())) {
507                    result.add(ref.getName().substring(R_TAGS.length()));
508                }
509            }
510            return result;
511        }
512    }
513}