blob: 942a017cc3a3c9059a1b5b428aac7ce42ed43ce9 [file] [log] [blame]
/*
* 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.
*/
package org.apache.maven.scm.provider.git.gitexe.command.status;
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.maven.scm.ScmFile;
import org.apache.maven.scm.ScmFileSet;
import org.apache.maven.scm.ScmFileStatus;
import org.apache.maven.scm.util.AbstractConsumer;
/**
* @author <a href="mailto:struberg@yahoo.de">Mark Struberg</a>
*/
public class GitStatusConsumer extends AbstractConsumer {
/**
* 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 final File workingDirectory;
private ScmFileSet scmFileSet;
/**
* Entries are relative to working directory, not to the repositoryroot
*/
private final List<ScmFile> changedFiles = new ArrayList<>();
private URI relativeRepositoryPath;
// ----------------------------------------------------------------------
//
// ----------------------------------------------------------------------
/**
* Consumer when workingDirectory and repositoryRootDirectory are the same
*
* @param workingDirectory the working directory
*/
public GitStatusConsumer(File workingDirectory) {
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 workingDirectory the working directory
* @param relativeRepositoryPath the working directory relative to the repository root
* @since 1.9
* @see GitStatusCommand#createRevparseShowPrefix(ScmFileSet)
*/
public GitStatusConsumer(File workingDirectory, URI relativeRepositoryPath) {
this(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 workingDirectory the working directory
* @param scmFileSet fileset with includes and excludes
* @since 1.11.0
* @see GitStatusCommand#createRevparseShowPrefix(ScmFileSet)
*/
public GitStatusConsumer(File workingDirectory, ScmFileSet scmFileSet) {
this(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 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(File workingDirectory, URI relativeRepositoryPath, ScmFileSet scmFileSet) {
this(workingDirectory, scmFileSet);
this.relativeRepositoryPath = relativeRepositoryPath;
}
// ----------------------------------------------------------------------
// StreamConsumer Implementation
// ----------------------------------------------------------------------
/**
* {@inheritDoc}
*/
public void consumeLine(String line) {
if (logger.isDebugEnabled()) {
logger.debug(line);
}
if (line == null || line.isEmpty()) {
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();
}
public 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);
}
}
}