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