001package org.apache.maven.scm.provider.git.gitexe.command.status;
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 java.io.File;
023import java.net.URI;
024import java.util.ArrayList;
025import java.util.List;
026import java.util.regex.Matcher;
027import java.util.regex.Pattern;
028
029import org.apache.commons.lang.StringUtils;
030import org.apache.maven.scm.ScmFile;
031import org.apache.maven.scm.ScmFileStatus;
032import org.apache.maven.scm.log.ScmLogger;
033import org.codehaus.plexus.util.cli.StreamConsumer;
034
035/**
036 * @author <a href="mailto:struberg@yahoo.de">Mark Struberg</a>
037 */
038public class GitStatusConsumer
039    implements StreamConsumer
040{
041
042    /**
043     * The pattern used to match added file lines
044     */
045    private static final Pattern ADDED_PATTERN = Pattern.compile( "^A[ M]* (.*)$" );
046
047    /**
048     * The pattern used to match modified file lines
049     */
050    private static final Pattern MODIFIED_PATTERN = Pattern.compile( "^ *M[ M]* (.*)$" );
051
052    /**
053     * The pattern used to match deleted file lines
054     */
055    private static final Pattern DELETED_PATTERN = Pattern.compile( "^ *D * (.*)$" );
056
057    /**
058     * The pattern used to match renamed file lines
059     */
060    private static final Pattern RENAMED_PATTERN = Pattern.compile( "^R  (.*) -> (.*)$" );
061
062    private ScmLogger logger;
063
064    private File workingDirectory;
065
066    /**
067     * Entries are relative to working directory, not to the repositoryroot
068     */
069    private List<ScmFile> changedFiles = new ArrayList<ScmFile>();
070
071    private URI relativeRepositoryPath;
072    
073    // ----------------------------------------------------------------------
074    //
075    // ----------------------------------------------------------------------
076
077    /**
078     * Consumer when workingDirectory and repositoryRootDirectory are the same
079     * 
080     * @param logger the logger
081     * @param workingDirectory the working directory
082     */
083    public GitStatusConsumer( ScmLogger logger, File workingDirectory )
084    {
085        this.logger = logger;
086        this.workingDirectory = workingDirectory;
087    }
088
089    /**
090     * Assuming that you have to discover the repositoryRoot, this is how you can get the
091     * <code>relativeRepositoryPath</code>
092     * <pre>
093     * URI.create( repositoryRoot ).relativize( fileSet.getBasedir().toURI() )
094     * </pre>
095     * 
096     * @param logger the logger
097     * @param workingDirectory the working directory
098     * @param relativeRepositoryPath the working directory relative to the repository root
099     * @since 1.9
100     * @see GitStatusCommand#createRevparseShowToplevelCommand(org.apache.maven.scm.ScmFileSet)
101     */
102    public GitStatusConsumer( ScmLogger logger, File workingDirectory, URI relativeRepositoryPath )
103    {
104        this( logger, workingDirectory );
105        this.relativeRepositoryPath = relativeRepositoryPath;
106    }
107
108    // ----------------------------------------------------------------------
109    // StreamConsumer Implementation
110    // ----------------------------------------------------------------------
111
112    /**
113     * {@inheritDoc}
114     */
115    public void consumeLine( String line )
116    {
117        if ( logger.isDebugEnabled() )
118        {
119            logger.debug( line );
120        }
121        if ( StringUtils.isEmpty( line ) )
122        {
123            return;
124        }
125
126        ScmFileStatus status = null;
127
128        List<String> files = new ArrayList<String>();
129        
130        Matcher matcher;
131        if ( ( matcher = ADDED_PATTERN.matcher( line ) ).find() )
132        {
133            status = ScmFileStatus.ADDED;
134            files.add( resolvePath( matcher.group( 1 ), relativeRepositoryPath ) );
135        }
136        else if ( ( matcher = MODIFIED_PATTERN.matcher( line ) ).find() )
137        {
138            status = ScmFileStatus.MODIFIED;
139            files.add( resolvePath( matcher.group( 1 ), relativeRepositoryPath ) );
140        }
141        else if ( ( matcher = DELETED_PATTERN.matcher( line ) ).find() )
142        {
143            status = ScmFileStatus.DELETED;
144            files.add( resolvePath( matcher.group( 1 ), relativeRepositoryPath ) );
145        }
146        else if ( ( matcher = RENAMED_PATTERN.matcher( line ) ).find() )
147        {
148            status = ScmFileStatus.RENAMED;
149            files.add( resolvePath( matcher.group( 1 ), relativeRepositoryPath ) );
150            files.add( resolvePath( matcher.group( 2 ), relativeRepositoryPath ) );
151            logger.debug( "RENAMED status for line '" + line + "' files added '" + matcher.group( 1 ) + "' '"
152                              + matcher.group( 2 ) );
153        }
154        else
155        {
156            logger.warn( "Ignoring unrecognized line: " + line );
157            return;
158        }
159
160        // If the file isn't a file; don't add it.
161        if ( !files.isEmpty() && status != null )
162        {
163            if ( workingDirectory != null )
164            {
165                if ( status == ScmFileStatus.RENAMED )
166                {
167                    String oldFilePath = files.get( 0 );
168                    String newFilePath = files.get( 1 );
169                    if ( isFile( oldFilePath ) )
170                    {
171                        logger.debug( "file '" + oldFilePath + "' is a file" );
172                        return;
173                    }
174                    else
175                    {
176                        logger.debug( "file '" + oldFilePath + "' not a file" );
177                    }
178                    if ( !isFile( newFilePath ) )
179                    {
180                        logger.debug( "file '" + newFilePath + "' not a file" );
181                        return;
182                    }
183                    else
184                    {
185                        logger.debug( "file '" + newFilePath + "' is a file" );
186                    }
187                }
188                else if ( status == ScmFileStatus.DELETED )
189                {
190                    if ( isFile( files.get( 0 ) ) )
191                    {
192                        return;
193                    }
194                }
195                else
196                {
197                    if ( !isFile( files.get( 0 ) ) )
198                    {
199                        return;
200                    }
201                }
202            }
203
204            for ( String file : files )
205            {
206                changedFiles.add( new ScmFile( file, status ) );
207            }
208        }
209    }
210
211    private boolean isFile( String file )
212    {
213        File targetFile;
214        if ( relativeRepositoryPath == null )
215        {
216            targetFile = new File( workingDirectory, file );
217        }
218        else
219        {
220            targetFile = new File( relativeRepositoryPath.getPath(), file );
221        }
222        return targetFile.isFile();
223    }
224
225    protected static String resolvePath( String fileEntry, URI path )
226    {
227        if ( path != null )
228        {
229            return resolveURI( fileEntry, path ).getPath();
230        }
231        else
232        {
233            return fileEntry;
234        }
235    }
236
237    /**
238     * 
239     * @param fileEntry the fileEntry, must not be {@code null}
240     * @param path the path, must not be {@code null}
241     * @return
242     */
243    public static URI resolveURI( String fileEntry, URI path )
244    {
245        // When using URI.create, spaces need to be escaped but not the slashes, so we can't use
246        // URLEncoder.encode( String, String )
247        // new File( String ).toURI() results in an absolute URI while path is relative, so that can't be used either.
248        String str = fileEntry.replace( " ", "%20" );
249        return path.relativize( URI.create( str ) );
250    }
251
252
253    public List<ScmFile> getChangedFiles()
254    {
255        return changedFiles;
256    }
257}