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