From jim.hague at acm.org Mon Oct 29 05:39:54 2007 From: jim.hague at acm.org (Jim Hague) Date: Mon Oct 29 05:40:13 2007 Subject: [Anthill-dev] Some Anthill OS patches Message-ID: <200710291139.54585.jim.hague@acm.org> I've accumulated a few modifications to Anthill OS that might be of wider interest. I recently made time to have a play with Mercurial's patch queues, and so have some patches against current CVS. These patches are, in order: build-log-scanner Scan build log for up to 4 regular expressions and apply different highlight colours to the build display. I use this to highlight builds with warnings in orange. svn-noauthor-tags Fix Subversion adapter to cope with '(no author)' author name. Fix mailed to Anthill list some time ago by Philip gnugy@rogers.com., but not in CVS. newline-at-end-of-version-file The version adapter doesn't put a newline at the end of the version file. I find I prefer one to be there to remove diff chatter. jdk-5-import-adjustment Adjust a couple of imports so that Anthill OS builds under JDK 5 and later. svn-checkout-happens-twice Each time the Subversion adapter does a checkout, it does it twice. bad-line-endings Fix up some line ending brouhaha in the sources. track-checkout-by-rev-id A bit of a big one, this. Explanation below. add-mercurial-repository-adapter Add a repository adapter for Mercurial (http://www.selenic.com/mercurial). Requires track-checkout-by-rev-id. I'll mail them in separate messages. They are also available in a Mercurial repository at http://hg.lunch.org.uk/Anthill. The repository is an import of the Anthill OS CVS, with the above changes as patches, so you'll need to do 'hg qclone' followed by 'hg qpush -a' to get all the patches applied. track-checkout-by-rev-id. Right now Anthill marks the point at which it does a built with a date/time with 1 second resolution. Changeset based VCS allow a more precise specification, the revision number, which can also be used as the target of the tag to ensure that what is tagged is *exactly* what was built. Also, at least one distributed VCS (yep, Mercurial) can't tag by time; time could mean either time the change was originally commited, or time the change hit this repository and it keeps neither. So this change switches Anthill to record the build point with an identifier from the VCS adapter, and changes the Subversion adapter to use changeset number instead of date. Along the way I fixed up a pet peeve (tagged sources are not the built sources), skip checkout when checking for new changes if the VCS can do that, and added a proper 'Do you want incremental builds?' option. I don't know how well it will all work with the other repository adapters, which are unchanged and use date/time as before. -- Jim Hague - jim.hague@acm.org Never trust a computer you can't lift. From jim.hague at acm.org Mon Oct 29 05:40:48 2007 From: jim.hague at acm.org (Jim Hague) Date: Mon Oct 29 05:41:09 2007 Subject: [Anthill-dev] Anthill OS patch: build-log-scanner Message-ID: <200710291140.48151.jim.hague@acm.org> # HG changeset patch # User Jim Hague # Date 1193654689 0 # Node ID 6362c184dbe5d5ce1c7aba995ccdc596d6151ad5 # Parent 83924153e379c44097cdbf14810a127d46235027 Add an adapter and step in the build that scans the build log. The scan watches for 'critical', 'important', 'normal' and 'todo' regular expressions. If one is found, the build result is coloured appropriately. Hence you can distiguish visually between, for example, 'built with no warnings' and 'built with warnings'. diff -r 83924153e379 -r 6362c184dbe5 conf/com.urbancode.anthill.adapter.SimpleWarningAdapter.properties --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/conf/com.urbancode.anthill.adapter.SimpleWarningAdapter.properties Mon Oct 29 10:44:49 2007 +0000 @@ -0,0 +1,23 @@ +# +# Defines the configuration parameters for the SimpleWarningAdapter +# Note that the parameter importance is in alphabetical importance of +# parameter name - thus, 'critical' is more important than 'important' +# simply by virtue of its lower place in the sort order. You can create +# fresh warning categories just by adding parameters in this file and +# creating appropriately named styles in anthill.style. +# +param.name.1=warning.pattern.critical +param.desc.1=A regular expression pattern. If any line in a build log matches this pattern, the build will be highlighted yellow on a successful build. +param.default.1= + +param.name.2=warning.pattern.important +param.desc.2=A regular expression pattern. If any line in a build log matches this pattern, the build will be highlighted orange on a successful build, provided there are no lines matching the critical pattern in the log. +param.default.2= + +param.name.3=warning.pattern.normal +param.desc.3=A regular expression pattern. If any line in a build log matches this pattern, the build will be highlighted purple on a successful build, provided there are no lines matching the critical or important patterns. +param.default.3= + +param.name.4=warning.pattern.todo +param.desc.4=A regular expression pattern. If any line in a build log matches this pattern, the build will be highlighted cyan on a successful build provided there are no lines matching any other warning pattern. +param.default.4= diff -r 83924153e379 -r 6362c184dbe5 conf/resultEmail.pgl --- a/conf/resultEmail.pgl Fri Oct 26 10:41:17 2007 +0100 +++ b/conf/resultEmail.pgl Mon Oct 29 10:44:49 2007 +0000 @@ -17,6 +17,9 @@ emailSubject.append("failed"); } else { emailSubject.append("succeeded"); + String warnType = _buildDef.getWarningType(); + if (warnType != null && warnType.length() > 0) + emailSubject.append(", warning code " + warnType); } subject = emailSubject.toString(); @@ -62,4 +65,4 @@ + buildLogFileName + buildLogString; %> -<%=body%> \ No newline at end of file +<%=body%> diff -r 83924153e379 -r 6362c184dbe5 projects/anthill.registry --- a/projects/anthill.registry Fri Oct 26 10:41:17 2007 +0100 +++ b/projects/anthill.registry Mon Oct 29 10:44:49 2007 +0000 @@ -14,6 +14,7 @@ anthill.mail.host = 192.168.2.203 anthill.mail.host = 192.168.2.203 anthill.mail.from = anthill@localhost anthill.version.adapter = com.urbancode.anthill.adapter.UrbanCodeVersionAdapter +anthill.warning.adapter = com.urbancode.anthill.adapter.SimpleWarningAdapter anthill.work.dir = work anthill.projects.dir = projects anthill.publish.dir.default = publishDir diff -r 83924153e379 -r 6362c184dbe5 source/main/java/com/urbancode/anthill/AnthillProject.java --- a/source/main/java/com/urbancode/anthill/AnthillProject.java Fri Oct 26 10:41:17 2007 +0100 +++ b/source/main/java/com/urbancode/anthill/AnthillProject.java Mon Oct 29 10:44:49 2007 +0000 @@ -180,6 +180,11 @@ public class AnthillProject implements B } //-------------------------------------------------------------------------- + public WarningAdapter getWarningAdapter() { + return properties.getWarningAdapter(); + } + + //-------------------------------------------------------------------------- /** * Implementation of Buildable */ diff -r 83924153e379 -r 6362c184dbe5 source/main/java/com/urbancode/anthill/BuildDefinition.java --- a/source/main/java/com/urbancode/anthill/BuildDefinition.java Fri Oct 26 10:41:17 2007 +0100 +++ b/source/main/java/com/urbancode/anthill/BuildDefinition.java Mon Oct 29 10:44:49 2007 +0000 @@ -22,6 +22,7 @@ public class BuildDefinition implements //************************************************************************** // INSTANCE //************************************************************************** + protected String warningType = null; protected boolean errorFlag = false; protected boolean loginErrorFlag = false; protected boolean forceBuildFlag = false; @@ -32,6 +33,7 @@ public class BuildDefinition implements protected StringBuffer logMessageBuffer = new StringBuffer(); protected List revisionList = null; protected List antParamList = new ArrayList(); + protected String buildLogFileName = null; //-------------------------------------------------------------------------- public BuildDefinition() { @@ -40,6 +42,16 @@ public class BuildDefinition implements //-------------------------------------------------------------------------- public BuildDefinition(AnthillProject project) { this.project = project; + } + + //-------------------------------------------------------------------------- + public void setWarningType(String warningType) { + this.warningType = warningType; + } + + //-------------------------------------------------------------------------- + public String getWarningType() { + return warningType; } //-------------------------------------------------------------------------- @@ -150,6 +162,16 @@ public class BuildDefinition implements //-------------------------------------------------------------------------- public Iterator getAntParamIterator() { return antParamList.iterator(); + } + + //-------------------------------------------------------------------------- + public String getBuildLogFileName() { + return buildLogFileName; + } + + //-------------------------------------------------------------------------- + public void setBuildLogFileName(String buildLogFileName) { + this.buildLogFileName = buildLogFileName; } //-------------------------------------------------------------------------- diff -r 83924153e379 -r 6362c184dbe5 source/main/java/com/urbancode/anthill/BuildManager.java --- a/source/main/java/com/urbancode/anthill/BuildManager.java Fri Oct 26 10:41:17 2007 +0100 +++ b/source/main/java/com/urbancode/anthill/BuildManager.java Mon Oct 29 10:44:49 2007 +0000 @@ -21,6 +21,7 @@ import com.urbancode.anthill.adapter.Rep import com.urbancode.anthill.adapter.RepositoryAdapter; import com.urbancode.anthill.adapter.RepositoryException; import com.urbancode.anthill.adapter.Revision; +import com.urbancode.anthill.adapter.WarningAdapter; import org.apache.commons.execute.Execute; import org.apache.log4j.Logger; @@ -70,12 +71,14 @@ public class BuildManager { RepositoryAdapter radapter = null; VersionAdapter vadapter = null; + WarningAdapter wadapter = null; boolean doBuild = false; Date buildDate = null; try { radapter = project.getRepositoryAdapter(); vadapter = project.getVersionAdapter(); + wadapter = project.getWarningAdapter(); def.appendLogMessage("Anthill version " + Anthill.getVersion() + "\n\n"); @@ -197,7 +200,17 @@ public class BuildManager { log.info("Step 7) Publish Project: "); publish(def); } - + + if ( !def.getErrorFlag() ) { + log.info("Step 8) Scan build log for warnings: "); + if (wadapter != null) { + + String warnType = + wadapter.getWarningType(new File(def.getBuildLogFileName())); + def.setWarningType(warnType); + properties.setLastBuildWarningType(warnType); + } + } } catch (Throwable e) { log.error(e.getMessage(), e); @@ -399,7 +412,7 @@ public class BuildManager { log.info("Build Project: "); buildProject(buildDef); - + buildDef.appendLogMessage("Build: OK\n"); buildDef.appendLogMessage(logMessage.toString()); } @@ -484,6 +497,7 @@ public class BuildManager { "-" + buildDef.getVersion() + "-build.log"; cmdList.add("-logfile"); cmdList.add(logFile); + buildDef.setBuildLogFileName(logFile); // add project ant params Iterator keys = properties.getBuildAntParams().iterator(); diff -r 83924153e379 -r 6362c184dbe5 source/main/java/com/urbancode/anthill/ProjectProperties.java --- a/source/main/java/com/urbancode/anthill/ProjectProperties.java Fri Oct 26 10:41:17 2007 +0100 +++ b/source/main/java/com/urbancode/anthill/ProjectProperties.java Mon Oct 29 10:44:49 2007 +0000 @@ -75,6 +75,7 @@ public class ProjectProperties { static public final String VERSION_PROPERTIES = "version"; static public final String REPOSITORY_ADAPTER = "repository.adapter"; static public final String REPOSITORY_PROPERTIES = "repository"; + static public final String WARNING_ADAPTER = "warning.adapter"; static public final String SCHEDULE_KEY = "schedule"; static public final String MAIL_HOST_KEY = "mail.host"; static public final String MAIL_FROM_KEY = "mail.from"; @@ -82,6 +83,7 @@ public class ProjectProperties { static public final String LAST_GOOD_BUILD_DATE_KEY = "lastGoodBuildDate"; static public final String LAST_BUILD_FAIL_DATE_KEY = "lastBuildFailDate"; static public final String LAST_BUILD_SUCCEEDED_KEY = "lastBuildSucceeded"; + static public final String LAST_BUILD_WARNING_TYPE_KEY = "lastBuildWarningType"; static public final String ANTHILL_URL_KEY = "server"; static public final String PUBLISH_URL_KEY = "publish.url"; static public final String LOCK_VERSION_KEY = "lock.version.file"; @@ -121,10 +123,12 @@ public class ProjectProperties { protected RepositoryAdapter repositoryAdapter = null; protected VersionAdapter versionAdapter = null; + protected WarningAdapter warningAdapter = null; protected AnthillSchedule schedule = null; protected String repositoryAdapterName = null; protected String versionAdapterName = null; + protected String warningAdapterName = null; protected String scheduleName = null; protected String anthillUrl = null; protected String publishUrl = null; @@ -144,6 +148,7 @@ public class ProjectProperties { protected String lastGoodBuildDate = null; protected String lastBuildFailDate = null; protected String lastBuildSucceeded = null; + protected String lastBuildWarningType = null; // protected String lockVersion = null; // protected String versionFilePath = null; protected String buildScriptPath = null; @@ -240,6 +245,11 @@ public class ProjectProperties { } //-------------------------------------------------------------------------- + public RegistryEntry getPropertyRegistryEntry(String entryName) { + return projectRegEntry.getChildRegistryEntry(entryName); + } + + //-------------------------------------------------------------------------- public RepositoryAdapter getRepositoryAdapter() { if (repositoryAdapter == null || !repositoryAdapter.getClass().getName().equals(repositoryAdapterName)) { @@ -301,6 +311,35 @@ public class ProjectProperties { } //-------------------------------------------------------------------------- + public WarningAdapter getWarningAdapter() { + if (warningAdapter == null || + !warningAdapter.getClass().getName().equals(warningAdapterName)) { + + try { + warningAdapter = WarningAdapterFactory.getWarningAdapter( + project, warningAdapterName); + } catch (Exception e) { + throw new IllegalStateException("exception while getting " + + warningAdapterName + " instance: " + e.getMessage()); + } + } + + return warningAdapter; + } + + //-------------------------------------------------------------------------- + public String getWarningAdapterName() { + return warningAdapterName; + } + + //-------------------------------------------------------------------------- + public void setWarningAdapterName(String warningAdapterName) { + setProperty(WARNING_ADAPTER, warningAdapterName); + this.warningAdapterName = warningAdapterName; + warningAdapter = null; + } + + //-------------------------------------------------------------------------- public AnthillSchedule getSchedule() { return schedule; } @@ -408,6 +447,19 @@ public class ProjectProperties { String flagStr = succeededFlag ? "true" : "false"; setProperty(LAST_BUILD_SUCCEEDED_KEY, flagStr); this.lastBuildSucceeded = flagStr; + } + + //-------------------------------------------------------------------------- + public String getLastBuildWarningType() { + return lastBuildWarningType; + } + + //-------------------------------------------------------------------------- + public void setLastBuildWarningType(String warningType) { + if (warningType == null) + warningType = ""; + setProperty(LAST_BUILD_WARNING_TYPE_KEY, warningType); + this.lastBuildWarningType = warningType; } //-------------------------------------------------------------------------- @@ -738,6 +790,11 @@ public class ProjectProperties { regEntry, VERSION_ADAPTER, versionAdapterName); versionAdapter = null; + // setup Warning Adapter Name + warningAdapterName = getPropValue( + regEntry, WARNING_ADAPTER, warningAdapterName); + warningAdapter = null; + // setup Schedule Name scheduleName = getPropValue(regEntry, SCHEDULE_KEY, scheduleName); if (scheduleName != null) { @@ -767,6 +824,10 @@ public class ProjectProperties { // setup Last Build Succeeded lastBuildSucceeded = getPropValue( regEntry, LAST_BUILD_SUCCEEDED_KEY, lastBuildSucceeded); + + // setup Last Build Had Warnings + lastBuildWarningType = getPropValue( + regEntry, LAST_BUILD_WARNING_TYPE_KEY, lastBuildWarningType); // setup Last Good Build Date lastGoodBuildDate = getPropValue( diff -r 83924153e379 -r 6362c184dbe5 source/main/java/com/urbancode/anthill/adapter/SimpleWarningAdapter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/source/main/java/com/urbancode/anthill/adapter/SimpleWarningAdapter.java Mon Oct 29 10:44:49 2007 +0000 @@ -0,0 +1,214 @@ +/* + * @(#)WarningAdapter.java + */ +package com.urbancode.anthill.adapter; + +import org.apache.log4j.Category; +import java.io.*; +import java.util.*; +import com.urbancode.anthill.AnthillProject; +import com.urbancode.anthill.ProjectProperties; +import com.urbancode.lib.registry.*; +import org.apache.regexp.*; + +/** + *

+ * A simple warning adapter. This just scans the log file line by line + * looking for a match against varions patterns. If a match is found, + * the associated warning type is returned.

+ * + * @author Jim Hague + */ +public class SimpleWarningAdapter extends WarningAdapter { + + //************************************************************************* + // CLASS + //************************************************************************* + + // Create Log4j category instance for logging + static private Category log = Category.getInstance(SimpleWarningAdapter.class.getName()); + + static public final String WARNING_PATTERNS = "warning.pattern"; + + //************************************************************************* + // INSTANCE + //************************************************************************* + + protected ProjectProperties properties = null; + protected Map patternMap = new HashMap(); + + /** + * Set the AnthillProject that this WarningAdapter belongs to. + * This method should only be called from the WarningAdapterFactory. + * + * @param the AnthillProject that this adapter belongs to + */ + protected void setAnthillProject(AnthillProject project) { + super.setAnthillProject(project); + this.properties = project.getProperties(); + + RegistryEntry entry = properties.getPropertyRegistryEntry(WARNING_PATTERNS); + if (entry != null) { + Iterator itr = entry.getKeyIterator(); + if (itr != null) { + String warnType; + String pattern; + + synchronized (patternMap) { + while(itr.hasNext()) { + warnType = ((String) itr.next()).trim(); + pattern = entry.getKeyValue(warnType).trim(); + if (warnType.trim().length() > 0 && pattern.length() > 0) + patternMap.put(warnType, entry.getKeyValue(warnType)); + } + } + } + } + } + + /** + * Add a new type/pattern to the map. + * + * @param warnType warning type + * @param pattern the pattern + */ + public void addWarningType(String warnType, String pattern) { + log.debug("add warnType " + warnType + " pattern " + pattern); + if (warnType == null || pattern == null) + return; + + warnType = warnType.trim(); + pattern = pattern.trim(); + if (warnType.length() > 0 && pattern.length() > 0) { + properties.setProperty(warnType, pattern); + synchronized (patternMap) { + patternMap.put(warnType, pattern); + } + } + } + + /** + * Remove a warning type from the map. + * + * @param warnType warning type + */ + public void removeWarningType(String warnType) { + log.debug("remove warnType " + warnType); + RegistryEntry entry = properties.getPropertyRegistryEntry(WARNING_PATTERNS); + if (entry != null) + entry.removeKey(warnType); + synchronized (patternMap) { + patternMap.remove(warnType); + } + } + + /** + * Return the patterns map. + * + * @return the map of warning patterns and values. + */ + public Map getWarningMap() { + return patternMap; + } + + /** + * Scan the log file looking for a warning situations. Return a + * string indicating a warning type, or null for no warnings. + * The warning patterns are searched on each line in decreasing order + * of warning importance. The warning type returned is the most + * important warning type found in the log file. + * + * @param logFile the log file to scan. + * @return the warning type, null if none. + */ + public String getWarningType(File logFile) + throws Exception + { + log.info("Scan log file " + logFile + " for warnings"); + + // Create an array of Warning, sorted into order of importance. + Warning[] matchers; + Iterator itr; + int patNo = 0; + + synchronized(patternMap) { + if (patternMap.isEmpty()) { + return null; + } + + matchers = new Warning[patternMap.size()]; + for ( itr = patternMap.keySet().iterator(); itr.hasNext(); ) { + try { + String wtype = (String) itr.next(); + String pattern = (String) patternMap.get(wtype); + RE re = new RE(pattern); + matchers[patNo++] = new Warning(wtype, re); + } catch (RESyntaxException rse) { + log.error("bad pattern " + rse); + } + } + } + Arrays.sort(matchers); + + // Read out and grep someone. + BufferedReader r = new BufferedReader(new FileReader(logFile)); + String line; + String res = null; + + // Consider all matches under this priority. If this hits zero, + // we've found a maximum priority warning in the file, so don't + // bother with the rest of the file. + int maxPatNo = matchers.length; + int i; + + while (maxPatNo > 0 && (line = r.readLine()) != null) { + // Don't try to match against empty lines. + if ( line.length() == 0 ) + continue; + + for (patNo = 0; patNo < maxPatNo; patNo++) { + if (matchers[patNo].getPattern().match(line)) { + res = matchers[patNo].getType(); + maxPatNo = patNo; + break; + } + } + } + r.close(); + + return res; + } +} + +class Warning + implements Comparable +{ + String wtype; + RE pattern; + + public Warning(String wtype, RE pattern) + { + this.wtype = wtype; + this.pattern = pattern; + } + + public int compareTo(Object o) + { + if ( o instanceof Warning ) + return wtype.compareTo(((Warning) o).wtype); + else + throw new ClassCastException("Can only compare Warnings with Warnings"); + } + + public String getType() + { + return wtype; + } + + public RE getPattern() + { + return pattern; + } +} + + diff -r 83924153e379 -r 6362c184dbe5 source/main/java/com/urbancode/anthill/adapter/WarningAdapter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/source/main/java/com/urbancode/anthill/adapter/WarningAdapter.java Mon Oct 29 10:44:49 2007 +0000 @@ -0,0 +1,47 @@ +/* + * @(#)WarningAdapter.java + */ +package com.urbancode.anthill.adapter; + +import java.io.File; +import com.urbancode.anthill.AnthillProject; + +/** + *

+ * An abstract class representing a scanner for extracting a warning type + * from a build log file.

+ * + * @author Jim Hague + */ +public abstract class WarningAdapter { + + protected AnthillProject project = null; + + /** + * Returns the AnthillProject that this WarningAdapter belongs to. + * + * @return AnthillProject that this WarningAdapter belongs to + */ + public AnthillProject getAnthillProject() { + return project; + } + + /** + * Set the AnthillProject that this WarningAdapter belongs to. + * This method should only be called from the WarningAdapterFactory. + * + * @paramproject the AnthillProject that this adapter belongs to + */ + protected void setAnthillProject(AnthillProject project) { + this.project = project; + } + + /** + * Search the log file and return a warning type or null if none. + * + * @param logFile the log file to scan. + * @return warning type, or null if none. + */ + public abstract String getWarningType(File logFile) + throws Exception; +} diff -r 83924153e379 -r 6362c184dbe5 source/main/java/com/urbancode/anthill/adapter/WarningAdapterFactory.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/source/main/java/com/urbancode/anthill/adapter/WarningAdapterFactory.java Mon Oct 29 10:44:49 2007 +0000 @@ -0,0 +1,41 @@ +/* + * @(#)WarningAdapterFactory.java + * + */ + +package com.urbancode.anthill.adapter; +import com.urbancode.anthill.AnthillProject; + +/** + *

+ * This is a factory to create WarningAdapter classes.

+ * + * @author Jim Hague + */ +public class WarningAdapterFactory { + + /** + * Create the specified WarningAdapter. + * + * @param warningAdapter the full classname of the adapter including + * the package + * @param versionFile the filename of the version file + * @return the requested WarningAdapter + */ + static public WarningAdapter getWarningAdapter(AnthillProject project, + String warningAdapter) + throws Exception { + + WarningAdapter adapter = null; + + try { + adapter = (WarningAdapter) Class.forName(warningAdapter).newInstance(); + adapter.setAnthillProject(project); + } catch (Exception e) { + throw new Exception("Unable to create warning adapter: " + + warningAdapter); + } + + return adapter; + } +} diff -r 83924153e379 -r 6362c184dbe5 source/main/java/com/urbancode/anthill/web/admin/ProjectPropertiesUpdateServlet.java --- a/source/main/java/com/urbancode/anthill/web/admin/ProjectPropertiesUpdateServlet.java Fri Oct 26 10:41:17 2007 +0100 +++ b/source/main/java/com/urbancode/anthill/web/admin/ProjectPropertiesUpdateServlet.java Mon Oct 29 10:44:49 2007 +0000 @@ -126,6 +126,7 @@ public class ProjectPropertiesUpdateServ if (propName.startsWith("version") || propName.startsWith("repository") || + propName.startsWith("warning") || propName.startsWith("profile") || propName.startsWith("build") || propName.startsWith("publish") || @@ -229,7 +230,8 @@ public class ProjectPropertiesUpdateServ if (!error) { req.setAttribute(WebKeys.ProjectKey, project); if ((fromPropPageName.equals(ScreenNames.repositoryPropScreen)) || - (fromPropPageName.equals(ScreenNames.versionAdapterPropScreen))) { + (fromPropPageName.equals(ScreenNames.versionAdapterPropScreen)) || + (fromPropPageName.equals(ScreenNames.warningAdapterPropScreen))) { context.getRequestDispatcher(ScreenNames.projectPropScreen).forward(req, res); } else { diff -r 83924153e379 -r 6362c184dbe5 source/main/java/com/urbancode/anthill/web/admin/ScreenNames.java --- a/source/main/java/com/urbancode/anthill/web/admin/ScreenNames.java Fri Oct 26 10:41:17 2007 +0100 +++ b/source/main/java/com/urbancode/anthill/web/admin/ScreenNames.java Mon Oct 29 10:44:49 2007 +0000 @@ -37,6 +37,7 @@ public interface ScreenNames { String profilePropScreen = "/profileProperties.jsp"; String scheduleScreen = "/schedule.jsp"; String versionAdapterPropScreen = "/versionAdapterProperties.jsp"; + String warningAdapterPropScreen = "/warningAdapterProperties.jsp"; String emptyPropScreen = "/emptyProperties.jsp"; String newBuildProjectScreen = "/specImplBuildProject.jsp"; String AnthillAdminServlet = "/AnthillAdminServlet"; diff -r 83924153e379 -r 6362c184dbe5 source/main/java/com/urbancode/anthill/web/admin/WarningAdapterPropertiesViewServlet.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/source/main/java/com/urbancode/anthill/web/admin/WarningAdapterPropertiesViewServlet.java Mon Oct 29 10:44:49 2007 +0000 @@ -0,0 +1,139 @@ +/* + * @(#)WarningAdapterPropertiesViewServlet.java + */ +package com.urbancode.anthill.web.admin; + +import org.apache.log4j.Category; + +import javax.servlet.*; +import javax.servlet.http.*; + +import com.urbancode.anthill.*; + +import java.net.URL; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.io.IOException; + +import java.util.Properties; + +/** + * + * @author jim.hague@acm.org + */ +public class WarningAdapterPropertiesViewServlet extends AnthillBaseServlet { + + //************************************************************************** + // CLASS + //************************************************************************** + + // Create Log4j category instance for logging + static private Category log = Category.getInstance(WarningAdapterPropertiesViewServlet.class.getName()); + + //************************************************************************** + // INSTANCE + //************************************************************************** + + /** Processes requests for both HTTP GET and POST methods. + * @param request servlet request + * @param response servlet response + */ + protected void processRequest(HttpServletRequest req, HttpServletResponse res) + throws ServletException, java.io.IOException { + log.debug("Processing request for WarningAdapterPropertiesViewServlet"); + ServletContext context = getServletContext(); + Anthill anthill = (Anthill)context.getAttribute(WebKeys.AnthillKey); + AnthillProject project = null; + String projectName = req.getParameter(WebKeys.ProjectNameKey); + log.debug("projectName: " + projectName); + boolean error = false; + String errorMessage = null; + + if (projectName != null) { + project = anthill.getProject(projectName); + String adapterName = + project.getProperties().getWarningAdapterName(); + log.debug("adapterName: " + adapterName); + try { + Properties props = loadAdapterConfigOptions(adapterName); + if (project != null) { + req.setAttribute(WebKeys.AnthillKey, anthill); + req.setAttribute(WebKeys.ProjectKey, project); + req.setAttribute(WebKeys.RepositoryConfigKey, props); + } else { + error = true; + errorMessage = "Could not find project with name: "+projectName; + } + } catch (Exception e) { + log.error(e.getMessage(), e); + error = true; + errorMessage = e.getMessage(); + } + } else { + error = true; + errorMessage = "ProjectName parameter can not be null"; + } + + if (error) { + req.setAttribute(WebKeys.errorMessageKey, errorMessage); + context.getRequestDispatcher(ScreenNames.errorScreen).forward(req , res); + } else { + Properties tempProps = (Properties)req.getAttribute(WebKeys.RepositoryConfigKey); + if (tempProps.isEmpty()) { + context.getRequestDispatcher(ScreenNames.emptyPropScreen).forward(req, res); + } + else { + context.getRequestDispatcher(ScreenNames.warningAdapterPropScreen).forward(req , res); + } + } + } + + /** Returns a short description of the servlet. + */ + public String getServletInfo() { + return "Short description"; + } + + private Properties loadAdapterConfigOptions(String adapterName) + throws Exception { + Anthill anthill = null; + try{ + anthill = Anthill.getAnthill(); + } + catch(Exception e){ + log.error("ANTHILL SINGLETON" + e.getMessage().toString()); + } + + log.debug("Loading " + adapterName); + String adapterConfigFileName = anthill.getAnthillRootDir().getAbsolutePath() + + File.separator + "conf" + File.separator + + adapterName + ".properties"; + log.debug("adapterConfigFileName " + adapterConfigFileName); + + Properties config_list = new Properties(); + InputStream rf = null; + try { + rf = new FileInputStream(new File(adapterConfigFileName)); + if (rf == null) { + String msg = "Error creating inputstream for properties file: " + adapterConfigFileName; + log.error(msg); + throw new IOException(msg); + } + config_list.load(rf); + log.debug("Loaded properties file: " + adapterConfigFileName); + } + finally { + try { + if (rf != null){ + rf.close(); + } + } + catch (IOException e){ + } + } + return config_list; + } + +} diff -r 83924153e379 -r 6362c184dbe5 source/main/webAdmin/WEB-INF/web.xml --- a/source/main/webAdmin/WEB-INF/web.xml Fri Oct 26 10:41:17 2007 +0100 +++ b/source/main/webAdmin/WEB-INF/web.xml Mon Oct 29 10:44:49 2007 +0000 @@ -110,6 +110,10 @@ com.urbancode.anthill.web.admin.VersionAdapterPropertiesViewServlet + WarningAdapterPropertiesViewServlet + com.urbancode.anthill.web.admin.WarningAdapterPropertiesViewServlet + + ViewProjectServlet com.urbancode.anthill.web.admin.ViewProjectServlet @@ -224,6 +228,10 @@ VersionAdapterPropertiesViewServlet /VersionAdapterPropertiesViewServlet + + + WarningAdapterPropertiesViewServlet + /WarningAdapterPropertiesViewServlet ViewProjectServlet diff -r 83924153e379 -r 6362c184dbe5 source/main/webAdmin/main.jsp --- a/source/main/webAdmin/main.jsp Fri Oct 26 10:41:17 2007 +0100 +++ b/source/main/webAdmin/main.jsp Mon Oct 29 10:44:49 2007 +0000 @@ -63,7 +63,11 @@ if (wasBuildGood) { buildDate = props.getLastGoodBuildDate(); if (buildDate != null) { - buildStatusClass = "succeeded"; + String warning = props.getLastBuildWarningType(); + buildStatusClass = + (warning == null || warning.length() == 0) + ? "succeeded" + : "warning-" + warning; buildDateStr = dtFormat.format(buildDate); } } @@ -210,4 +214,4 @@ Running on Java <%= System.getProperty(" Running on Java <%= System.getProperty("java.version") %> - \ No newline at end of file + diff -r 83924153e379 -r 6362c184dbe5 source/main/webAdmin/projectProperties.jsp --- a/source/main/webAdmin/projectProperties.jsp Fri Oct 26 10:41:17 2007 +0100 +++ b/source/main/webAdmin/projectProperties.jsp Mon Oct 29 10:44:49 2007 +0000 @@ -22,6 +22,9 @@ String versionAdapter = properties.getVersionAdapterName(); if (versionAdapter == null) versionAdapter = ""; + String warningAdapter = properties.getWarningAdapterName(); + if (warningAdapter == null) warningAdapter = ""; + // String versionFile = properties.getVersionFilePath(); // if (versionFile == null) versionFile = ""; @@ -100,6 +103,23 @@ Configure <%=project.getProperties().getRepositoryAdapterName()%> + + + + + The class name of the warning adapter. The warning adapter + classifies message in the build log into status warnings. + + + + anthill.warning.adapter + + + Configure <%=project.getProperties().getWarningAdapterName()%> + diff -r 83924153e379 -r 6362c184dbe5 source/main/webAdmin/style/anthillStyle.jsp --- a/source/main/webAdmin/style/anthillStyle.jsp Fri Oct 26 10:41:17 2007 +0100 +++ b/source/main/webAdmin/style/anthillStyle.jsp Mon Oct 29 10:44:49 2007 +0000 @@ -80,6 +80,22 @@ TD.succeeded { background-color: #00FF00; } +TD.warning-critical { + background-color: #FFFF00; +} + +TD.warning-important { + background-color: #FF7F00; +} + +TD.warning-normal { + background-color: #FF00FF; +} + +TD.warning-todo { + background-color: #00FFFF; +} + TD.failed { background-color: #FF0000; } diff -r 83924153e379 -r 6362c184dbe5 source/main/webAdmin/warningAdapterProperties.jsp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/source/main/webAdmin/warningAdapterProperties.jsp Mon Oct 29 10:44:49 2007 +0000 @@ -0,0 +1,71 @@ +<%@ page contentType="text/html"%> +<%@ page import="com.urbancode.lib.registry.RegistryEntry" %> +<%@ page import="com.urbancode.anthill.*" %> +<%@ page import= "com.urbancode.anthill.web.admin.WebKeys" %> +<%@ page import= "com.urbancode.anthill.web.admin.ScreenNames" %> +<%@ page import= "java.util.*" %> + + + WarningAdapter Properties + + + +<% + Anthill anthill = (Anthill)request.getAttribute(WebKeys.AnthillKey); + AnthillProject project = (AnthillProject)request.getAttribute(WebKeys.ProjectKey); + ProjectProperties properties = project.getProperties(); + Properties adapterProperties = (Properties)request.getAttribute(WebKeys.RepositoryConfigKey); + + String projectName = project.getProjectName(); + if (projectName == null) projectName = ""; + +%> + +

<%= properties.getWarningAdapterName() %> Properties

+ +
+ + + + + +<% +String name = null; +int i = 1; +while ((name = adapterProperties.getProperty("param.name." + i)) != null) { + String description = adapterProperties.getProperty("param.desc." + i); + String def = adapterProperties.getProperty("param.default." + i); + String value = properties.getProperty(name) == null ? def : properties.getProperty(name); +%> + + + + + + + +<% + i++; +} +%> + + + +
<%=description%>
<%=name%> + " + size="72"> +
+
+ +
+

+Anthill version <%= Anthill.getVersion() %>
+ + + -- Jim Hague - jim.hague@acm.org Never trust a computer you can't lift. From jim.hague at acm.org Mon Oct 29 05:41:23 2007 From: jim.hague at acm.org (Jim Hague) Date: Mon Oct 29 05:41:40 2007 Subject: [Anthill-dev] Anthill OS patch: svn-noauthor-tags Message-ID: <200710291141.23943.jim.hague@acm.org> # HG changeset patch # User Jim Hague # Date 1193654689 0 # Node ID 1b81fccfec8bab44a3c00e527a2da1327f28d63d # Parent 6362c184dbe5d5ce1c7aba995ccdc596d6151ad5 The Subversion author name may be '(no author)' if doing anonymous write access via Apache. Adjust regular expression to cope. Thanks Philip gnugy@rogers.com. diff -r 6362c184dbe5 -r 1b81fccfec8b source/main/java/com/urbancode/anthill/adapter/SubversionRepositoryAdapter.java --- a/source/main/java/com/urbancode/anthill/adapter/SubversionRepositoryAdapter.java Mon Oct 29 10:44:49 2007 +0000 +++ b/source/main/java/com/urbancode/anthill/adapter/SubversionRepositoryAdapter.java Mon Oct 29 10:44:49 2007 +0000 @@ -314,11 +314,13 @@ public class SubversionRepositoryAdapter try { - headerRE = new RE("^r(\\d+) \\| ([^\\|]+) \\| ([^\\(]+)"); - - // line starting with 3 spaces and then an A,D,M, or R followed by - // '/' or '\' and a bunch of non spaces (a path). - fileRE = new RE("^ ([ADMR]) [/\\\\](\\S+)"); + // Note that the username may be '(no author)'. See + // Subversion FAQ. + headerRE = new RE("^r(\\d+) \\| (.+?) \\| ([^\\(]+)"); + + // line starting with 3 spaces and then an A,D,M, or R followed by + // '/' or '\' and a bunch of non spaces (a path). + fileRE = new RE("^ ([ADMR]) [/\\\\](\\S+)"); } catch (RESyntaxException rse) { @@ -429,7 +431,6 @@ public class SubversionRepositoryAdapter * @param file file to prepare for editing */ public void prepareFileForEdit(String file) throws RepositoryException { - // intentionally left blank. } /** @@ -446,8 +447,8 @@ public class SubversionRepositoryAdapter throw (new RepositoryException("No label specified")); } - tag = tag.replace('$', '_').replace(',', '_'). - replace(':', '_').replace(';', '_'); + tag = tag.replace('$', '_').replace(',', '_').replace('.', '_') + .replace(':', '_').replace(';', '_').replace('@', '_'); log.info("Tagging entire project with label: " + tag); -- Jim Hague - jim.hague@acm.org Never trust a computer you can't lift. From jim.hague at acm.org Mon Oct 29 05:43:18 2007 From: jim.hague at acm.org (Jim Hague) Date: Mon Oct 29 05:43:35 2007 Subject: [Anthill-dev] Anthill OS patch: newline-at-end-of-version-file Message-ID: <200710291143.18791.jim.hague@acm.org> # HG changeset patch # User Jim Hague # Date 1193654689 0 # Node ID 2e05a10db3b6f9cd70ddd2c7b7515d23b2040769 # Parent 1b81fccfec8bab44a3c00e527a2da1327f28d63d The version file can get written without a trailing newline. This causes unnecessary diff chatter, so add one. diff -r 1b81fccfec8b -r 2e05a10db3b6 source/main/java/com/urbancode/anthill/adapter/VersionModifier.java --- a/source/main/java/com/urbancode/anthill/adapter/VersionModifier.java Mon Oct 29 10:44:49 2007 +0000 +++ b/source/main/java/com/urbancode/anthill/adapter/VersionModifier.java Mon Oct 29 10:44:49 2007 +0000 @@ -143,6 +143,7 @@ public class VersionModifier { bw.write(specVersion); bw.newLine(); bw.write(implVersion); + bw.newLine(); } catch (Exception e) { throw new IllegalArgumentException("Trouble updating version file."); @@ -164,14 +165,18 @@ public class VersionModifier { throw new IllegalArgumentException("VersionStr parameter " + "can't be null."); } - FileWriter writer = null; + BufferedWriter bw = null; try { - writer = new FileWriter(versionFile); - writer.write(versionStr); + bw = new BufferedWriter(new FileWriter(versionFile)); + bw.write(versionStr); + bw.newLine(); + } + catch (Exception e) { + throw new IllegalArgumentException("Trouble updating version file."); } finally { - if (writer != null) { + if (bw != null) { try { - writer.close(); + bw.close(); } catch (Exception e) {} } } -- Jim Hague - jim.hague@acm.org Never trust a computer you can't lift. From jim.hague at acm.org Mon Oct 29 05:44:04 2007 From: jim.hague at acm.org (Jim Hague) Date: Mon Oct 29 05:44:20 2007 Subject: [Anthill-dev] Anthill OS patch: jdk-5-import-adjustment Message-ID: <200710291144.04401.jim.hague@acm.org> # HG changeset patch # User Jim Hague # Date 1193654689 0 # Node ID 87e3f05138b53d785e2b885430b64d64d4fdd204 # Parent 2e05a10db3b6f9cd70ddd2c7b7515d23b2040769 Adjust a couple of imports so Anthill builds under JDK5 and 6. diff -r 2e05a10db3b6 -r 87e3f05138b5 source/main/java/com/urbancode/anthill/web/admin/BuildDependencyGroupStepTwoServlet.java --- a/source/main/java/com/urbancode/anthill/web/admin/BuildDependencyGroupStepTwoServlet.java Mon Oct 29 10:44:49 2007 +0000 +++ b/source/main/java/com/urbancode/anthill/web/admin/BuildDependencyGroupStepTwoServlet.java Mon Oct 29 10:44:49 2007 +0000 @@ -6,7 +6,8 @@ */ package com.urbancode.anthill.web.admin; import org.apache.log4j.Logger; -import java.util.*; +import java.util.Enumeration; +import java.util.Iterator; import javax.servlet.*; import javax.servlet.http.*; import com.urbancode.anthill.*; -- Jim Hague - jim.hague@acm.org Never trust a computer you can't lift. From jim.hague at acm.org Mon Oct 29 05:44:30 2007 From: jim.hague at acm.org (Jim Hague) Date: Mon Oct 29 05:44:47 2007 Subject: [Anthill-dev] Anthill OS patch: svn-checkout-happens-twice Message-ID: <200710291144.30346.jim.hague@acm.org> # HG changeset patch # User Jim Hague # Date 1193654689 0 # Node ID d92cb5077023c8f20f6a6be66da40432f2b29ec6 # Parent 87e3f05138b53d785e2b885430b64d64d4fdd204 The Subversion getWorkingProjectCopy() calls its counterpart in the superclass. Oops! This will cause the getWorkingProjectCopy .pgl to run, and a checkout done. Back in the subclass, the same thing happens again. So remove the superclass call. diff -r 87e3f05138b5 -r d92cb5077023 source/main/java/com/urbancode/anthill/adapter/SubversionRepositoryAdapter.java --- a/source/main/java/com/urbancode/anthill/adapter/SubversionRepositoryAdapter.java Mon Oct 29 10:44:49 2007 +0000 +++ b/source/main/java/com/urbancode/anthill/adapter/SubversionRepositoryAdapter.java Mon Oct 29 10:44:49 2007 +0000 @@ -132,8 +132,6 @@ public class SubversionRepositoryAdapter */ public void getWorkingProjectCopy(BuildDefinition def) throws RepositoryException { - super.getWorkingProjectCopy(def); - log.debug("Getting working copy of project: " + project.getProjectName()); Process p = null; StreamPumper errorPumper = null; -- Jim Hague - jim.hague@acm.org Never trust a computer you can't lift. From jim.hague at acm.org Mon Oct 29 05:44:57 2007 From: jim.hague at acm.org (Jim Hague) Date: Mon Oct 29 05:45:15 2007 Subject: [Anthill-dev] Anthill OS patch: bad-line-endings Message-ID: <200710291144.57208.jim.hague@acm.org> # HG changeset patch # User Jim Hague # Date 1193654689 0 # Node ID 59f096cd7c6e8e354e3daf736bd7580be6a0f4ee # Parent d92cb5077023c8f20f6a6be66da40432f2b29ec6 Correct line endings. Some are ^M some are ^M^J. Set to a proper line ending. diff -r d92cb5077023 -r 59f096cd7c6e source/main/java/com/urbancode/anthill/web/admin/AnthillFlushQueueServlet.java --- a/source/main/java/com/urbancode/anthill/web/admin/AnthillFlushQueueServlet.java Mon Oct 29 10:44:49 2007 +0000 +++ b/source/main/java/com/urbancode/anthill/web/admin/AnthillFlushQueueServlet.java Mon Oct 29 10:44:49 2007 +0000 @@ -1,4 +1,30 @@ -/* * @(#)AnthillFlushQueueServlet.java * * License - * The contents of this file are subject to the Urbancode Public License * Version 1.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.urbancode.com/licenses/UPL/1_0/ * * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the * License for the specific language governing rights and limitations * under the License. * * The Initial Developer of the Original Code is Urbancode Software * Development, Inc. ("Urbancode"). Portions created by Urbancode are * Copyright (C) Urbancode Software Development, Inc. All Rights Reserved. */ package com.urbancode.anthill.web.admin; import org.apache.log4j.Logger; import javax.servlet.*; import javax.servlet.http.*; import com.urbancode.anthill.*; import com.urbancode.anthill.util.*; import com.urbancode.anthill.web.admin.WebKeys; +/* + * @(#)AnthillFlushQueueServlet.java + * + * License - + * The contents of this file are subject to the Urbancode Public License + * Version 1.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.urbancode.com/licenses/UPL/1_0/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + * License for the specific language governing rights and limitations + * under the License. + * + * The Initial Developer of the Original Code is Urbancode Software + * Development, Inc. ("Urbancode"). Portions created by Urbancode are + * Copyright (C) Urbancode Software Development, Inc. All Rights Reserved. + */ + +package com.urbancode.anthill.web.admin; +import org.apache.log4j.Logger; +import javax.servlet.*; +import javax.servlet.http.*; +import com.urbancode.anthill.*; +import com.urbancode.anthill.util.*; +import com.urbancode.anthill.web.admin.WebKeys; + /** * * @author Robert Dobbins @@ -55,7 +81,8 @@ public class AnthillFlushQueueServlet ex errorMessage = e.getMessage(); } if (!error) { - context.getRequestDispatcher(ScreenNames.AnthillAdminServlet).forward(req, res); + context.getRequestDispatcher(ScreenNames.AnthillAdminServlet).forward(req, res); + } else { req.setAttribute(WebKeys.errorMessageKey, errorMessage); @@ -85,5 +112,6 @@ public class AnthillFlushQueueServlet ex */ public String getServletInfo() { return "Short description"; - } -} \ No newline at end of file + } + +} -- Jim Hague - jim.hague@acm.org Never trust a computer you can't lift. From jim.hague at acm.org Mon Oct 29 05:45:54 2007 From: jim.hague at acm.org (Jim Hague) Date: Mon Oct 29 05:46:11 2007 Subject: [Anthill-dev] Anthill OS patch: add-mercurial-repository-adapter Message-ID: <200710291145.55075.jim.hague@acm.org> # HG changeset patch # User Jim Hague # Date 1193655841 0 # Node ID 0343b4614b64a11619eb34c79cd63ffc0c343be9 # Parent 6ff47cce62c822d52336f06e48d9dd5143bb1256 Add Mercurial repository adapter. This adapter expects to be pointed to an existing Mercurial repository. It will build within that repository and increment and check in the build version and tag within the repository. A clean build (the default) will delete all working files from the repository and do an update to get clean working files. diff -r 6ff47cce62c8 -r 0343b4614b64 source/main/java/com/urbancode/anthill/adapter/MercurialRepositoryAdapter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/source/main/java/com/urbancode/anthill/adapter/MercurialRepositoryAdapter.java Mon Oct 29 11:04:01 2007 +0000 @@ -0,0 +1,598 @@ +/* + * MercurialRepositoryAdapter.java + */ +package com.urbancode.anthill.adapter; + +import org.apache.log4j.Logger; +import com.urbancode.anthill.util.FileRemover; +import com.urbancode.anthill.util.StreamPumper; +import com.urbancode.anthill.BuildDefinition; +import java.io.*; +import java.net.URLDecoder; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.List; +import java.util.ListIterator; +import java.util.ArrayList; +import java.util.Date; +import java.util.Map; +import java.util.HashMap; +import com.urbancode.pagelet.*; +import org.apache.regexp.*; + + +/** + *

+ * An implementation of RepositoryAdapter that interfaces with + * a command-line Mercurial client to work with a Mercurial repository.

+ *

+ * The following extra properties must be specified for this adapter in the + * .anthill file: + *

    + *
  • HG_DIR - absolute path to the directory containing the repository + *
  • HG_BRANCHNAME - the branch name on which Anthill should work + *
  • HG_USERNAME - the username Anthill should use + *

+ * + * @author Jim Hague + */ +public class MercurialRepositoryAdapter extends ProfileRepositoryAdapter { + + //************************************************************************* + // CLASS + //************************************************************************* + + // Create Log4j category instance for logging + static private Logger log = + Logger.getLogger(MercurialRepositoryAdapter.class.getName()); + // Create Log4j category instance for logging + static private Logger streamLog = + Logger.getLogger(MercurialRepositoryAdapter.class.getName()+"Stream"); + + static public final String ADAPTER_SUFFIX = "mercurial"; + + static protected final String GET_REVISIONS_SINCE_PAGELET = "getRevisionsSince.pgl"; + static protected final String GET_HEADS_PAGELET = "getHeads.pgl"; + + // The various property keys + static public final String REPO_DIR_KEY = "repository.mercurial.dir"; + static public final String BRANCHNAME_KEY = "repository.mercurial.branchname"; + static public final String USERNAME_KEY = "repository.mercurial.anthill.username"; + + // The comment for build increment commits. + public static final String BUILD_INCREMENT_COMMENT = "Anthill: Increment build number"; + + // The comment for tag commits. + public static final String TAG_COMMENT = "Anthill: Tag build"; + + // date format in build id. + public static SimpleDateFormat BUILD_DATE = new SimpleDateFormat(); + + private static final String NEW_LINE = System.getProperty("line.separator"); + + //************************************************************************* // Instance + //************************************************************************* + + String anthillUserName; + + /** + * Create a new MercurialRepositoryAdapter + */ + public MercurialRepositoryAdapter() { + } + + public String getAdapterSuffix() { + return ADAPTER_SUFFIX; + } + + protected void calculateRepositoryProperties() + throws RepositoryException { + localProjectDirName = project.getProperties().getProperty(REPO_DIR_KEY); + branchName = project.getProperties().getProperty(BRANCHNAME_KEY); + anthillUserName = project.getProperties().getProperty(USERNAME_KEY); + } + + /** + * Get the repository head revision. This checks for the existance + * of multiple heads and throws an exception if so. + * + * @param def info about the build. + * @return The repository heads as a ChangesetRevision. + */ + String getHeadRev(BuildDefinition def) + throws RepositoryException { + + log.debug("Getting heads of project: " + project.getProjectName()); + Process p = null; + StreamPumper errorPumper = null; + int exitcode = 0; + List heads; + + try { + Map tempMap = new HashMap(); + tempMap.put("Adapter", this); + tempMap.put("Properties", project.getProperties()); + + Pagelet pagelet = getPageletFactory().getPagelet(makeProfilePageletName(GET_HEADS_PAGELET)); + if (pagelet != null) { + log.debug("Have getHeads pagelet."); + } + else { + log.debug("Pagelet is null in checkout!"); + } + String commandString = pagelet.service(tempMap); + log.debug("Get heads command: " + commandString); + p = Runtime.getRuntime().exec(toArray(commandString)); + + // pump the error stream. + errorPumper = new StreamPumper(p.getErrorStream(), "getHeads", + System.err, true); + errorPumper.start(); + + // get and parse the input stream + heads = parseLogCommandResult(p.getInputStream(), false); + + exitcode = p.waitFor(); + } + catch (RepositoryException e) { + throw e; + } + catch (Exception e) { + throw new RepositoryException( + "hg heads failed: " + e.getMessage(), + e); + } + finally { + if (errorPumper != null) { + try { + errorPumper.join(); + } catch (InterruptedException e) { + throw new RepositoryException(e); + } + } + } + // handle errors + if (exitcode != 0) + throw (new RepositoryException("hg heads failed. Exit code: " + exitcode)); + + if ( heads.size() == 0 ) + throw new RepositoryException("No head info - check branchname"); + if ( heads.size() > 1 ) + throw new RepositoryException("Multiple heads"); + + ChangesetRevision head = (ChangesetRevision) heads.get(0); + return head.changeId; + } + + /** + * Check out the project. + *

+ * First check the repo head. If there is more than one head, + * refuse to go any further. Otherwise request a checkout of + * the head. + * + * @param def info about the build. + * @return An id for the checked out source. The rev id plus the date. + */ + public String getWorkingProjectCopyAndId(BuildDefinition def) + throws RepositoryException { + List heads; + + String buildId = getHeadRev(def); + + log.debug("Getting working copy of project: " + project.getProjectName()); + Process p = null; + StreamPumper errorPumper = null; + int exitcode = 0; + try { + Map tempMap = new HashMap(); + tempMap.put("Adapter", this); + tempMap.put("Properties", project.getProperties()); + tempMap.put("AtChange", getRevisionFromBuildId(buildId)); + if (def.getVersionedBuildFlag()){ + log.info("Retrieving project version " + def.getVersion()); + tempMap.put("Version", def.getVersion().trim()); + } + + Pagelet pagelet = getPageletFactory().getPagelet(makeProfilePageletName(WORKING_PROJECT_PAGELET)); + if (pagelet != null) { + log.debug("Have checkout pagelet."); + } + else { + log.debug("Pagelet is null in checkout!"); + } + String commandString = pagelet.service(tempMap); + log.debug("Checkout Command: " + commandString); + executeCommand(commandString, "checkout"); + } + catch (RepositoryException e) { + throw e; + } + catch (Exception e) { + throw new RepositoryException( + "Checkout failed: " + e.getMessage(), + e); + } + + // Wait for a second to ensure that any file change (i.e. the + // build number incrementing) doesn't happen in the same second + // as the file update. See Mercurial issue 618. + try { + Thread.currentThread().sleep(1000); + } + catch (InterruptedException ie) { + } + + return buildId + " " + BUILD_DATE.format(new Date()); + } + + /** + * See if the adapter can call getRevisionsSince() + * without calling getWorkingProjectCopyAndId() + * first. In other words, can you check repository history + * without a checkout? Since we need to point to a local repo + * to work in, we can. + * + * @return true if a checkout required. + */ + public boolean logRequiresCheckout() throws RepositoryException { + return false; + } + + /** + * Returns a List of Revision objects detailing the changes that have + * been made since the specified change id. + * + * @param id the last change built. + * @return List. + */ + public List getRevisionsSince(String id) + throws RepositoryException { + return getRevisionsBetween(id, null); + } + + /** + * Returns a List of Revision objects detailing the changes that have + * been made after fromId up to and including + * toId. + *

+ * It isn't particularly simple to get per-file change info out of + * Subversion and I'm not sure how much sense it makes to describe + * a changeset per-file. So invent ChangesetRevision instead. + * + * @param fromId start at the revision after this one. + * @param toId end at this revision. + * @return List. + */ + public List getRevisionsBetween(String fromId, String toId) + throws RepositoryException { + + Process p = null; + StreamPumper errorPumper = null; + int exitcode = 0; + List revisionList = new ArrayList(); + try { + Map tempMap = new HashMap(); + tempMap.put("Adapter", this); + tempMap.put("Properties", project.getProperties()); + + fromId = getRevisionFromBuildId(fromId); + toId = getRevisionFromBuildId(toId); + + // We want from the change after the first one. + if ( fromId == null ) + fromId = "0"; + + tempMap.put("FromChange", fromId); + tempMap.put("ToChange", toId); + + Pagelet pagelet = getPageletFactory().getPagelet(makeProfilePageletName(GET_REVISIONS_SINCE_PAGELET)); + String commandString = pagelet.service(tempMap); + log.debug("Get revisions since command: " + commandString); + p = Runtime.getRuntime().exec(toArray(commandString)); + + // pump the error stream. + errorPumper = new StreamPumper(p.getErrorStream(), "getRevisions", + System.err, true); + errorPumper.start(); + + // get and parse the input stream + InputStream input = p.getInputStream(); + revisionList = parseLogCommandResult(input, true); + + exitcode = p.waitFor(); + + } + catch (Exception e) { + log.error(e.getMessage() + " thrown in new catch"); + e.printStackTrace(); + throw new RepositoryException(e.getMessage()); + } + finally { + if (errorPumper != null) { + try { + errorPumper.join(); + } catch (InterruptedException e) { + throw new RepositoryException(e); + } + } + } + + // handle errors + if (exitcode != 0) { + throw (new RepositoryException("hg log failed. Exit code: " + exitcode)); + } + + // Remove any changes in the list that are either the fromId + // or which were due to the Anthill user. + for ( ListIterator li = revisionList.listIterator(); + li.hasNext(); ) + { + ChangesetRevision rev = (ChangesetRevision) li.next(); + + if ( rev.userName.compareTo(anthillUserName) == 0 ) + li.remove(); + else { + String localRevNo = getRevisionFromBuildId(rev.changeId); + + if ( localRevNo.compareTo(fromId) == 0 ) + li.remove(); + } + } + + return revisionList; + } + + /** + * Parse the output of 'hg log' or 'hg heads'. + *

+ * Build a new ChangesetRevision for + * each revision we find. When each ChangesetRevision is complete, + * add it to the list of revisions. + * + * @param in the input stream to read. + * @param isLog true if parsing output of 'hg log'. + * @return a list of ChangesetRevisions. + */ + protected List parseLogCommandResult(InputStream in, boolean isLog) + throws IOException, RepositoryException { + final int PARSE_REV_START = 1; + final int PARSE_REV_FILELIST = 2; + final int PARSE_REV_COMMENT = 3; + + BufferedReader br = new BufferedReader(new InputStreamReader(in)); + ChangesetRevision rev = null; + String comment = null; + RE headerRE = null; + RE fileRE = null; + int parseState = PARSE_REV_START; + List revList = new ArrayList(); + + try + { + // Revision (rev and short node), branch (may be empty), user + // and timestamp (hgdate). + headerRE = new RE("^r([:digit:]+:[:xdigit:]+) \\| (.*) \\| (.+) \\| ([:digit:]+) -?[:digit:]+"); + + // Line with added/deleted/modified file names separated by + // '|'. + fileRE = new RE("(.*) \\| (.*) \\| (.*)"); + } + catch (RESyntaxException rse) + { + log.error(rse); + throw new RepositoryException(rse); + } + + for ( String line = br.readLine(); line != null; line = br.readLine() ){ + boolean addChange = false; + + switch (parseState) + { + case PARSE_REV_START: + // Just look out for lines matching the header. + if ( headerRE.match(line) ) + { + String revId = headerRE.getParen(1); + String branch = headerRE.getParen(2); + String user = headerRE.getParen(3); + String unixtime = headerRE.getParen(4); + + if ( branch.length() == 0 ) + branch = "default"; + + // Ignore entries for branches other than the one we're + // interested in. + if ( branch.compareTo(branchName) == 0 ) + { + long timems; + + try + { + timems = Long.parseLong(unixtime) * 1000L; + } + catch (NumberFormatException nfe) + { + throw new RepositoryException(nfe); + } + + Date date = new Date(timems); + + rev = new ChangesetRevision(revId, user, date); + + log.debug("RevID: " + revId); + log.debug("User: " + user); + log.debug("Date: " + rev.date); + + if ( isLog ) + parseState = PARSE_REV_FILELIST; + else + addChange = true; + } + } + break; + + case PARSE_REV_FILELIST: + // Grab the file info lines. Note that this won't + // copy with filenames with space, but that's a + // Mercurial problem. + if (fileRE.match(line)) { + for ( int i = 1; i <= 3; i++ ) + { + String flist = fileRE.getParen(i).trim(); + if ( flist.length() == 0 ) + continue; + + String[] files = flist.split(":"); + + for ( int j = 0; j < files.length; j++ ) { + String fname = URLDecoder.decode(files[j], "UTF-8"); + Revision fileRevision = new Revision(); + fileRevision.fileName = fname; + + switch (i) { + case 1: + rev.addAddedFile(fileRevision); + break; + case 2: + rev.addDeletedFile(fileRevision); + break; + case 3: + rev.addModifiedFile(fileRevision); + break; + } + } + } + + parseState = PARSE_REV_COMMENT; + } + break; + + case PARSE_REV_COMMENT: + // There is one comment line, URLencoded. + comment = URLDecoder.decode(line, "UTF-8"); + // End of revision info. + if ( rev != null ) + addChange = true; + parseState = PARSE_REV_START; + break; + } + + if ( addChange ) { + rev.comment = comment; + revList.add(rev); + + comment = null; + rev = null; + } + } + + return revList; + } + + /** + * Mercurial doesn't at present have any pre-edit that needs + * doing as far as I can see. + * + * @param file file to prepare for editing + */ + public void prepareFileForEdit(String file) throws RepositoryException { + } + + /** + * Explain that the tag is a build number. + */ + public String makeTagFromVersion(String version) { + return "build-" + version; + } + + /** + * Labels all relevant project files with the provided tag. + * A Mercurial label/tag operation is simply a server-side copy of + * the project, but that means we must make sure we copy the project + * at the revision at which we built it. + * + * @param id The build id (i.e. repository revision) to tag + * @param tag The label to tag the files with + */ + public void label(String id, String tag) throws RepositoryException { + if (tag == null || tag.length() == 0) { + throw (new RepositoryException("No label specified")); + } + + tag = tag.replace(':', '_'); + + log.info("Tagging entire project with label: " + tag); + + try { + Map tempMap = new HashMap(); + tempMap.put("Properties", project.getProperties()); + tempMap.put("Adapter", this); + tempMap.put("Tag", tag); + tempMap.put("BuildRevision", getRevisionFromBuildId(id)); + Pagelet pagelet = getPageletFactory().getPagelet(makeProfilePageletName(LABEL_PAGELET)); + executeCommand(pagelet.service(tempMap), "Label"); + } + catch (RepositoryException e) { + throw e; + } + catch (Exception e) { + throw new RepositoryException("Label failed: " + e.getMessage(), e); + } + } + + /** + * reverts changes made so far in this instance of the adapter. + * In other words, clears out the repository ready for a fresh + * checkout. + */ + public void revert() throws RepositoryException { + // Delete everything in the repo except .hg. + try { + log.info("Cleaning up local files: "); + final File projdir = new File(getLocalProjectDirName()); + + FilenameFilter notHgDir = new FilenameFilter() { + public boolean accept(File dir, String name) { + return !(dir.equals(projdir) && name.startsWith(".hg")); + } + }; + File files[] = projdir.listFiles(notHgDir); + for ( int i = 0; i < files.length; i++ ) + FileRemover.removeFile(files[i]); + } + catch (Exception e) { + throw new RepositoryException("revert failed", e); + } + } + + /** + * The build ID is rev:node followed by the date of the + * build. From the ID extract the revision number. If the build ID + * is null or is corrupted and has no revision number, return + * null. + * + * @param id the build ID. + * @return a string with the revision from the build ID, or + * null if the build ID is not valid. + */ + String getRevisionFromBuildId(String buildId) + { + if ( buildId == null ) + return null; + + // A revision is a positive number. So look for the first + // non-digit in the build id. + buildId = buildId.trim(); + for ( int i = 0; i < buildId.length(); i++ ) + if ( !Character.isDigit(buildId.charAt(i)) ) { + buildId = buildId.substring(0, i); + break; + } + + if ( buildId.length() == 0 ) + return null; + else + return buildId; + } +} diff -r 6ff47cce62c8 -r 0343b4614b64 source/main/profiles/Unix/unix_mercurial/anthill.style --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/source/main/profiles/Unix/unix_mercurial/anthill.style Mon Oct 29 11:04:01 2007 +0000 @@ -0,0 +1,8 @@ +header = 'r{rev}:{node|short} | {branches} | {author} | {date|hgdate}\n' +changeset = '{file_adds} | {file_dels} | {files}\n{desc|urlescape}\n' +file = '{file|urlescape}:' +last_file = '{file|urlescape}' +file_add = '{file_add|urlescape}:' +last_file_add = '{file_add|urlescape}' +file_del = '{file_del|urlescape}:' +last_file_del = '{file_del|urlescape}' diff -r 6ff47cce62c8 -r 0343b4614b64 source/main/profiles/Unix/unix_mercurial/getHeads.pgl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/source/main/profiles/Unix/unix_mercurial/getHeads.pgl Mon Oct 29 11:04:01 2007 +0000 @@ -0,0 +1,17 @@ +<%@ import="com.urbancode.anthill.ProjectProperties" %> +<%@ import="com.urbancode.anthill.adapter.*" %> +<%@ import="com.urbancode.anthill.Anthill" %> +<% +Anthill anthill = Anthill.getAnthill(); + +ProjectProperties pp = (ProjectProperties)context.get("Properties"); +MercurialRepositoryAdapter ra = (MercurialRepositoryAdapter)context.get("Adapter"); + +String repoDir = pp.getProperty(MercurialRepositoryAdapter.REPO_DIR_KEY); +String pageletDir = anthill.getAnthillRootDir().getAbsolutePath() + + File.separator + "conf" + + File.separator + "profiles"+ File.separator + "Unix" + + File.separator + "unix_mercurial" + File.separator; +%> + +sh <%=pageletDir%>getHeads.sh --repository <%=repoDir%> --style <%=pageletDir%>anthill.style diff -r 6ff47cce62c8 -r 0343b4614b64 source/main/profiles/Unix/unix_mercurial/getHeads.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/source/main/profiles/Unix/unix_mercurial/getHeads.sh Mon Oct 29 11:04:01 2007 +0000 @@ -0,0 +1,4 @@ +#!/bin/bash +echo "Executing: hg heads $@" +hg heads "$@" + diff -r 6ff47cce62c8 -r 0343b4614b64 source/main/profiles/Unix/unix_mercurial/getRevisionsSince.pgl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/source/main/profiles/Unix/unix_mercurial/getRevisionsSince.pgl Mon Oct 29 11:04:01 2007 +0000 @@ -0,0 +1,28 @@ +<%@ import="com.urbancode.anthill.ProjectProperties" %> +<%@ import="com.urbancode.anthill.adapter.*" %> +<%@ import="com.urbancode.anthill.Anthill" %> +<% +Anthill anthill = Anthill.getAnthill(); + +ProjectProperties pp = (ProjectProperties)context.get("Properties"); +MercurialRepositoryAdapter ra = (MercurialRepositoryAdapter)context.get("Adapter"); + + +String fromChange = (String)context.get("FromChange"); +String toChange = (String)context.get("ToChange"); +String repoDir = pp.getProperty(MercurialRepositoryAdapter.REPO_DIR_KEY); +String pageletDir = anthill.getAnthillRootDir().getAbsolutePath() + + File.separator + "conf" + + File.separator + "profiles"+ File.separator + "Unix" + + File.separator + "unix_mercurial" + File.separator; + +String revString = "-r " + fromChange + ":"; + +if (toChange != null) { + revString += toChange; +} + +// Currently --debug is necessary to get Mercurial to sort files into +// added and deleted categories, rather than just lump them all into modified. +%> +sh <%=pageletDir%>getRevisionsSince.sh --debug --repository <%=repoDir%> --style <%=pageletDir%>anthill.style <%=revString%> diff -r 6ff47cce62c8 -r 0343b4614b64 source/main/profiles/Unix/unix_mercurial/getRevisionsSince.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/source/main/profiles/Unix/unix_mercurial/getRevisionsSince.sh Mon Oct 29 11:04:01 2007 +0000 @@ -0,0 +1,4 @@ +#!/bin/bash +echo "Executing: hg log $@" +hg log "$@" + diff -r 6ff47cce62c8 -r 0343b4614b64 source/main/profiles/Unix/unix_mercurial/getWorkingProject.pgl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/source/main/profiles/Unix/unix_mercurial/getWorkingProject.pgl Mon Oct 29 11:04:01 2007 +0000 @@ -0,0 +1,27 @@ +<%@ import="com.urbancode.anthill.ProjectProperties" %> +<%@ import="com.urbancode.anthill.adapter.*" %> +<%@ import="com.urbancode.anthill.Anthill" %> +<% +Anthill anthill = Anthill.getAnthill(); + +ProjectProperties pp = (ProjectProperties)context.get("Properties"); +ProfileRepositoryAdapter ra = (ProfileRepositoryAdapter)context.get("Adapter"); + +String atChange = (String)context.get("AtChange"); +String repoDir = pp.getProperty(MercurialRepositoryAdapter.REPO_DIR_KEY); +String pageletDir = anthill.getAnthillRootDir().getAbsolutePath() + + File.separator + "conf" + + File.separator + "profiles"+ File.separator + "Unix" + + File.separator + "unix_mercurial" + File.separator; + +String version = (String)context.get("Version"); + +String revString = ""; + +if ( version != null && !version.trim().equals("") ) + revString = "\"" + version + "\""; +else if ( atChange != null ) + revString = atChange; +%> + +sh <%=pageletDir%>getWorkingProject.sh --repository <%=repoDir%> --clean <%=revString%> diff -r 6ff47cce62c8 -r 0343b4614b64 source/main/profiles/Unix/unix_mercurial/getWorkingProject.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/source/main/profiles/Unix/unix_mercurial/getWorkingProject.sh Mon Oct 29 11:04:01 2007 +0000 @@ -0,0 +1,4 @@ +#!/bin/sh +echo "Executing: hg update $@" +hg update "$@" + diff -r 6ff47cce62c8 -r 0343b4614b64 source/main/profiles/Unix/unix_mercurial/label.pgl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/source/main/profiles/Unix/unix_mercurial/label.pgl Mon Oct 29 11:04:01 2007 +0000 @@ -0,0 +1,22 @@ +<%@ import="com.urbancode.anthill.adapter.*" %> +<%@ import="com.urbancode.anthill.ProjectProperties" %> +<%@ import="com.urbancode.anthill.Anthill" %> +<% +Anthill anthill = Anthill.getAnthill(); + +ProfileRepositoryAdapter ra = (ProfileRepositoryAdapter)context.get("Adapter"); +ProjectProperties pp = (ProjectProperties)context.get("Properties"); +String tag = (String)context.get("Tag"); +String buildRevision = (String)context.get("BuildRevision"); +String user = pp.getProperty(MercurialRepositoryAdapter.USERNAME_KEY).trim(); +String repoDir = pp.getProperty(MercurialRepositoryAdapter.REPO_DIR_KEY); +String pageletDir = anthill.getAnthillRootDir().getAbsolutePath() + + File.separator + "conf" + + File.separator + "profiles"+ File.separator + "Unix" + + File.separator + "unix_mercurial" + File.separator; + +String msg = "-m \"" + MercurialRepositoryAdapter.TAG_COMMENT + "\""; +String useropt = "--user \"" + user + "\""; +%> + +sh <%=pageletDir%>label.sh --repository <%=repoDir%> <%=useropt%> -r <%=buildRevision%> <%=msg%> <%=tag%> diff -r 6ff47cce62c8 -r 0343b4614b64 source/main/profiles/Unix/unix_mercurial/label.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/source/main/profiles/Unix/unix_mercurial/label.sh Mon Oct 29 11:04:01 2007 +0000 @@ -0,0 +1,3 @@ +#!/bin/bash +echo "Executing: hg tag $@" +hg tag "$@" diff -r 6ff47cce62c8 -r 0343b4614b64 source/main/profiles/Unix/unix_mercurial/postEditPagelet.pgl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/source/main/profiles/Unix/unix_mercurial/postEditPagelet.pgl Mon Oct 29 11:04:01 2007 +0000 @@ -0,0 +1,22 @@ +<%@ import="com.urbancode.anthill.adapter.*" %> +<%@ import="com.urbancode.anthill.ProjectProperties" %> +<%@ import="com.urbancode.anthill.Anthill" %> +<% +Anthill anthill = Anthill.getAnthill(); + +ProfileRepositoryAdapter ra = (ProfileRepositoryAdapter)context.get("Adapter"); +ProjectProperties pp = (ProjectProperties)context.get("Properties"); +String postEditFile = (String)context.get("File"); +String user = pp.getProperty(MercurialRepositoryAdapter.USERNAME_KEY).trim(); +String repoDir = pp.getProperty(MercurialRepositoryAdapter.REPO_DIR_KEY); +String pageletDir = anthill.getAnthillRootDir().getAbsolutePath() + + File.separator + "conf" + + File.separator + "profiles"+ File.separator + "Unix" + + File.separator + "unix_mercurial" + File.separator; + +postEditFile = repoDir + File.separator + postEditFile; + +String msg = "-m \"" + MercurialRepositoryAdapter.BUILD_INCREMENT_COMMENT + "\""; +String useropt = "--user \"" + user + "\""; +%> +sh <%=pageletDir%>postEditPagelet.sh --repository <%=repoDir%> <%=useropt%> <%=msg%> <%=postEditFile%> diff -r 6ff47cce62c8 -r 0343b4614b64 source/main/profiles/Unix/unix_mercurial/postEditPagelet.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/source/main/profiles/Unix/unix_mercurial/postEditPagelet.sh Mon Oct 29 11:04:01 2007 +0000 @@ -0,0 +1,3 @@ +#!/bin/bash +echo "Executing: hg commit $@" +hg commit "$@" -- Jim Hague - jim.hague@acm.org Never trust a computer you can't lift.