changeset 285:aeaf4d594967 Eluru-6.0.x

Use customized xml output from the 'hg log' command Do that to parse commit messages correctly (TW-18036). Also 'hg log' can provide information on changed files, so we will not run a 'hg status' for every found cset, that should improve changes collecting performance. Use custom xml format mainly because of the difference in the author output. Default xml splits the author to the person and the email, while default verbose log uses unsplitted author. It is not clear how to make original author from the person and the email, because author|person is not empty even if there is no person in the ui.username config. Also default xml uses date format rfc3339date, which is harder to parse. root: /home/nd/sandbox/hg-plugin/original/ HG: branch: Eluru-6.0.x HG: committing 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/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.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 mercurial-server/resources/buildServerResources/log.template HG: Press C-c C-c when you are done editing.
author Dmitry Neverov <dmitry.neverov@jetbrains.com>
date Mon, 29 Aug 2011 17:31:31 +0400
parents f0358b0d9b58
children 3ef645a4e068
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/MercurialVcsSupport.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 10 files changed, 242 insertions(+), 157 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial-common/mercurial-common.iml	Mon Aug 29 11:41:03 2011 +0400
+++ b/mercurial-common/mercurial-common.iml	Mon Aug 29 17:31:31 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	Mon Aug 29 11:41:03 2011 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/ChangeSet.java	Mon Aug 29 17:31:31 2011 +0400
@@ -16,7 +16,6 @@
 package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
 
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 import java.util.ArrayList;
 import java.util.Date;
@@ -29,8 +28,8 @@
   @NotNull private String myUser;
   @NotNull private Date myTimestamp;
   private String myDescription;
-  private boolean myContainsFiles;
-  private List<ChangeSetRevision> myParents;
+  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);
@@ -56,15 +55,12 @@
     myDescription = description;
   }
 
-  public void setContainsFiles(final boolean containsFiles) {
-    myContainsFiles = containsFiles;
+  public void addParent(@NotNull ChangeSetRevision rev) {
+    myParents.add(rev);
   }
 
-  public void addParent(@NotNull ChangeSetRevision rev) {
-    if (myParents == null) {
-      myParents = new ArrayList<ChangeSetRevision>();
-    }
-    myParents.add(rev);
+  public void setModifiedFiles(@NotNull final List<ModifiedFile> files) {
+    myModifiedFiles = files;
   }
 
   /**
@@ -93,20 +89,13 @@
     return myDescription;
   }
 
-  /**
-   * Returns parrents of this change set, or null if there were no parents.
-   * @return see above
-   */
-  @Nullable
+  @NotNull
   public List<ChangeSetRevision> getParents() {
     return myParents;
   }
 
-  /**
-   * Returns true if this change has changed files
-   * @return see above
-   */
-  public boolean containsFiles() {
-    return myContainsFiles;
+  @NotNull
+  public List<ModifiedFile> getModifiedFiles() {
+    return myModifiedFiles;
   }
 }
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommandUtil.java	Mon Aug 29 11:41:03 2011 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommandUtil.java	Mon Aug 29 17:31:31 2011 +0400
@@ -78,7 +78,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	Mon Aug 29 11:41:03 2011 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommand.java	Mon Aug 29 17:31:31 2011 +0400
@@ -16,11 +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;
@@ -29,20 +34,15 @@
 import java.util.Locale;
 
 public class LogCommand extends BaseCommand {
-  private final static Logger LOG = Logger.getInstance(LogCommand.class.getName());
+
   private String myFromId;
   private String myToId;
-  private ArrayList<String> myPaths;
-  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 final File myTemplate;
 
-  public LogCommand(@NotNull Settings settings) {
+  public LogCommand(@NotNull Settings settings, @NotNull final File template) {
     super(settings);
+    myTemplate = template;
   }
 
   public void setFromRevId(String id) {
@@ -53,16 +53,10 @@
     myToId = id;
   }
 
-  public void setPaths(final List<String> relPaths) {
-    myPaths = new ArrayList<String>(relPaths);
-  }
-
   public List<ChangeSet> execute() throws VcsException {
     GeneralCommandLine cli = createCommandLine();
     cli.addParameter("log");
-    cli.addParameter("-v");
-    cli.addParameter("--style");
-    cli.addParameter("default");
+    cli.addParameter("--style=" + myTemplate.getAbsolutePath());
     cli.addParameter("-b");
     cli.addParameter(getSettings().getBranchName());
     cli.addParameter("-r");
@@ -71,91 +65,115 @@
     String to = myToId;
     if (to == null) to = "tip";
     cli.addParameter(from + ":" + to);
-    if (myPaths != null) {
-      for (String path: myPaths) {
-        cli.addParameter(path);
-      }
-    }
 
     ExecResult res = runCommand(cli);
-    return parseChangeSets(res.getStdout());
+    try {
+      return parseChangeSetsXml(res.getStdout());
+    } 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 IOException, JDOMException, 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 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;
+    }
+  }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-server/resources/buildServerResources/log.template	Mon Aug 29 17:31:31 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/MercurialVcsSupport.java	Mon Aug 29 11:41:03 2011 +0400
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java	Mon Aug 29 17:31:31 2011 +0400
@@ -54,14 +54,18 @@
  * <p>Personal builds (remote runs) are not yet supported, they require corresponding functionality from the IDE.
  */
 public class MercurialVcsSupport extends ServerVcsSupport implements LabelingSupport, VcsFileContentProvider {
+
+  private final String LOG_TEMPLATE_NAME = "log.template";
   private ConcurrentMap<String, Lock> myWorkDirLocks= new ConcurrentHashMap<String, Lock>();
   private VcsManager myVcsManager;
   private File myDefaultWorkFolderParent;
+  private File myLogTemplate;
 
   public MercurialVcsSupport(@NotNull final VcsManager vcsManager,
                              @NotNull ServerPaths paths,
                              @NotNull final SBuildServer server,
-                             @NotNull EventDispatcher<BuildServerListener> dispatcher) {
+                             @NotNull EventDispatcher<BuildServerListener> dispatcher) throws Exception {
+    myLogTemplate = createLogTemplate(paths.getPluginDataDirectory());
     myVcsManager = vcsManager;
     myDefaultWorkFolderParent = new File(paths.getCachesDir(), "mercurial");
     dispatcher.addListener(new BuildServerAdapter() {
@@ -96,10 +100,12 @@
     });
   }
 
-  private Collection<ModifiedFile> computeModifiedFilesForMergeCommit(final Settings settings, final ChangeSet cur) throws VcsException {
-    ChangedFilesCommand cfc = new ChangedFilesCommand(settings);
-    cfc.setRevId(cur.getId());
-    return cfc.execute();
+  private File createLogTemplate(@NotNull final File templateFileDir) throws IOException {
+    File template = new File(templateFileDir, LOG_TEMPLATE_NAME);
+    if (!template.exists()) {
+      FileUtil.copyResource(MercurialVcsSupport.class, "/buildServerResources/log.template", template);
+    }
+    return template;
   }
 
   private List<VcsChange> toVcsChanges(final List<ModifiedFile> modifiedFiles, String prevVer, String curVer, final IncludeRule includeRule) {
@@ -469,12 +475,12 @@
           public List<ModificationData> collectChanges(@NotNull final IncludeRule includeRule) throws VcsException {
             syncClonedRepository(root);
 
-            // first obtain changes between specified versions
             List<ModificationData> result = new ArrayList<ModificationData>();
-            if (currentVersion == null) return result;
+            if (currentVersion == null)
+              return result;
 
             Settings settings = createSettings(root);
-            LogCommand lc = new LogCommand(settings);
+            LogCommand lc = new LogCommand(settings, myLogTemplate);
             String fromId = new ChangeSetRevision(fromVersion).getId();
             lc.setFromRevId(fromId);
             lc.setToRevId(new ChangeSetRevision(currentVersion).getId());
@@ -483,36 +489,19 @@
               return result;
             }
 
-            // invoke status command for each changeset and determine what files were modified in these changesets
-            StatusCommand st = new StatusCommand(settings);
             ChangeSet prev = new ChangeSet(fromVersion);
             for (ChangeSet cur : changeSets) {
-              if (cur.getId().equals(fromId)) continue; // skip already reported changeset
-
-              String prevId = prev.getId();
-              List<ChangeSetRevision> curParents = cur.getParents();
-              boolean merge = curParents != null && curParents.size() > 1;
-              if (curParents != null && !merge) {
-                prevId = curParents.get(0).getId();
-              }
+              if (cur.getId().equals(fromId))
+                continue; // skip already reported changeset
 
-              List<ModifiedFile> modifiedFiles = new ArrayList<ModifiedFile>();
-              if (merge) {
-                modifiedFiles.addAll(computeModifiedFilesForMergeCommit(settings, cur));
-              } else {
-                st.setFromRevId(prevId);
-                st.setToRevId(cur.getId());
-                modifiedFiles = 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)
+              boolean merge = cur.getParents().size() > 1;
+              List<ModifiedFile> modifiedFiles = cur.getModifiedFiles();
               List<VcsChange> files = toVcsChanges(modifiedFiles, prev.getFullVersion(), cur.getFullVersion(), includeRule);
-              if (files.isEmpty() && !merge) continue;
+              if (files.isEmpty() && !merge)
+                continue;
               ModificationData md = new ModificationData(cur.getTimestamp(), files, cur.getDescription(), cur.getUser(), root, cur.getFullVersion(), cur.getId());
-              if (merge) {
+              if (merge)
                 md.setCanBeIgnored(false);
-              }
               result.add(md);
               prev = cur;
             }
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java	Mon Aug 29 11:41:03 2011 +0400
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java	Mon Aug 29 17:31:31 2011 +0400
@@ -36,9 +36,10 @@
 import java.io.File;
 import java.io.FilenameFilter;
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 
@@ -400,11 +401,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	Mon Aug 29 11:41:03 2011 +0400
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommandTest.java	Mon Aug 29 17:31:31 2011 +0400
@@ -15,23 +15,39 @@
  */
 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.BeforeMethod;
 import org.testng.annotations.Test;
 
+import java.io.File;
 import java.io.IOException;
 import java.util.List;
 
 @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);
   }
 
+
+  public void tearDown() {
+    myTempFiles.cleanup();
+  }
+
+
   public void testOneChangeSet() throws Exception {
     final String toId = "9875b412a788";
     List<ChangeSet> changes = runLog(null, toId);
@@ -41,7 +57,7 @@
     assertEquals(toId, changeSet.getId());
     assertEquals("pavel@localhost", changeSet.getUser());
     assertEquals("dir1 created", changeSet.getDescription());
-    assertNull(changeSet.getParents());
+    assertTrue(changeSet.getParents().isEmpty());
   }
 
   public void testMoreThanOneChangeSet() throws Exception {
@@ -83,10 +99,35 @@
             "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) throws VcsException {
-        LogCommand lc = new LogCommand(settings);
+        LogCommand lc = new LogCommand(settings, myTemplateFile);
         lc.setFromRevId(fromId);
         lc.setToRevId(toId);
         return lc.execute();
--- a/mercurial.ipr	Mon Aug 29 11:41:03 2011 +0400
+++ b/mercurial.ipr	Mon Aug 29 17:31:31 2011 +0400
@@ -28,6 +28,9 @@
     <option name="USE_USER_FILTER" value="false" />
     <option name="USER" value="" />
   </component>
+  <component name="ClearCaseSharedConfig">
+    <option name="myUseUcmModel" value="true" />
+  </component>
   <component name="CodeStyleProjectProfileManger">
     <option name="PROJECT_PROFILE" />
     <option name="USE_PROJECT_LEVEL_SETTINGS" value="false" />
@@ -50,6 +53,7 @@
       <entry name="?*.tld" />
       <entry name="?*.jsp" />
       <entry name="?*.tag" />
+      <entry name="?*.template" />
     </wildcardResourcePatterns>
     <annotationProcessing enabled="false" useClasspath="true" />
   </component>
@@ -108,12 +112,13 @@
     <option name="PROJECT_PROFILE" value="Project Default" />
     <option name="USE_PROJECT_PROFILE" value="true" />
     <version value="1.0" />
-    <list size="5">
+    <list size="6">
       <item index="0" class="java.lang.String" itemvalue="TYPO" />
-      <item index="1" class="java.lang.String" itemvalue="INFO" />
-      <item index="2" class="java.lang.String" itemvalue="WARNING" />
-      <item index="3" class="java.lang.String" itemvalue="ERROR" />
-      <item index="4" class="java.lang.String" itemvalue="SERVER PROBLEM" />
+      <item index="1" class="java.lang.String" itemvalue="WEAK WARNING" />
+      <item index="2" class="java.lang.String" itemvalue="INFO" />
+      <item index="3" class="java.lang.String" itemvalue="WARNING" />
+      <item index="4" class="java.lang.String" itemvalue="ERROR" />
+      <item index="5" class="java.lang.String" itemvalue="SERVER PROBLEM" />
     </list>
   </component>
   <component name="JavadocGenerationManager">
@@ -386,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/jmock-SNAPSHOT.jar!/" />
--- a/mercurial.xml	Mon Aug 29 11:41:03 2011 +0400
+++ b/mercurial.xml	Mon Aug 29 17:31:31 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/jmock-SNAPSHOT.jar"/>
   </path>
@@ -213,12 +220,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">
@@ -226,12 +235,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>
   
   
@@ -310,6 +321,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">
@@ -327,6 +339,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>
   
   
@@ -407,6 +420,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">
@@ -426,6 +440,7 @@
     <path refid="library.log4j.classpath"/>
     <pathelement location="${mercurial-common.output.dir}"/>
     <path refid="library.teamcityapi-common.classpath"/>
+    <path refid="library.jdom.classpath"/>
   </path>
   
   
@@ -520,6 +535,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"/>
@@ -555,6 +571,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"/>