View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.scm.provider.git.jgit.command;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.Date;
26  import java.util.HashSet;
27  import java.util.List;
28  import java.util.Optional;
29  import java.util.Set;
30  
31  import org.apache.commons.lang3.StringUtils;
32  import org.apache.maven.scm.ScmFile;
33  import org.apache.maven.scm.ScmFileSet;
34  import org.apache.maven.scm.ScmFileStatus;
35  import org.apache.maven.scm.provider.git.repository.GitScmProviderRepository;
36  import org.apache.maven.scm.util.FilenameUtils;
37  import org.eclipse.jgit.api.AddCommand;
38  import org.eclipse.jgit.api.Git;
39  import org.eclipse.jgit.api.PushCommand;
40  import org.eclipse.jgit.api.RmCommand;
41  import org.eclipse.jgit.api.Status;
42  import org.eclipse.jgit.api.TransportConfigCallback;
43  import org.eclipse.jgit.api.errors.GitAPIException;
44  import org.eclipse.jgit.diff.DiffEntry;
45  import org.eclipse.jgit.diff.DiffEntry.ChangeType;
46  import org.eclipse.jgit.diff.DiffFormatter;
47  import org.eclipse.jgit.diff.RawTextComparator;
48  import org.eclipse.jgit.errors.CorruptObjectException;
49  import org.eclipse.jgit.errors.IncorrectObjectTypeException;
50  import org.eclipse.jgit.errors.MissingObjectException;
51  import org.eclipse.jgit.errors.StopWalkException;
52  import org.eclipse.jgit.lib.Constants;
53  import org.eclipse.jgit.lib.ObjectId;
54  import org.eclipse.jgit.lib.ProgressMonitor;
55  import org.eclipse.jgit.lib.Ref;
56  import org.eclipse.jgit.lib.Repository;
57  import org.eclipse.jgit.lib.RepositoryBuilder;
58  import org.eclipse.jgit.lib.StoredConfig;
59  import org.eclipse.jgit.lib.TextProgressMonitor;
60  import org.eclipse.jgit.revwalk.RevCommit;
61  import org.eclipse.jgit.revwalk.RevFlag;
62  import org.eclipse.jgit.revwalk.RevSort;
63  import org.eclipse.jgit.revwalk.RevWalk;
64  import org.eclipse.jgit.revwalk.filter.CommitTimeRevFilter;
65  import org.eclipse.jgit.revwalk.filter.RevFilter;
66  import org.eclipse.jgit.transport.CredentialsProvider;
67  import org.eclipse.jgit.transport.PushResult;
68  import org.eclipse.jgit.transport.RefSpec;
69  import org.eclipse.jgit.transport.RemoteRefUpdate;
70  import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
71  import org.eclipse.jgit.util.io.DisabledOutputStream;
72  import org.slf4j.Logger;
73  import org.slf4j.LoggerFactory;
74  
75  import static org.eclipse.jgit.lib.Constants.R_TAGS;
76  
77  /**
78   * JGit utility functions.
79   *
80   * @author <a href="mailto:struberg@yahoo.de">Mark Struberg</a>
81   * @author Dominik Bartholdi (imod)
82   * @since 1.9
83   */
84  public class JGitUtils {
85      private static final Logger LOGGER = LoggerFactory.getLogger(JGitUtils.class);
86  
87      private JGitUtils() {
88          // no op
89      }
90  
91      /**
92       * Opens a JGit repository in the current directory or a parent directory.
93       *
94       * @param basedir the directory to start with
95       * @throws IOException if the repository cannot be opened
96       */
97      public static Git openRepo(File basedir) throws IOException {
98          return new Git(new RepositoryBuilder()
99                  .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 }