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 }