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.io.UnsupportedEncodingException;
24  import java.net.URI;
25  import java.net.URLEncoder;
26  import java.util.ArrayList;
27  import java.util.Collection;
28  import java.util.Date;
29  import java.util.HashSet;
30  import java.util.List;
31  import java.util.Set;
32  import java.util.function.BiConsumer;
33  
34  import org.apache.commons.lang3.StringUtils;
35  import org.apache.maven.scm.ScmFile;
36  import org.apache.maven.scm.ScmFileSet;
37  import org.apache.maven.scm.ScmFileStatus;
38  import org.apache.maven.scm.provider.git.repository.GitScmProviderRepository;
39  import org.eclipse.jgit.api.AddCommand;
40  import org.eclipse.jgit.api.Git;
41  import org.eclipse.jgit.api.PushCommand;
42  import org.eclipse.jgit.api.RmCommand;
43  import org.eclipse.jgit.api.Status;
44  import org.eclipse.jgit.api.errors.GitAPIException;
45  import org.eclipse.jgit.api.errors.InvalidRemoteException;
46  import org.eclipse.jgit.api.errors.TransportException;
47  import org.eclipse.jgit.diff.DiffEntry;
48  import org.eclipse.jgit.diff.DiffEntry.ChangeType;
49  import org.eclipse.jgit.diff.DiffFormatter;
50  import org.eclipse.jgit.diff.RawTextComparator;
51  import org.eclipse.jgit.errors.CorruptObjectException;
52  import org.eclipse.jgit.errors.IncorrectObjectTypeException;
53  import org.eclipse.jgit.errors.MissingObjectException;
54  import org.eclipse.jgit.errors.StopWalkException;
55  import org.eclipse.jgit.lib.Constants;
56  import org.eclipse.jgit.lib.ObjectId;
57  import org.eclipse.jgit.lib.ProgressMonitor;
58  import org.eclipse.jgit.lib.Ref;
59  import org.eclipse.jgit.lib.Repository;
60  import org.eclipse.jgit.lib.RepositoryBuilder;
61  import org.eclipse.jgit.lib.StoredConfig;
62  import org.eclipse.jgit.lib.TextProgressMonitor;
63  import org.eclipse.jgit.revwalk.RevCommit;
64  import org.eclipse.jgit.revwalk.RevFlag;
65  import org.eclipse.jgit.revwalk.RevSort;
66  import org.eclipse.jgit.revwalk.RevWalk;
67  import org.eclipse.jgit.revwalk.filter.CommitTimeRevFilter;
68  import org.eclipse.jgit.revwalk.filter.RevFilter;
69  import org.eclipse.jgit.transport.CredentialsProvider;
70  import org.eclipse.jgit.transport.PushResult;
71  import org.eclipse.jgit.transport.RefSpec;
72  import org.eclipse.jgit.transport.RemoteRefUpdate;
73  import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
74  import org.eclipse.jgit.util.io.DisabledOutputStream;
75  import org.slf4j.Logger;
76  import org.slf4j.LoggerFactory;
77  
78  import static org.eclipse.jgit.lib.Constants.R_TAGS;
79  
80  /**
81   * JGit utility functions.
82   *
83   * @author <a href="mailto:struberg@yahoo.de">Mark Struberg</a>
84   * @author Dominik Bartholdi (imod)
85   * @since 1.9
86   */
87  public class JGitUtils {
88      private static final Logger LOGGER = LoggerFactory.getLogger(JGitUtils.class);
89  
90      private JGitUtils() {
91          // no op
92      }
93  
94      /**
95       * Opens a JGit repository in the current directory or a parent directory.
96       * @param basedir The directory to start with
97       * @throws IOException If the repository cannot be opened
98       */
99      public static Git openRepo(File basedir) throws IOException {
100         return new Git(new RepositoryBuilder()
101                 .readEnvironment()
102                 .findGitDir(basedir)
103                 .setMustExist(true)
104                 .build());
105     }
106 
107     /**
108      * Closes the repository wrapped by the passed git object
109      * @param git
110      */
111     public static void closeRepo(Git git) {
112         if (git != null && git.getRepository() != null) {
113             git.getRepository().close();
114         }
115     }
116 
117     /**
118      * Construct a logging ProgressMonitor for all JGit operations.
119      *
120      * @return a ProgressMonitor for use
121      */
122     public static ProgressMonitor getMonitor() {
123         // X TODO write an own ProgressMonitor which logs to ScmLogger!
124         return new TextProgressMonitor();
125     }
126 
127     /**
128      * Prepares the in memory configuration of git to connect to the configured
129      * repository. It configures the following settings in memory: <br>
130      * <ul><li>push url</li> <li>fetch url</li></ul>
131      * <p>
132      *
133      * @param git        the instance to configure (only in memory, not saved)
134      * @param repository the repo config to be used
135      * @return {@link CredentialsProvider} in case there are credentials
136      *         informations configured in the repository.
137      */
138     public static CredentialsProvider prepareSession(Git git, GitScmProviderRepository repository) {
139         StoredConfig config = git.getRepository().getConfig();
140         config.setString("remote", "origin", "url", repository.getFetchUrl());
141         config.setString("remote", "origin", "pushURL", repository.getPushUrl());
142 
143         // make sure we do not log any passwords to the output
144         String password = StringUtils.isNotBlank(repository.getPassword())
145                 ? repository.getPassword().trim()
146                 : "no-pwd-defined";
147         // if password contains special characters it won't match below.
148         // Try encoding before match. (Passwords without will be unaffected)
149         try {
150             password = URLEncoder.encode(password, "UTF-8");
151         } catch (UnsupportedEncodingException e) {
152             // UTF-8 should be valid
153             // TODO use a logger
154             System.out.println("Ignore UnsupportedEncodingException when trying to encode password");
155         }
156         LOGGER.info("fetch url: " + repository.getFetchUrl().replace(password, "******"));
157         LOGGER.info("push url: " + repository.getPushUrl().replace(password, "******"));
158         return getCredentials(repository);
159     }
160 
161     /**
162      * Creates a credentials provider from the information passed in the
163      * repository. Current implementation supports: <br>
164      * <ul><li>UserName/Password</li></ul>
165      * <p>
166      *
167      * @param repository the config to get the details from
168      * @return <code>null</code> if there is not enough info to create a
169      *         provider with
170      */
171     public static CredentialsProvider getCredentials(GitScmProviderRepository repository) {
172         if (StringUtils.isNotBlank(repository.getUser()) && StringUtils.isNotBlank(repository.getPassword())) {
173             return new UsernamePasswordCredentialsProvider(
174                     repository.getUser().trim(), repository.getPassword().trim());
175         }
176 
177         return null;
178     }
179 
180     public static Iterable<PushResult> push(Git git, GitScmProviderRepository repo, RefSpec refSpec)
181             throws GitAPIException, InvalidRemoteException, TransportException {
182         CredentialsProvider credentials = prepareSession(git, repo);
183         PushCommand command = git.push()
184                 .setRefSpecs(refSpec)
185                 .setCredentialsProvider(credentials)
186                 .setTransportConfigCallback(
187                         new JGitTransportConfigCallback(new ScmProviderAwareSshdSessionFactory(repo, LOGGER)));
188 
189         Iterable<PushResult> pushResultList = command.call();
190         for (PushResult pushResult : pushResultList) {
191             Collection<RemoteRefUpdate> ru = pushResult.getRemoteUpdates();
192             for (RemoteRefUpdate remoteRefUpdate : ru) {
193                 LOGGER.info(remoteRefUpdate.getStatus() + " - " + remoteRefUpdate);
194             }
195         }
196         return pushResultList;
197     }
198 
199     /**
200      * Does the Repository have any commits?
201      *
202      * @param repo
203      * @return false if there are no commits
204      */
205     public static boolean hasCommits(Repository repo) {
206         if (repo != null && repo.getDirectory().exists()) {
207             return (new File(repo.getDirectory(), "objects").list().length > 2)
208                     || (new File(repo.getDirectory(), "objects/pack").list().length > 0);
209         }
210         return false;
211     }
212 
213     /**
214      * get a list of all files in the given commit
215      *
216      * @param repository the repo
217      * @param commit     the commit to get the files from
218      * @return a list of files included in the commit
219      * @throws MissingObjectException
220      * @throws IncorrectObjectTypeException
221      * @throws CorruptObjectException
222      * @throws IOException
223      */
224     public static List<ScmFile> getFilesInCommit(Repository repository, RevCommit commit)
225             throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException {
226         return getFilesInCommit(repository, commit, null);
227     }
228 
229     /**
230      * get a list of all files in the given commit
231      *
232      * @param repository the repo
233      * @param commit     the commit to get the files from
234      * @param baseDir    the directory to which the returned files should be relative.
235      *                   May be {@code null} in case they should be relative to the working directory root.
236      * @return a list of files included in the commit
237      *
238      * @throws MissingObjectException
239      * @throws IncorrectObjectTypeException
240      * @throws CorruptObjectException
241      * @throws IOException
242      */
243     public static List<ScmFile> getFilesInCommit(Repository repository, RevCommit commit, File baseDir)
244             throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException {
245         List<ScmFile> list = new ArrayList<>();
246         if (JGitUtils.hasCommits(repository)) {
247 
248             try (RevWalk rw = new RevWalk(repository);
249                     DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
250                 RevCommit realParent = commit.getParentCount() > 0 ? commit.getParent(0) : commit;
251                 RevCommit parent = rw.parseCommit(realParent.getId());
252                 df.setRepository(repository);
253                 df.setDiffComparator(RawTextComparator.DEFAULT);
254                 df.setDetectRenames(true);
255                 List<DiffEntry> diffs = df.scan(parent.getTree(), commit.getTree());
256                 for (DiffEntry diff : diffs) {
257                     final String path;
258                     if (baseDir != null) {
259                         path = relativize(baseDir.toURI(), new File(repository.getWorkTree(), diff.getNewPath()));
260                     } else {
261                         path = diff.getNewPath();
262                     }
263                     list.add(new ScmFile(path, ScmFileStatus.CHECKED_IN));
264                 }
265             }
266         }
267         return list;
268     }
269 
270     /**
271      * Translate a {@code FileStatus} in the matching {@code ScmFileStatus}.
272      *
273      * @param changeType
274      * @return the matching ScmFileStatus
275      */
276     public static ScmFileStatus getScmFileStatus(ChangeType changeType) {
277         switch (changeType) {
278             case ADD:
279                 return ScmFileStatus.ADDED;
280             case MODIFY:
281                 return ScmFileStatus.MODIFIED;
282             case DELETE:
283                 return ScmFileStatus.DELETED;
284             case RENAME:
285                 return ScmFileStatus.RENAMED;
286             case COPY:
287                 return ScmFileStatus.COPIED;
288             default:
289                 return ScmFileStatus.UNKNOWN;
290         }
291     }
292 
293     /**
294      * Adds all files in the given fileSet to the repository.
295      *
296      * @param git     the repo to add the files to
297      * @param fileSet the set of files within the workspace, the files are added
298      *                relative to the basedir of this fileset
299      * @return a list of added files
300      * @throws GitAPIException
301      */
302     public static List<ScmFile> addAllFiles(Git git, ScmFileSet fileSet) throws GitAPIException {
303         URI workingCopyRootUri = git.getRepository().getWorkTree().toURI();
304         AddCommand add = git.add();
305         callWithRepositoryRelativeFilePath(
306                 (relativeFile, absoluteFile) -> {
307                     if (absoluteFile.exists()) {
308                         add.addFilepattern(relativeFile);
309                     }
310                 },
311                 workingCopyRootUri,
312                 fileSet);
313         add.call();
314 
315         Status status = git.status().call();
316 
317         Set<String> allInIndex = new HashSet<>();
318         allInIndex.addAll(status.getAdded());
319         allInIndex.addAll(status.getChanged());
320         return getScmFilesForAllFileSetFilesContainedInRepoPath(
321                 workingCopyRootUri, fileSet, allInIndex, ScmFileStatus.ADDED);
322     }
323 
324     /**
325      * Remove all files in the given fileSet from the repository.
326      *
327      * @param git     the repo to remove the files from
328      * @param fileSet the set of files within the workspace, the files are removed
329      *                relative to the basedir of this fileset
330      * @return a list of removed files
331      * @throws GitAPIException
332      */
333     public static List<ScmFile> removeAllFiles(Git git, ScmFileSet fileSet) throws GitAPIException {
334         URI workingCopyRootUri = git.getRepository().getWorkTree().toURI();
335         RmCommand remove = git.rm();
336         callWithRepositoryRelativeFilePath(
337                 (relativeFile, absoluteFile) -> remove.addFilepattern(relativeFile), workingCopyRootUri, fileSet);
338         remove.call();
339 
340         Status status = git.status().call();
341 
342         Set<String> allInIndex = new HashSet<>(status.getRemoved());
343         return getScmFilesForAllFileSetFilesContainedInRepoPath(
344                 workingCopyRootUri, fileSet, allInIndex, ScmFileStatus.DELETED);
345     }
346 
347     /**
348      * For each file in the {@code fileSet} call the {@code fileCallback} with the file path relative to the repository
349      * root (forward slashes as separator) and the absolute file path.
350      * @param repoFileCallback the callback to call for each file in the fileset
351      * @param git the git repository
352      * @param fileSet the file set to traverse
353      */
354     private static void callWithRepositoryRelativeFilePath(
355             BiConsumer<String, File> fileCallback, URI workingCopyRootUri, ScmFileSet fileSet) {
356         for (File file : fileSet.getFileList()) {
357             if (!file.isAbsolute()) {
358                 file = new File(fileSet.getBasedir().getPath(), file.getPath());
359             }
360             String path = relativize(workingCopyRootUri, file);
361             fileCallback.accept(path, file);
362         }
363     }
364 
365     private static List<ScmFile> getScmFilesForAllFileSetFilesContainedInRepoPath(
366             URI workingCopyRootUri, ScmFileSet fileSet, Set<String> repoFilePaths, ScmFileStatus fileStatus) {
367         List<ScmFile> files = new ArrayList<>(repoFilePaths.size());
368         callWithRepositoryRelativeFilePath(
369                 (relativeFile, absoluteFile) -> {
370                     // check if repo relative path is contained
371                     if (repoFilePaths.contains(relativeFile)) {
372                         // returned ScmFiles should be relative to given fileset's basedir
373                         ScmFile scmfile =
374                                 new ScmFile(relativize(fileSet.getBasedir().toURI(), absoluteFile), fileStatus);
375                         files.add(scmfile);
376                     }
377                 },
378                 workingCopyRootUri,
379                 fileSet);
380         return files;
381     }
382 
383     private static String relativize(URI baseUri, File f) {
384         String path = f.getPath();
385         if (f.isAbsolute()) {
386             path = baseUri.relativize(new File(path).toURI()).getPath();
387         }
388         return path;
389     }
390 
391     /**
392      * Get a list of commits between two revisions.
393      *
394      * @param repo     the repository to work on
395      * @param sortings sorting
396      * @param fromRev  start revision
397      * @param toRev    if null, falls back to head
398      * @param fromDate from which date on
399      * @param toDate   until which date
400      * @param maxLines max number of lines
401      * @return a list of commits, might be empty, but never <code>null</code>
402      * @throws IOException
403      * @throws MissingObjectException
404      * @throws IncorrectObjectTypeException
405      */
406     public static List<RevCommit> getRevCommits(
407             Repository repo,
408             RevSort[] sortings,
409             String fromRev,
410             String toRev,
411             final Date fromDate,
412             final Date toDate,
413             int maxLines)
414             throws IOException, MissingObjectException, IncorrectObjectTypeException {
415 
416         List<RevCommit> revs = new ArrayList<>();
417 
418         ObjectId fromRevId = fromRev != null ? repo.resolve(fromRev) : null;
419         ObjectId toRevId = toRev != null ? repo.resolve(toRev) : null;
420 
421         if (sortings == null || sortings.length == 0) {
422             sortings = new RevSort[] {RevSort.TOPO, RevSort.COMMIT_TIME_DESC};
423         }
424 
425         try (RevWalk walk = new RevWalk(repo)) {
426             for (final RevSort s : sortings) {
427                 walk.sort(s, true);
428             }
429 
430             if (fromDate != null && toDate != null) {
431                 // walk.setRevFilter( CommitTimeRevFilter.between( fromDate, toDate ) );
432                 walk.setRevFilter(new RevFilter() {
433                     @Override
434                     public boolean include(RevWalk walker, RevCommit cmit)
435                             throws StopWalkException, MissingObjectException, IncorrectObjectTypeException,
436                                     IOException {
437                         int cmtTime = cmit.getCommitTime();
438 
439                         return (cmtTime >= (fromDate.getTime() / 1000)) && (cmtTime <= (toDate.getTime() / 1000));
440                     }
441 
442                     @Override
443                     public RevFilter clone() {
444                         return this;
445                     }
446                 });
447             } else {
448                 if (fromDate != null) {
449                     walk.setRevFilter(CommitTimeRevFilter.after(fromDate));
450                 }
451                 if (toDate != null) {
452                     walk.setRevFilter(CommitTimeRevFilter.before(toDate));
453                 }
454             }
455 
456             if (fromRevId != null) {
457                 RevCommit c = walk.parseCommit(fromRevId);
458                 c.add(RevFlag.UNINTERESTING);
459                 RevCommit real = walk.parseCommit(c);
460                 walk.markUninteresting(real);
461             }
462 
463             if (toRevId != null) {
464                 RevCommit c = walk.parseCommit(toRevId);
465                 c.remove(RevFlag.UNINTERESTING);
466                 RevCommit real = walk.parseCommit(c);
467                 walk.markStart(real);
468             } else {
469                 final ObjectId head = repo.resolve(Constants.HEAD);
470                 if (head == null) {
471                     throw new RuntimeException("Cannot resolve " + Constants.HEAD);
472                 }
473                 RevCommit real = walk.parseCommit(head);
474                 walk.markStart(real);
475             }
476 
477             int n = 0;
478             for (final RevCommit c : walk) {
479                 n++;
480                 if (maxLines != -1 && n > maxLines) {
481                     break;
482                 }
483 
484                 revs.add(c);
485             }
486             return revs;
487         }
488     }
489 
490     /**
491      * Get a list of tags that has been set in the specified commit.
492      *
493      * @param repo the repository to work on
494      * @param commit the commit for which we want the tags
495      * @return a list of tags, might be empty, and never <code>null</code>
496      */
497     public static List<String> getTags(Repository repo, RevCommit commit) throws IOException {
498         List<Ref> refList = repo.getRefDatabase().getRefsByPrefix(R_TAGS);
499 
500         try (RevWalk revWalk = new RevWalk(repo)) {
501             ObjectId commitId = commit.getId();
502             List<String> result = new ArrayList<>();
503 
504             for (Ref ref : refList) {
505                 ObjectId tagId = ref.getObjectId();
506                 RevCommit tagCommit = revWalk.parseCommit(tagId);
507                 if (commitId.equals(tagCommit.getId())) {
508                     result.add(ref.getName().substring(R_TAGS.length()));
509                 }
510             }
511             return result;
512         }
513     }
514 }