001package org.apache.maven.scm.provider.git.jgit.command;
002
003import org.apache.maven.scm.ScmException;
004import org.apache.maven.scm.ScmFile;
005import org.apache.maven.scm.ScmFileSet;
006import org.apache.maven.scm.ScmFileStatus;
007import org.apache.maven.scm.log.ScmLogger;
008import org.apache.maven.scm.provider.git.repository.GitScmProviderRepository;
009import org.apache.maven.scm.util.FilenameUtils;
010import org.codehaus.plexus.util.StringUtils;
011import org.eclipse.jgit.api.AddCommand;
012import org.eclipse.jgit.api.Git;
013import org.eclipse.jgit.api.Status;
014import org.eclipse.jgit.api.errors.GitAPIException;
015import org.eclipse.jgit.api.errors.InvalidRemoteException;
016import org.eclipse.jgit.api.errors.NoFilepatternException;
017import org.eclipse.jgit.api.errors.TransportException;
018import org.eclipse.jgit.diff.DiffEntry;
019import org.eclipse.jgit.diff.DiffEntry.ChangeType;
020import org.eclipse.jgit.diff.DiffFormatter;
021import org.eclipse.jgit.diff.RawTextComparator;
022import org.eclipse.jgit.errors.CorruptObjectException;
023import org.eclipse.jgit.errors.IncorrectObjectTypeException;
024import org.eclipse.jgit.errors.MissingObjectException;
025import org.eclipse.jgit.errors.StopWalkException;
026import org.eclipse.jgit.lib.Constants;
027import org.eclipse.jgit.lib.ObjectId;
028import org.eclipse.jgit.lib.ProgressMonitor;
029import org.eclipse.jgit.lib.Repository;
030import org.eclipse.jgit.lib.StoredConfig;
031import org.eclipse.jgit.lib.TextProgressMonitor;
032import org.eclipse.jgit.revwalk.RevCommit;
033import org.eclipse.jgit.revwalk.RevFlag;
034import org.eclipse.jgit.revwalk.RevSort;
035import org.eclipse.jgit.revwalk.RevWalk;
036import org.eclipse.jgit.revwalk.filter.CommitTimeRevFilter;
037import org.eclipse.jgit.revwalk.filter.RevFilter;
038import org.eclipse.jgit.transport.CredentialsProvider;
039import org.eclipse.jgit.transport.PushResult;
040import org.eclipse.jgit.transport.RefSpec;
041import org.eclipse.jgit.transport.RemoteRefUpdate;
042import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
043import org.eclipse.jgit.util.io.DisabledOutputStream;
044
045import java.io.File;
046import java.io.IOException;
047import java.net.URI;
048import java.util.ArrayList;
049import java.util.Collection;
050import java.util.Date;
051import java.util.HashSet;
052import java.util.Iterator;
053import java.util.List;
054import java.util.Set;
055
056/*
057 * Licensed to the Apache Software Foundation (ASF) under one
058 * or more contributor license agreements.  See the NOTICE file
059 * distributed with this work for additional information
060 * regarding copyright ownership.  The ASF licenses this file
061 * to you under the Apache License, Version 2.0 (the
062 * "License"); you may not use this file except in compliance
063 * with the License.  You may obtain a copy of the License at
064 *
065 * http://www.apache.org/licenses/LICENSE-2.0
066 *
067 * Unless required by applicable law or agreed to in writing,
068 * software distributed under the License is distributed on an
069 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
070 * KIND, either express or implied.  See the License for the
071 * specific language governing permissions and limitations
072 * under the License.
073 */
074
075/**
076 * JGit utility functions.
077 *
078 * @author <a href="mailto:struberg@yahoo.de">Mark Struberg</a>
079 * @author Dominik Bartholdi (imod)
080 * @since 1.9
081 */
082public class JGitUtils
083{
084    
085    private JGitUtils()
086    {
087        // no op
088    }
089
090    /**
091     * Closes the repository wrapped by the passed git object
092     * @param git 
093     */
094    public static void closeRepo( Git git )
095    {
096        if ( git != null && git.getRepository() != null )
097        {
098            git.getRepository().close();
099        }
100    }
101
102    /**
103     * Construct a logging ProgressMonitor for all JGit operations.
104     *
105     * @param logger
106     * @return a ProgressMonitor for use
107     */
108    public static ProgressMonitor getMonitor( ScmLogger logger )
109    {
110        // X TODO write an own ProgressMonitor which logs to ScmLogger!
111        return new TextProgressMonitor();
112    }
113
114    /**
115     * Prepares the in memory configuration of git to connect to the configured
116     * repository. It configures the following settings in memory: <br />
117     * <li>push url</li> <li>fetch url</li>
118     * <p/>
119     *
120     * @param logger     used to log some details
121     * @param git        the instance to configure (only in memory, not saved)
122     * @param repository the repo config to be used
123     * @return {@link CredentialsProvider} in case there are credentials
124     *         informations configured in the repository.
125     */
126    public static CredentialsProvider prepareSession( ScmLogger logger, Git git, GitScmProviderRepository repository )
127    {
128        StoredConfig config = git.getRepository().getConfig();
129        config.setString( "remote", "origin", "url", repository.getFetchUrl() );
130        config.setString( "remote", "origin", "pushURL", repository.getPushUrl() );
131
132        // make sure we do not log any passwords to the output
133        String password =
134            StringUtils.isNotBlank( repository.getPassword() ) ? repository.getPassword().trim() : "no-pwd-defined";
135        logger.info( "fetch url: " + repository.getFetchUrl().replace( password, "******" ) );
136        logger.info( "push url: " + repository.getPushUrl().replace( password, "******" ) );
137        return getCredentials( repository );
138    }
139
140    /**
141     * Creates a credentials provider from the information passed in the
142     * repository. Current implementation supports: <br />
143     * <li>UserName/Password</li>
144     * <p/>
145     *
146     * @param repository the config to get the details from
147     * @return <code>null</code> if there is not enough info to create a
148     *         provider with
149     */
150    public static CredentialsProvider getCredentials( GitScmProviderRepository repository )
151    {
152        if ( StringUtils.isNotBlank( repository.getUser() ) && StringUtils.isNotBlank( repository.getPassword() ) )
153        {
154            return new UsernamePasswordCredentialsProvider( repository.getUser().trim(),
155                                                            repository.getPassword().trim() );
156        }
157        return null;
158    }
159
160    public static Iterable<PushResult> push( ScmLogger logger, Git git, GitScmProviderRepository repo, RefSpec refSpec )
161        throws GitAPIException, InvalidRemoteException, TransportException
162    {
163        CredentialsProvider credentials = JGitUtils.prepareSession( logger, git, repo );
164        Iterable<PushResult> pushResultList =
165            git.push().setCredentialsProvider( credentials ).setRefSpecs( refSpec ).call();
166        for ( PushResult pushResult : pushResultList )
167        {
168            Collection<RemoteRefUpdate> ru = pushResult.getRemoteUpdates();
169            for ( RemoteRefUpdate remoteRefUpdate : ru )
170            {
171                logger.info( remoteRefUpdate.getStatus() + " - " + remoteRefUpdate.toString() );
172            }
173        }
174        return pushResultList;
175    }
176
177    /**
178     * Does the Repository have any commits?
179     *
180     * @param repo
181     * @return false if there are no commits
182     */
183    public static boolean hasCommits( Repository repo )
184    {
185        if ( repo != null && repo.getDirectory().exists() )
186        {
187            return ( new File( repo.getDirectory(), "objects" ).list().length > 2 ) || (
188                new File( repo.getDirectory(), "objects/pack" ).list().length > 0 );
189        }
190        return false;
191    }
192
193    /**
194     * get a list of all files in the given commit
195     *
196     * @param repository the repo
197     * @param commit     the commit to get the files from
198     * @return a list of files included in the commit
199     * @throws MissingObjectException
200     * @throws IncorrectObjectTypeException
201     * @throws CorruptObjectException
202     * @throws IOException
203     */
204    public static List<ScmFile> getFilesInCommit( Repository repository, RevCommit commit )
205        throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException
206    {
207        List<ScmFile> list = new ArrayList<ScmFile>();
208        if ( JGitUtils.hasCommits( repository ) )
209        {
210            RevWalk rw = new RevWalk( repository );
211            RevCommit realParant = commit.getParentCount() > 0 ? commit.getParent( 0 ) : commit;
212            RevCommit parent = rw.parseCommit( realParant.getId() );
213            DiffFormatter df = new DiffFormatter( DisabledOutputStream.INSTANCE );
214            df.setRepository( repository );
215            df.setDiffComparator( RawTextComparator.DEFAULT );
216            df.setDetectRenames( true );
217            List<DiffEntry> diffs = df.scan( parent.getTree(), commit.getTree() );
218            for ( DiffEntry diff : diffs )
219            {
220                list.add( new ScmFile( diff.getNewPath(), ScmFileStatus.CHECKED_IN ) );
221            }
222            rw.release();
223        }
224        return list;
225    }
226
227    /**
228     * Translate a {@code FileStatus} in the matching {@code ScmFileStatus}.
229     *
230     * @param changeType
231     * @return the matching ScmFileStatus
232     * @throws ScmException if the given Status cannot be translated
233     */
234    public static ScmFileStatus getScmFileStatus( ChangeType changeType )
235    {
236        switch ( changeType )
237        {
238            case ADD:
239                return ScmFileStatus.ADDED;
240            case MODIFY:
241                return ScmFileStatus.MODIFIED;
242            case DELETE:
243                return ScmFileStatus.DELETED;
244            case RENAME:
245                return ScmFileStatus.RENAMED;
246            case COPY:
247                return ScmFileStatus.COPIED;
248            default:
249                return ScmFileStatus.UNKNOWN;
250        }
251    }
252
253    /**
254     * Adds all files in the given fileSet to the repository.
255     *
256     * @param git     the repo to add the files to
257     * @param fileSet the set of files within the workspace, the files are added
258     *                relative to the basedir of this fileset
259     * @return a list of added files
260     * @throws GitAPIException
261     * @throws NoFilepatternException
262     */
263    public static List<ScmFile> addAllFiles( Git git, ScmFileSet fileSet )
264        throws GitAPIException, NoFilepatternException
265    {
266        URI baseUri = fileSet.getBasedir().toURI();
267        AddCommand add = git.add();
268        for ( File file : fileSet.getFileList() )
269        {
270            if ( !file.isAbsolute() )
271            {
272                file = new File( fileSet.getBasedir().getPath(), file.getPath() );
273            }
274
275            if ( file.exists() )
276            {
277                String path = relativize( baseUri, file );
278                add.addFilepattern( path );
279                add.addFilepattern( file.getAbsolutePath() );
280            }
281        }
282        add.call();
283        
284        Status status = git.status().call();
285
286        Set<String> allInIndex = new HashSet<String>();
287        allInIndex.addAll( status.getAdded() );
288        allInIndex.addAll( status.getChanged() );
289
290        // System.out.println("All in index: "+allInIndex.size());
291
292        List<ScmFile> addedFiles = new ArrayList<ScmFile>( allInIndex.size() );
293
294        // rewrite all detected files to now have status 'checked_in'
295        for ( String entry : allInIndex )
296        {
297            ScmFile scmfile = new ScmFile( entry, ScmFileStatus.ADDED );
298
299            // if a specific fileSet is given, we have to check if the file is
300            // really tracked
301            for ( Iterator<File> itfl = fileSet.getFileList().iterator(); itfl.hasNext(); )
302            {
303                String path = relativize( baseUri, itfl.next() );
304                if ( FilenameUtils.normalizeFilename( path ).equals( FilenameUtils.normalizeFilename( scmfile.getPath() ) ) )
305                {
306                    addedFiles.add( scmfile );
307                }
308            }
309        }
310        return addedFiles;
311    }
312
313    private static String relativize( URI baseUri, File f )
314    {
315        String path = f.getPath();
316        if ( f.isAbsolute() )
317        {
318            path = baseUri.relativize( new File( path ).toURI() ).getPath();
319        }
320        return path;
321    }
322
323    /**
324     * Get a list of commits between two revisions.
325     *
326     * @param repo     the repository to work on
327     * @param sortings sorting
328     * @param fromRev  start revision
329     * @param toRev    if null, falls back to head
330     * @param fromDate from which date on
331     * @param toDate   until which date
332     * @param maxLines max number of lines
333     * @return a list of commits, might be empty, but never <code>null</code>
334     * @throws IOException
335     * @throws MissingObjectException
336     * @throws IncorrectObjectTypeException
337     */
338    public static List<RevCommit> getRevCommits( Repository repo, RevSort[] sortings, String fromRev, String toRev,
339                                                 final Date fromDate, final Date toDate, int maxLines )
340        throws IOException, MissingObjectException, IncorrectObjectTypeException
341    {
342
343        List<RevCommit> revs = new ArrayList<RevCommit>();
344        RevWalk walk = new RevWalk( repo );
345
346        ObjectId fromRevId = fromRev != null ? repo.resolve( fromRev ) : null;
347        ObjectId toRevId = toRev != null ? repo.resolve( toRev ) : null;
348
349        if ( sortings == null || sortings.length == 0 )
350        {
351            sortings = new RevSort[]{ RevSort.TOPO, RevSort.COMMIT_TIME_DESC };
352        }
353
354        for ( final RevSort s : sortings )
355        {
356            walk.sort( s, true );
357        }
358
359        if ( fromDate != null && toDate != null )
360        {
361            //walk.setRevFilter( CommitTimeRevFilter.between( fromDate, toDate ) );
362            walk.setRevFilter( new RevFilter()
363            {
364                @Override
365                public boolean include( RevWalk walker, RevCommit cmit )
366                    throws StopWalkException, MissingObjectException, IncorrectObjectTypeException, IOException
367                {
368                    int cmtTime = cmit.getCommitTime();
369
370                    return ( cmtTime >= (  fromDate.getTime() / 1000  ) ) && ( cmtTime <= ( toDate.getTime() / 1000) );
371                }
372
373                @Override
374                public RevFilter clone()
375                {
376                    return this;
377                }
378            } );
379        }
380        else
381        {
382            if ( fromDate != null )
383            {
384                walk.setRevFilter( CommitTimeRevFilter.after( fromDate ) );
385            }
386            if ( toDate != null )
387            {
388                walk.setRevFilter( CommitTimeRevFilter.before( toDate ) );
389            }
390        }
391
392        if ( fromRevId != null )
393        {
394            RevCommit c = walk.parseCommit( fromRevId );
395            c.add( RevFlag.UNINTERESTING );
396            RevCommit real = walk.parseCommit( c );
397            walk.markUninteresting( real );
398        }
399
400        if ( toRevId != null )
401        {
402            RevCommit c = walk.parseCommit( toRevId );
403            c.remove( RevFlag.UNINTERESTING );
404            RevCommit real = walk.parseCommit( c );
405            walk.markStart( real );
406        }
407        else
408        {
409            final ObjectId head = repo.resolve( Constants.HEAD );
410            if ( head == null )
411            {
412                throw new RuntimeException( "Cannot resolve " + Constants.HEAD );
413            }
414            RevCommit real = walk.parseCommit( head );
415            walk.markStart( real );
416        }
417
418        int n = 0;
419        for ( final RevCommit c : walk )
420        {
421            n++;
422            if ( maxLines != -1 && n > maxLines )
423            {
424                break;
425            }
426
427            revs.add( c );
428        }
429        return revs;
430    }
431
432}