changeset 564:d012388935fb Faradi-7.1.x

TW-26379 optimize changes collecting Take revisions of all branches into account
author Dmitry Neverov <dmitry.neverov@jetbrains.com>
date Wed, 06 Mar 2013 15:49:37 +0400
parents 81fa236998d4
children 844fc8f99c29
files mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/HgRepo.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/HgVcsRoot.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LoadDagCommand.java mercurial-server/resources/buildServerResources/dag.template mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/OperationContext.java mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/RepoFactory.java mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/ServerHgRepo.java mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CollectChangesCommand.java mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CollectChangesNoRevsets.java mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CollectChangesWithRevsets.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/HgVersionConstraint.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/ModificationDataMatcher.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/RequiredHgVersion.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/UnrelatedResitoriesTest.java mercurial-tests/testData/README mercurial-tests/testData/rep2/hg/store/00changelog.i mercurial-tests/testData/rep2/hg/store/00manifest.i mercurial-tests/testData/rep2/hg/store/data/a.i mercurial-tests/testData/rep2/hg/store/fncache mercurial-tests/testData/rep2/hg/store/undo
diffstat 22 files changed, 489 insertions(+), 30 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/HgRepo.java	Thu Jan 10 21:15:25 2013 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/HgRepo.java	Wed Mar 06 15:49:37 2013 +0400
@@ -179,6 +179,10 @@
     }
   }
 
+  public String getHgPath() {
+    return myHgPath;
+  }
+
   @Override
   public String toString() {
     return myWorkingDir.getAbsolutePath();
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/HgVcsRoot.java	Thu Jan 10 21:15:25 2013 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/HgVcsRoot.java	Wed Mar 06 15:49:37 2013 +0400
@@ -157,4 +157,8 @@
   public String getSimplifiedName() {
     return myRoot.getSimplifiedName();
   }
+
+  public VcsRoot getOriginalRoot() {
+    return myRoot;
+  }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LoadDagCommand.java	Wed Mar 06 15:49:37 2013 +0400
@@ -0,0 +1,52 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import com.intellij.openapi.util.Pair;
+import jetbrains.buildServer.util.StringUtil;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+public class LoadDagCommand extends VcsRootCommand {
+
+  private final File myDagLogTemplate;
+
+  public LoadDagCommand(@NotNull CommandSettings commandSettings,
+                        @NotNull String hgPath,
+                        @NotNull File workingDir,
+                        @NotNull AuthSettings authSettings,
+                        @NotNull File dagLogTemplate) {
+    super(commandSettings, hgPath, workingDir, authSettings);
+    myDagLogTemplate = dagLogTemplate;
+  }
+
+  @NotNull
+  public List<Pair<String, String>> call() throws VcsException {
+    List<Pair<String, String>> edges = new ArrayList<Pair<String, String>>();
+    MercurialCommandLine cli = createCommandLine();
+    cli.addParameter("log");
+    cli.addParameter("--style=" + myDagLogTemplate.getAbsolutePath());
+    CommandResult res = runCommand(cli);
+    String output = res.getStdout();
+    String fromNode = null;
+    for (String line : StringUtil.splitByLines(output)) {
+      String[] revs = line.split(" ");
+      if (revs.length == 0)
+        continue;
+      if (fromNode != null) {
+        edges.add(Pair.create(fromNode, revs[0]));
+        fromNode = null;
+      }
+      if (revs.length == 1) {
+        fromNode = revs[0];
+      } else {
+        edges.add(Pair.create(revs[0], revs[1]));
+        if (revs.length == 3)
+          edges.add(Pair.create(revs[0], revs[2]));
+      }
+    }
+    return edges;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-server/resources/buildServerResources/dag.template	Wed Mar 06 15:49:37 2013 +0400
@@ -0,0 +1,2 @@
+changeset = '{node|short} {parents}\n'
+parent = '{node|short} '
\ No newline at end of file
--- a/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java	Thu Jan 10 21:15:25 2013 +0400
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java	Wed Mar 06 15:49:37 2013 +0400
@@ -39,6 +39,7 @@
 import java.io.IOException;
 import java.util.*;
 
+import static java.util.Arrays.asList;
 import static jetbrains.buildServer.buildTriggers.vcs.mercurial.HgFileUtil.deleteDir;
 
 /**
@@ -528,6 +529,9 @@
                                                @NotNull CheckoutRules rules) throws VcsException {
     Set<String> reportedCsetIds = new HashSet<String>();
     List<ModificationData> changes = new ArrayList<ModificationData>();
+    HgVcsRoot hgRoot = myHgVcsRootFactory.createHgRoot(root);
+    OperationContext ctx = new OperationContext(this, myRepoFactory, myHgPathProvider, fromState, toState);
+    ctx.syncRepository(hgRoot);
     for (Map.Entry<String, String> entry : toState.getBranchRevisions().entrySet()) {
       String branch = entry.getKey();
       String toRevision = entry.getValue();
@@ -536,7 +540,9 @@
         fromRevision = fromState.getBranchRevisions().get(fromState.getDefaultBranchName());
       if (toRevision.equals(fromRevision))
         continue;
-      List<ModificationData> branchChanges = collectChanges(root, fromRevision, toRevision, rules);
+
+      Collection<String> fromRevisions = ctx.getFromRevisionsForBranch(hgRoot, fromRevision, toRevision);
+      List<ModificationData> branchChanges = collectChanges(hgRoot, fromRevisions, toRevision, rules);
       for (ModificationData change : branchChanges) {
         if (reportedCsetIds.add(change.getVersion()))
           changes.add(change);
@@ -555,7 +561,7 @@
     String mergeBase = getMergeBase(hgRoot, fromRootRevision, toRevision);
     if (mergeBase == null)
       return Collections.emptyList();
-    return collectChanges(toRoot, mergeBase, toRootRevision, checkoutRules);
+    return collectChanges(hgRoot, asList(mergeBase), toRootRevision, checkoutRules);
   }
 
 
@@ -593,9 +599,14 @@
   public List<ModificationData> collectChanges(@NotNull VcsRoot root, @NotNull String fromVersion, @Nullable String currentVersion, @NotNull CheckoutRules checkoutRules) throws VcsException {
     HgVcsRoot hgRoot = myHgVcsRootFactory.createHgRoot(root);
     syncRepository(hgRoot);
+    return collectChanges(hgRoot, asList(fromVersion), currentVersion, checkoutRules);
+  }
+
+
+  private List<ModificationData> collectChanges(@NotNull HgVcsRoot root, @NotNull Collection<String> fromVersions, @Nullable String currentVersion, @NotNull CheckoutRules checkoutRules) throws VcsException {
     List<ModificationData> result = new ArrayList<ModificationData>();
-    for (ChangeSet cset : getChangesets(hgRoot, fromVersion, currentVersion)) {
-      result.add(createModificationData(cset, root, checkoutRules));
+    for (ChangeSet cset : getChangesets(root, fromVersions, currentVersion)) {
+      result.add(createModificationData(cset, root.getOriginalRoot(), checkoutRules));
     }
     return result;
   }
@@ -627,20 +638,25 @@
 
 
   @NotNull
-  private List<ChangeSet> getChangesets(@NotNull final HgVcsRoot root, @NotNull final String fromVersion, @Nullable final String toVersion) throws VcsException {
+  private List<ChangeSet> getChangesets(@NotNull final HgVcsRoot root,
+                                        @NotNull final Collection<String> fromVersions,
+                                        @Nullable final String toVersion) throws VcsException {
     if (toVersion == null)
       return Collections.emptyList();
-    String fromCommit = new ChangeSetRevision(fromVersion).getId();
+    List<String> fromCommits = new ArrayList<String>();
+    for (String fromVersion : fromVersions) {
+      fromCommits.add(new ChangeSetRevision(fromVersion).getId());
+    }
     String toCommit = new ChangeSetRevision(toVersion).getId();
     try {
       List<ChangeSet> changesets = createRepo(root).collectChanges(root)
-              .fromRevision(fromCommit)
+              .fromRevision(fromCommits)
               .toRevision(toCommit)
               .call();
       Iterator<ChangeSet> iter = changesets.iterator();
       while (iter.hasNext()) {
         ChangeSet cset = iter.next();
-        if (cset.getId().equals(fromCommit))
+        if (fromVersions.contains(cset.getId()))
           iter.remove();//skip already reported changes
       }
       return changesets;
@@ -727,7 +743,7 @@
     return label.replace(':', '_').replace('\r', '_').replace('\n', '_');
   }
 
-  private File getWorkingDir(HgVcsRoot root) {
+  File getWorkingDir(HgVcsRoot root) {
     File customDir = root.getCustomWorkingDir();
     return customDir != null ? customDir : myMirrorManager.getMirrorDir(root.getRepository());
   }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/OperationContext.java	Wed Mar 06 15:49:37 2013 +0400
@@ -0,0 +1,180 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import com.intellij.openapi.util.Pair;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.AuthSettings;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.HgVcsRoot;
+import jetbrains.buildServer.util.graph.*;
+import jetbrains.buildServer.vcs.*;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.*;
+
+import static java.util.Collections.singleton;
+
+public class OperationContext {
+
+  private final MercurialVcsSupport myVcs;
+  private final RepoFactory myRepoFactory;
+  private final HgPathProvider myHgPathProvider;
+  private final RepositoryState myFromState;
+  private final RepositoryState myToState;
+  private final Map<VcsRootKey, DAG<String>> myDags = new HashMap<VcsRootKey, DAG<String>>();
+  private Set<File> mySyncedDirs = new HashSet<File>();
+  private Map<File, ServerHgRepo> myRepos = new HashMap<File, ServerHgRepo>();
+
+  public OperationContext(@NotNull MercurialVcsSupport vcs,
+                          @NotNull RepoFactory repoFactory,
+                          @NotNull HgPathProvider hgPathProvider,
+                          @NotNull RepositoryState fromState,
+                          @NotNull RepositoryState toState) {
+    myVcs = vcs;
+    myRepoFactory = repoFactory;
+    myHgPathProvider = hgPathProvider;
+    myFromState = fromState;
+    myToState = toState;
+  }
+
+  public void syncRepository(@NotNull final HgVcsRoot root) throws VcsException {
+    File dir = myVcs.getWorkingDir(root);
+    if (mySyncedDirs.contains(dir))
+      return;
+    myVcs.syncRepository(root);
+    mySyncedDirs.add(dir);
+  }
+
+  @NotNull
+  public ServerHgRepo createRepo(@NotNull File workingDir, @NotNull String hgPath, @NotNull AuthSettings authSettings) throws VcsException {
+    ServerHgRepo repo = myRepos.get(workingDir);
+    if (repo != null)
+      return repo;
+    repo = myRepoFactory.create(workingDir, hgPath, authSettings);
+    myRepos.put(workingDir, repo);
+    return repo;
+  }
+
+  /**
+   * Collecting changes is per branch, but we should take revisions of other branches
+   * into account otherwise plugin can report redundant changes. Consider the following graph:
+   *
+   * default
+   * | topic
+   * |  |
+   * V  V
+   *103
+   * | 102
+   * | |
+   *101|
+   * |\|
+   * | 100
+   * | |
+   *99 |
+   *.....
+   * | |
+   * |/
+   * 1
+   *
+   * Let's say plugin collects changes between states {default:99, topic:100} and {default: 103, topic:102}.
+   * If we run hg log -r 'ancestors(103)-ancestors(99)' it will return changes [1..103]. But changes
+   * [1..100] were probably already reported and TeamCity will not persist them.
+   *
+   * This method detects such a cases and returns (99, 100) for 103.
+   */
+  @NotNull
+  public Collection<String> getFromRevisionsForBranch(@NotNull HgVcsRoot root,
+                                                      @NotNull String fromRevision,
+                                                      @NotNull String toRevision) throws VcsException {
+    syncRepository(root);
+    ServerHgRepo repo = createRepo(myVcs.getWorkingDir(root), myHgPathProvider.getHgPath(root), root.getAuthSettings());
+
+    if (!repo.supportRevsets())
+      return singleton(fromRevision);
+
+    Set<String> fromRevisions = new HashSet<String>();
+    if (myToState.getBranchRevisions().size() > 1) {
+      VcsRootKey rootKey = VcsRootKey.create(root);
+      DAG<String> dag = myDags.get(rootKey);
+      if (dag == null) {
+        dag = repo.loadDag();
+        myDags.put(rootKey, dag);
+      }
+      FindIntervalVisitor visitor = new FindIntervalVisitor(dag, myFromState.getBranchRevisions().values());
+      dag.breadthFirstSearch(toRevision, visitor);
+      fromRevisions.addAll(visitor.getEndpoints());
+    } else {
+      fromRevisions.add(fromRevision);
+    }
+
+    if (fromRevisions.isEmpty())
+      fromRevisions.add(toRevision);
+
+    return fromRevisions;
+  }
+
+
+  private final static class VcsRootKey extends Pair<String, String> {
+    private VcsRootKey(@NotNull String repositoryUrl, @Nullable String subrepoPath) {
+      super(repositoryUrl, subrepoPath);
+    }
+
+    private static VcsRootKey create(@NotNull VcsRoot root) {
+      return new VcsRootKey(root.getProperty(Constants.REPOSITORY_PROP), null);
+    }
+  }
+
+
+  private final static class FindIntervalVisitor extends BFSVisitorAdapter<String> {
+
+    private final DAG<String> myDag;
+    private final Collection<String> myUninterestingRevisions;
+    private final Set<String> myVisitedUninterestingRevisions = new HashSet<String>();
+    private final Set<String> myEndpoints = new HashSet<String>();
+
+    private FindIntervalVisitor(@NotNull DAG<String> dag, @NotNull Collection<String> uninteresting) {
+      myDag = dag;
+      myUninterestingRevisions = uninteresting;
+    }
+
+    @Override
+    public boolean discover(@NotNull String revision) {
+      markUninteresting();
+      if (!isInteresting(revision)) {
+        addEndpoint(revision);
+        return false;
+      }
+      return true;
+    }
+
+    private void markUninteresting() {
+      if (!myVisitedUninterestingRevisions.isEmpty())
+        return;
+      for (String rev : myUninterestingRevisions) {
+        myDag.breadthFirstSearch(rev, new MarkUninterestingRevisions());
+      }
+    }
+
+    private boolean isInteresting(@NotNull String revision) {
+      return !myVisitedUninterestingRevisions.contains(revision);
+    }
+
+    private void addEndpoint(@NotNull String revision) {
+      myEndpoints.add(revision);
+    }
+
+    @NotNull
+    public Set<String> getEndpoints() {
+      return myEndpoints;
+    }
+
+    private class MarkUninterestingRevisions extends BFSVisitorAdapter<String> {
+      @Override
+      public boolean discover(@NotNull String node) {
+        if (myVisitedUninterestingRevisions.contains(node))
+          return false;
+        myVisitedUninterestingRevisions.add(node);
+        return true;
+      }
+    }
+  }
+}
--- a/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/RepoFactory.java	Thu Jan 10 21:15:25 2013 +0400
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/RepoFactory.java	Wed Mar 06 15:49:37 2013 +0400
@@ -21,6 +21,7 @@
   private final CommandSettingsFactory myCommandSettingsFactory;
   private File myLogTemplate;
   private File myLogNoFilesTemplate;
+  private File myDagTemplate;
 
   public RepoFactory(@NotNull ServerPluginConfig config,
                      @NotNull CommandSettingsFactory commandSettingsFactory) throws IOException {
@@ -28,13 +29,15 @@
     myCommandSettingsFactory = commandSettingsFactory;
     myLogTemplate = createLogTemplate();
     myLogNoFilesTemplate = createLogNoFilesTemplate();
+    myDagTemplate = createDagTemplate();
   }
 
   @NotNull
   public ServerHgRepo create(@NotNull File workingDir,
                              @NotNull String hgPath,
                              @NotNull AuthSettings authSettings) throws VcsException {
-    return new ServerHgRepo(myCommandSettingsFactory, myConfig, workingDir, hgPath, authSettings).withLogTemplates(getTemplate(), getLogNoFilesTemplate());
+    return new ServerHgRepo(myCommandSettingsFactory, myConfig, workingDir, hgPath, authSettings)
+            .withLogTemplates(getTemplate(), getLogNoFilesTemplate(), getDagTemplate());
   }
 
   public void dispose() {
@@ -63,15 +66,32 @@
     }
   }
 
+  private File getDagTemplate() throws VcsException {
+    if (myDagTemplate.isFile() && myDagTemplate.exists())
+      return myDagTemplate;
+    try {
+      myDagTemplate = createLogNoFilesTemplate();
+      return myDagTemplate;
+    } catch (IOException e) {
+      throw new VcsException("Cannot create mercurial log template", e);
+    }
+  }
+
   private File createLogTemplate() throws IOException {
-    File template = createTempFile("teamcity", "hg.log.template");
-    FileUtil.copyResource(RepoFactory.class, "/buildServerResources/log.template", template);
-    return template;
+    return createTmpTemplateFile("hg.log.template", "/buildServerResources/log.template");
   }
 
   private File createLogNoFilesTemplate() throws IOException {
-    File template = createTempFile("teamcity", "hg.short.log.template");
-    FileUtil.copyResource(RepoFactory.class, "/buildServerResources/log.no.files.template", template);
+    return createTmpTemplateFile("hg.short.log.template", "/buildServerResources/log.no.files.template");
+  }
+
+  private File createDagTemplate() throws IOException {
+    return createTmpTemplateFile("hg.dag.template", "/buildServerResources/dag.template");
+  }
+
+  private File createTmpTemplateFile(@NotNull String tmpFileSuffix, @NotNull String resourcePath) throws IOException {
+    File template = createTempFile("teamcity", tmpFileSuffix);
+    FileUtil.copyResource(RepoFactory.class, resourcePath, template);
     return template;
   }
 }
--- a/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/ServerHgRepo.java	Thu Jan 10 21:15:25 2013 +0400
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/ServerHgRepo.java	Wed Mar 06 15:49:37 2013 +0400
@@ -1,12 +1,16 @@
 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 jetbrains.buildServer.vcs.VcsRoot;
 import jetbrains.buildServer.vcs.VcsRootInstance;
 import org.jetbrains.annotations.NotNull;
 
 import java.io.File;
+import java.util.List;
 
 /**
  * @author dmitry.neverov
@@ -18,6 +22,7 @@
   private final ServerPluginConfig myConfig;
   private File myLogTemplate;
   private File myLogNoFilesTemplate;
+  private File myDagTemplate;
 
   public ServerHgRepo(@NotNull CommandSettingsFactory commandSettingsFactory,
                       @NotNull ServerPluginConfig config,
@@ -29,9 +34,12 @@
     myConfig = config;
   }
 
-  public ServerHgRepo withLogTemplates(@NotNull File logTemplate, @NotNull File logNoFilesTemplate) {
+  public ServerHgRepo withLogTemplates(@NotNull File logTemplate,
+                                       @NotNull File logNoFilesTemplate,
+                                       @NotNull File dagTemplate) {
     myLogTemplate = logTemplate;
     myLogNoFilesTemplate = logNoFilesTemplate;
+    myDagTemplate = dagTemplate;
     return this;
   }
 
@@ -73,4 +81,15 @@
     long parentId = ((VcsRootInstance) root).getParentId();
     return myConfig.getRevsetParentRootIds().contains(parentId);
   }
+
+  public boolean supportRevsets() throws VcsException {
+    HgVersion hgVersion = getHgVersion();
+    return hgVersion.isEqualsOrGreaterThan(REVSET_HG_VERSION);
+  }
+
+  @NotNull
+  public DAG<String> loadDag() throws VcsException {
+    List<Pair<String, String>> edges = new LoadDagCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir, myAuthSettings, myDagTemplate).call();
+    return DAGs.createFromEdges(edges);
+  }
 }
--- a/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CollectChangesCommand.java	Thu Jan 10 21:15:25 2013 +0400
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CollectChangesCommand.java	Wed Mar 06 15:49:37 2013 +0400
@@ -10,16 +10,16 @@
  */
 public abstract class CollectChangesCommand {
 
-  protected String myFromRevision;
+  protected List<String> myFromRevisions;
   protected String myToRevision;
 
   @NotNull
-  public abstract List<ChangeSet> call(@NotNull String fromCommit, @NotNull String toCommit) throws VcsException;
+  public abstract List<ChangeSet> call(@NotNull List<String> fromCommits, @NotNull String toCommit) throws VcsException;
 
   public abstract List<ChangeSet> call() throws VcsException;
 
-  public CollectChangesCommand fromRevision(@NotNull String fromRevision) {
-    myFromRevision = fromRevision;
+  public CollectChangesCommand fromRevision(@NotNull List<String> fromRevisions) {
+    myFromRevisions = fromRevisions;
     return this;
   }
 
--- a/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CollectChangesNoRevsets.java	Thu Jan 10 21:15:25 2013 +0400
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CollectChangesNoRevsets.java	Wed Mar 06 15:49:37 2013 +0400
@@ -26,7 +26,13 @@
 
   @Override
   public List<ChangeSet> call() throws VcsException {
-    return call(myFromRevision, myToRevision);
+    return call(myFromRevisions.get(0), myToRevision);
+  }
+
+  @NotNull
+  @Override
+  public List<ChangeSet> call(@NotNull List<String> fromCommits, @NotNull String toCommit) throws VcsException {
+    return call(fromCommits.get(0), toCommit);
   }
 
   @NotNull
--- a/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CollectChangesWithRevsets.java	Thu Jan 10 21:15:25 2013 +0400
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CollectChangesWithRevsets.java	Wed Mar 06 15:49:37 2013 +0400
@@ -19,14 +19,20 @@
 
   @Override
   public List<ChangeSet> call() throws VcsException {
-    return call(myFromRevision, myToRevision);
+    return call(myFromRevisions, myToRevision);
   }
 
   @NotNull
-  public List<ChangeSet> call(@NotNull final String fromCommit, @NotNull final String toCommit) throws VcsException {
+  public List<ChangeSet> call(@NotNull final List<String> fromCommits, @NotNull final String toCommit) throws VcsException {
+    StringBuilder revsets = new StringBuilder();
+    revsets.append("ancestors(").append(toCommit).append(") ");
+    for (String from : fromCommits) {
+      revsets.append(" - ancestors(").append(from).append(")");
+    }
+
     return myRepo.log()
             .showCommitsFromAllBranches()
-            .withRevsets("ancestors(" + toCommit + ") - ancestors(" + fromCommit + ")")
+            .withRevsets(revsets.toString())
             .call();
   }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/HgVersionConstraint.java	Wed Mar 06 15:49:37 2013 +0400
@@ -0,0 +1,37 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.TestCommandSettingsFactory;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.VersionCommand;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.exception.ParseHgVersionException;
+import org.jetbrains.annotations.NotNull;
+import org.testng.annotations.DataProvider;
+
+import java.io.File;
+import java.lang.reflect.Method;
+
+public class HgVersionConstraint {
+
+  @DataProvider
+  public static Object[][] installedHgVersion(Method testMethod) throws Exception {
+    HgVersion actualVersion = getHgVersion();
+    RequiredHgVersion constaint = testMethod.getAnnotation(RequiredHgVersion.class);
+    if (constaint == null)
+      constaint = testMethod.getDeclaringClass().getAnnotation(RequiredHgVersion.class);
+    if (constaint == null)
+      return new Object[][] { new Object[] { actualVersion }};
+    HgVersion requiredMinVersion = parse(constaint.min());
+    if (!actualVersion.isEqualsOrGreaterThan(requiredMinVersion))
+      return new Object[0][];
+    return new Object[][] { new Object[] { actualVersion }};
+  }
+
+  private static HgVersion getHgVersion() throws Exception {
+    VersionCommand versionCommand = new VersionCommand(new TestCommandSettingsFactory().create(), Util.getHgPath(), new File(".."));
+    return versionCommand.call();
+  }
+
+  private static HgVersion parse(@NotNull String version) throws ParseHgVersionException {
+    return HgVersion.parse("Mercurial Distributed SCM (version " + version);
+  }
+
+}
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java	Thu Jan 10 21:15:25 2013 +0400
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java	Wed Mar 06 15:49:37 2013 +0400
@@ -15,7 +15,6 @@
  */
 package jetbrains.buildServer.buildTriggers.vcs.mercurial;
 
-import com.intellij.execution.configurations.GeneralCommandLine;
 import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.*;
 import jetbrains.buildServer.util.TestFor;
 import jetbrains.buildServer.vcs.*;
@@ -35,13 +34,16 @@
 import java.util.*;
 
 import static com.intellij.openapi.util.io.FileUtil.*;
+import static jetbrains.buildServer.buildTriggers.vcs.mercurial.ModificationDataMatcher.modificationData;
 import static jetbrains.buildServer.buildTriggers.vcs.mercurial.Util.buildPatch;
 import static jetbrains.buildServer.buildTriggers.vcs.mercurial.Util.copyRepository;
 import static jetbrains.buildServer.buildTriggers.vcs.mercurial.VcsRootBuilder.vcsRoot;
 import static jetbrains.buildServer.util.Util.map;
 import static jetbrains.buildServer.vcs.RepositoryStateFactory.createRepositoryState;
+import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.not;
 
 @Test
 public class MercurialVcsSupportTest extends BaseMercurialTestCase {
@@ -635,6 +637,54 @@
   }
 
 
+  @RequiredHgVersion(min = "1.7.0")
+  @Test(dataProviderClass = HgVersionConstraint.class, dataProvider = "installedHgVersion")
+  public void should_not_report_redundant_changes_after_merge(@NotNull HgVersion _) throws Exception {
+    VcsRootImpl root = createVcsRoot(myRep2Path);
+    List<ModificationData> changes = myVcs.collectChanges(root,
+            createRepositoryState(map("default", "505c5b9d01e6", "personal-branch", "9ec402c74298"), "default"),
+            createRepositoryState(map("default", "df04faa7575a", "personal-branch", "9ec402c74298"), "default"),
+            CheckoutRules.DEFAULT);
+    assertEquals(changes.size(), 1);
+  }
+
+
+  @RequiredHgVersion(min = "1.7.0")
+  @Test(dataProviderClass = HgVersionConstraint.class, dataProvider = "installedHgVersion")
+  public void should_not_report_duplicate_changes(@NotNull HgVersion _) throws Exception {
+    VcsRootImpl root = createVcsRoot(myRep2Path);
+    List<ModificationData> changes = myVcs.collectChanges(root,
+            createRepositoryState(map("default", "505c5b9d01e6", "personal-branch", "96b78d73081d"), "default"),
+            createRepositoryState(map("default", "df04faa7575a", "personal-branch", "9ec402c74298"), "default"),
+            CheckoutRules.DEFAULT);
+    assertThat(changes, not(hasItem(modificationData().withVersion("dec47d2d49bf"))));
+  }
+
+
+  @RequiredHgVersion(min = "1.7.0")
+  @Test(dataProviderClass = HgVersionConstraint.class, dataProvider = "installedHgVersion")
+  public void should_not_report_duplicate_changes2(@NotNull HgVersion _) throws Exception {
+    VcsRootImpl root = createVcsRoot(myRep2Path);
+    List<ModificationData> changes = myVcs.collectChanges(root,
+            createRepositoryState(map("default", "528572bbf77b", "personal-branch", "27184c50d7ef"), "default"),
+            createRepositoryState(map("default", "4780519e01aa", "personal-branch", "fd50e4842211"), "default"),
+            CheckoutRules.DEFAULT);
+    assertThat(changes, not(hasItem(modificationData().withVersion("4dbb87c381be"))));
+    assertThat(changes.size(), is(4));
+  }
+
+
+  @RequiredHgVersion(min = "1.7.0")
+  @Test(dataProviderClass = HgVersionConstraint.class, dataProvider = "installedHgVersion")
+  public void should_not_report_all_changes_in_repository_if_default_branch_is_unrelated(@NotNull HgVersion _) throws Exception {
+    VcsRootImpl root = createVcsRoot(myRep2Path);
+    List<ModificationData> changes = myVcs.collectChanges(root,
+            createRepositoryState(map("NULL", "1f355761350e"), "NULL"),
+            createRepositoryState(map("NULL", "1f355761350e", "personal-branch", "fd50e4842211"), "NULL"),
+            CheckoutRules.DEFAULT);
+    assertEquals(0, changes.size());
+  }
+
   private void assertFiles(final List<String> expectedFiles, final ModificationData modificationData) {
     Set<String> actualFiles = new HashSet<String>();
     for (VcsChange vc: modificationData.getChanges()) {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/ModificationDataMatcher.java	Wed Mar 06 15:49:37 2013 +0400
@@ -0,0 +1,33 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import jetbrains.buildServer.vcs.ModificationData;
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+import org.jetbrains.annotations.NotNull;
+
+public class ModificationDataMatcher extends TypeSafeMatcher<ModificationData> {
+
+  private String myVersion;
+
+  @Override
+  public boolean matchesSafely(ModificationData m) {
+    if (myVersion != null && !myVersion.equals(m.getDisplayVersion()))
+      return false;
+    return true;
+  }
+
+  public void describeTo(Description description) {
+    description.appendText("modification");
+    if (myVersion != null)
+      description.appendText(" with version ").appendValue(myVersion);
+  }
+
+  public static ModificationDataMatcher modificationData() {
+    return new ModificationDataMatcher();
+  }
+
+  public ModificationDataMatcher withVersion(@NotNull String version) {
+    myVersion = version;
+    return this;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/RequiredHgVersion.java	Wed Mar 06 15:49:37 2013 +0400
@@ -0,0 +1,9 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import java.lang.annotation.ElementType;
+
+@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
+@java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, ElementType.TYPE})
+public @interface RequiredHgVersion {
+  java.lang.String min() default "1.5.2";
+}
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/UnrelatedResitoriesTest.java	Thu Jan 10 21:15:25 2013 +0400
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/UnrelatedResitoriesTest.java	Wed Mar 06 15:49:37 2013 +0400
@@ -61,7 +61,7 @@
     syncRepository();
     repositoryBecamesUnrelated();
     String currentVersion = syncRepository();
-    assertEquals(CURRENT_VERSION_OF_NEW_REPO, currentVersion);
+    assertEquals("4780519e01aa", currentVersion);
   }
 
 
--- a/mercurial-tests/testData/README	Thu Jan 10 21:15:25 2013 +0400
+++ b/mercurial-tests/testData/README	Wed Mar 06 15:49:37 2013 +0400
@@ -24,6 +24,26 @@
 
 rep2 history:
 
+o    31:1f355761350e create branch NULL (NULL) tip
+
+o    30:4780519e01aa continue default tip
+|
+| o  29:fd50e4842211 continue topic (topic)
+| |
+o |  28:737c6f57ef84 merge topic
+|\|
+| o  27:2a368008e4d9 topic2 (topic)
+| |
+| o  26:27184c50d7ef topic1 (topic)
+| |
+| o  25:4dbb87c381be start topic (topic)
+| |
+o |  24:528572bbf77b default3
+| |
+o |  23:029040d32471 default2
+| |
+o |  22:32f2afc19d75 default1
+|/
 o    21:a2672ee13212 Tag for test //add tag t1 on 43023ea3f13b
 |
 o    20:43023ea3f13b  Merge closed personal-branch
Binary file mercurial-tests/testData/rep2/hg/store/00changelog.i has changed
Binary file mercurial-tests/testData/rep2/hg/store/00manifest.i has changed
Binary file mercurial-tests/testData/rep2/hg/store/data/a.i has changed
--- a/mercurial-tests/testData/rep2/hg/store/fncache	Thu Jan 10 21:15:25 2013 +0400
+++ b/mercurial-tests/testData/rep2/hg/store/fncache	Wed Mar 06 15:49:37 2013 +0400
@@ -1,11 +1,12 @@
 data/dir4/file4.txt.i
+data/dir1/file1.txt.i
 data/dir3/file3.txt.i
-data/dir1/file1.txt.i
-data/file.txt.i
 data/.hgtags.i
 data/dir4/file42.txt.i
 data/dir5/file5.txt.i
-data/dir2/file2.txt.i
+data/file.txt.i
+data/a.i
 data/dir4/file43.txt.i
+data/dir6/file6.txt.i
 data/dir4/file41.txt.i
-data/dir6/file6.txt.i
+data/dir2/file2.txt.i
Binary file mercurial-tests/testData/rep2/hg/store/undo has changed