blob: 7d850da805d06088d7e3873ba22c05ba490e3a3f [file] [log] [blame]
package org.apache.maven.scm.provider.git.gitexe.command.status;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.apache.maven.scm.ScmFile;
import org.apache.maven.scm.ScmFileStatus;
import org.apache.maven.scm.log.ScmLogger;
import org.codehaus.plexus.util.cli.StreamConsumer;
/**
* @author <a href="mailto:struberg@yahoo.de">Mark Struberg</a>
*/
public class GitStatusConsumer
implements StreamConsumer
{
/**
* The pattern used to match added file lines
*/
private static final Pattern ADDED_PATTERN = Pattern.compile( "^A[ M]* (.*)$" );
/**
* The pattern used to match modified file lines
*/
private static final Pattern MODIFIED_PATTERN = Pattern.compile( "^ *M[ M]* (.*)$" );
/**
* The pattern used to match deleted file lines
*/
private static final Pattern DELETED_PATTERN = Pattern.compile( "^ *D * (.*)$" );
/**
* The pattern used to match renamed file lines
*/
private static final Pattern RENAMED_PATTERN = Pattern.compile( "^R (.*) -> (.*)$" );
private ScmLogger logger;
private File workingDirectory;
/**
* Entries are relative to working directory, not to the repositoryroot
*/
private List<ScmFile> changedFiles = new ArrayList<ScmFile>();
private URI relativeRepositoryPath;
// ----------------------------------------------------------------------
//
// ----------------------------------------------------------------------
/**
* Consumer when workingDirectory and repositoryRootDirectory are the same
*
* @param logger the logger
* @param workingDirectory the working directory
*/
public GitStatusConsumer( ScmLogger logger, File workingDirectory )
{
this.logger = logger;
this.workingDirectory = workingDirectory;
}
/**
* Assuming that you have to discover the repositoryRoot, this is how you can get the
* <code>relativeRepositoryPath</code>
* <pre>
* URI.create( repositoryRoot ).relativize( fileSet.getBasedir().toURI() )
* </pre>
*
* @param logger the logger
* @param workingDirectory the working directory
* @param relativeRepositoryPath the working directory relative to the repository root
* @since 1.9
* @see GitStatusCommand#createRevparseShowToplevelCommand(org.apache.maven.scm.ScmFileSet)
*/
public GitStatusConsumer( ScmLogger logger, File workingDirectory, URI relativeRepositoryPath )
{
this( logger, workingDirectory );
this.relativeRepositoryPath = relativeRepositoryPath;
}
// ----------------------------------------------------------------------
// StreamConsumer Implementation
// ----------------------------------------------------------------------
/**
* {@inheritDoc}
*/
public void consumeLine( String line )
{
if ( logger.isDebugEnabled() )
{
logger.debug( line );
}
if ( StringUtils.isEmpty( line ) )
{
return;
}
ScmFileStatus status = null;
List<String> files = new ArrayList<String>();
Matcher matcher;
if ( ( matcher = ADDED_PATTERN.matcher( line ) ).find() )
{
status = ScmFileStatus.ADDED;
files.add( resolvePath( matcher.group( 1 ), relativeRepositoryPath ) );
}
else if ( ( matcher = MODIFIED_PATTERN.matcher( line ) ).find() )
{
status = ScmFileStatus.MODIFIED;
files.add( resolvePath( matcher.group( 1 ), relativeRepositoryPath ) );
}
else if ( ( matcher = DELETED_PATTERN.matcher( line ) ).find() )
{
status = ScmFileStatus.DELETED;
files.add( resolvePath( matcher.group( 1 ), relativeRepositoryPath ) );
}
else if ( ( matcher = RENAMED_PATTERN.matcher( line ) ).find() )
{
status = ScmFileStatus.RENAMED;
files.add( resolvePath( matcher.group( 1 ), relativeRepositoryPath ) );
files.add( resolvePath( matcher.group( 2 ), relativeRepositoryPath ) );
logger.debug( "RENAMED status for line '" + line + "' files added '" + matcher.group( 1 ) + "' '"
+ matcher.group( 2 ) );
}
else
{
logger.warn( "Ignoring unrecognized line: " + line );
return;
}
// If the file isn't a file; don't add it.
if ( !files.isEmpty() && status != null )
{
if ( workingDirectory != null )
{
if ( status == ScmFileStatus.RENAMED )
{
String oldFilePath = files.get( 0 );
String newFilePath = files.get( 1 );
if ( isFile( oldFilePath ) )
{
logger.debug( "file '" + oldFilePath + "' is a file" );
return;
}
else
{
logger.debug( "file '" + oldFilePath + "' not a file" );
}
if ( !isFile( newFilePath ) )
{
logger.debug( "file '" + newFilePath + "' not a file" );
return;
}
else
{
logger.debug( "file '" + newFilePath + "' is a file" );
}
}
else if ( status == ScmFileStatus.DELETED )
{
if ( isFile( files.get( 0 ) ) )
{
return;
}
}
else
{
if ( !isFile( files.get( 0 ) ) )
{
return;
}
}
}
for ( String file : files )
{
changedFiles.add( new ScmFile( file, status ) );
}
}
}
private boolean isFile( String file )
{
File targetFile;
if ( relativeRepositoryPath == null )
{
targetFile = new File( workingDirectory, file );
}
else
{
targetFile = new File( relativeRepositoryPath.getPath(), file );
}
return targetFile.isFile();
}
protected static String resolvePath( String fileEntry, URI path )
{
/* Quotes may be included (from the git status line) when an fileEntry includes spaces */
String cleanedEntry = stripQuotes( fileEntry );
if ( path != null )
{
return resolveURI( cleanedEntry, path ).getPath();
}
else
{
return cleanedEntry;
}
}
/**
*
* @param fileEntry the fileEntry, must not be {@code null}
* @param path the path, must not be {@code null}
* @return
*/
public static URI resolveURI( String fileEntry, URI path )
{
// When using URI.create, spaces need to be escaped but not the slashes, so we can't use
// URLEncoder.encode( String, String )
// new File( String ).toURI() results in an absolute URI while path is relative, so that can't be used either.
return path.relativize( URI.create( stripQuotes( fileEntry ).replace( " ", "%20" ) ) );
}
public List<ScmFile> getChangedFiles()
{
return changedFiles;
}
/**
* @param str the (potentially quoted) string, must not be {@code null}
* @return the string with a pair of double quotes removed (if they existed)
*/
private static String stripQuotes( String str )
{
int strLen = str.length();
return ( strLen > 0 && str.startsWith( "\"" ) && str.endsWith( "\"" ) ) ? unescape( str.substring( 1, strLen - 1 ) ) : str;
}
/**
* Dequote a quoted string generated by git status --porcelain.
* The leading and trailing quotes have already been removed.
* @param fileEntry
* @return
*/
private static String unescape( String fileEntry )
{
// If there are no escaped characters, just return the input argument
int pos = fileEntry.indexOf( '\\' );
if ( pos == -1 )
{
return fileEntry;
}
// We have escaped characters
byte[] inba = fileEntry.getBytes();
int inSub = 0; // Input subscript into fileEntry
byte[] outba = new byte[fileEntry.length()];
int outSub = 0; // Output subscript into outba
while ( true )
{
System.arraycopy( inba, inSub, outba, outSub, pos - inSub );
outSub += pos - inSub;
inSub = pos + 1;
switch ( (char) inba[inSub++] )
{
case '"':
outba[outSub++] = '"';
break;
case 'a':
outba[outSub++] = 7; // Bell
break;
case 'b':
outba[outSub++] = '\b';
break;
case 't':
outba[outSub++] = '\t';
break;
case 'n':
outba[outSub++] = '\n';
break;
case 'v':
outba[outSub++] = 11; // Vertical tab
break;
case 'f':
outba[outSub++] = '\f';
break;
case 'r':
outba[outSub++] = '\f';
break;
case '\\':
outba[outSub++] = '\\';
break;
case '0':
case '1':
case '2':
case '3':
// This assumes that the octal escape here is valid.
byte b = (byte) ( ( inba[inSub - 1] - '0' ) << 6 );
b |= (byte) ( ( inba[inSub++] - '0' ) << 3 );
b |= (byte) ( inba[inSub++] - '0' );
outba[outSub++] = b;
break;
default:
//This is an invalid escape in a string. Just copy it.
outba[outSub++] = '\\';
inSub--;
break;
}
pos = fileEntry.indexOf( '\\', inSub);
if ( pos == -1 ) // No more backslashes; we're done
{
System.arraycopy( inba, inSub, outba, outSub, inba.length - inSub );
outSub += inba.length - inSub;
break;
}
}
try
{
// explicit say UTF-8, otherwise it'll fail at least on Windows cmdline
return new String(outba, 0, outSub, "UTF-8");
}
catch ( UnsupportedEncodingException e )
{
throw new RuntimeException( e );
}
}
}