| 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.net.URISyntaxException; |
| 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.ScmFileSet; |
| 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; |
| |
| private ScmFileSet scmFileSet; |
| |
| /** |
| * 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#createRevparseShowPrefix(ScmFileSet) |
| */ |
| public GitStatusConsumer( ScmLogger logger, File workingDirectory, URI relativeRepositoryPath ) |
| { |
| this( logger, workingDirectory ); |
| this.relativeRepositoryPath = relativeRepositoryPath; |
| } |
| |
| /** |
| * 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 scmFileSet fileset with includes and excludes |
| * @since 1.11.0 |
| * @see GitStatusCommand#createRevparseShowPrefix(ScmFileSet) |
| */ |
| public GitStatusConsumer( ScmLogger logger, File workingDirectory, ScmFileSet scmFileSet ) |
| { |
| this( logger, workingDirectory ); |
| this.scmFileSet = scmFileSet; |
| } |
| |
| /** |
| * 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 |
| * @param scmFileSet fileset with includes and excludes |
| * @since 1.11.0 |
| * @see GitStatusCommand#createRevparseShowPrefix(ScmFileSet) |
| */ |
| public GitStatusConsumer( ScmLogger logger, File workingDirectory, URI relativeRepositoryPath, |
| ScmFileSet scmFileSet ) |
| { |
| this( logger, workingDirectory, scmFileSet ); |
| 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() ) |
| { |
| 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 ) |
| { |
| if ( this.scmFileSet != null && !isFileNameInFileList( this.scmFileSet.getFileList(), file ) ) |
| { |
| // skip adding this file |
| } |
| else |
| { |
| changedFiles.add( new ScmFile( file, status ) ); |
| } |
| } |
| } |
| } |
| |
| private boolean isFileNameInFileList( List<File> fileList, String fileName ) |
| { |
| if ( relativeRepositoryPath == null ) |
| { |
| return fileList.contains( new File( fileName ) ); |
| } |
| else |
| { |
| for ( File f : fileList ) |
| { |
| File file = new File( relativeRepositoryPath.getPath(), fileName ); |
| if ( file.getPath().endsWith( f.getName() ) ) |
| { |
| return true; |
| } |
| } |
| return fileList.isEmpty(); |
| } |
| |
| } |
| |
| private boolean isFile( String file ) |
| { |
| File targetFile = new File( workingDirectory, 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 TODO |
| */ |
| 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( uriFromPath( stripQuotes ( fileEntry ) ) ); |
| } |
| |
| /** |
| * Create an URI whose getPath() returns the given path and getScheme() returns null. The path may contain spaces, |
| * colons, and other special characters. |
| * |
| * @param path the path. |
| * @return the new URI |
| */ |
| public static URI uriFromPath( String path ) |
| { |
| try |
| { |
| if ( path != null && path.indexOf( ':' ) != -1 ) |
| { |
| // prefixing the path so the part preceding the colon does not become the scheme |
| String tmp = new URI( null, null, "/x" + path, null ).toString().substring( 2 ); |
| // the colon is not escaped by default |
| return new URI( tmp.replace( ":", "%3A" ) ); |
| } |
| else |
| { |
| return new URI( null, null, path, null ); |
| } |
| } |
| catch ( URISyntaxException x ) |
| { |
| throw new IllegalArgumentException( x.getMessage(), x ); |
| } |
| } |
| |
| 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 TODO |
| */ |
| 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 ); |
| } |
| } |
| } |