View Javadoc
1   package org.apache.maven.scm.provider.git.jgit.command;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   * http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import org.apache.maven.scm.ScmFile;
23  import org.apache.maven.scm.ScmFileSet;
24  import org.apache.maven.scm.ScmFileStatus;
25  import org.apache.maven.scm.log.ScmLogger;
26  import org.apache.maven.scm.provider.git.repository.GitScmProviderRepository;
27  import org.apache.maven.scm.util.FilenameUtils;
28  import org.codehaus.plexus.util.StringUtils;
29  import org.eclipse.jgit.api.AddCommand;
30  import org.eclipse.jgit.api.Git;
31  import org.eclipse.jgit.api.PushCommand;
32  import org.eclipse.jgit.api.Status;
33  import org.eclipse.jgit.api.errors.GitAPIException;
34  import org.eclipse.jgit.api.errors.InvalidRemoteException;
35  import org.eclipse.jgit.api.errors.NoFilepatternException;
36  import org.eclipse.jgit.api.errors.TransportException;
37  import org.eclipse.jgit.diff.DiffEntry;
38  import org.eclipse.jgit.diff.DiffEntry.ChangeType;
39  import org.eclipse.jgit.diff.DiffFormatter;
40  import org.eclipse.jgit.diff.RawTextComparator;
41  import org.eclipse.jgit.errors.CorruptObjectException;
42  import org.eclipse.jgit.errors.IncorrectObjectTypeException;
43  import org.eclipse.jgit.errors.MissingObjectException;
44  import org.eclipse.jgit.errors.StopWalkException;
45  import org.eclipse.jgit.lib.Constants;
46  import org.eclipse.jgit.lib.ObjectId;
47  import org.eclipse.jgit.lib.ProgressMonitor;
48  import org.eclipse.jgit.lib.Repository;
49  import org.eclipse.jgit.lib.RepositoryBuilder;
50  import org.eclipse.jgit.lib.StoredConfig;
51  import org.eclipse.jgit.lib.TextProgressMonitor;
52  import org.eclipse.jgit.revwalk.RevCommit;
53  import org.eclipse.jgit.revwalk.RevFlag;
54  import org.eclipse.jgit.revwalk.RevSort;
55  import org.eclipse.jgit.revwalk.RevWalk;
56  import org.eclipse.jgit.revwalk.filter.CommitTimeRevFilter;
57  import org.eclipse.jgit.revwalk.filter.RevFilter;
58  import org.eclipse.jgit.transport.CredentialsProvider;
59  import org.eclipse.jgit.transport.PushResult;
60  import org.eclipse.jgit.transport.RefSpec;
61  import org.eclipse.jgit.transport.RemoteRefUpdate;
62  import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
63  import org.eclipse.jgit.util.io.DisabledOutputStream;
64  
65  import java.io.File;
66  import java.io.IOException;
67  import java.io.UnsupportedEncodingException;
68  import java.net.URI;
69  import java.net.URLEncoder;
70  import java.util.ArrayList;
71  import java.util.Collection;
72  import java.util.Date;
73  import java.util.HashSet;
74  import java.util.Iterator;
75  import java.util.List;
76  import java.util.Set;
77  
78  /**
79   * JGit utility functions.
80   *
81   * @author <a href="mailto:struberg@yahoo.de">Mark Struberg</a>
82   * @author Dominik Bartholdi (imod)
83   * @since 1.9
84   */
85  public class JGitUtils
86  {
87  
88      private JGitUtils()
89      {
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      {
100         return new Git( new RepositoryBuilder().readEnvironment().findGitDir( basedir ).setMustExist( true ).build() );
101     }
102 
103     /**
104      * Closes the repository wrapped by the passed git object
105      * @param git 
106      */
107     public static void closeRepo( Git git )
108     {
109         if ( git != null && git.getRepository() != null )
110         {
111             git.getRepository().close();
112         }
113     }
114 
115     /**
116      * Construct a logging ProgressMonitor for all JGit operations.
117      *
118      * @param logger
119      * @return a ProgressMonitor for use
120      */
121     public static ProgressMonitor getMonitor( ScmLogger logger )
122     {
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 logger     used to log some details
134      * @param git        the instance to configure (only in memory, not saved)
135      * @param repository the repo config to be used
136      * @return {@link CredentialsProvider} in case there are credentials
137      *         informations configured in the repository.
138      */
139     public static CredentialsProvider prepareSession( ScmLogger logger, Git git, GitScmProviderRepository repository )
140     {
141         StoredConfig config = git.getRepository().getConfig();
142         config.setString( "remote", "origin", "url", repository.getFetchUrl() );
143         config.setString( "remote", "origin", "pushURL", repository.getPushUrl() );
144 
145         // make sure we do not log any passwords to the output
146         String password =
147             StringUtils.isNotBlank( repository.getPassword() ) ? repository.getPassword().trim() : "no-pwd-defined";
148         // if password contains special characters it won't match below.
149         // Try encoding before match. (Passwords without will be unaffected)
150         try
151         {
152             password = URLEncoder.encode( password, "UTF-8" );
153         }
154         catch ( UnsupportedEncodingException e )
155         {
156             // UTF-8 should be valid
157             // TODO use a logger
158             System.out.println( "Ignore UnsupportedEncodingException when trying to encode password" );
159         }
160         logger.info( "fetch url: " + repository.getFetchUrl().replace( password, "******" ) );
161         logger.info( "push url: " + repository.getPushUrl().replace( password, "******" ) );
162         return getCredentials( repository );
163     }
164 
165     /**
166      * Creates a credentials provider from the information passed in the
167      * repository. Current implementation supports: <br>
168      * <ul><li>UserName/Password</li></ul>
169      * <p>
170      *
171      * @param repository the config to get the details from
172      * @return <code>null</code> if there is not enough info to create a
173      *         provider with
174      */
175     public static CredentialsProvider getCredentials( GitScmProviderRepository repository )
176     {
177         if ( StringUtils.isNotBlank( repository.getUser() ) && StringUtils.isNotBlank( repository.getPassword() ) )
178         {
179             return new UsernamePasswordCredentialsProvider( repository.getUser().trim(),
180                                                             repository.getPassword().trim() );
181         }
182 
183 
184         return null;
185     }
186 
187     public static Iterable<PushResult> push( ScmLogger logger, Git git, GitScmProviderRepository repo, RefSpec refSpec )
188         throws GitAPIException, InvalidRemoteException, TransportException
189     {
190         CredentialsProvider credentials = JGitUtils.prepareSession( logger, git, repo );
191         PushCommand command = git.push().setRefSpecs( refSpec ).setCredentialsProvider( credentials )
192                 .setTransportConfigCallback( new JGitTransportConfigCallback( repo, logger ) );
193 
194         Iterable<PushResult> pushResultList = command.call();
195         for ( PushResult pushResult : pushResultList )
196         {
197             Collection<RemoteRefUpdate> ru = pushResult.getRemoteUpdates();
198             for ( RemoteRefUpdate remoteRefUpdate : ru )
199             {
200                 logger.info( remoteRefUpdate.getStatus() + " - " + remoteRefUpdate.toString() );
201             }
202         }
203         return pushResultList;
204     }
205 
206     /**
207      * Does the Repository have any commits?
208      *
209      * @param repo
210      * @return false if there are no commits
211      */
212     public static boolean hasCommits( Repository repo )
213     {
214         if ( repo != null && repo.getDirectory().exists() )
215         {
216             return ( new File( repo.getDirectory(), "objects" ).list().length > 2 ) || (
217                 new File( repo.getDirectory(), "objects/pack" ).list().length > 0 );
218         }
219         return false;
220     }
221 
222     /**
223      * get a list of all files in the given commit
224      *
225      * @param repository the repo
226      * @param commit     the commit to get the files from
227      * @return a list of files included in the commit
228      * @throws MissingObjectException
229      * @throws IncorrectObjectTypeException
230      * @throws CorruptObjectException
231      * @throws IOException
232      */
233     public static List<ScmFile> getFilesInCommit( Repository repository, RevCommit commit )
234         throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException
235     {
236         List<ScmFile> list = new ArrayList<ScmFile>();
237         if ( JGitUtils.hasCommits( repository ) )
238         {
239            
240             try ( RevWalk rw = new RevWalk( repository );
241                   DiffFormatter df = new DiffFormatter( DisabledOutputStream.INSTANCE ) ) {
242                 RevCommit realParent = commit.getParentCount() > 0 ? commit.getParent( 0 ) : commit;
243                 RevCommit parent = rw.parseCommit( realParent.getId() );
244                     df.setRepository( repository );
245                 df.setDiffComparator( RawTextComparator.DEFAULT );
246                 df.setDetectRenames( true );
247                 List<DiffEntry> diffs = df.scan( parent.getTree(), commit.getTree() );
248                 for ( DiffEntry diff : diffs )
249                 {
250                     list.add( new ScmFile( diff.getNewPath(), ScmFileStatus.CHECKED_IN ) );
251                 }
252             }
253         }
254         return list;
255     }
256 
257     /**
258      * Translate a {@code FileStatus} in the matching {@code ScmFileStatus}.
259      *
260      * @param changeType
261      * @return the matching ScmFileStatus
262      */
263     public static ScmFileStatus getScmFileStatus( ChangeType changeType )
264     {
265         switch ( changeType )
266         {
267             case ADD:
268                 return ScmFileStatus.ADDED;
269             case MODIFY:
270                 return ScmFileStatus.MODIFIED;
271             case DELETE:
272                 return ScmFileStatus.DELETED;
273             case RENAME:
274                 return ScmFileStatus.RENAMED;
275             case COPY:
276                 return ScmFileStatus.COPIED;
277             default:
278                 return ScmFileStatus.UNKNOWN;
279         }
280     }
281 
282     /**
283      * Adds all files in the given fileSet to the repository.
284      *
285      * @param git     the repo to add the files to
286      * @param fileSet the set of files within the workspace, the files are added
287      *                relative to the basedir of this fileset
288      * @return a list of added files
289      * @throws GitAPIException
290      * @throws NoFilepatternException
291      */
292     public static List<ScmFile> addAllFiles( Git git, ScmFileSet fileSet )
293         throws GitAPIException, NoFilepatternException
294     {
295         URI baseUri = fileSet.getBasedir().toURI();
296         AddCommand add = git.add();
297         for ( File file : fileSet.getFileList() )
298         {
299             if ( !file.isAbsolute() )
300             {
301                 file = new File( fileSet.getBasedir().getPath(), file.getPath() );
302             }
303 
304             if ( file.exists() )
305             {
306                 String path = relativize( baseUri, file );
307                 add.addFilepattern( path );
308                 add.addFilepattern( file.getAbsolutePath() );
309             }
310         }
311         add.call();
312         
313         Status status = git.status().call();
314 
315         Set<String> allInIndex = new HashSet<String>();
316         allInIndex.addAll( status.getAdded() );
317         allInIndex.addAll( status.getChanged() );
318 
319         // System.out.println("All in index: "+allInIndex.size());
320 
321         List<ScmFile> addedFiles = new ArrayList<ScmFile>( allInIndex.size() );
322 
323         // rewrite all detected files to now have status 'checked_in'
324         for ( String entry : allInIndex )
325         {
326             ScmFile scmfile = new ScmFile( entry, ScmFileStatus.ADDED );
327 
328             // if a specific fileSet is given, we have to check if the file is
329             // really tracked
330             for ( Iterator<File> itfl = fileSet.getFileList().iterator(); itfl.hasNext(); )
331             {
332                 String path = FilenameUtils.normalizeFilename( relativize( baseUri, itfl.next() ) );
333                 if ( path.equals( FilenameUtils.normalizeFilename( scmfile.getPath() ) ) )
334                 {
335                     addedFiles.add( scmfile );
336                 }
337             }
338         }
339         return addedFiles;
340     }
341 
342     private static String relativize( URI baseUri, File f )
343     {
344         String path = f.getPath();
345         if ( f.isAbsolute() )
346         {
347             path = baseUri.relativize( new File( path ).toURI() ).getPath();
348         }
349         return path;
350     }
351 
352     /**
353      * Get a list of commits between two revisions.
354      *
355      * @param repo     the repository to work on
356      * @param sortings sorting
357      * @param fromRev  start revision
358      * @param toRev    if null, falls back to head
359      * @param fromDate from which date on
360      * @param toDate   until which date
361      * @param maxLines max number of lines
362      * @return a list of commits, might be empty, but never <code>null</code>
363      * @throws IOException
364      * @throws MissingObjectException
365      * @throws IncorrectObjectTypeException
366      */
367     public static List<RevCommit> getRevCommits( Repository repo, RevSort[] sortings, String fromRev, String toRev,
368                                                  final Date fromDate, final Date toDate, int maxLines )
369         throws IOException, MissingObjectException, IncorrectObjectTypeException
370     {
371 
372         List<RevCommit> revs = new ArrayList<RevCommit>();
373 
374         ObjectId fromRevId = fromRev != null ? repo.resolve( fromRev ) : null;
375         ObjectId toRevId = toRev != null ? repo.resolve( toRev ) : null;
376 
377         if ( sortings == null || sortings.length == 0 )
378         {
379             sortings = new RevSort[]{ RevSort.TOPO, RevSort.COMMIT_TIME_DESC };
380         }
381 
382         try ( RevWalk walk = new RevWalk( repo ) ) {
383             for ( final RevSort s : sortings )
384             {
385                 walk.sort( s, true );
386             }
387     
388             if ( fromDate != null && toDate != null )
389             {
390                 //walk.setRevFilter( CommitTimeRevFilter.between( fromDate, toDate ) );
391                 walk.setRevFilter( new RevFilter()
392                 {
393                     @Override
394                     public boolean include( RevWalk walker, RevCommit cmit )
395                         throws StopWalkException, MissingObjectException, IncorrectObjectTypeException, IOException
396                     {
397                         int cmtTime = cmit.getCommitTime();
398     
399                         return ( cmtTime >= ( fromDate.getTime() / 1000 ) ) && ( cmtTime <= ( toDate.getTime() / 1000 ) );
400                     }
401     
402                     @Override
403                     public RevFilter clone()
404                     {
405                         return this;
406                     }
407                 } );
408             }
409             else
410             {
411                 if ( fromDate != null )
412                 {
413                     walk.setRevFilter( CommitTimeRevFilter.after( fromDate ) );
414                 }
415                 if ( toDate != null )
416                 {
417                     walk.setRevFilter( CommitTimeRevFilter.before( toDate ) );
418                 }
419             }
420     
421             if ( fromRevId != null )
422             {
423                 RevCommit c = walk.parseCommit( fromRevId );
424                 c.add( RevFlag.UNINTERESTING );
425                 RevCommit real = walk.parseCommit( c );
426                 walk.markUninteresting( real );
427             }
428     
429             if ( toRevId != null )
430             {
431                 RevCommit c = walk.parseCommit( toRevId );
432                 c.remove( RevFlag.UNINTERESTING );
433                 RevCommit real = walk.parseCommit( c );
434                 walk.markStart( real );
435             }
436             else
437             {
438                 final ObjectId head = repo.resolve( Constants.HEAD );
439                 if ( head == null )
440                 {
441                     throw new RuntimeException( "Cannot resolve " + Constants.HEAD );
442                 }
443                 RevCommit real = walk.parseCommit( head );
444                 walk.markStart( real );
445             }
446     
447             int n = 0;
448             for ( final RevCommit c : walk )
449             {
450                 n++;
451                 if ( maxLines != -1 && n > maxLines )
452                 {
453                     break;
454                 }
455     
456                 revs.add( c );
457             }
458             return revs;
459         }
460     }
461 
462 }