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.PushCommand;
032import org.eclipse.jgit.api.Status;
033import org.eclipse.jgit.api.errors.GitAPIException;
034import org.eclipse.jgit.api.errors.InvalidRemoteException;
035import org.eclipse.jgit.api.errors.NoFilepatternException;
036import org.eclipse.jgit.api.errors.TransportException;
037import org.eclipse.jgit.diff.DiffEntry;
038import org.eclipse.jgit.diff.DiffEntry.ChangeType;
039import org.eclipse.jgit.diff.DiffFormatter;
040import org.eclipse.jgit.diff.RawTextComparator;
041import org.eclipse.jgit.errors.CorruptObjectException;
042import org.eclipse.jgit.errors.IncorrectObjectTypeException;
043import org.eclipse.jgit.errors.MissingObjectException;
044import org.eclipse.jgit.errors.StopWalkException;
045import org.eclipse.jgit.lib.Constants;
046import org.eclipse.jgit.lib.ObjectId;
047import org.eclipse.jgit.lib.ProgressMonitor;
048import org.eclipse.jgit.lib.Repository;
049import org.eclipse.jgit.lib.RepositoryBuilder;
050import org.eclipse.jgit.lib.StoredConfig;
051import org.eclipse.jgit.lib.TextProgressMonitor;
052import org.eclipse.jgit.revwalk.RevCommit;
053import org.eclipse.jgit.revwalk.RevFlag;
054import org.eclipse.jgit.revwalk.RevSort;
055import org.eclipse.jgit.revwalk.RevWalk;
056import org.eclipse.jgit.revwalk.filter.CommitTimeRevFilter;
057import org.eclipse.jgit.revwalk.filter.RevFilter;
058import org.eclipse.jgit.transport.CredentialsProvider;
059import org.eclipse.jgit.transport.PushResult;
060import org.eclipse.jgit.transport.RefSpec;
061import org.eclipse.jgit.transport.RemoteRefUpdate;
062import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
063import org.eclipse.jgit.util.io.DisabledOutputStream;
064
065import java.io.File;
066import java.io.IOException;
067import java.io.UnsupportedEncodingException;
068import java.net.URI;
069import java.net.URLEncoder;
070import java.util.ArrayList;
071import java.util.Collection;
072import java.util.Date;
073import java.util.HashSet;
074import java.util.Iterator;
075import java.util.List;
076import java.util.Set;
077
078/**
079 * JGit utility functions.
080 *
081 * @author <a href="mailto:struberg@yahoo.de">Mark Struberg</a>
082 * @author Dominik Bartholdi (imod)
083 * @since 1.9
084 */
085public class JGitUtils
086{
087
088    private JGitUtils()
089    {
090        // no op
091    }
092
093    /**
094     * Opens a JGit repository in the current directory or a parent directory.
095     * @param basedir The directory to start with
096     * @throws IOException If the repository cannot be opened
097     */
098    public static Git openRepo( File basedir ) throws IOException
099    {
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            {
243                RevCommit realParent = commit.getParentCount() > 0 ? commit.getParent( 0 ) : commit;
244                RevCommit parent = rw.parseCommit( realParent.getId() );
245                    df.setRepository( repository );
246                df.setDiffComparator( RawTextComparator.DEFAULT );
247                df.setDetectRenames( true );
248                List<DiffEntry> diffs = df.scan( parent.getTree(), commit.getTree() );
249                for ( DiffEntry diff : diffs )
250                {
251                    list.add( new ScmFile( diff.getNewPath(), ScmFileStatus.CHECKED_IN ) );
252                }
253            }
254        }
255        return list;
256    }
257
258    /**
259     * Translate a {@code FileStatus} in the matching {@code ScmFileStatus}.
260     *
261     * @param changeType
262     * @return the matching ScmFileStatus
263     */
264    public static ScmFileStatus getScmFileStatus( ChangeType changeType )
265    {
266        switch ( changeType )
267        {
268            case ADD:
269                return ScmFileStatus.ADDED;
270            case MODIFY:
271                return ScmFileStatus.MODIFIED;
272            case DELETE:
273                return ScmFileStatus.DELETED;
274            case RENAME:
275                return ScmFileStatus.RENAMED;
276            case COPY:
277                return ScmFileStatus.COPIED;
278            default:
279                return ScmFileStatus.UNKNOWN;
280        }
281    }
282
283    /**
284     * Adds all files in the given fileSet to the repository.
285     *
286     * @param git     the repo to add the files to
287     * @param fileSet the set of files within the workspace, the files are added
288     *                relative to the basedir of this fileset
289     * @return a list of added files
290     * @throws GitAPIException
291     * @throws NoFilepatternException
292     */
293    public static List<ScmFile> addAllFiles( Git git, ScmFileSet fileSet )
294        throws GitAPIException, NoFilepatternException
295    {
296        URI baseUri = fileSet.getBasedir().toURI();
297        AddCommand add = git.add();
298        for ( File file : fileSet.getFileList() )
299        {
300            if ( !file.isAbsolute() )
301            {
302                file = new File( fileSet.getBasedir().getPath(), file.getPath() );
303            }
304
305            if ( file.exists() )
306            {
307                String path = relativize( baseUri, file );
308                add.addFilepattern( path );
309                add.addFilepattern( file.getAbsolutePath() );
310            }
311        }
312        add.call();
313
314        Status status = git.status().call();
315
316        Set<String> allInIndex = new HashSet<String>();
317        allInIndex.addAll( status.getAdded() );
318        allInIndex.addAll( status.getChanged() );
319
320        // System.out.println("All in index: "+allInIndex.size());
321
322        List<ScmFile> addedFiles = new ArrayList<ScmFile>( allInIndex.size() );
323
324        // rewrite all detected files to now have status 'checked_in'
325        for ( String entry : allInIndex )
326        {
327            ScmFile scmfile = new ScmFile( entry, ScmFileStatus.ADDED );
328
329            // if a specific fileSet is given, we have to check if the file is
330            // really tracked
331            for ( Iterator<File> itfl = fileSet.getFileList().iterator(); itfl.hasNext(); )
332            {
333                String path = FilenameUtils.normalizeFilename( relativize( baseUri, itfl.next() ) );
334                if ( path.equals( FilenameUtils.normalizeFilename( scmfile.getPath() ) ) )
335                {
336                    addedFiles.add( scmfile );
337                }
338            }
339        }
340        return addedFiles;
341    }
342
343    private static String relativize( URI baseUri, File f )
344    {
345        String path = f.getPath();
346        if ( f.isAbsolute() )
347        {
348            path = baseUri.relativize( new File( path ).toURI() ).getPath();
349        }
350        return path;
351    }
352
353    /**
354     * Get a list of commits between two revisions.
355     *
356     * @param repo     the repository to work on
357     * @param sortings sorting
358     * @param fromRev  start revision
359     * @param toRev    if null, falls back to head
360     * @param fromDate from which date on
361     * @param toDate   until which date
362     * @param maxLines max number of lines
363     * @return a list of commits, might be empty, but never <code>null</code>
364     * @throws IOException
365     * @throws MissingObjectException
366     * @throws IncorrectObjectTypeException
367     */
368    public static List<RevCommit> getRevCommits( Repository repo, RevSort[] sortings, String fromRev, String toRev,
369                                                 final Date fromDate, final Date toDate, int maxLines )
370        throws IOException, MissingObjectException, IncorrectObjectTypeException
371    {
372
373        List<RevCommit> revs = new ArrayList<RevCommit>();
374
375        ObjectId fromRevId = fromRev != null ? repo.resolve( fromRev ) : null;
376        ObjectId toRevId = toRev != null ? repo.resolve( toRev ) : null;
377
378        if ( sortings == null || sortings.length == 0 )
379        {
380            sortings = new RevSort[]{ RevSort.TOPO, RevSort.COMMIT_TIME_DESC };
381        }
382
383        try ( RevWalk walk = new RevWalk( repo ) )
384        {
385            for ( final RevSort s : sortings )
386            {
387                walk.sort( s, true );
388            }
389
390            if ( fromDate != null && toDate != null )
391            {
392                //walk.setRevFilter( CommitTimeRevFilter.between( fromDate, toDate ) );
393                walk.setRevFilter( new RevFilter()
394                {
395                    @Override
396                    public boolean include( RevWalk walker, RevCommit cmit )
397                        throws StopWalkException, MissingObjectException, IncorrectObjectTypeException, IOException
398                    {
399                        int cmtTime = cmit.getCommitTime();
400
401                        return ( cmtTime >= ( fromDate.getTime() / 1000 ) )
402                                && ( cmtTime <= ( toDate.getTime() / 1000 ) );
403                    }
404
405                    @Override
406                    public RevFilter clone()
407                    {
408                        return this;
409                    }
410                } );
411            }
412            else
413            {
414                if ( fromDate != null )
415                {
416                    walk.setRevFilter( CommitTimeRevFilter.after( fromDate ) );
417                }
418                if ( toDate != null )
419                {
420                    walk.setRevFilter( CommitTimeRevFilter.before( toDate ) );
421                }
422            }
423
424            if ( fromRevId != null )
425            {
426                RevCommit c = walk.parseCommit( fromRevId );
427                c.add( RevFlag.UNINTERESTING );
428                RevCommit real = walk.parseCommit( c );
429                walk.markUninteresting( real );
430            }
431
432            if ( toRevId != null )
433            {
434                RevCommit c = walk.parseCommit( toRevId );
435                c.remove( RevFlag.UNINTERESTING );
436                RevCommit real = walk.parseCommit( c );
437                walk.markStart( real );
438            }
439            else
440            {
441                final ObjectId head = repo.resolve( Constants.HEAD );
442                if ( head == null )
443                {
444                    throw new RuntimeException( "Cannot resolve " + Constants.HEAD );
445                }
446                RevCommit real = walk.parseCommit( head );
447                walk.markStart( real );
448            }
449
450            int n = 0;
451            for ( final RevCommit c : walk )
452            {
453                n++;
454                if ( maxLines != -1 && n > maxLines )
455                {
456                    break;
457                }
458
459                revs.add( c );
460            }
461            return revs;
462        }
463    }
464
465}