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 }