blob: 02d356a1dd3c309f255b1f6d16740b793f69df9c [file] [log] [blame]
/*
* Copyright 2008 Google Inc.
*
* Licensed 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 com.google.gwt.ant.taskdefs;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
import java.io.File;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.StringReader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A Svn interface task, because the initial solution of <exec> and
* <propertyregex> is unhappy in ant 1.6.5, and while that's old, it's not quite
* "too old" for us to care.
*/
public class SvnInfo extends Task {
/**
* Structured svn info.
*/
static class Info {
/**
* The relative path of this svn working copy within the root of the remote
* repository. That is, if the root is "http://example.com/svn" and the
* working copy URL is "http://example.com/svn/tags/w00t", this will be set
* to "tags/w00t".
*/
public final String branch;
/**
* The revision of this working copy. Initially set to the value parsed from
* "svn info" given a more detailed value via "svnversion".
*/
public String revision;
public Info(String branch, String revision) {
this.branch = branch;
this.revision = revision;
}
}
/**
* A regex that matches a URL.
*/
static final String URL_REGEX = "\\w+://\\S*";
/**
* A pattern that matches the URL line in svn info output. Note that it
* <i>also</i> matches Repository Root; to support i18n subversion clients,
* we're positionally dependent that URL will be the first match, and
* Repository Root the second.
*/
private static final Pattern BRANCH_PATTERN = Pattern.compile("[^:]*:\\s*("
+ URL_REGEX + ")\\s*");
/**
* A pattern that matches the Revision line in svn info output. <i>Also</i>
* matches Last Changed Rev; we're positionally dependent (revision is
* earlier) to support internationalized svn client output.
*/
private static final Pattern REVISION_PATTERN = Pattern.compile("[^:]*:\\s*(\\d+)\\s*");
/**
* A pattern that matches the Repository Root line in svn info output.
*/
private static final Pattern ROOT_PATTERN = Pattern.compile("[^:]*:\\s*("
+ URL_REGEX + "/svn)\\s*");
/**
* Returns true if this git working copy matches the specified svn revision,
* and also has no local modifications.
*/
static boolean doesGitWorkingCopyMatchSvnRevision(File dir, String svnRevision) {
String workingRev = getGitWorkingRev(dir);
String targetRev = getGitRevForSvnRev(dir, svnRevision);
if (!workingRev.equals(targetRev)) {
return false;
}
String status = getGitStatus(dir);
return status.contains("nothing to commit (working directory clean)");
}
/**
* Returns the git commit number matching the specified svn revision.
*/
static String getGitRevForSvnRev(File dir, String svnRevision) {
String output = CommandRunner.getCommandOutput(dir, "git", "svn",
"find-rev", "r" + svnRevision);
output = output.trim();
if (output.length() == 0) {
throw new BuildException("git svn find-rev didn't give any answer");
}
return output;
}
/**
* Runs "git status" and returns the result.
*/
static String getGitStatus(File dir) {
// git status returns 1 for a status code, so just don't check it.
String output = CommandRunner.getCommandOutput(false, dir, "git", "status");
if (output.length() == 0) {
throw new BuildException("git status didn't give any answer");
}
return output;
}
/**
* Runs "git svn info", returning the output as a string.
*/
static String getGitSvnInfo(File dir) {
String output = CommandRunner.getCommandOutput(dir, "git", "svn", "info");
if (output.length() == 0) {
throw new BuildException("git svn info didn't give any answer");
}
return output;
}
/**
* Returns the current git commit number of the working copy.
*/
static String getGitWorkingRev(File dir) {
String output = CommandRunner.getCommandOutput(dir, "git", "rev-list",
"--max-count=1", "HEAD");
output = output.trim();
if (output.length() == 0) {
throw new BuildException("git rev-list didn't give any answer");
}
return output;
}
/**
* Runs "svn info", returning the output as a string.
*/
static String getSvnInfo(File dir) {
String output = CommandRunner.getCommandOutput(dir, "svn", "info");
if (output.length() == 0) {
throw new BuildException("svn info didn't give any answer");
}
return output;
}
/**
* Runs "svnversion", returning the output as a string.
*/
static String getSvnVersion(File dir) {
String output = CommandRunner.getCommandOutput(dir, "svnversion", ".");
output = output.trim();
if (output.length() == 0) {
throw new BuildException("svnversion didn't give any answer");
}
return output;
}
/**
* Determine if this directory is a part of a .git repository.
*
* @param dir working directory to start looking for the repository.
* @return <code>true</code> if a .git repo is found. Returns
* <code>false</false> if a .git repo cannot be found, or if
* this directory is part of a subversion repository.
*/
static boolean looksLikeGit(File dir) {
if (looksLikeSvn(dir)) {
return false;
}
File gitDir = findGitDir(dir);
if (gitDir != null && gitDir.isDirectory()) {
return new File(gitDir, "svn").isDirectory();
}
return false;
}
/**
* Returns <code>true</code> if the specified directory looks like an svn
* working copy.
*/
static boolean looksLikeSvn(File dir) {
return new File(dir, ".svn").isDirectory();
}
/**
* Parses the output of running "svn info".
*/
static Info parseInfo(String svnInfo) {
String rootUrl = null;
String branchUrl = null;
String revision = null;
LineNumberReader lnr = new LineNumberReader(new StringReader(svnInfo));
try {
for (String line = lnr.readLine(); line != null; line = lnr.readLine()) {
Matcher m;
if ((m = ROOT_PATTERN.matcher(line)) != null && m.matches()) {
rootUrl = m.group(1);
} else if ((m = BRANCH_PATTERN.matcher(line)) != null && m.matches()) {
if (branchUrl == null) {
branchUrl = m.group(1);
} // else skip the 2nd and later matches
} else if ((m = REVISION_PATTERN.matcher(line)) != null && m.matches()) {
if (revision == null) {
revision = m.group(1);
} // else skip the 2nd and later matches
}
}
} catch (IOException e) {
throw new BuildException("Should never happen", e);
}
if (rootUrl == null) {
throw new BuildException("svn info didn't get root URL: " + svnInfo);
}
if (branchUrl == null) {
throw new BuildException("svn info didn't get branch URL: " + svnInfo);
}
if (revision == null) {
throw new BuildException("svn info didn't get revision: " + svnInfo);
}
rootUrl = removeTrailingSlash(rootUrl);
branchUrl = removeTrailingSlash(branchUrl);
if (!branchUrl.startsWith(rootUrl)) {
throw new BuildException("branch URL (" + branchUrl + ") and root URL ("
+ rootUrl + ") did not match");
}
String branch;
if (branchUrl.length() == rootUrl.length()) {
branch = "";
} else {
branch = branchUrl.substring(rootUrl.length() + 1);
}
return new Info(branch, revision);
}
static String removeTrailingSlash(String url) {
if (url.endsWith("/")) {
return url.substring(0, url.length() - 1);
}
return url;
}
/**
* Find the GIT working directory.
*
* First checks for the presence of the env variable GIT_DIR, then, looks up
* the the tree for a directory named '.git'.
*
* @param dir Current working directory
* @return An object representing the .git directory. Returns
* <code>null</code> if none can be found.
*/
private static File findGitDir(File dir) {
String gitDirPath = System.getenv("GIT_DIR");
if (gitDirPath != null) {
File gitDir = new File(gitDirPath).getAbsoluteFile();
if (gitDir.isDirectory()) {
return gitDir;
}
}
dir = dir.getAbsoluteFile();
while (dir != null) {
File gitDir = new File(dir, ".git");
if (gitDir.isDirectory()) {
return gitDir;
}
dir = dir.getParentFile();
}
return null;
}
private String fileprop;
private String outprop;
private String workdir;
public SvnInfo() {
super();
}
@Override
public void execute() throws BuildException {
if (outprop == null) {
throw new BuildException(
"<svninfo> task requires an outputproperty attribute");
}
if (workdir == null) {
workdir = getProject().getProperty("basedir");
}
File workDirFile = new File(workdir);
if (!workDirFile.isDirectory()) {
throw new BuildException(workdir + " is not a directory");
}
if (getProject().getProperty(outprop) == null) {
Info info;
if (looksLikeSvn(workDirFile)) {
info = parseInfo(getSvnInfo(workDirFile));
// Use svnversion to get a more exact revision string.
info.revision = getSvnVersion(workDirFile);
} else if (looksLikeGit(workDirFile)) {
info = parseInfo(getGitSvnInfo(workDirFile));
// Add a 'M' tag if this working copy is not pristine.
if (!doesGitWorkingCopyMatchSvnRevision(workDirFile, info.revision)) {
info.revision += "M";
}
} else {
info = new Info("unknown", "unknown");
}
getProject().setNewProperty(outprop, info.branch + "@" + info.revision);
} else {
String propval = getProject().getProperty(outprop);
if (!propval.matches("[^@]+@[0-9]+")) {
throw new BuildException(
"predefined " + outprop +
"should look like branch-spec@revison-number");
}
}
if (fileprop != null) {
String outpropval = getProject().getProperty(outprop);
int atIndex = outpropval.indexOf('@');
String branch = outpropval.substring(0, atIndex);
String revision = outpropval.substring(atIndex + 1);
getProject().setNewProperty(fileprop,
branch.replace('/', '-') + "-" + revision.replace(':', '-'));
}
}
/**
* Establishes the directory used as the SVN workspace to fetch version
* information.
*
* @param srcdir workspace directory name
*/
public void setDirectory(String srcdir) {
workdir = srcdir;
}
/**
* Establishes the property containing the SVN output string, branch@rev.
*
* @param propname Name of a property
*/
public void setOutputFileProperty(String propname) {
fileprop = propname;
}
/**
* Establishes the property containing the SVN output string, branch@rev.
*
* @param propname Name of a property
*/
public void setOutputProperty(String propname) {
outprop = propname;
}
}