changeset 299:e9e7d9fcf57d

Use customized xml output from the 'hg log' command instead of running 'hg status' for every commit
author Dmitry Neverov <dmitry.neverov@jetbrains.com>
date Thu, 08 Sep 2011 12:56:56 +0400
parents 9b9fd71911eb
children fa88221586c9
files mercurial-common/mercurial-common.iml mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/ChangeSet.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommandUtil.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommand.java mercurial-server/resources/buildServerResources/log.template mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/CommandFactory.java mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MergeBaseNoRevsets.java mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MergeBaseWithRevsets.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommandTest.java mercurial.ipr mercurial.xml
diffstat 13 files changed, 265 insertions(+), 160 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial-common/mercurial-common.iml	Thu Sep 08 11:27:21 2011 +0400
+++ b/mercurial-common/mercurial-common.iml	Thu Sep 08 12:56:56 2011 +0400
@@ -10,6 +10,7 @@
     <orderEntry type="sourceFolder" forTests="false" />
     <orderEntry type="library" exported="" name="TeamCityAPI-common" level="project" />
     <orderEntry type="library" exported="" name="IDEA-openapi" level="project" />
+    <orderEntry type="library" name="jdom" level="project" />
   </component>
 </module>
 
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/ChangeSet.java	Thu Sep 08 11:27:21 2011 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/ChangeSet.java	Thu Sep 08 12:56:56 2011 +0400
@@ -28,8 +28,8 @@
   @NotNull private String myUser;
   @NotNull private Date myTimestamp;
   private String myDescription;
-  private boolean myContainsFiles;
   private List<ChangeSetRevision> myParents = new ArrayList<ChangeSetRevision>();
+  private List<ModifiedFile> myModifiedFiles = new ArrayList<ModifiedFile>();
 
   public ChangeSet(final int revNumber, @NotNull final String id) {
     super(revNumber, id);
@@ -55,10 +55,6 @@
     myDescription = description;
   }
 
-  public void setContainsFiles(final boolean containsFiles) {
-    myContainsFiles = containsFiles;
-  }
-
   public void addParent(@NotNull ChangeSetRevision rev) {
     myParents.add(rev);
   }
@@ -99,19 +95,19 @@
   }
 
   /**
-   * Returns true if this change has changed files
-   * @return see above
-   */
-  public boolean containsFiles() {
-    return myContainsFiles;
-  }
-
-
-  /**
    * Check if changeset is initial changeset (has no parents)
    * @return true if changeset is initial changeset
    */
   public boolean isInitial() {
-    return getParents() == null;
+    return getParents().isEmpty();
+  }
+
+  public void setModifiedFiles(@NotNull final List<ModifiedFile> files) {
+    myModifiedFiles = files;
+  }
+
+  @NotNull
+  public List<ModifiedFile> getModifiedFiles() {
+    return myModifiedFiles;
   }
 }
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommandUtil.java	Thu Sep 08 11:27:21 2011 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommandUtil.java	Thu Sep 08 12:56:56 2011 +0400
@@ -104,7 +104,7 @@
     removePrivateData(privateData, res);
 
     CommandUtil.checkCommandFailed(cmdStr, res);
-    Loggers.VCS.debug(res.getStdout());
+    Loggers.VCS.debug("Command " + cmdStr + " output:\n" + res.getStdout());
     return res;
   }
 
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommand.java	Thu Sep 08 11:27:21 2011 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommand.java	Thu Sep 08 12:56:56 2011 +0400
@@ -16,12 +16,16 @@
 package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
 
 import com.intellij.execution.configurations.GeneralCommandLine;
-import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.util.JDOMUtil;
 import jetbrains.buildServer.ExecResult;
 import jetbrains.buildServer.vcs.VcsException;
+import org.jdom.Document;
+import org.jdom.Element;
+import org.jdom.JDOMException;
 import org.jetbrains.annotations.NotNull;
 
 import java.io.File;
+import java.io.IOException;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
@@ -31,26 +35,20 @@
 
 public class LogCommand extends VcsRootCommand {
 
-  private final static Logger LOG = Logger.getInstance(LogCommand.class.getName());
   private final static String ZERO_PARENT_ID = "0000000000000000000000000000000000000000";
+  private static final String DATE_FORMAT = "EEE MMM d HH:mm:ss yyyy Z";
 
   private String myFromId;
   private String myToId;
-  private ArrayList<String> myPaths;
   private Integer myLimit = null;
-  private static final String CHANGESET_PREFIX = "changeset:";
-  private static final String USER_PREFIX = "user:";
-  private static final String PARENT_PREFIX = "parent:";
-  private static final String DATE_PREFIX = "date:";
-  private static final String DATE_FORMAT = "EEE MMM d HH:mm:ss yyyy Z";
-  private static final String DESCRIPTION_PREFIX = "description:";
-  private static final String FILES_PREFIX = "files:";
   private String myBranchName;
   private boolean myCalculateParents = true;
   private String myRevsets;
+  private final File myTemplate;
 
-  public LogCommand(@NotNull Settings settings, @NotNull File workingDir) {
+  public LogCommand(@NotNull Settings settings, @NotNull File workingDir, @NotNull final File template) {
     super(settings, workingDir);
+    myTemplate = template;
     myBranchName = settings.getBranchName();
   }
 
@@ -62,10 +60,6 @@
     myToId = id;
   }
 
-  public void setPaths(final List<String> relPaths) {
-    myPaths = new ArrayList<String>(relPaths);
-  }
-
   public void setLimit(final int limit) {
     myLimit = limit;
   }
@@ -86,8 +80,7 @@
     GeneralCommandLine cli = createCommandLine();
     cli.addParameter("log");
     cli.addParameter("-v");
-    cli.addParameter("--style");
-    cli.addParameter("default");
+    cli.addParameter("--style=" + myTemplate.getAbsolutePath());
     if (myBranchName != null) {
       cli.addParameter("-b");
       cli.addParameter(getSettings().getBranchName());
@@ -104,98 +97,122 @@
       cli.addParameter("--limit");
       cli.addParameter(myLimit.toString());
     }
-    if (myPaths != null) {
-      for (String path: myPaths) {
-        cli.addParameter(path);
-      }
-    }
 
     ExecResult res = runCommand(cli);
-    List<ChangeSet> csets = parseChangeSets(res.getStdout());
-    if (myCalculateParents)
-      assignTrivialParents(csets);
-    return csets;
+    try {
+      List<ChangeSet> changes = parseChangeSetsXml(res.getStdout());
+      if (myCalculateParents)
+        assignTrivialParents(changes);
+      return changes;
+    } catch (Exception e) {
+      throw new VcsException("Error while parsing log output:\n" + res.getStdout(), e);
+    }
   }
 
-  public static List<ChangeSet> parseChangeSets(final String stdout) {
-    List<ChangeSet> result = new ArrayList<ChangeSet>();
-    String[] lines = stdout.split("\n");
-    ChangeSet current = null;
-    int lineNum = 0;
-    boolean insideDescription = false;
-    StringBuilder descr = new StringBuilder();
-    while (lineNum < lines.length) {
-      String line = lines[lineNum];
-      lineNum++;
-
-      if (line.startsWith(CHANGESET_PREFIX)) {
-        insideDescription = false;
-        if (current != null) {
-          current.setDescription(descr.toString().trim());
-          descr.setLength(0);
-        }
-
-        String revAndId = line.substring(CHANGESET_PREFIX.length()).trim();
-        try {
-          current = new ChangeSet(revAndId);
-          result.add(current);
-        } catch (IllegalArgumentException e) {
-          LOG.warn("Unable to extract changeset id from the line: " + line);
-        }
-
-        continue;
-      }
-
-      if (current == null) continue;
-
-      if (line.startsWith(USER_PREFIX)) {
-        current.setUser(line.substring(USER_PREFIX.length()).trim());
-        continue;
-      }
 
-      if (line.startsWith(FILES_PREFIX)) {
-        current.setContainsFiles(true);
-        continue;
-      }
-
-      if (line.startsWith(PARENT_PREFIX)) {
-        String parentRev = line.substring(PARENT_PREFIX.length()).trim();
-        current.addParent(new ChangeSetRevision(parentRev));
-        continue;
-      }
+  private List<ChangeSet> parseChangeSetsXml(@NotNull final String xml) throws JDOMException, IOException, ParseException {
+    Document doc = JDOMUtil.loadDocument(xml);
+    Element log = doc.getRootElement();
+    return parseLog(log);
+  }
 
-      if (line.startsWith(DATE_PREFIX)) {
-        String date = line.substring(DATE_PREFIX.length()).trim();
-        try {
-          Date parsedDate = new SimpleDateFormat(DATE_FORMAT, Locale.ENGLISH).parse(date);
-          current.setTimestamp(parsedDate);
-        } catch (ParseException e) {
-          LOG.warn("Unable to parse date: " + date);
-          current = null;
-        }
-
-        continue;
-      }
 
-      if (line.startsWith(DESCRIPTION_PREFIX)) {
-        insideDescription = true;
-        continue;
-      }
-
-      if (insideDescription) {
-        descr.append(line).append("\n");
-      }
+  private List<ChangeSet> parseLog(@NotNull final Element logElement) throws ParseException {
+    List<ChangeSet> result = new ArrayList<ChangeSet>();
+    for (Object o : logElement.getChildren("logentry")) {
+      Element entry = (Element) o;
+      result.add(parseLogEntry(entry));
     }
-
-    if (insideDescription) {
-      current.setDescription(descr.toString().trim());
-    }
-
     return result;
   }
 
 
-  private void assignTrivialParents(List<ChangeSet> csets) throws VcsException {
+  private ChangeSet parseLogEntry(@NotNull final Element logEntry) throws ParseException {
+    ChangeSet cset = new ChangeSet(getRevision(logEntry), getId(logEntry));
+    addParents(cset, logEntry);
+    cset.setUser(getAuthor(logEntry));
+    cset.setDescription(getDescription(logEntry));
+    cset.setTimestamp(getDate(logEntry));
+    cset.setModifiedFiles(getModifiedFiles(logEntry));
+    return cset;
+  }
+
+
+  private int getRevision(@NotNull final Element logEntry) {
+    return Integer.parseInt(logEntry.getAttribute("revision").getValue());
+  }
+
+
+  private String getId(@NotNull final Element logEntry) {
+    return logEntry.getAttribute("shortnode").getValue();
+  }
+
+
+  private void addParents(@NotNull final ChangeSet cset, @NotNull final Element logEntry) {
+    List parents = logEntry.getChildren("parent");
+    for (Object p : parents) {
+      Element parent = (Element) p;
+      ChangeSetRevision parentCset = getParent(parent);
+      cset.addParent(parentCset);
+    }
+  }
+
+
+  private ChangeSetRevision getParent(@NotNull final Element parent) {
+    return new ChangeSetRevision(getRevision(parent), getId(parent));
+  }
+
+
+  private String getAuthor(@NotNull final Element logEntry) {
+    Element author = logEntry.getChild("author");
+    return author.getAttribute("original").getValue();
+  }
+
+
+  private String getDescription(@NotNull final Element logEntry) {
+    Element msg = logEntry.getChild("msg");
+    return msg.getText();
+  }
+
+
+  private Date getDate(@NotNull final Element logEntry) throws ParseException {
+    Element date = logEntry.getChild("date");
+    return new SimpleDateFormat(DATE_FORMAT, Locale.ENGLISH).parse(date.getText());
+  }
+
+
+  private List<ModifiedFile> getModifiedFiles(@NotNull final Element logEntry) {
+    List<ModifiedFile> result = new ArrayList<ModifiedFile>();
+    Element paths = logEntry.getChild("paths");
+    for (Object o : paths.getChildren("path")) {
+      Element path = (Element) o;
+      result.add(getModifiedFile(path));
+    }
+    return result;
+  }
+
+
+  private ModifiedFile getModifiedFile(@NotNull final Element path) {
+    String filePath = path.getText();
+    ModifiedFile.Status status = getStatus(path);
+    return new ModifiedFile(status, filePath);
+  }
+
+
+  private ModifiedFile.Status getStatus(@NotNull final Element path) {
+    String action = path.getAttribute("action").getValue();
+    if (action.equals("A")) {
+      return ModifiedFile.Status.ADDED;
+    } else if (action.equals("M")) {
+      return ModifiedFile.Status.MODIFIED;
+    } else if (action.equals("R")) {
+      return ModifiedFile.Status.REMOVED;
+    } else {
+      return ModifiedFile.Status.UNKNOWN;
+    }
+  }
+
+  private void assignTrivialParents(final @NotNull List<ChangeSet> csets) throws VcsException {
     for (ChangeSet cset : csets) {
       if (cset.getParents().isEmpty()) {
         int parentRevNumber = cset.getRevNumber() - 1;
@@ -218,5 +235,4 @@
       return output;
     }
   }
-
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-server/resources/buildServerResources/log.template	Thu Sep 08 12:56:56 2011 +0400
@@ -0,0 +1,17 @@
+header = '<?xml version="1.0"?>\n<log>\n'
+footer = '</log>\n'
+
+changeset = '<logentry revision="{rev}" node="{node}" shortnode="{node|short}">\n{branches}{tags}{parents}<author original="{author|xmlescape}" email="{author|email|xmlescape}">{author|person|xmlescape}</author>\n<date>{date|date|xmlescape}</date>\n<msg xml:space="preserve">{desc|xmlescape}</msg>\n<paths>\n{file_adds}{file_dels}{file_mods}</paths>\n{file_copies}</logentry>\n'
+
+file_add  = '<path action="A">{file_add|xmlescape}</path>\n'
+file_mod  = '<path action="M">{file_mod|xmlescape}</path>\n'
+file_del  = '<path action="R">{file_del|xmlescape}</path>\n'
+
+start_file_copies = '<copies>\n'
+file_copy = '<copy source="{source|xmlescape}">{name|xmlescape}</copy>\n'
+end_file_copies = '</copies>\n'
+
+parent = '<parent revision="{rev}" node="{node}" shortnode="{node|short}"/>\n'
+branch = '<branch>{branch|xmlescape}</branch>\n'
+tag = '<tag>{tag|xmlescape}</tag>\n'
+extra = '<extra key="{key|xmlescape}">{value|xmlescape}</extra>\n'
--- a/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/CommandFactory.java	Thu Sep 08 11:27:21 2011 +0400
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/CommandFactory.java	Thu Sep 08 12:56:56 2011 +0400
@@ -1,27 +1,33 @@
 package jetbrains.buildServer.buildTriggers.vcs.mercurial;
 
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.LogCommand;
 import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.MergeBaseCommand;
 import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.Settings;
 import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.VersionCommand;
 import jetbrains.buildServer.serverSide.ServerPaths;
+import jetbrains.buildServer.util.FileUtil;
 import jetbrains.buildServer.vcs.VcsException;
 import org.jetbrains.annotations.NotNull;
 
 import java.io.File;
+import java.io.IOException;
 
 /**
  * @author dmitry.neverov
  */
-public class CommandFactory {
+public final class CommandFactory {
 
   //hg version which supports revsets
   private final static HgVersion REVSET_HG_VERSION = new HgVersion(1, 7, 0);
+  private final static String LOG_TEMPLATE_NAME = "log.template";
 
   private final File myDefaultWorkingDir;
+  private final File myLogTemplate;
 
 
-  public CommandFactory(@NotNull final ServerPaths paths) {
+  public CommandFactory(@NotNull final ServerPaths paths) throws IOException {
     myDefaultWorkingDir = new File(paths.getCachesDir(), "mercurial");
+    myLogTemplate = createLogTemplate(paths.getPluginDataDirectory());
   }
 
 
@@ -30,9 +36,23 @@
     VersionCommand versionCommand = new VersionCommand(settings, myDefaultWorkingDir);
     HgVersion hgVersion = versionCommand.execute();
     if (hgVersion.isEqualsOrGreaterThan(REVSET_HG_VERSION))
-      return new MergeBaseWithRevsets(settings, workingDir);
+      return new MergeBaseWithRevsets(settings, workingDir, this);
     else
-      return new MergeBaseNoRevsets(settings, workingDir);
+      return new MergeBaseNoRevsets(settings, workingDir, this);
   }
 
+
+  @NotNull
+  public LogCommand createLog(@NotNull final Settings settings, @NotNull final File workingDir) {
+    return new LogCommand(settings, workingDir, myLogTemplate);
+  }
+
+
+  private File createLogTemplate(@NotNull final File templateFileDir) throws IOException {
+    File template = new File(templateFileDir, LOG_TEMPLATE_NAME);
+    if (!template.exists()) {
+      FileUtil.copyResource(CommandFactory.class, "/buildServerResources/log.template", template);
+    }
+    return template;
+  }
 }
--- a/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java	Thu Sep 08 11:27:21 2011 +0400
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java	Thu Sep 08 12:56:56 2011 +0400
@@ -119,13 +119,6 @@
     }
   }
 
-  private Collection<ModifiedFile> computeModifiedFilesForMergeCommit(final Settings settings, final ChangeSet cur) throws VcsException {
-    File workingDir = getWorkingDir(settings);
-    ChangedFilesCommand cfc = new ChangedFilesCommand(settings, workingDir);
-    cfc.setRevId(cur.getId());
-    return cfc.execute();
-  }
-
   private List<VcsChange> toVcsChanges(final List<ModifiedFile> modifiedFiles, String prevVer, String curVer, CheckoutRules rules) {
     List<VcsChange> files = new ArrayList<VcsChange>();
     for (ModifiedFile mf: modifiedFiles) {
@@ -518,7 +511,7 @@
     if (mergeBase == null)
       return null;
 
-    LogCommand lc = new LogCommand(settings, getWorkingDir(settings));
+    LogCommand lc = myCommandFactory.createLog(settings, getWorkingDir(settings));
     lc.setFromRevId(new ChangeSetRevision(mergeBase).getId());
     lc.setToRevId(new ChangeSetRevision(branchVersion).getId());
     lc.showCommitsFromAllBranches();
@@ -561,7 +554,7 @@
 
   @NotNull
   private String getMinusNthCommit(@NotNull Settings settings, int n) throws VcsException {
-    LogCommand log = new LogCommand(settings, getWorkingDir(settings));
+    LogCommand log = myCommandFactory.createLog(settings, getWorkingDir(settings));
     log.setFromRevId(settings.getBranchName());
     if (n > 0)
       log.setLimit(n);
@@ -581,10 +574,11 @@
 
     // first obtain changes between specified versions
     List<ModificationData> result = new ArrayList<ModificationData>();
-    if (currentVersion == null) return result;
+    if (currentVersion == null)
+      return result;
 
     File workingDir = getWorkingDir(settings);
-    LogCommand lc = new LogCommand(settings, workingDir);
+    LogCommand lc = myCommandFactory.createLog(settings, workingDir);
     String fromId = new ChangeSetRevision(fromVersion).getId();
     lc.setFromRevId(fromId);
     lc.setToRevId(new ChangeSetRevision(currentVersion).getId());
@@ -593,32 +587,20 @@
       return result;
     }
 
-    // invoke status command for each changeset and determine what files were modified in these changesets
-    StatusCommand st = new StatusCommand(settings, workingDir);
     ChangeSet prev = new ChangeSet(fromVersion);
     for (ChangeSet cur : changeSets) {
-      if (cur.getId().equals(fromId)) continue; // skip already reported changeset
+      if (cur.getId().equals(fromId))
+        continue; // skip already reported changeset
 
       List<ChangeSetRevision> curParents = cur.getParents();
       boolean mergeCommit = curParents.size() > 1;
-      List<ModifiedFile> modifiedFiles = new ArrayList<ModifiedFile>();
-      if (mergeCommit) {
-        modifiedFiles.addAll(computeModifiedFilesForMergeCommit(settings, cur));
-      } else {
-        if (!curParents.isEmpty())
-          st.setFromRevId(curParents.get(0).getId());
-        st.setToRevId(cur.getId());
-        modifiedFiles.addAll(st.execute());
-      }
-
-      // changeset full version will be set into VcsChange structure and
-      // stored in database (note that getContent method will be invoked with this version)
+      List<ModifiedFile> modifiedFiles = cur.getModifiedFiles();
       List<VcsChange> files = toVcsChanges(modifiedFiles, prev.getFullVersion(), cur.getFullVersion(), checkoutRules);
-      if (files.isEmpty() && !mergeCommit) continue;
+      if (files.isEmpty() && !mergeCommit)
+        continue;
       ModificationData md = new ModificationData(cur.getTimestamp(), files, cur.getDescription(), cur.getUser(), root, cur.getFullVersion(), cur.getId());
-      if (mergeCommit) {
+      if (mergeCommit)
         md.setCanBeIgnored(false);
-      }
       result.add(md);
       prev = cur;
     }
--- a/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MergeBaseNoRevsets.java	Thu Sep 08 11:27:21 2011 +0400
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MergeBaseNoRevsets.java	Thu Sep 08 12:56:56 2011 +0400
@@ -15,20 +15,21 @@
  * Implementation of merge-base for hg versions which don't have revsets
  * @author dmitry.neverov
  */
-public class MergeBaseNoRevsets implements MergeBaseCommand {
+public final class MergeBaseNoRevsets implements MergeBaseCommand {
 
   private final Settings mySettings;
   private final File myWorkingDir;
+  private final CommandFactory myCommandFactory;
 
-
-  public MergeBaseNoRevsets(@NotNull final Settings settings, @NotNull File workingDir) {
+  public MergeBaseNoRevsets(@NotNull final Settings settings, @NotNull final File workingDir, @NotNull final CommandFactory commandFactory) {
     mySettings = settings;
     myWorkingDir = workingDir;
+    myCommandFactory = commandFactory;
   }
 
 
   @Nullable
-  public String execute(@NotNull String revision1, @NotNull String revision2) {
+  public String execute(@NotNull final String revision1, @NotNull final String revision2) {
     if (revision1.equals(revision2))
       return revision1;
     try {
@@ -44,8 +45,8 @@
   }
 
 
-  private List<ChangeSet> getRevisionsReachableFrom(@NotNull String revision) throws VcsException {
-    LogCommand log = new LogCommand(mySettings, myWorkingDir);
+  private List<ChangeSet> getRevisionsReachableFrom(@NotNull final String revision) throws VcsException {
+    LogCommand log = myCommandFactory.createLog(mySettings, myWorkingDir);
     log.setFromRevId(new ChangeSetRevision(revision).getId());
     log.showCommitsFromAllBranches();
     log.setToRevId("0");
--- a/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MergeBaseWithRevsets.java	Thu Sep 08 11:27:21 2011 +0400
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MergeBaseWithRevsets.java	Thu Sep 08 12:56:56 2011 +0400
@@ -11,19 +11,21 @@
  * Implementation of merge-base using hg revsets
  * @author dmitry.neverov
  */
-public class MergeBaseWithRevsets implements MergeBaseCommand {
+public final class MergeBaseWithRevsets implements MergeBaseCommand {
 
   private final Settings mySettings;
   private final File myWorkingDir;
+  private final CommandFactory myCommandFactory;
 
-  public MergeBaseWithRevsets(@NotNull Settings settings, @NotNull File workingDir) {
+  public MergeBaseWithRevsets(@NotNull final Settings settings, @NotNull final File workingDir, @NotNull final CommandFactory commandFactory) {
     mySettings = settings;
     myWorkingDir = workingDir;
+    myCommandFactory = commandFactory;
   }
 
-  public String execute(@NotNull String revision1, @NotNull String revision2) throws VcsException {
+  public String execute(@NotNull final String revision1, @NotNull final String revision2) throws VcsException {
     try {
-      LogCommand log = new LogCommand(mySettings, myWorkingDir);
+      LogCommand log = myCommandFactory.createLog(mySettings, myWorkingDir);
       log.setRevsets("ancestor(" + new ChangeSetRevision(revision1).getId() + ", " + new ChangeSetRevision(revision2).getId() + ")");
       log.showCommitsFromAllBranches();
       log.setCalculateParents(false);
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java	Thu Sep 08 11:27:21 2011 +0400
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java	Thu Sep 08 12:56:56 2011 +0400
@@ -475,11 +475,11 @@
 
 
   private void assertFiles(final List<String> expectedFiles, final ModificationData modificationData) {
-    List<String> actualFiles = new ArrayList<String>();
+    Set<String> actualFiles = new HashSet<String>();
     for (VcsChange vc: modificationData.getChanges()) {
       actualFiles.add(toFileStatus(vc.getType()) + " " + vc.getRelativeFileName());
     }
-    Assert.assertEquals("Actual files: " + actualFiles.toString(), expectedFiles, actualFiles);
+    Assert.assertEquals("Actual files: " + actualFiles.toString(), new HashSet<String>(expectedFiles), actualFiles);
   }
 
   private String toFileStatus(VcsChange.Type type) {
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommandTest.java	Thu Sep 08 11:27:21 2011 +0400
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommandTest.java	Thu Sep 08 12:56:56 2011 +0400
@@ -15,8 +15,12 @@
  */
 package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
 
+import jetbrains.buildServer.TempFiles;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.MercurialVcsSupport;
+import jetbrains.buildServer.util.FileUtil;
 import jetbrains.buildServer.vcs.VcsException;
 import org.jetbrains.annotations.NotNull;
+import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
@@ -26,13 +30,27 @@
 
 @Test
 public class LogCommandTest extends BaseCommandTestCase {
+
+  private TempFiles myTempFiles = new TempFiles();
+  private File myTemplateFile;
+
+
   @BeforeMethod
   @Override
   protected void setUp() throws Exception {
     super.setUp();
     setRepository("mercurial-tests/testData/rep1", true);
+    myTemplateFile = myTempFiles.createTempFile();
+    FileUtil.copyResource(MercurialVcsSupport.class, "/buildServerResources/log.template", myTemplateFile);
   }
 
+
+  @AfterMethod
+  public void tearDown() {
+    myTempFiles.cleanup();
+  }
+
+
   public void testOneChangeSet() throws Exception {
     final String toId = "9875b412a788";
     List<ChangeSet> changes = runLog(null, toId);
@@ -83,10 +101,37 @@
             "bbb", changes.get(0).getDescription());
   }
 
+
+  public void log_result_should_contain_changed_files() throws Exception {
+    final String fromId = "7209b1f1d793";
+    final String toId = "b06a290a363b";
+    List<ChangeSet> csets = runLog(fromId, toId);
+    assertEquals(3, csets.size());
+
+    List<ModifiedFile> files = csets.get(0).getModifiedFiles();
+    assertEquals(1, files.size());
+    ModifiedFile file = files.get(0);
+    assertEquals(ModifiedFile.Status.ADDED, file.getStatus());
+    assertEquals("dir1/file4.txt", file.getPath());
+
+    files = csets.get(1).getModifiedFiles();
+    assertEquals(1, files.size());
+    file = files.get(0);
+    assertEquals(ModifiedFile.Status.REMOVED, file.getStatus());
+    assertEquals("dir1/file4.txt", file.getPath());
+
+    files = csets.get(2).getModifiedFiles();
+    assertEquals(1, files.size());
+    file = files.get(0);
+    assertEquals(ModifiedFile.Status.MODIFIED, file.getStatus());
+    assertEquals("dir1/file3.txt", file.getPath());
+  }
+
+
   private List<ChangeSet> runLog(final String fromId, final String toId) throws IOException, VcsException {
     return runCommand(new CommandExecutor<List<ChangeSet>>() {
       public List<ChangeSet> execute(@NotNull final Settings settings, @NotNull File workingDir) throws VcsException {
-        LogCommand lc = new LogCommand(settings, workingDir);
+        LogCommand lc = new LogCommand(settings, workingDir, myTemplateFile);
         lc.setFromRevId(fromId);
         lc.setToRevId(toId);
         return lc.execute();
--- a/mercurial.ipr	Thu Sep 08 11:27:21 2011 +0400
+++ b/mercurial.ipr	Thu Sep 08 12:56:56 2011 +0400
@@ -53,6 +53,7 @@
       <entry name="?*.tld" />
       <entry name="?*.jsp" />
       <entry name="?*.tag" />
+      <entry name="?*.template" />
     </wildcardResourcePatterns>
     <annotationProcessing enabled="false" useClasspath="true" />
   </component>
@@ -390,6 +391,13 @@
       <JAVADOC />
       <SOURCES />
     </library>
+    <library name="jdom">
+      <CLASSES>
+        <root url="jar://$TeamCityDistribution$/webapps/ROOT/WEB-INF/lib/jdom.jar!/" />
+      </CLASSES>
+      <JAVADOC />
+      <SOURCES />
+    </library>
     <library name="JMock">
       <CLASSES>
         <root url="jar://$PROJECT_DIR$/mercurial-tests/lib/hamcrest-library-1.1.jar!/" />
--- a/mercurial.xml	Thu Sep 08 11:27:21 2011 +0400
+++ b/mercurial.xml	Thu Sep 08 12:56:56 2011 +0400
@@ -36,6 +36,8 @@
     <exclude name="**/*.orig/**"/>
     <exclude name="**/*.lib/**"/>
     <exclude name="**/*~/**"/>
+    <exclude name="**/__pycache__/**"/>
+    <exclude name="**/.bundle/**"/>
   </patternset>
   <patternset id="library.patterns">
     <include name="*.zip"/>
@@ -57,6 +59,7 @@
     <include name="**/?*.tld"/>
     <include name="**/?*.jsp"/>
     <include name="**/?*.tag"/>
+    <include name="**/?*.template"/>
   </patternset>
   
   <!-- JDK definitions -->
@@ -92,6 +95,10 @@
     <pathelement location="${path.variable.teamcitydistribution}/webapps/ROOT/WEB-INF/lib/util.jar"/>
   </path>
   
+  <path id="library.jdom.classpath">
+    <pathelement location="${path.variable.teamcitydistribution}/webapps/ROOT/WEB-INF/lib/jdom.jar"/>
+  </path>
+  
   <path id="library.jmock.classpath">
     <pathelement location="${basedir}/mercurial-tests/lib/hamcrest-core-1.1.jar"/>
     <pathelement location="${basedir}/mercurial-tests/lib/hamcrest-library-1.1.jar"/>
@@ -216,12 +223,14 @@
     <path refid="${module.jdk.classpath.mercurial-common}"/>
     <path refid="library.teamcityapi-common.classpath"/>
     <path refid="library.idea-openapi.classpath"/>
+    <path refid="library.jdom.classpath"/>
   </path>
   
   <path id="mercurial-common.runtime.production.module.classpath">
     <pathelement location="${mercurial-common.output.dir}"/>
     <path refid="library.teamcityapi-common.classpath"/>
     <path refid="library.idea-openapi.classpath"/>
+    <path refid="library.jdom.classpath"/>
   </path>
   
   <path id="mercurial-common.module.classpath">
@@ -229,12 +238,14 @@
     <pathelement location="${mercurial-common.output.dir}"/>
     <path refid="library.teamcityapi-common.classpath"/>
     <path refid="library.idea-openapi.classpath"/>
+    <path refid="library.jdom.classpath"/>
   </path>
   
   <path id="mercurial-common.runtime.module.classpath">
     <pathelement location="${mercurial-common.output.dir}"/>
     <path refid="library.teamcityapi-common.classpath"/>
     <path refid="library.idea-openapi.classpath"/>
+    <path refid="library.jdom.classpath"/>
   </path>
   
   
@@ -313,6 +324,7 @@
     <pathelement location="${mercurial-common.output.dir}"/>
     <path refid="library.teamcityapi-common.classpath"/>
     <path refid="library.idea-openapi.classpath"/>
+    <path refid="library.jdom.classpath"/>
   </path>
   
   <path id="mercurial-agent.module.classpath">
@@ -330,6 +342,7 @@
     <pathelement location="${mercurial-common.output.dir}"/>
     <path refid="library.teamcityapi-common.classpath"/>
     <path refid="library.idea-openapi.classpath"/>
+    <path refid="library.jdom.classpath"/>
   </path>
   
   
@@ -410,6 +423,7 @@
     <path refid="library.log4j.classpath"/>
     <pathelement location="${mercurial-common.output.dir}"/>
     <path refid="library.teamcityapi-common.classpath"/>
+    <path refid="library.jdom.classpath"/>
   </path>
   
   <path id="mercurial-server.module.classpath">
@@ -429,6 +443,7 @@
     <path refid="library.log4j.classpath"/>
     <pathelement location="${mercurial-common.output.dir}"/>
     <path refid="library.teamcityapi-common.classpath"/>
+    <path refid="library.jdom.classpath"/>
   </path>
   
   
@@ -523,6 +538,7 @@
     <path refid="library.log4j.classpath"/>
     <pathelement location="${mercurial-common.output.dir}"/>
     <path refid="library.teamcityapi-common.classpath"/>
+    <path refid="library.jdom.classpath"/>
     <path refid="library.junit.classpath"/>
     <path refid="library.testng.classpath"/>
     <path refid="library.jmock.classpath"/>
@@ -558,6 +574,7 @@
     <path refid="library.log4j.classpath"/>
     <pathelement location="${mercurial-common.output.dir}"/>
     <path refid="library.teamcityapi-common.classpath"/>
+    <path refid="library.jdom.classpath"/>
     <path refid="library.junit.classpath"/>
     <path refid="library.testng.classpath"/>
     <path refid="library.jmock.classpath"/>