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