/******************************************************************************** * CruiseControl, a Continuous Integration Toolkit * Copyright (c) 2007, ThoughtWorks, Inc. * 200 E. Randolph, 25th Floor * Chicago, IL 60601 USA * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * + Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * + Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the * names of its contributors may be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ********************************************************************************/ package net.sourceforge.cruisecontrol.sourcecontrols; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringReader; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.sourceforge.cruisecontrol.CruiseControlException; import net.sourceforge.cruisecontrol.Modification; import net.sourceforge.cruisecontrol.SourceControl; import net.sourceforge.cruisecontrol.util.Commandline; import net.sourceforge.cruisecontrol.util.IO; import net.sourceforge.cruisecontrol.util.StreamLogger; import net.sourceforge.cruisecontrol.util.ValidationHelper; import org.apache.log4j.Logger; import org.jdom.Document; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.input.SAXBuilder; /** * This class implements the SourceControl methods for a Mercurial repository. * * @author <a href="jerome@coffeebreaks.org">Jerome Lacoste</a> * @see <a href="http://www.selenic.com/mercurial">Mercurial web site</a> */ public class Mercurial implements SourceControl { static final DateFormat HG_DATE_PARSER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z"); private static final Logger LOG = Logger.getLogger(Mercurial.class); private final SourceControlProperties properties = new SourceControlProperties(); /** * Configuration parameters */ private String localWorkingCopy = "."; private String hgCommand = INCOMING_CMD; private static final String INCOMING_CMD = "incoming"; private static final String LOG_CMD = "log"; static final String MODIFICATION_XML_TEMPLATE = "<hgChange>\n\t<author>{author|escape}</author>\n\t" + "<rev>{rev}</rev>\n\t<node>{node}</node>\n\t<description>{desc|escape}</description>\n\t" + "<date>{date|hgdate}</date>\n\t" + "<addedFiles>{file_adds}</addedFiles>\n\t" + "<removedFiles>{file_dels}</removedFiles>\n\t" + "<changedFiles>{files}</changedFiles>\n</hgChange>\n"; public Map<String, String> getProperties() { return properties.getPropertiesAndReset(); } public void setProperty(final String property) { properties.assignPropertyName(property); } public void setPropertyOnDelete(final String propertyOnDelete) { properties.assignPropertyOnDeleteName(propertyOnDelete); } /** * Sets the local working copy to use when making calls to mercurial. * * @param localWorkingCopy String indicating the relative or absolute path * to the local working copy of the mercurial * repository of which to find the log history. */ public void setLocalWorkingCopy(final String localWorkingCopy) { this.localWorkingCopy = localWorkingCopy; } /** * Sets the hg command to use when checking modifications. * * @param hgCommand String either "incoming" or "log". */ public void setHgCommand(final String hgCommand) { this.hgCommand = hgCommand; } /** * This method validates that the local working copy location has been specified. * * @throws net.sourceforge.cruisecontrol.CruiseControlException * Thrown when the repository location and * the local working copy location are both * null */ public void validate() throws CruiseControlException { final File workingDir = new File(localWorkingCopy); ValidationHelper.assertTrue(workingDir.exists() && workingDir.isDirectory(), "'localWorkingCopy' must be an existing directory. Was " + workingDir.getAbsolutePath()); ValidationHelper.assertTrue(INCOMING_CMD.equals(hgCommand) || LOG_CMD.equals(hgCommand), "'hgCommand' must be either " + INCOMING_CMD + " or " + LOG_CMD); } /** * Returns a list of modifications detailing all the changes between * the last build and the latest revision in the repository. * * @param lastBuildDate date of last build * @param now current date * @return the list of modifications, or an empty list if we failed * to retrieve the changes. */ public List<Modification> getModifications(final Date lastBuildDate, final Date now) { final String version = getMercurialVersion(); LOG.info("Using Mercurial: '" + version + "'"); Commandline command = null; List<Modification> modifications = Collections.emptyList(); try { command = buildHistoryCommand(lastBuildDate, now); modifications = execHistoryCommand(command); // TODO: should we filter out the results ? // modifications = filterModifications(modifications, lastBuildDate, now); } catch (Exception e) { LOG.error("Error executing mercurial history command " + command, e); } fillPropertiesIfNeeded(modifications); return modifications; } private String getMercurialVersion() { Commandline command = null; try { command = buildVersionCommand(); return execVersionCommand(command); } catch (Exception e) { LOG.error("Error executing mercurial version command " + command, e); return "version unresolved..."; } } /** * Generates the command line for the hg incoming or log command. * <p/> * For example: * <p/> * 'hg incoming --template "........."' * * @param from date of last build * @param to current date * @return history command * @throws net.sourceforge.cruisecontrol.CruiseControlException * exception */ Commandline buildHistoryCommand(final Date from, final Date to) throws CruiseControlException { final Commandline command = new Commandline(); command.setWorkingDirectory(localWorkingCopy); command.setExecutable("hg"); if (INCOMING_CMD.equals(hgCommand)) { usingIncomingToGetModifications(command); } else { usingLogToGetModifications(from, to, command); } return command; } private void usingIncomingToGetModifications(final Commandline command) { command.createArgument(INCOMING_CMD); command.createArgument("--debug"); command.createArgument("--template"); command.createArgument(MODIFICATION_XML_TEMPLATE); } private void usingLogToGetModifications(final Date from, final Date to, final Commandline command) { command.createArgument(LOG_CMD); command.createArgument("--debug"); command.createArguments("--date", HG_DATE_PARSER.format(from) + " to " + HG_DATE_PARSER.format(to)); command.createArguments("--template", MODIFICATION_XML_TEMPLATE); command.createArgument(new File(localWorkingCopy).getAbsolutePath()); } private static List<Modification> execHistoryCommand(final Commandline command) throws InterruptedException, IOException, ParseException, JDOMException { LOG.debug("Executing command: " + command); final Process p = command.execute(); final Thread stderr = logErrorStream(p); final InputStream commandOutputStream = p.getInputStream(); final List<Modification> modifications = parseStream(commandOutputStream); p.waitFor(); stderr.join(); IO.close(p); return modifications; } /** * Generates the command line for the hg version command. * <p/> * 'hg version' * * @return version command * @throws net.sourceforge.cruisecontrol.CruiseControlException * exception */ Commandline buildVersionCommand() throws CruiseControlException { final Commandline command = new Commandline(); command.setWorkingDirectory(localWorkingCopy); command.setExecutable("hg"); command.createArgument("version"); return command; } private String execVersionCommand(final Commandline command) throws CruiseControlException { LOG.debug("Executing command: " + command); try { final Process p = command.execute(); final Thread stderr = logErrorStream(p); final InputStream svnStream = p.getInputStream(); final String revision = parseVersionStream(svnStream); p.waitFor(); stderr.join(); IO.close(p); return revision; } catch (IOException e) { throw new CruiseControlException(e); } catch (ParseException e) { throw new CruiseControlException(e); } catch (InterruptedException e) { throw new CruiseControlException(e); } } static String parseVersionStream(final InputStream svnStream) throws ParseException, IOException { final InputStreamReader reader = new InputStreamReader(svnStream, "UTF-8"); return HgVersionParser.parse(reader); } private static Thread logErrorStream(final Process p) { final Thread stderr = new Thread(StreamLogger.getWarnPumper(LOG, p.getErrorStream())); stderr.start(); return stderr; } static List<Modification> parseStream(final InputStream inputStream) throws JDOMException, IOException, ParseException { final BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); String line; final StringBuffer buffer = new StringBuffer(); boolean startFound = false; while ((line = br.readLine()) != null) { startFound |= line.startsWith("<"); if (startFound) { buffer.append(line).append("\n"); } } final Reader reader = new StringReader("<hgChanges>" + buffer.toString() + "</hgChanges>"); try { return HgLogParser.parse(reader); } finally { reader.close(); } } void fillPropertiesIfNeeded(final List modifications) { if (!modifications.isEmpty()) { properties.modificationFound(); String maxRevision = ""; for (int i = 0; i < modifications.size(); i++) { final Modification modification = (Modification) modifications.get(i); final Modification.ModifiedFile file = modification.files.get(0); if (i == modifications.size() - 1) { maxRevision = modification.revision; } if (file.action.equals("deleted")) { properties.deletionFound(); } } properties.put("hgrevision", maxRevision); } } /* public static DateFormat getOutDateFormatter() { return Iso8601DateParser.ISO8601_DATE_PARSER; } */ static final class HgLogParser { private HgLogParser() { } static List<Modification> parse(final Reader reader) throws ParseException, JDOMException, IOException { final SAXBuilder builder = new SAXBuilder(false); final Document document = builder.build(reader); return parseDOMTree(document); } static List<Modification> parseDOMTree(final Document document) throws ParseException { final List<Modification> modifications = new ArrayList<Modification>(); final Element rootElement = document.getRootElement(); final List logEntries = rootElement.getChildren("hgChange"); for (Iterator iterator = logEntries.iterator(); iterator.hasNext();) { final Element logEntry = (Element) iterator.next(); final List<Modification> modificationsOfRevision = parseLogEntry(logEntry); modifications.addAll(modificationsOfRevision); } return modifications; } static List<Modification> parseLogEntry(final Element logEntry) throws ParseException { final List<Modification> modifications = new ArrayList<Modification>(); final String userName = logEntry.getChildText("author"); final String revision = logEntry.getChildText("rev") + ":" + logEntry.getChildText("node"); final String comment = logEntry.getChildText("description"); // final Date modifiedTime = convertIso8601Date(logEntry.getChildText("date")); final Date modifiedTime = convertHgDate(logEntry.getChildText("date")); final String[] addedFiles = getFiles(logEntry.getChildText("addedFiles")); final String[] removedFiles = getFiles(logEntry.getChildText("removedFiles")); final String[] changedFiles = getFiles(logEntry.getChildText("changedFiles")); addModifications(modifications, userName, revision, comment, modifiedTime, addedFiles, "added"); addModifications(modifications, userName, revision, comment, modifiedTime, changedFiles, "modified"); addModifications(modifications, userName, revision, comment, modifiedTime, removedFiles, "removed"); return modifications; } private static void addModifications(final List<Modification> modifications, final String userName, final String revision, final String comment, final Date modifiedTime, final String[] files, final String action) { for (int i = 0; i < files.length; i++) { final String filePath = files[i]; addModifications(modifications, userName, revision, comment, modifiedTime, filePath, action); } } private static void addModifications(final List<Modification> modifications, final String userName, final String revision, final String comment, final Date modifiedTime, final String filePath, final String action) { final Modification modification = new Modification("mercurial"); modification.modifiedTime = modifiedTime; modification.userName = userName; modification.comment = comment; modification.revision = revision; final Modification.ModifiedFile modfile = modification.createModifiedFile(filePath, null); modfile.action = action; modfile.revision = modification.revision; modifications.add(modification); } private static String[] getFiles(final String childText) { if (childText.length() == 0) { return new String[0]; } return childText.split(" "); } /** * Converts the specified SVN date string into a Date. * * @param date with format "2007-08-29 21:44 +0200" * @return converted date * @throws java.text.ParseException if specified date doesn't match the expected format */ /* private static Date convertIso8601Date(String date) throws ParseException { try { return Iso8601DateParser.parse(date); } catch (IllegalArgumentException e) { throw new ParseException(e.getMessage(), 0); } } */ /** * Converts the specified SVN date string into a Date. * * @param date with format "2007-08-29 21:44 +0200" * @return converted date * @throws java.text.ParseException if specified date doesn't match the expected format */ static Date convertHgDate(final String date) throws ParseException { try { return HgDateParser.parse(date); } catch (IllegalArgumentException e) { final ParseException parseException = new ParseException(e.getMessage(), 0); parseException.initCause(e); throw parseException; } } } // not used as Mercurial don't display the seconds. Too bad. Will this be fixed in a further revision ? // would be nice as is a more readable thatn the hgdate format... // 2007-08-29 21:44 +0200 /* private static final class Iso8601DateParser { private Iso8601DateParser() { } private static final SimpleDateFormat ISO8601_DATE_PARSER = new SimpleDateFormat("yyyy-MM-d HH:mm Z"); private static Date parse(String date) throws ParseException { return ISO8601_DATE_PARSER.parse(date); } } */ // 1188223879 -7200 private static final class HgDateParser { private HgDateParser() { } private static Date parse(final String date) throws ParseException { final Pattern p = Pattern.compile("([0-9]*) (.*)"); final Matcher m = p.matcher(date); if (!m.matches()) { throw new ParseException("HgDateParser: no match of " + date, 0); } final Calendar c = Calendar.getInstance(TimeZone.getTimeZone("GMT")); c.setTimeInMillis(Long.parseLong(m.group(1)) * 1000); return c.getTime(); } } static final class HgVersionParser { private HgVersionParser() { } public static String parse(final Reader reader) throws ParseException, IOException { final BufferedReader myReader = new BufferedReader(reader); final String versionLine = myReader.readLine(); if (versionLine == null) { throw new IllegalStateException("hg version returned nothing"); } final Pattern p = Pattern.compile("Mercurial Distributed SCM \\((.*)\\)"); final Matcher m = p.matcher(versionLine); if (!m.matches()) { throw new ParseException("HgVersionParser: no match of " + versionLine, 0); } return m.group(1); } } }