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.RepositoryBuilder;
049import org.eclipse.jgit.lib.StoredConfig;
050import org.eclipse.jgit.lib.TextProgressMonitor;
051import org.eclipse.jgit.revwalk.RevCommit;
052import org.eclipse.jgit.revwalk.RevFlag;
053import org.eclipse.jgit.revwalk.RevSort;
054import org.eclipse.jgit.revwalk.RevWalk;
055import org.eclipse.jgit.revwalk.filter.CommitTimeRevFilter;
056import org.eclipse.jgit.revwalk.filter.RevFilter;
057import org.eclipse.jgit.transport.CredentialsProvider;
058import org.eclipse.jgit.transport.PushResult;
059import org.eclipse.jgit.transport.RefSpec;
060import org.eclipse.jgit.transport.RemoteRefUpdate;
061import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
062import org.eclipse.jgit.util.io.DisabledOutputStream;
063
064import java.io.File;
065import java.io.IOException;
066import java.io.UnsupportedEncodingException;
067import java.net.URI;
068import java.net.URLEncoder;
069import java.util.ArrayList;
070import java.util.Collection;
071import java.util.Date;
072import java.util.HashSet;
073import java.util.Iterator;
074import java.util.List;
075import java.util.Set;
076
077/**
078 * JGit utility functions.
079 *
080 * @author <a href="mailto:struberg@yahoo.de">Mark Struberg</a>
081 * @author Dominik Bartholdi (imod)
082 * @since 1.9
083 */
084public class JGitUtils
085{
086    
087    private JGitUtils()
088    {
089        // no op
090    }
091
092    /**
093     * Opens a JGit repository in the current directory or a parent directory.
094     * @param basedir The directory to start with
095     * @throws IOException If the repository cannot be opened
096     */
097    public static Git openRepo( File basedir ) throws IOException
098    {
099        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}