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 <code>relativeRepositoryPath</code>
091     * <pre>
092     * URI.create( repositoryRoot ).relativize( fileSet.getBasedir().toURI() )
093     * </pre>
094     * 
095     * @param logger the logger
096     * @param workingDirectory the working directory
097     * @param relativeRepositoryPath the working directory relative to the repository root
098     * @since 1.9
099     * @see GitStatusCommand#createRevparseShowToplevelCommand(org.apache.maven.scm.ScmFileSet)
100     */
101    public GitStatusConsumer( ScmLogger logger, File workingDirectory, URI relativeRepositoryPath )
102    {
103        this( logger, workingDirectory );
104        this.relativeRepositoryPath = relativeRepositoryPath;
105    }
106
107    // ----------------------------------------------------------------------
108    // StreamConsumer Implementation
109    // ----------------------------------------------------------------------
110
111    /**
112     * {@inheritDoc}
113     */
114    public void consumeLine( String line )
115    {
116        if ( logger.isDebugEnabled() )
117        {
118            logger.debug( line );
119        }
120        if ( StringUtils.isEmpty( line ) )
121        {
122            return;
123        }
124
125        ScmFileStatus status = null;
126
127        List<String> files = new ArrayList<String>();
128        
129        Matcher matcher;
130        if ( ( matcher = ADDED_PATTERN.matcher( line ) ).find() )
131        {
132            status = ScmFileStatus.ADDED;
133            files.add(resolvePath(matcher.group(1), relativeRepositoryPath));
134        }
135        else if ( ( matcher = MODIFIED_PATTERN.matcher( line ) ).find() )
136        {
137            status = ScmFileStatus.MODIFIED;
138            files.add(resolvePath(matcher.group(1), relativeRepositoryPath));
139        }
140        else if ( ( matcher = DELETED_PATTERN.matcher( line ) ) .find() )
141        {
142            status = ScmFileStatus.DELETED;
143            files.add(resolvePath(matcher.group(1), relativeRepositoryPath));
144        }
145        else if ( ( matcher = RENAMED_PATTERN.matcher( line ) ).find() )
146        {
147            status = ScmFileStatus.RENAMED;
148            files.add( resolvePath( matcher.group( 1 ), relativeRepositoryPath ) );
149            files.add( resolvePath( matcher.group( 2 ), relativeRepositoryPath ) );
150            logger.debug( "RENAMED status for line '" + line + "' files added '" + matcher.group( 1 ) + "' '"
151                              + matcher.group( 2 ) );
152        }
153        else
154        {
155                logger.warn( "Ignoring unrecognized line: " +  line );
156                return;
157        }
158
159        // If the file isn't a file; don't add it.
160        if ( !files.isEmpty() && status != null )
161        {
162            if ( workingDirectory != null )
163            {
164                if ( status == ScmFileStatus.RENAMED )
165                {
166                    String oldFilePath = files.get( 0 );
167                    String newFilePath = files.get( 1 );
168                    if ( isFile( oldFilePath ) )
169                    {
170                        logger.debug(
171                            "file '" + oldFilePath + "' is a file" );
172                        return;
173                    }
174                    else
175                    {
176                        logger.debug(
177                            "file '" + oldFilePath + "' not a file" );
178                    }
179                    if ( !isFile( newFilePath ) )
180                    {
181                        logger.debug(
182                            "file '" + newFilePath + "' not a file" );
183                        return;
184                    }
185                    else
186                    {
187                        logger.debug(
188                            "file '" + newFilePath + "' is a file" );
189                    }
190                }
191                else if ( status == ScmFileStatus.DELETED )
192                {
193                    if ( isFile( files.get( 0 ) ) )
194                    {
195                        return;
196                    }
197                }
198                else
199                {
200                    if ( !isFile( files.get( 0 ) ) )
201                    {
202                        return;
203                    }
204                }
205            }
206
207            for ( String file : files )
208            {
209                changedFiles.add( new ScmFile( file, status ) );
210            }
211        }
212    }
213
214    private boolean isFile( String file )
215    {
216        File targetFile;
217        if ( relativeRepositoryPath == null )
218        {
219            targetFile = new File( workingDirectory, file );
220        }
221        else
222        {
223            targetFile = new File( relativeRepositoryPath.getPath(), file );
224        }
225        return targetFile.isFile();
226    }
227
228    protected static String resolvePath( String fileEntry, URI path )
229    {
230        if ( path != null )
231        {
232            return resolveURI( fileEntry, path ).getPath();
233        }
234        else
235        {
236            return fileEntry;
237        }
238    }
239
240    /**
241     * 
242     * @param fileEntry the fileEntry, must not be {@code null}
243     * @param path the path, must not be {@code null}
244     * @return
245     */
246    public static URI resolveURI( String fileEntry, URI path )
247    {
248        // When using URI.create, spaces need to be escaped but not the slashes, so we can't use URLEncoder.encode( String, String )
249        // new File( String ).toURI() results in an absolute URI while path is relative, so that can't be used either.
250        String str = fileEntry.replace( " ", "%20" );
251        return path.relativize( URI.create( str ) );
252    }
253
254
255    public List<ScmFile> getChangedFiles()
256    {
257        return changedFiles;
258    }
259}