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