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.io.UnsupportedEncodingException; 024import java.net.URI; 025import java.net.URISyntaxException; 026import java.util.ArrayList; 027import java.util.List; 028import java.util.regex.Matcher; 029import java.util.regex.Pattern; 030 031import org.apache.commons.lang.StringUtils; 032import org.apache.maven.scm.ScmFile; 033import org.apache.maven.scm.ScmFileStatus; 034import org.apache.maven.scm.log.ScmLogger; 035import org.codehaus.plexus.util.cli.StreamConsumer; 036 037/** 038 * @author <a href="mailto:struberg@yahoo.de">Mark Struberg</a> 039 */ 040public class GitStatusConsumer 041 implements StreamConsumer 042{ 043 044 /** 045 * The pattern used to match added file lines 046 */ 047 private static final Pattern ADDED_PATTERN = Pattern.compile( "^A[ M]* (.*)$" ); 048 049 /** 050 * The pattern used to match modified file lines 051 */ 052 private static final Pattern MODIFIED_PATTERN = Pattern.compile( "^ *M[ M]* (.*)$" ); 053 054 /** 055 * The pattern used to match deleted file lines 056 */ 057 private static final Pattern DELETED_PATTERN = Pattern.compile( "^ *D * (.*)$" ); 058 059 /** 060 * The pattern used to match renamed file lines 061 */ 062 private static final Pattern RENAMED_PATTERN = Pattern.compile( "^R (.*) -> (.*)$" ); 063 064 private ScmLogger logger; 065 066 private File workingDirectory; 067 068 /** 069 * Entries are relative to working directory, not to the repositoryroot 070 */ 071 private List<ScmFile> changedFiles = new ArrayList<ScmFile>(); 072 073 private URI relativeRepositoryPath; 074 075 // ---------------------------------------------------------------------- 076 // 077 // ---------------------------------------------------------------------- 078 079 /** 080 * Consumer when workingDirectory and repositoryRootDirectory are the same 081 * 082 * @param logger the logger 083 * @param workingDirectory the working directory 084 */ 085 public GitStatusConsumer( ScmLogger logger, File workingDirectory ) 086 { 087 this.logger = logger; 088 this.workingDirectory = workingDirectory; 089 } 090 091 /** 092 * Assuming that you have to discover the repositoryRoot, this is how you can get the 093 * <code>relativeRepositoryPath</code> 094 * <pre> 095 * URI.create( repositoryRoot ).relativize( fileSet.getBasedir().toURI() ) 096 * </pre> 097 * 098 * @param logger the logger 099 * @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}