View Javadoc
1   package org.apache.maven.scm.provider.git.gitexe.command.status;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   * http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.File;
23  import java.io.UnsupportedEncodingException;
24  import java.net.URI;
25  import java.util.ArrayList;
26  import java.util.List;
27  import java.util.regex.Matcher;
28  import java.util.regex.Pattern;
29  
30  import org.apache.commons.lang.StringUtils;
31  import org.apache.maven.scm.ScmFile;
32  import org.apache.maven.scm.ScmFileStatus;
33  import org.apache.maven.scm.log.ScmLogger;
34  import org.codehaus.plexus.util.cli.StreamConsumer;
35  
36  /**
37   * @author <a href="mailto:struberg@yahoo.de">Mark Struberg</a>
38   */
39  public class GitStatusConsumer
40      implements StreamConsumer
41  {
42  
43      /**
44       * The pattern used to match added file lines
45       */
46      private static final Pattern ADDED_PATTERN = Pattern.compile( "^A[ M]* (.*)$" );
47  
48      /**
49       * The pattern used to match modified file lines
50       */
51      private static final Pattern MODIFIED_PATTERN = Pattern.compile( "^ *M[ M]* (.*)$" );
52  
53      /**
54       * The pattern used to match deleted file lines
55       */
56      private static final Pattern DELETED_PATTERN = Pattern.compile( "^ *D * (.*)$" );
57  
58      /**
59       * The pattern used to match renamed file lines
60       */
61      private static final Pattern RENAMED_PATTERN = Pattern.compile( "^R  (.*) -> (.*)$" );
62  
63      private ScmLogger logger;
64  
65      private File workingDirectory;
66  
67      /**
68       * Entries are relative to working directory, not to the repositoryroot
69       */
70      private List<ScmFile> changedFiles = new ArrayList<ScmFile>();
71  
72      private URI relativeRepositoryPath;
73      
74      // ----------------------------------------------------------------------
75      //
76      // ----------------------------------------------------------------------
77  
78      /**
79       * Consumer when workingDirectory and repositoryRootDirectory are the same
80       * 
81       * @param logger the logger
82       * @param workingDirectory the working directory
83       */
84      public GitStatusConsumer( ScmLogger logger, File workingDirectory )
85      {
86          this.logger = logger;
87          this.workingDirectory = workingDirectory;
88      }
89  
90      /**
91       * Assuming that you have to discover the repositoryRoot, this is how you can get the
92       * <code>relativeRepositoryPath</code>
93       * <pre>
94       * URI.create( repositoryRoot ).relativize( fileSet.getBasedir().toURI() )
95       * </pre>
96       * 
97       * @param logger the logger
98       * @param workingDirectory the working directory
99       * @param relativeRepositoryPath the working directory relative to the repository root
100      * @since 1.9
101      * @see GitStatusCommand#createRevparseShowToplevelCommand(org.apache.maven.scm.ScmFileSet)
102      */
103     public GitStatusConsumer( ScmLogger logger, File workingDirectory, URI relativeRepositoryPath )
104     {
105         this( logger, workingDirectory );
106         this.relativeRepositoryPath = relativeRepositoryPath;
107     }
108 
109     // ----------------------------------------------------------------------
110     // StreamConsumer Implementation
111     // ----------------------------------------------------------------------
112 
113     /**
114      * {@inheritDoc}
115      */
116     public void consumeLine( String line )
117     {
118         if ( logger.isDebugEnabled() )
119         {
120             logger.debug( line );
121         }
122         if ( StringUtils.isEmpty( line ) )
123         {
124             return;
125         }
126 
127         ScmFileStatus status = null;
128 
129         List<String> files = new ArrayList<String>();
130         
131         Matcher matcher;
132         if ( ( matcher = ADDED_PATTERN.matcher( line ) ).find() )
133         {
134             status = ScmFileStatus.ADDED;
135             files.add( resolvePath( matcher.group( 1 ), relativeRepositoryPath ) );
136         }
137         else if ( ( matcher = MODIFIED_PATTERN.matcher( line ) ).find() )
138         {
139             status = ScmFileStatus.MODIFIED;
140             files.add( resolvePath( matcher.group( 1 ), relativeRepositoryPath ) );
141         }
142         else if ( ( matcher = DELETED_PATTERN.matcher( line ) ).find() )
143         {
144             status = ScmFileStatus.DELETED;
145             files.add( resolvePath( matcher.group( 1 ), relativeRepositoryPath ) );
146         }
147         else if ( ( matcher = RENAMED_PATTERN.matcher( line ) ).find() )
148         {
149             status = ScmFileStatus.RENAMED;
150             files.add( resolvePath( matcher.group( 1 ), relativeRepositoryPath ) );
151             files.add( resolvePath( matcher.group( 2 ), relativeRepositoryPath ) );
152             logger.debug( "RENAMED status for line '" + line + "' files added '" + matcher.group( 1 ) + "' '"
153                               + matcher.group( 2 ) );
154         }
155         else
156         {
157             logger.warn( "Ignoring unrecognized line: " + line );
158             return;
159         }
160 
161         // If the file isn't a file; don't add it.
162         if ( !files.isEmpty() && status != null )
163         {
164             if ( workingDirectory != null )
165             {
166                 if ( status == ScmFileStatus.RENAMED )
167                 {
168                     String oldFilePath = files.get( 0 );
169                     String newFilePath = files.get( 1 );
170                     if ( isFile( oldFilePath ) )
171                     {
172                         logger.debug( "file '" + oldFilePath + "' is a file" );
173                         return;
174                     }
175                     else
176                     {
177                         logger.debug( "file '" + oldFilePath + "' not a file" );
178                     }
179                     if ( !isFile( newFilePath ) )
180                     {
181                         logger.debug( "file '" + newFilePath + "' not a file" );
182                         return;
183                     }
184                     else
185                     {
186                         logger.debug( "file '" + newFilePath + "' is a file" );
187                     }
188                 }
189                 else if ( status == ScmFileStatus.DELETED )
190                 {
191                     if ( isFile( files.get( 0 ) ) )
192                     {
193                         return;
194                     }
195                 }
196                 else
197                 {
198                     if ( !isFile( files.get( 0 ) ) )
199                     {
200                         return;
201                     }
202                 }
203             }
204 
205             for ( String file : files )
206             {
207                 changedFiles.add( new ScmFile( file, status ) );
208             }
209         }
210     }
211 
212     private boolean isFile( String file )
213     {
214         File targetFile;
215         if ( relativeRepositoryPath == null )
216         {
217             targetFile = new File( workingDirectory, file );
218         }
219         else
220         {
221             targetFile = new File( relativeRepositoryPath.getPath(), file );
222         }
223         return targetFile.isFile();
224     }
225 
226     protected static String resolvePath( String fileEntry, URI path )
227     {
228         /* Quotes may be included (from the git status line) when an fileEntry includes spaces */
229         String cleanedEntry = stripQuotes( fileEntry );
230         if ( path != null )
231         {
232             return resolveURI( cleanedEntry, path ).getPath();
233         }
234         else
235         {
236             return cleanedEntry;
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
249         // URLEncoder.encode( String, String )
250         // new File( String ).toURI() results in an absolute URI while path is relative, so that can't be used either.
251         return path.relativize( URI.create( stripQuotes( fileEntry ).replace( " ", "%20" ) ) );
252     }
253 
254 
255     public List<ScmFile> getChangedFiles()
256     {
257         return changedFiles;
258     }
259 
260     /**
261      * @param str the (potentially quoted) string, must not be {@code null}
262      * @return the string with a pair of double quotes removed (if they existed)
263      */
264     private static String stripQuotes( String str )
265     {
266         int strLen = str.length();
267         return ( strLen > 0 && str.startsWith( "\"" ) && str.endsWith( "\"" ) ) ? unescape( str.substring( 1, strLen - 1 ) ) : str;
268     }
269     
270     /**
271      * Dequote a quoted string generated by git status --porcelain.
272      * The leading and trailing quotes have already been removed. 
273      * @param fileEntry
274      * @return
275      */
276     private static String unescape( String fileEntry )
277     {
278         // If there are no escaped characters, just return the input argument
279         int pos = fileEntry.indexOf( '\\' );
280         if ( pos == -1 )
281         {
282             return fileEntry;
283         }
284         
285         // We have escaped characters
286         byte[] inba = fileEntry.getBytes();
287         int inSub = 0;      // Input subscript into fileEntry
288         byte[] outba = new byte[fileEntry.length()];
289         int outSub = 0;     // Output subscript into outba
290         
291         while ( true )
292         {
293             System.arraycopy( inba,  inSub,  outba, outSub, pos - inSub );
294             outSub += pos - inSub;
295             inSub = pos + 1;
296             switch ( (char) inba[inSub++] )
297             {
298                 case '"':
299                     outba[outSub++] = '"';
300                     break;
301                     
302                 case 'a':
303                     outba[outSub++] = 7;        // Bell
304                     break;
305                     
306                 case 'b':
307                     outba[outSub++] = '\b';
308                     break;
309                     
310                 case 't':
311                     outba[outSub++] = '\t';
312                     break;
313                     
314                 case 'n':
315                     outba[outSub++] = '\n';
316                     break;
317                     
318                 case 'v':
319                     outba[outSub++] = 11;       // Vertical tab
320                     break;
321                     
322                 case 'f':
323                     outba[outSub++] = '\f';
324                     break;
325                     
326                 case 'r':
327                     outba[outSub++] = '\f';
328                     break;
329                     
330                 case '\\':
331                     outba[outSub++] = '\\';
332                     break;
333                     
334                 case '0':
335                 case '1':
336                 case '2':
337                 case '3':
338                     // This assumes that the octal escape here is valid.
339                     byte b = (byte) ( ( inba[inSub - 1] - '0' ) << 6 );
340                     b |= (byte) ( ( inba[inSub++] - '0' ) << 3 );
341                     b |= (byte) ( inba[inSub++] - '0' );
342                     outba[outSub++] = b;
343                     break;
344                     
345                 default:
346                     //This is an invalid escape in a string.  Just copy it.
347                     outba[outSub++] = '\\';
348                     inSub--;
349                     break;
350             }
351             pos = fileEntry.indexOf( '\\', inSub);
352             if ( pos == -1 )        // No more backslashes; we're done
353             {
354                 System.arraycopy( inba, inSub, outba, outSub, inba.length - inSub );
355                 outSub += inba.length - inSub;
356                 break;
357             }
358         }
359         try
360         {
361             // explicit say UTF-8, otherwise it'll fail at least on Windows cmdline
362             return new String(outba, 0, outSub, "UTF-8");
363         }
364         catch ( UnsupportedEncodingException e )
365         {
366           throw new RuntimeException( e );    
367         }
368     }
369 }