/* * 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; } }