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