changeset 276:8c10f5cec37d

TW-17797 Fix merge-base calculation Add MergeBaseCommand and 2 its implementations. Implementation for hg 1.7+ uses revsets to get last common ancestors. Implementation for hg <1.7 calculates merge-base manually using information about parents.
author Dmitry Neverov <dmitry.neverov@jetbrains.com>
date Thu, 04 Aug 2011 12:04:38 +0400
parents 13f3e7d0c42c
children f80e17ac2da6
files mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/HgVersion.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/BaseCommand.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/ChangeSet.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/IdentifyCommand.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/Init.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommand.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/MergeBaseCommand.java mercurial-server/src/META-INF/build-server-plugin-mercurial.xml 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/BaseMercurialTestCase.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/Util.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/VcsRootBuilder.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/BaseCommandTest.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommandTest.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/VersionCommandTest.java mercurial-tests/src/testng.xml mercurial-tests/testData/README
diffstat 21 files changed, 370 insertions(+), 114 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/HgVersion.java	Mon Aug 01 16:38:05 2011 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/HgVersion.java	Thu Aug 04 12:04:38 2011 +0400
@@ -14,7 +14,7 @@
   private final int myThird;
 
 
-  HgVersion(int major, int minor, int third) {
+  public HgVersion(int major, int minor, int third) {
     myMajor = major;
     myMinor = minor;
     myThird = third;
@@ -33,6 +33,11 @@
   }
 
 
+  public boolean isEqualsOrGreaterThan(HgVersion other) {
+    return compareTo(other) >= 0;
+  }
+
+
   @Override
   public String toString() {
     return myMajor + "." + myMinor + "." + myThird;
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/BaseCommand.java	Mon Aug 01 16:38:05 2011 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/BaseCommand.java	Thu Aug 04 12:04:38 2011 +0400
@@ -31,11 +31,11 @@
  */
 public class BaseCommand {
   private final Settings mySettings;
-  private final String myWorkDirectory;
+  private final File myWorkDirectory;
 
   public BaseCommand(@NotNull final Settings settings, @NotNull File workingDir) {
     mySettings = settings;
-    myWorkDirectory = workingDir.getAbsolutePath();
+    myWorkDirectory = workingDir;
   }
 
 
@@ -43,13 +43,13 @@
     return mySettings;
   }
 
-  public String getWorkDirectory() {
+  public File getWorkDirectory() {
     return myWorkDirectory;
   }
 
   protected GeneralCommandLine createCommandLine() {
     GeneralCommandLine cli = createCL();
-    cli.setWorkDirectory(myWorkDirectory);
+    cli.setWorkDirectory(myWorkDirectory.getAbsolutePath());
     cli.setPassParentEnvs(true);
     return cli;
   }
@@ -81,6 +81,7 @@
    * case when $PATH contains <mercurial_install_dir>/bin and doesn't contain <mercurial_install_dir> 
    * and hg executable is set to 'hg'. To fix it - run hg using windows shell which expand
    * hg to hg.cmd correctly.
+   * @param cli command line in which to setup hg executable
    */
   private void setupExecutable(GeneralCommandLine cli) {
     if (SystemInfo.isWindows && getSettings().getHgCommandPath().equals("hg")) {
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/ChangeSet.java	Mon Aug 01 16:38:05 2011 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/ChangeSet.java	Thu Aug 04 12:04:38 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;
@@ -30,7 +29,7 @@
   @NotNull private Date myTimestamp;
   private String myDescription;
   private boolean myContainsFiles;
-  private List<ChangeSetRevision> myParents;
+  private List<ChangeSetRevision> myParents = new ArrayList<ChangeSetRevision>();
 
   public ChangeSet(final int revNumber, @NotNull final String id) {
     super(revNumber, id);
@@ -61,9 +60,6 @@
   }
 
   public void addParent(@NotNull ChangeSetRevision rev) {
-    if (myParents == null) {
-      myParents = new ArrayList<ChangeSetRevision>();
-    }
     myParents.add(rev);
   }
 
@@ -94,10 +90,10 @@
   }
 
   /**
-   * Returns parrents of this change set, or null if there were no parents.
+   * Returns parrents of this change set
    * @return see above
    */
-  @Nullable
+  @NotNull
   public List<ChangeSetRevision> getParents() {
     return myParents;
   }
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/IdentifyCommand.java	Mon Aug 01 16:38:05 2011 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/IdentifyCommand.java	Thu Aug 04 12:04:38 2011 +0400
@@ -30,6 +30,7 @@
 
   private boolean myInLocalRepository = false;
   private ChangeSet myChangeSet;
+  private Integer myRevisionNumber;
 
   public IdentifyCommand(@NotNull Settings settings, @NotNull File workingDir) {
     super(settings, workingDir);
@@ -43,17 +44,24 @@
     myChangeSet = changeSet;
   }
 
+  public void setRevisionNumber(int revisionNumber) {
+    myRevisionNumber = revisionNumber;
+  }
+
   public String execute() throws VcsException {
     GeneralCommandLine cli = createCL();
     cli.addParameter("identify");
     if (myInLocalRepository) {
-      cli.setWorkDirectory(this.getWorkDirectory());
+      cli.setWorkDirectory(this.getWorkDirectory().getAbsolutePath());
     } else {
       cli.addParameter(getSettings().getRepositoryUrl());
     }
     if (myChangeSet != null) {
       cli.addParameter("--rev");
       cli.addParameter(myChangeSet.getId());
+    } else if (myRevisionNumber != null) {
+      cli.addParameter("--rev");
+      cli.addParameter(myRevisionNumber.toString());
     }
     ExecResult res = runCommand(cli);
     failIfNotEmptyStdErr(cli, res);
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/Init.java	Mon Aug 01 16:38:05 2011 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/Init.java	Thu Aug 04 12:04:38 2011 +0400
@@ -20,7 +20,7 @@
   }
 
   public void execute() throws VcsException {
-    new File(getWorkDirectory()).mkdirs();
+    getWorkDirectory().mkdirs();
     GeneralCommandLine cli = createCommandLine();
     cli.addParameter("init");
     runCommand(cli);
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommand.java	Mon Aug 01 16:38:05 2011 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommand.java	Thu Aug 04 12:04:38 2011 +0400
@@ -30,7 +30,10 @@
 import java.util.Locale;
 
 public class LogCommand extends BaseCommand {
+
   private final static Logger LOG = Logger.getInstance(LogCommand.class.getName());
+  private final static String ZERO_PARENT_ID = "0000000000000000000000000000000000000000";
+
   private String myFromId;
   private String myToId;
   private ArrayList<String> myPaths;
@@ -43,6 +46,8 @@
   private static final String DESCRIPTION_PREFIX = "description:";
   private static final String FILES_PREFIX = "files:";
   private String myBranchName;
+  private boolean myCalculateParents = true;
+  private String myRevsets;
 
   public LogCommand(@NotNull Settings settings, @NotNull File workingDir) {
     super(settings, workingDir);
@@ -65,8 +70,16 @@
     myLimit = limit;
   }
 
-  public void setBranchName(String branchName) {
-    myBranchName = branchName;
+  public void showCommitsFromAllBranches() {
+    myBranchName = null;
+  }
+
+  public void setCalculateParents(boolean doCalculate) {
+    myCalculateParents = doCalculate;
+  }
+
+  public void setRevsets(String revsets) {
+    myRevsets = revsets;
   }
 
   public List<ChangeSet> execute() throws VcsException {
@@ -80,11 +93,13 @@
       cli.addParameter(getSettings().getBranchName());
     }
     cli.addParameter("-r");
-    String from = myFromId;
-    if (from == null) from = "0";
-    String to = myToId;
-    if (to == null) to = "tip";
-    cli.addParameter(from + ":" + to);
+    if (myRevsets != null) {
+      cli.addParameter(myRevsets);
+    } else {
+      String from = myFromId != null ? myFromId : "0";
+      String to = myToId != null ? myToId : "tip";
+      cli.addParameter(from + ":" + to);
+    }
     if (myLimit != null) {
       cli.addParameter("--limit");
       cli.addParameter(myLimit.toString());
@@ -96,7 +111,10 @@
     }
 
     ExecResult res = runCommand(cli);
-    return parseChangeSets(res.getStdout());
+    List<ChangeSet> csets = parseChangeSets(res.getStdout());
+    if (myCalculateParents)
+      assignTrivialParents(csets);
+    return csets;
   }
 
   public static List<ChangeSet> parseChangeSets(final String stdout) {
@@ -176,4 +194,29 @@
     return result;
   }
 
+
+  private void assignTrivialParents(List<ChangeSet> csets) throws VcsException {
+    for (ChangeSet cset : csets) {
+      if (cset.getParents().isEmpty()) {
+        int parentRevNumber = cset.getRevNumber() - 1;
+        String parentId = getIdOf(parentRevNumber);
+        cset.addParent(new ChangeSetRevision(parentRevNumber, parentId));
+      }
+    }
+  }
+
+  private String getIdOf(int revNumber) throws VcsException {
+    if (revNumber < 0)
+      return ZERO_PARENT_ID;
+    IdentifyCommand identify = new IdentifyCommand(getSettings(), getWorkDirectory());
+    identify.setInLocalRepository(true);
+    identify.setRevisionNumber(revNumber);
+    String output = identify.execute().trim();
+    if (output.contains(" ")) {
+      return output.substring(0, output.indexOf(" "));
+    } else {
+      return output;
+    }
+  }
+
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/MergeBaseCommand.java	Thu Aug 04 12:04:38 2011 +0400
@@ -0,0 +1,25 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Analog of git merge-base. It returns a last common ancestor between two revisions.
+ *
+ * @author dmitry.neverov
+ */
+public interface MergeBaseCommand {
+
+  /**
+   * Returns hash of least common ancestor between two revisions or null
+   * if common ancestor is not found
+   * @param revision1 first revision
+   * @param revision2 second revision
+   * @return see above
+   * @throws VcsException if some commands fail
+   */
+  @Nullable
+  String execute(@NotNull String revision1, @NotNull String revision2) throws VcsException;
+
+}
--- a/mercurial-server/src/META-INF/build-server-plugin-mercurial.xml	Mon Aug 01 16:38:05 2011 +0400
+++ b/mercurial-server/src/META-INF/build-server-plugin-mercurial.xml	Thu Aug 04 12:04:38 2011 +0400
@@ -4,4 +4,5 @@
 <beans default-autowire="constructor">
   <bean id="mercurialServer" class="jetbrains.buildServer.buildTriggers.vcs.mercurial.MercurialVcsSupport" />
   <bean id="config" class="jetbrains.buildServer.buildTriggers.vcs.mercurial.PluginConfigImpl" />
+  <bean id="commandFactory" class="jetbrains.buildServer.buildTriggers.vcs.mercurial.CommandFactory" />
 </beans>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/CommandFactory.java	Thu Aug 04 12:04:38 2011 +0400
@@ -0,0 +1,38 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+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.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+
+/**
+ * @author dmitry.neverov
+ */
+public class CommandFactory {
+
+  //hg version which supports revsets
+  private final static HgVersion REVSET_HG_VERSION = new HgVersion(1, 7, 0);
+
+  private final File myDefaultWorkingDir;
+
+
+  public CommandFactory(@NotNull final ServerPaths paths) {
+    myDefaultWorkingDir = new File(paths.getCachesDir(), "mercurial");
+  }
+
+
+  @NotNull
+  public MergeBaseCommand createMergeBase(@NotNull Settings settings, @NotNull File workingDir) throws VcsException {
+    VersionCommand versionCommand = new VersionCommand(settings, myDefaultWorkingDir);
+    HgVersion hgVersion = versionCommand.execute();
+    if (hgVersion.isEqualsOrGreaterThan(REVSET_HG_VERSION))
+      return new MergeBaseWithRevsets(settings, workingDir);
+    else
+      return new MergeBaseNoRevsets(settings, workingDir);
+  }
+
+}
--- a/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java	Mon Aug 01 16:38:05 2011 +0400
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java	Thu Aug 04 12:04:38 2011 +0400
@@ -53,22 +53,26 @@
  * <p>Working copy of repository is created in the $TEAMCITY_DATA_PATH/system/caches/hg_&lt;hash code> folder.
  * <p>Personal builds (remote runs) are not yet supported, they require corresponding functionality from the IDE.
  */
-public class MercurialVcsSupport extends ServerVcsSupport implements LabelingSupport, VcsFileContentProvider, BranchSupport {
+public class MercurialVcsSupport extends ServerVcsSupport implements LabelingSupport, VcsFileContentProvider, BranchSupport,
+        CollectChangesByCheckoutRules {
   private ConcurrentMap<String, Lock> myWorkDirLocks= new ConcurrentHashMap<String, Lock>();
   private VcsManager myVcsManager;
   private File myDefaultWorkFolderParent;
   private MirrorManager myMirrorManager;
   private final PluginConfig myConfig;
+  private final CommandFactory myCommandFactory;
 
   public MercurialVcsSupport(@NotNull final VcsManager vcsManager,
                              @NotNull final ServerPaths paths,
                              @NotNull final SBuildServer server,
                              @NotNull final EventDispatcher<BuildServerListener> dispatcher,
-                             @NotNull final PluginConfig config) {
+                             @NotNull final PluginConfig config,
+                             @NotNull final CommandFactory commandFactory) {
     myVcsManager = vcsManager;
     myDefaultWorkFolderParent = new File(paths.getCachesDir(), "mercurial");
     myMirrorManager = new MirrorManager(myDefaultWorkFolderParent);
     myConfig = config;
+    myCommandFactory = commandFactory;
     dispatcher.addListener(new BuildServerAdapter() {
       @Override
       public void cleanupFinished() {
@@ -497,12 +501,15 @@
     VcsRoot branchRoot = createBranchRoot(root, branchName);
     String baseVersion = getCurrentVersion(root);
     String branchVersion = getCurrentVersion(branchRoot);
-    String branchPoint = getBranchPoint(settings, baseVersion, branchVersion);
+    String mergeBase = getMergeBase(settings, baseVersion, branchVersion);
+
+    if (mergeBase == null)
+      return null;
 
     LogCommand lc = new LogCommand(settings, getWorkingDir(settings));
-    lc.setFromRevId(new ChangeSetRevision(branchPoint).getId());
+    lc.setFromRevId(new ChangeSetRevision(mergeBase).getId());
     lc.setToRevId(new ChangeSetRevision(branchVersion).getId());
-    lc.setBranchName(null);//do not limit output to particular branch, return all commits
+    lc.showCommitsFromAllBranches();
     List<ChangeSet> changeSets = lc.execute();
     if (changeSets.size() > 1) {//when branch points to the commit in original branch we get 1 cset
       String branchId = changeSets.get(1).getId();
@@ -524,90 +531,87 @@
   public List<ModificationData> collectChanges(@NotNull VcsRoot fromRoot, @NotNull String fromRootRevision,
                                                @NotNull VcsRoot toRoot, @Nullable String toRootRevision,
                                                @NotNull CheckoutRules checkoutRules) throws VcsException {
-    //we get all branches while clone, if vcs roots are related it is doesn't matter in which one search for branch point
-    Settings settings = createSettings(fromRoot);
+    Settings settings = createSettings(toRoot);
     syncRepository(settings);
-    String branchPoint = getBranchPoint(settings, fromRootRevision, toRootRevision);
-    return ((CollectChangesByCheckoutRules) getCollectChangesPolicy()).collectChanges(toRoot, branchPoint, toRootRevision, checkoutRules);
+    String toRevision = toRootRevision != null ? toRootRevision : getCurrentVersion(toRoot);
+    String mergeBase = getMergeBase(settings, fromRootRevision, toRevision);
+    if (mergeBase == null)
+      mergeBase = getMinusNthCommit(settings, 10);
+    return collectChanges(toRoot, mergeBase, toRootRevision, checkoutRules);
   }
 
 
-  private String getBranchPoint(@NotNull Settings settings, String branchOneRev, String branchTwoRev) throws VcsException {
-    if (branchOneRev.equals(branchTwoRev))
-      return branchOneRev;
-    File workingDir = getWorkingDir(settings);
-    LogCommand lc = new LogCommand(settings, workingDir);
-    lc.setFromRevId(new ChangeSetRevision(branchOneRev).getId());
-    lc.setToRevId(new ChangeSetRevision(branchTwoRev).getId());
-    lc.setLimit(1);
-    List<ChangeSet> changeSets = lc.execute();
-    ChangeSet cs = changeSets.get(0);
-    if (cs.isInitial()) {
-      return cs.getId();
-    } else {
-      return cs.getParents().get(0).getId();
-    }
+  @Nullable
+  private String getMergeBase(@NotNull Settings settings, @NotNull String revision1, @NotNull String revision2) throws VcsException {
+    return myCommandFactory.createMergeBase(settings, getWorkingDir(settings)).execute(revision1, revision2);
   }
 
+
+  @NotNull
+  private String getMinusNthCommit(@NotNull Settings settings, int n) throws VcsException {
+    LogCommand log = new LogCommand(settings, getWorkingDir(settings));
+    log.setFromRevId(settings.getBranchName());
+    if (n > 0)
+      log.setLimit(n);
+    List<ChangeSet> changeSets = log.execute();
+    return changeSets.get(changeSets.size() - 1).getId();
+  }
+
+
   @NotNull
   public CollectChangesPolicy getCollectChangesPolicy() {
-    return new CollectChangesByCheckoutRules() {
-      @NotNull
-      public List<ModificationData> collectChanges(@NotNull VcsRoot root, @NotNull String fromVersion, @Nullable String currentVersion, @NotNull CheckoutRules checkoutRules) throws VcsException {
-        Settings settings = createSettings(root);
-        syncRepository(settings);
-
-        // first obtain changes between specified versions
-        List<ModificationData> result = new ArrayList<ModificationData>();
-        if (currentVersion == null) return result;
-
-        File workingDir = getWorkingDir(settings);
-        LogCommand lc = new LogCommand(settings, workingDir);
-        String fromId = new ChangeSetRevision(fromVersion).getId();
-        lc.setFromRevId(fromId);
-        lc.setToRevId(new ChangeSetRevision(currentVersion).getId());
-        List<ChangeSet> changeSets = lc.execute();
-        if (changeSets.isEmpty()) {
-          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
+    return this;
+  }
 
-          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();
-          }
+  public List<ModificationData> collectChanges(@NotNull VcsRoot root, @NotNull String fromVersion, @Nullable String currentVersion, @NotNull CheckoutRules checkoutRules) throws VcsException {
+    Settings settings = createSettings(root);
+    syncRepository(settings);
 
-          List<ModifiedFile> modifiedFiles = new ArrayList<ModifiedFile>();
-          if (merge) {
-            modifiedFiles.addAll(computeModifiedFilesForMergeCommit(settings, cur));
-          } else {
-            st.setFromRevId(prevId);
-            st.setToRevId(cur.getId());
-            modifiedFiles = st.execute();
-          }
+    // first obtain changes between specified versions
+    List<ModificationData> result = new ArrayList<ModificationData>();
+    if (currentVersion == null) return result;
 
-          // changeset full version will be set into VcsChange structure and
-          // stored in database (note that getContent method will be invoked with this version)
-          List<VcsChange> files = toVcsChanges(modifiedFiles, prev.getFullVersion(), cur.getFullVersion(), checkoutRules);
-          if (files.isEmpty() && !merge) continue;
-          ModificationData md = new ModificationData(cur.getTimestamp(), files, cur.getDescription(), cur.getUser(), root, cur.getFullVersion(), cur.getId());
-          if (merge) {
-            md.setCanBeIgnored(false);
-          }
-          result.add(md);
-          prev = cur;
-        }
+    File workingDir = getWorkingDir(settings);
+    LogCommand lc = new LogCommand(settings, workingDir);
+    String fromId = new ChangeSetRevision(fromVersion).getId();
+    lc.setFromRevId(fromId);
+    lc.setToRevId(new ChangeSetRevision(currentVersion).getId());
+    List<ChangeSet> changeSets = lc.execute();
+    if (changeSets.isEmpty()) {
+      return result;
+    }
 
-        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
+
+      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<VcsChange> files = toVcsChanges(modifiedFiles, prev.getFullVersion(), cur.getFullVersion(), checkoutRules);
+      if (files.isEmpty() && !mergeCommit) continue;
+      ModificationData md = new ModificationData(cur.getTimestamp(), files, cur.getDescription(), cur.getUser(), root, cur.getFullVersion(), cur.getId());
+      if (mergeCommit) {
+        md.setCanBeIgnored(false);
+      }
+      result.add(md);
+      prev = cur;
+    }
+
+    return result;
   }
 
   @NotNull
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MergeBaseNoRevsets.java	Thu Aug 04 12:04:38 2011 +0400
@@ -0,0 +1,63 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import com.intellij.openapi.util.Pair;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.*;
+import jetbrains.buildServer.util.graph.DAG;
+import jetbrains.buildServer.util.graph.DAGs;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.*;
+
+/**
+ * Implementation of merge-base for hg versions which don't have revsets
+ * @author dmitry.neverov
+ */
+public class MergeBaseNoRevsets implements MergeBaseCommand {
+
+  private final Settings mySettings;
+  private final File myWorkingDir;
+
+
+  public MergeBaseNoRevsets(@NotNull final Settings settings, @NotNull File workingDir) {
+    mySettings = settings;
+    myWorkingDir = workingDir;
+  }
+
+
+  @Nullable
+  public String execute(@NotNull String revision1, @NotNull String revision2) {
+    if (revision1.equals(revision2))
+      return revision1;
+    try {
+      List<Pair<String, String>> edges = new ArrayList<Pair<String, String>>();
+      fillEdges(edges, getRevisionsReachableFrom(revision1));
+      fillEdges(edges, getRevisionsReachableFrom(revision2));
+      DAG<String> dag = DAGs.createFromEdges(edges);
+      List<String> commonAncestors = dag.getCommonAncestors(new ChangeSetRevision(revision1).getId(), new ChangeSetRevision(revision2).getId());
+      return commonAncestors.isEmpty() ? null : commonAncestors.get(0);
+    } catch (VcsException e) {
+      return null;
+    }
+  }
+
+
+  private List<ChangeSet> getRevisionsReachableFrom(@NotNull String revision) throws VcsException {
+    LogCommand log = new LogCommand(mySettings, myWorkingDir);
+    log.setFromRevId(new ChangeSetRevision(revision).getId());
+    log.showCommitsFromAllBranches();
+    log.setToRevId("0");
+    return log.execute();
+  }
+
+
+  private void fillEdges(List<Pair<String, String>> edges, List<ChangeSet> csets) {
+    for (ChangeSet cset : csets) {
+      for (ChangeSetRevision parent : cset.getParents()) {
+        edges.add(Pair.create(cset.getId(), parent.getId()));
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MergeBaseWithRevsets.java	Thu Aug 04 12:04:38 2011 +0400
@@ -0,0 +1,36 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.*;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * Implementation of merge-base using hg revsets
+ * @author dmitry.neverov
+ */
+public class MergeBaseWithRevsets implements MergeBaseCommand {
+
+  private final Settings mySettings;
+  private final File myWorkingDir;
+
+  public MergeBaseWithRevsets(@NotNull Settings settings, @NotNull File workingDir) {
+    mySettings = settings;
+    myWorkingDir = workingDir;
+  }
+
+  public String execute(@NotNull String revision1, @NotNull String revision2) throws VcsException {
+    try {
+      LogCommand log = new LogCommand(mySettings, myWorkingDir);
+      log.setRevsets("ancestor(" + new ChangeSetRevision(revision1).getId() + ", " + new ChangeSetRevision(revision2).getId() + ")");
+      log.showCommitsFromAllBranches();
+      log.setCalculateParents(false);
+      List<ChangeSet> csets = log.execute();
+      return csets.isEmpty() ? null : csets.get(0).getId();
+    } catch (VcsException e) {
+      return null;
+    }
+  }
+}
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/BaseMercurialTestCase.java	Mon Aug 01 16:38:05 2011 +0400
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/BaseMercurialTestCase.java	Thu Aug 04 12:04:38 2011 +0400
@@ -51,17 +51,13 @@
   }
 
   protected VcsRootImpl createVcsRoot(@NotNull String repPath) throws IOException {
-    VcsRootImpl vcsRoot = new VcsRootImpl(1, Constants.VCS_NAME);
-    vcsRoot.addProperty(Constants.HG_COMMAND_PATH_PROP, new File(Util.getHgPath()).getAbsolutePath());
     File repository = LocalRepositoryUtil.prepareRepository(repPath);
-    vcsRoot.addProperty(Constants.REPOSITORY_PROP, repository.getAbsolutePath());
-    return vcsRoot;
+    return new VcsRootBuilder().repository(repository.getAbsolutePath()).build();
   }
 
   protected VcsRootImpl createVcsRoot(@NotNull String repPath, @NotNull String branchName) throws IOException {
-    VcsRootImpl vcsRoot = createVcsRoot(repPath);
-    vcsRoot.addProperty(Constants.BRANCH_NAME_PROP, branchName);
-    return vcsRoot;
+    File repository = LocalRepositoryUtil.prepareRepository(repPath);
+    return new VcsRootBuilder().repository(repository.getAbsolutePath()).branch(branchName).build();
   }
 
   protected void cleanRepositoryAfterTest(@NotNull String repPath) {
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java	Mon Aug 01 16:38:05 2011 +0400
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java	Thu Aug 04 12:04:38 2011 +0400
@@ -63,7 +63,7 @@
     File systemDir = myTempFiles.createTempDir();
     myServerPaths = new ServerPaths(systemDir.getAbsolutePath(), systemDir.getAbsolutePath(), systemDir.getAbsolutePath());
     assertTrue(new File(myServerPaths.getCachesDir()).mkdirs());
-    myVcs = new MercurialVcsSupport((VcsManager)vcsManagerMock.proxy(), myServerPaths, (SBuildServer)serverMock.proxy(), dispatcher, createPluginConfig());
+    myVcs = new MercurialVcsSupport((VcsManager)vcsManagerMock.proxy(), myServerPaths, (SBuildServer)serverMock.proxy(), dispatcher, createPluginConfig(), new CommandFactory(myServerPaths));
   }
 
   protected String getTestDataPath() {
@@ -82,7 +82,7 @@
   }
 
   private List<ModificationData> collectChanges(@NotNull VcsRoot vcsRoot, @NotNull String from, @NotNull String to, @NotNull CheckoutRules rules) throws VcsException {
-    return ((CollectChangesByCheckoutRules) myVcs.getCollectChangesPolicy()).collectChanges(vcsRoot, from, to, rules);
+    return myVcs.collectChanges(vcsRoot, from, to, rules);
   }
 
   public void test_collect_changes_between_two_same_roots() throws Exception {
@@ -92,6 +92,13 @@
     do_check_for_collect_changes(changes);
   }
 
+  public void test_collect_changes_from_non_existing_revision() throws Exception {
+    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
+    VcsRootImpl sameVcsRoot = createVcsRoot(simpleRepo());
+    List<ModificationData> changes = myVcs.collectChanges(vcsRoot, "0:9875b412a789", sameVcsRoot, "3:9522278aa38d", new CheckoutRules(""));
+    assertFalse(changes.isEmpty());//should return some changes from the toRoot
+  }
+
   public void test_collect_changes() throws Exception {
     VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
     List<ModificationData> changes = collectChanges(vcsRoot, "0:9875b412a788", "3:9522278aa38d", new CheckoutRules(""));
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/Util.java	Mon Aug 01 16:38:05 2011 +0400
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/Util.java	Thu Aug 04 12:04:38 2011 +0400
@@ -1,5 +1,6 @@
 package jetbrains.buildServer.buildTriggers.vcs.mercurial;
 
+import java.io.File;
 import java.io.IOException;
 
 /**
@@ -14,7 +15,7 @@
     if (providedHg != null) {
       return providedHg;
     } else {
-      return "mercurial-tests/testData/bin/hg.exe";
+      return new File("mercurial-tests/testData/bin/hg.exe").getAbsolutePath();
     }
   }
 
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/VcsRootBuilder.java	Mon Aug 01 16:38:05 2011 +0400
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/VcsRootBuilder.java	Thu Aug 04 12:04:38 2011 +0400
@@ -13,6 +13,7 @@
   private String myRepository;
   private String myUsername;
   private String myPassword;
+  private String myBranch;
   private long myRootId = 1L;
 
   public VcsRootImpl build() throws IOException {
@@ -21,6 +22,7 @@
     vcsRoot.addProperty(Constants.HG_COMMAND_PATH_PROP, Util.getHgPath());
     vcsRoot.addProperty(Constants.USERNAME, myUsername);
     vcsRoot.addProperty(Constants.PASSWORD, myPassword);
+    vcsRoot.addProperty(Constants.BRANCH_NAME_PROP, myBranch);
     return vcsRoot;
   }
 
@@ -43,6 +45,12 @@
   }
 
 
+  public VcsRootBuilder branch(@NotNull String branch) {
+    myBranch = branch;
+    return this;
+  }
+
+
   public VcsRootBuilder rootId(long rootId) {
     myRootId = rootId;
     return this;
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/BaseCommandTest.java	Mon Aug 01 16:38:05 2011 +0400
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/BaseCommandTest.java	Thu Aug 04 12:04:38 2011 +0400
@@ -2,8 +2,7 @@
 
 import com.intellij.execution.configurations.GeneralCommandLine;
 import com.intellij.openapi.util.SystemInfo;
-import jetbrains.buildServer.buildTriggers.vcs.mercurial.Constants;
-import jetbrains.buildServer.buildTriggers.vcs.mercurial.Util;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.VcsRootBuilder;
 import jetbrains.buildServer.vcs.impl.VcsRootImpl;
 import junit.framework.TestCase;
 import org.testng.annotations.Test;
@@ -18,9 +17,7 @@
 public class BaseCommandTest extends TestCase {
 
   public void should_quote_command_line_arguments() throws IOException {
-    VcsRootImpl root = new VcsRootImpl(1, "rootForTest");
-    root.addProperty(Constants.REPOSITORY_PROP, "http://some.org/repo.hg");
-    root.addProperty(Constants.HG_COMMAND_PATH_PROP, Util.getHgPath());
+    VcsRootImpl root = new VcsRootBuilder().repository("http://some.org/repo.hg").build();
     File workingDir = new File("some dir");
     Settings settings = new Settings(root);
 
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommandTest.java	Mon Aug 01 16:38:05 2011 +0400
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommandTest.java	Thu Aug 04 12:04:38 2011 +0400
@@ -42,7 +42,6 @@
     assertEquals(toId, changeSet.getId());
     assertEquals("pavel@localhost", changeSet.getUser());
     assertEquals("dir1 created", changeSet.getDescription());
-    assertNull(changeSet.getParents());
   }
 
   public void testMoreThanOneChangeSet() throws Exception {
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/VersionCommandTest.java	Mon Aug 01 16:38:05 2011 +0400
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/VersionCommandTest.java	Thu Aug 04 12:04:38 2011 +0400
@@ -4,12 +4,14 @@
 import jetbrains.buildServer.buildTriggers.vcs.mercurial.VcsRootBuilder;
 import jetbrains.buildServer.vcs.impl.VcsRootImpl;
 import junit.framework.TestCase;
+import org.testng.annotations.Test;
 
 import java.io.File;
 
 /**
  * @author dmitry.neverov
  */
+@Test
 public class VersionCommandTest extends TestCase {
 
   public void test() throws Exception {
--- a/mercurial-tests/src/testng.xml	Mon Aug 01 16:38:05 2011 +0400
+++ b/mercurial-tests/src/testng.xml	Thu Aug 04 12:04:38 2011 +0400
@@ -12,6 +12,8 @@
       <class name="jetbrains.buildServer.buildTriggers.vcs.mercurial.AgentSideCheckoutTest"/>
       <class name="jetbrains.buildServer.buildTriggers.vcs.mercurial.SettingsTest"/>
       <class name="jetbrains.buildServer.buildTriggers.vcs.mercurial.MirrorManagerTest"/>
+      <class name="jetbrains.buildServer.buildTriggers.vcs.mercurial.HgVersionTest"/>
+      <class name="jetbrains.buildServer.buildTriggers.vcs.mercurial.command.VersionCommandTest"/>
     </classes>
   </test>
 </suite>
--- a/mercurial-tests/testData/README	Mon Aug 01 16:38:05 2011 +0400
+++ b/mercurial-tests/testData/README	Thu Aug 04 12:04:38 2011 +0400
@@ -1,3 +1,27 @@
+rep1 history:
+@   10:9c6a6b4aede0 Multiline description tip
+|
+| o  9:9babcf2d5705 name with space branch name with space
+| |
+| o  8:04c3ae4c6312 file modified test_branch
+| |
+| o  7:376dcf05cd2a new file added in the test_branch test_branch
+|/
+o    6:b9deb9a1c6f4 files with spaces added
+|
+o    5:1d2cc6f3bc29 modified in subdir
+|
+o    4:b06a290a363b file modified
+|
+o    3:9522278aa38d file removed
+|
+o    2:7209b1f1d793 file4.txt added
+|
+o    1:1d446e82d356 new file added
+|
+o    0:9875b412a788 dir1 created
+
+
 rep2 history:
 
 @    18:df04faa7575a merge personal-branch tip