001: /********************************************************************************
002: * CruiseControl, a Continuous Integration Toolkit
003: * Copyright (c) 2007, ThoughtWorks, Inc.
004: * 200 E. Randolph, 25th Floor
005: * Chicago, IL 60601 USA
006: * All rights reserved.
007: *
008: * Redistribution and use in source and binary forms, with or without
009: * modification, are permitted provided that the following conditions
010: * are met:
011: *
012: * + Redistributions of source code must retain the above copyright
013: * notice, this list of conditions and the following disclaimer.
014: *
015: * + Redistributions in binary form must reproduce the above
016: * copyright notice, this list of conditions and the following
017: * disclaimer in the documentation and/or other materials provided
018: * with the distribution.
019: *
020: * + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the
021: * names of its contributors may be used to endorse or promote
022: * products derived from this software without specific prior
023: * written permission.
024: *
025: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
026: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
027: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
028: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
029: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
030: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
031: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
032: * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
033: * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
034: * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
035: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
036: ********************************************************************************/package net.sourceforge.cruisecontrol.sourcecontrols;
037:
038: import net.sourceforge.cruisecontrol.CruiseControlException;
039: import net.sourceforge.cruisecontrol.Modification;
040: import net.sourceforge.cruisecontrol.SourceControl;
041: import net.sourceforge.cruisecontrol.util.Commandline;
042: import net.sourceforge.cruisecontrol.util.IO;
043: import net.sourceforge.cruisecontrol.util.StreamLogger;
044: import net.sourceforge.cruisecontrol.util.ValidationHelper;
045: import org.apache.log4j.Logger;
046: import org.jdom.Document;
047: import org.jdom.Element;
048: import org.jdom.JDOMException;
049: import org.jdom.input.SAXBuilder;
050:
051: import java.io.BufferedReader;
052: import java.io.File;
053: import java.io.InputStream;
054: import java.io.InputStreamReader;
055: import java.io.IOException;
056: import java.io.Reader;
057: import java.io.StringReader;
058: import java.text.ParseException;
059: import java.util.ArrayList;
060: import java.util.Calendar;
061: import java.util.Collections;
062: import java.util.Date;
063: import java.util.Iterator;
064: import java.util.List;
065: import java.util.Map;
066: import java.util.TimeZone;
067: import java.util.regex.Matcher;
068: import java.util.regex.Pattern;
069:
070: /**
071: * This class implements the SourceControl methods for a Mercurial repository.
072: *
073: * @author <a href="jerome@coffeebreaks.org">Jerome Lacoste</a>
074: * @see <a href="http://www.selenic.com/mercurial">Mercurial web site</a>
075: */
076: public class Mercurial implements SourceControl {
077:
078: private static final Logger LOG = Logger.getLogger(Mercurial.class);
079:
080: private final SourceControlProperties properties = new SourceControlProperties();
081:
082: /**
083: * Configuration parameters
084: */
085: private String localWorkingCopy;
086:
087: static final String INCOMING_XML_TEMPLATE = "<hgChange>\n\t<author>{author}</author>\n\t<rev>{rev}</rev>\n\t"
088: + "<node>{node}</node>\n\t<description>{desc|escape}</description>\n\t<date>{date|hgdate}</date>\n\t"
089: + "<addedFiles>{file_adds}</addedFiles>\n\t<removedFiles>{file_dels}</removedFiles>\n\t"
090: + "<changedFiles>{files}</changedFiles>\n</hgChange>\n";
091:
092: public Map getProperties() {
093: return properties.getPropertiesAndReset();
094: }
095:
096: public void setProperty(String property) {
097: properties.assignPropertyName(property);
098: }
099:
100: public void setPropertyOnDelete(String propertyOnDelete) {
101: properties.assignPropertyOnDeleteName(propertyOnDelete);
102: }
103:
104: /**
105: * Sets the local working copy to use when making calls to mercurial.
106: *
107: * @param localWorkingCopy String indicating the relative or absolute path
108: * to the local working copy of the mercurial
109: * repository of which to find the log history.
110: */
111: public void setLocalWorkingCopy(String localWorkingCopy) {
112: this .localWorkingCopy = localWorkingCopy;
113: }
114:
115: /**
116: * This method validates that at least the repository location or the local
117: * working copy location has been specified.
118: *
119: * @throws net.sourceforge.cruisecontrol.CruiseControlException
120: * Thrown when the repository location and
121: * the local working copy location are both
122: * null
123: */
124: public void validate() throws CruiseControlException {
125: if (localWorkingCopy != null) {
126: File workingDir = new File(localWorkingCopy);
127: ValidationHelper.assertTrue(workingDir.exists()
128: && workingDir.isDirectory(),
129: "'localWorkingCopy' must be an existing directory. Was "
130: + workingDir.getAbsolutePath());
131: }
132: }
133:
134: /**
135: * Returns a list of modifications detailing all the changes between
136: * the last build and the latest revision in the repository.
137: *
138: * @return the list of modifications, or an empty list if we failed
139: * to retrieve the changes.
140: */
141: public List getModifications(Date lastBuildDate, Date now) {
142: String version = getMercurialVersion();
143: LOG.info("Using Mercurial: '" + version + "'");
144: Commandline command = null;
145: List modifications = Collections.EMPTY_LIST;
146: try {
147: command = buildHistoryCommand();
148:
149: modifications = execHistoryCommand(command);
150:
151: // TODO: should we filter out the results ?
152: // modifications = filterModifications(modifications, lastBuildDate, now);
153:
154: } catch (Exception e) {
155: LOG.error("Error executing mercurial history command "
156: + command, e);
157: }
158: fillPropertiesIfNeeded(modifications);
159: return modifications;
160: }
161:
162: private String getMercurialVersion() {
163: Commandline command = null;
164: try {
165: command = buildVersionCommand();
166:
167: return execVersionCommand(command);
168: } catch (Exception e) {
169: LOG.error("Error executing mercurial version command "
170: + command, e);
171: return "version unresolved...";
172: }
173: }
174:
175: /**
176: * Generates the command line for the hg incoming command.
177: * <p/>
178: * For example:
179: * <p/>
180: * 'hg incoming --template "........."'
181: *
182: * @return history command
183: * @throws net.sourceforge.cruisecontrol.CruiseControlException
184: * exception
185: */
186: Commandline buildHistoryCommand() throws CruiseControlException {
187:
188: Commandline command = new Commandline();
189: command.setExecutable("hg");
190:
191: if (localWorkingCopy != null) {
192: command.setWorkingDirectory(localWorkingCopy);
193: }
194:
195: command.createArgument("incoming");
196: // --debug required to get file_adds and file_dels to work in this version of mercurial (0.9.4+20070830)
197: command.createArgument("--debug");
198: command.createArgument("--template");
199: command.createArgument(INCOMING_XML_TEMPLATE);
200:
201: return command;
202: }
203:
204: private static List execHistoryCommand(Commandline command)
205: throws InterruptedException, IOException, ParseException,
206: JDOMException {
207:
208: LOG.debug("Executing command: " + command);
209:
210: Process p = command.execute();
211:
212: Thread stderr = logErrorStream(p);
213: InputStream commandOutputStream = p.getInputStream();
214: List modifications = parseStream(commandOutputStream);
215:
216: p.waitFor();
217: stderr.join();
218: IO.close(p);
219:
220: return modifications;
221: }
222:
223: /**
224: * Generates the command line for the hg version command.
225: * <p/>
226: * 'hg version'
227: *
228: * @return version command
229: * @throws net.sourceforge.cruisecontrol.CruiseControlException
230: * exception
231: */
232: Commandline buildVersionCommand() throws CruiseControlException {
233:
234: Commandline command = new Commandline();
235: command.setExecutable("hg");
236:
237: if (localWorkingCopy != null) {
238: command.setWorkingDirectory(localWorkingCopy);
239: }
240:
241: command.createArgument("version");
242:
243: return command;
244: }
245:
246: private String execVersionCommand(Commandline command)
247: throws CruiseControlException {
248:
249: LOG.debug("Executing command: " + command);
250:
251: try {
252: Process p = command.execute();
253:
254: Thread stderr = logErrorStream(p);
255: InputStream svnStream = p.getInputStream();
256: String revision = parseVersionStream(svnStream);
257:
258: p.waitFor();
259: stderr.join();
260: IO.close(p);
261:
262: return revision;
263: } catch (IOException e) {
264: throw new CruiseControlException(e);
265: } catch (ParseException e) {
266: throw new CruiseControlException(e);
267: } catch (InterruptedException e) {
268: throw new CruiseControlException(e);
269: }
270: }
271:
272: static String parseVersionStream(InputStream svnStream)
273: throws ParseException, IOException {
274: InputStreamReader reader = new InputStreamReader(svnStream,
275: "UTF-8");
276: return HgVersionParser.parse(reader);
277: }
278:
279: private static Thread logErrorStream(Process p) {
280: Thread stderr = new Thread(StreamLogger.getWarnPumper(LOG, p
281: .getErrorStream()));
282: stderr.start();
283: return stderr;
284: }
285:
286: static List/*Modification*/parseStream(InputStream inputStream)
287: throws JDOMException, IOException, ParseException {
288:
289: Reader reader = new InputStreamReader(inputStream, "UTF-8");
290: BufferedReader br = new BufferedReader(reader);
291: String line;
292: StringBuffer buffer = new StringBuffer();
293: boolean startFound = false;
294: while ((line = br.readLine()) != null) {
295: startFound |= line.startsWith("<");
296: if (startFound) {
297: buffer.append(line).append("\n");
298: }
299: }
300: reader = new StringReader("<hgChanges>" + buffer.toString()
301: + "</hgChanges>");
302: try {
303: return HgLogParser.parse(reader);
304: } finally {
305: reader.close();
306: }
307: }
308:
309: void fillPropertiesIfNeeded(List modifications) {
310: if (!modifications.isEmpty()) {
311: properties.modificationFound();
312:
313: String maxRevision = "";
314: for (int i = 0; i < modifications.size(); i++) {
315: Modification modification = (Modification) modifications
316: .get(i);
317: Modification.ModifiedFile file = (Modification.ModifiedFile) modification.files
318: .get(0);
319: if (i == modifications.size() - 1) {
320: maxRevision = modification.revision;
321: }
322: if (file.action.equals("deleted")) {
323: properties.deletionFound();
324: }
325: }
326: properties.put("hgrevision", maxRevision);
327: }
328: }
329:
330: /*
331: public static DateFormat getOutDateFormatter() {
332: return Iso8601DateParser.ISO8601_DATE_PARSER;
333: }
334: */
335:
336: static final class HgLogParser {
337:
338: private HgLogParser() {
339: }
340:
341: static List/*Modification*/parse(Reader reader)
342: throws ParseException, JDOMException, IOException {
343:
344: SAXBuilder builder = new SAXBuilder(false);
345: Document document = builder.build(reader);
346: return parseDOMTree(document);
347: }
348:
349: static List/*Modification*/parseDOMTree(Document document)
350: throws ParseException {
351: List modifications = new ArrayList();
352:
353: Element rootElement = document.getRootElement();
354: List logEntries = rootElement.getChildren("hgChange");
355: for (Iterator iterator = logEntries.iterator(); iterator
356: .hasNext();) {
357: Element logEntry = (Element) iterator.next();
358:
359: List/*Modification*/modificationsOfRevision = parseLogEntry(logEntry);
360: modifications.addAll(modificationsOfRevision);
361: }
362:
363: return modifications;
364: }
365:
366: static List/*Modification*/parseLogEntry(Element logEntry)
367: throws ParseException {
368: List modifications = new ArrayList();
369:
370: String userName = logEntry.getChildText("author");
371: String revision = logEntry.getChildText("rev") + ":"
372: + logEntry.getChildText("node");
373: String comment = logEntry.getChildText("description");
374: // Date modifiedTime = convertIso8601Date(logEntry.getChildText("date"));
375: Date modifiedTime = convertHgDate(logEntry
376: .getChildText("date"));
377: String[] addedFiles = getFiles(logEntry
378: .getChildText("addedFiles"));
379: String[] removedFiles = getFiles(logEntry
380: .getChildText("removedFiles"));
381: String[] changedFiles = getFiles(logEntry
382: .getChildText("changedFiles"));
383:
384: addModifications(modifications, userName, revision,
385: comment, modifiedTime, addedFiles, "added");
386: addModifications(modifications, userName, revision,
387: comment, modifiedTime, changedFiles, "modified");
388: addModifications(modifications, userName, revision,
389: comment, modifiedTime, removedFiles, "removed");
390:
391: return modifications;
392: }
393:
394: private static void addModifications(List modifications,
395: String userName, String revision, String comment,
396: Date modifiedTime, String[] files, String action) {
397: for (int i = 0; i < files.length; i++) {
398: String filePath = files[i];
399: addModifications(modifications, userName, revision,
400: comment, modifiedTime, filePath, action);
401: }
402: }
403:
404: private static void addModifications(List modifications,
405: String userName, String revision, String comment,
406: Date modifiedTime, String filePath, String action) {
407: Modification modification = new Modification("mercurial");
408:
409: modification.modifiedTime = modifiedTime;
410: modification.userName = userName;
411: modification.comment = comment;
412: modification.revision = revision;
413:
414: Modification.ModifiedFile modfile = modification
415: .createModifiedFile(filePath, null);
416: modfile.action = action;
417: modfile.revision = modification.revision;
418:
419: modifications.add(modification);
420: }
421:
422: private static String[] getFiles(String childText) {
423: if (childText.length() == 0) {
424: return new String[0];
425: }
426: return childText.split(" ");
427: }
428:
429: /**
430: * Converts the specified SVN date string into a Date.
431: *
432: * @param date with format "2007-08-29 21:44 +0200"
433: * @return converted date
434: * @throws java.text.ParseException if specified date doesn't match the expected format
435: */
436: /*
437: private static Date convertIso8601Date(String date) throws ParseException {
438: try {
439: return Iso8601DateParser.parse(date);
440: } catch (IllegalArgumentException e) {
441: throw new ParseException(e.getMessage(), 0);
442: }
443: }
444: */
445:
446: /**
447: * Converts the specified SVN date string into a Date.
448: *
449: * @param date with format "2007-08-29 21:44 +0200"
450: * @return converted date
451: * @throws java.text.ParseException if specified date doesn't match the expected format
452: */
453: static Date convertHgDate(String date) throws ParseException {
454: try {
455: return HgDateParser.parse(date);
456: } catch (IllegalArgumentException e) {
457: ParseException parseException = new ParseException(e
458: .getMessage(), 0);
459: parseException.initCause(e);
460: throw parseException;
461: }
462: }
463: }
464:
465: // not used as Mercurial don't display the seconds. Too bad. Will this be fixed in a further revision ?
466: // would be nice as is a more readable thatn the hgdate format...
467: // 2007-08-29 21:44 +0200
468: /*
469: private static final class Iso8601DateParser {
470: private Iso8601DateParser() {
471: }
472:
473: private static final SimpleDateFormat ISO8601_DATE_PARSER = new SimpleDateFormat("yyyy-MM-d HH:mm Z");
474:
475: private static Date parse(String date) throws ParseException {
476: return ISO8601_DATE_PARSER.parse(date);
477: }
478: }
479: */
480:
481: // 1188223879 -7200
482: private static final class HgDateParser {
483: private HgDateParser() {
484: }
485:
486: private static Date parse(String date) throws ParseException {
487: Pattern p = Pattern.compile("([0-9]*) (.*)");
488: Matcher m = p.matcher(date);
489: if (!m.matches()) {
490: throw new ParseException("HgDateParser: no match of "
491: + date, 0);
492: }
493: Calendar c = Calendar.getInstance(TimeZone
494: .getTimeZone("GMT"));
495: c.setTimeInMillis(Long.parseLong(m.group(1)) * 1000);
496: return c.getTime();
497: }
498: }
499:
500: static final class HgVersionParser {
501: private HgVersionParser() {
502: }
503:
504: public static String parse(Reader reader)
505: throws ParseException, IOException {
506: BufferedReader myReader = new BufferedReader(reader);
507: String versionLine = myReader.readLine();
508: if (versionLine == null) {
509: throw new IllegalStateException(
510: "hg version returned nothing");
511: }
512:
513: Pattern p = Pattern
514: .compile("Mercurial Distributed SCM \\((.*)\\)");
515: Matcher m = p.matcher(versionLine);
516: if (!m.matches()) {
517: throw new ParseException(
518: "HgVersionParser: no match of " + versionLine,
519: 0);
520: }
521: return m.group(1);
522: }
523: }
524: }
|