changeset 305:844986ea478d

Detect an update of subrepository URL and do clean checkout in this case. Because hg itself cannot do update correctly
author Dmitry Neverov <dmitry.neverov@jetbrains.com>
date Fri, 09 Sep 2011 12:05:23 +0400
parents fa88221586c9 (current diff) e9cdb499350d (diff)
children 659287a241c2
files
diffstat 52 files changed, 530 insertions(+), 182 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial-agent/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialAgentSideVcsSupport.java	Thu Sep 08 13:36:01 2011 +0400
+++ b/mercurial-agent/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialAgentSideVcsSupport.java	Fri Sep 09 12:05:23 2011 +0400
@@ -15,6 +15,7 @@
  */
 package jetbrains.buildServer.buildTriggers.vcs.mercurial;
 
+import com.intellij.openapi.util.Pair;
 import jetbrains.buildServer.agent.AgentRunningBuild;
 import jetbrains.buildServer.agent.BuildAgentConfiguration;
 import jetbrains.buildServer.agent.BuildProgressLogger;
@@ -32,7 +33,7 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.util.Collections;
+import java.util.*;
 
 import static jetbrains.buildServer.buildTriggers.vcs.mercurial.command.CommandUtil.removePrivateData;
 
@@ -48,30 +49,7 @@
   }
 
   public IncludeRuleUpdater getUpdater(@NotNull final VcsRoot vcsRoot, @NotNull final CheckoutRules checkoutRules, @NotNull final String toVersion, @NotNull final File checkoutDirectory, @NotNull final AgentRunningBuild build, boolean cleanCheckoutRequested) throws VcsException {
-    final BuildProgressLogger logger = build.getBuildLogger();
-    final boolean useLocalMirrors = isUseLocalMirrors(build);
-
-    return new IncludeRuleUpdater() {
-      public void process(@NotNull final IncludeRule includeRule, @NotNull final File workingDir) throws VcsException {
-        try {
-          checkRuleIsValid(includeRule);
-          Settings settings = new Settings(myHgPathProvider, vcsRoot);
-          if (useLocalMirrors) {
-            updateLocalMirror(vcsRoot, logger);
-          }
-          updateRepository(workingDir, settings, logger, useLocalMirrors);
-          updateWorkingDir(settings, workingDir, toVersion, logger);
-        } catch (Exception e) {
-          if (e instanceof VcsException)
-            throw (VcsException) e;
-          else
-            throw new VcsException(e);
-        }
-      }
-
-      public void dispose() throws VcsException {
-      }
-    };
+    return new MercurialIncludeRuleUpdater(myMirrorManager, myHgPathProvider, vcsRoot, toVersion, build);
   }
 
   @NotNull
@@ -85,95 +63,4 @@
   public UpdatePolicy getUpdatePolicy() {
     return this;
   }
-
-  private boolean isUseLocalMirrors(AgentRunningBuild build) {
-    String value = build.getSharedConfigParameters().get("teamcity.hg.use.local.mirrors");
-    return "true".equals(value);
-  }
-
-  private void initRepository(File workingDir, Settings settings, BuildProgressLogger logger, boolean useLocalMirrors) throws VcsException {
-    try {
-      String defaultPullUrl = getDefaultPullUrl(settings, useLocalMirrors);
-      logger.message("Init repository at " + workingDir.getAbsolutePath() + ", remote repository is " +
-              removePrivateData(defaultPullUrl, Collections.singleton(settings.getPassword())));
-      new Init(settings, workingDir, defaultPullUrl).execute();
-    } catch (IOException e) {
-      throw new VcsException("Error while initializing repository at " + workingDir.getAbsolutePath(), e);
-    }
-  }
-
-  private void updateRepository(File workingDir, Settings settings, BuildProgressLogger logger, boolean useLocalMirrors) throws VcsException, IOException {
-    if (!Settings.isValidRepository(workingDir)) {
-      initRepository(workingDir, settings, logger, useLocalMirrors);
-    } else {
-      ensureUseRightRepository(workingDir, settings, logger, useLocalMirrors);
-    }
-    String defaultPullUrl = getDefaultPullUrl(settings, useLocalMirrors);
-    logger.message("Start pulling changes from " + removePrivateData(defaultPullUrl, Collections.singleton(settings.getPassword())));
-    new PullCommand(settings, workingDir).execute();
-    logger.message("Changes successfully pulled");
-  }
-
-  private void ensureUseRightRepository(File workingDir, Settings settings, BuildProgressLogger logger, boolean useLocalMirrors) throws VcsException {
-    boolean clonedFromWrongRepository = useLocalMirrors && !isClonedFromLocalMirror(settings, workingDir)
-                                     || !useLocalMirrors && isClonedFromLocalMirror(settings, workingDir);
-
-    if (clonedFromWrongRepository) {
-      String rightRepository = useLocalMirrors ? "local mirror" : "remote repository";
-      String wrongRepository = useLocalMirrors ? "remote repository" : "local mirror";
-      logger.message("Repository in working directory is cloned from " + wrongRepository + ", clone it from " + rightRepository);
-      FileUtil.delete(workingDir);
-      initRepository(workingDir, settings, logger, useLocalMirrors);
-    }
-  }
-
-  private void updateLocalMirror(VcsRoot root, BuildProgressLogger logger) throws VcsException, IOException {
-    Settings settings = new Settings(myHgPathProvider, root);
-    File mirrorDir = myMirrorManager.getMirrorDir(settings.getRepositoryUrl());
-    logger.message("Update local mirror at " + mirrorDir);
-    if (!Settings.isValidRepository(mirrorDir)) {
-      initRepository(mirrorDir, settings, logger, false);
-    }
-    final String defaultPullUrl = getDefaultPullUrl(settings, true);
-    logger.message("Start pulling changes from " + removePrivateData(defaultPullUrl, Collections.singleton(settings.getPassword())));
-    new PullCommand(settings, mirrorDir).execute();
-    logger.message("Local mirror changes successfully pulled");
-  }
-
-  private void updateWorkingDir(final Settings settings, File workingDir, @NotNull final String version, final BuildProgressLogger logger) throws VcsException {
-    logger.message("Updating folder " + workingDir.getAbsolutePath() + " to revision " + version);
-    UpdateCommand uc = new UpdateCommand(settings, workingDir);
-    ChangeSet cs = new ChangeSet(version);
-    uc.setToId(cs.getId());
-    uc.execute();
-    logger.message("Folder successfully updated");
-  }
-
-  private String getDefaultPullUrl(Settings settings, boolean useLocalMirror) throws IOException {
-    if (useLocalMirror) {
-      File mirrorDir = myMirrorManager.getMirrorDir(settings.getRepositoryUrl());
-      return mirrorDir.getCanonicalPath();
-    } else {
-      return settings.getRepositoryUrl();
-    }
-  }
-
-  private void checkRuleIsValid(IncludeRule includeRule) throws VcsException {
-    if (includeRule.getTo() != null && includeRule.getTo().length() > 0) {
-      if (!".".equals(includeRule.getFrom()) && includeRule.getFrom().length() != 0) {
-        throw new VcsException("Invalid include rule: " + includeRule.toString() + ", Mercurial plugin supports mapping of the form: +:.=>dir only.");
-      }
-    }
-  }
-
-  public boolean isClonedFromLocalMirror(Settings settings, File workingDir) {
-    try {
-      File mirrorDir = myMirrorManager.getMirrorDir(settings.getRepositoryUrl());
-      File hgrc = new File(workingDir, ".hg" + File.separator + "hgrc");
-      String config = FileUtil.readText(hgrc);
-      return config.contains("default = " + mirrorDir.getCanonicalPath());
-    } catch (Exception e) {
-      return false;
-    }
-  }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-agent/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialIncludeRuleUpdater.java	Fri Sep 09 12:05:23 2011 +0400
@@ -0,0 +1,292 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import com.intellij.openapi.util.Pair;
+import jetbrains.buildServer.agent.AgentRunningBuild;
+import jetbrains.buildServer.agent.BuildProgressLogger;
+import jetbrains.buildServer.agent.vcs.IncludeRuleUpdater;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.*;
+import jetbrains.buildServer.util.FileUtil;
+import jetbrains.buildServer.vcs.IncludeRule;
+import jetbrains.buildServer.vcs.VcsException;
+import jetbrains.buildServer.vcs.VcsRoot;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static jetbrains.buildServer.buildTriggers.vcs.mercurial.command.CommandUtil.removePrivateData;
+
+/**
+ * @author dmitry.neverov
+ */
+public class MercurialIncludeRuleUpdater implements IncludeRuleUpdater {
+
+  private final MirrorManager myMirrorManager;
+  private final HgPathProvider myHgPathProvider;
+  private final VcsRoot myRoot;
+  private final Settings mySettings;
+  private final String myToVersion;
+  private final BuildProgressLogger myLogger;
+  private final boolean myUseLocalMirrors;
+
+
+  public MercurialIncludeRuleUpdater(@NotNull final MirrorManager mirrorManager,
+                                     @NotNull final HgPathProvider hgPathProvider,
+                                     @NotNull final VcsRoot root,
+                                     @NotNull final String toVersion,
+                                     @NotNull final AgentRunningBuild build) {
+    myMirrorManager = mirrorManager;
+    myHgPathProvider = hgPathProvider;
+    myRoot = root;
+    mySettings = new Settings(myHgPathProvider, myRoot);
+    myToVersion = toVersion;
+    myLogger = build.getBuildLogger();
+    myUseLocalMirrors = isUseLocalMirrors(build);
+  }
+
+
+  public void process(@NotNull IncludeRule rule, @NotNull File workingDir) throws VcsException {
+    try {
+      checkRuleIsValid(rule);
+      if (myUseLocalMirrors)
+        updateLocalMirror();
+      updateRepository(workingDir);
+      updateWorkingDir(workingDir);
+    } catch (Exception e) {
+      throwVcsException(e);
+    }
+  }
+
+
+  public void dispose() throws VcsException {
+  }
+
+
+  private void throwVcsException(Exception e) throws VcsException {
+    if (e instanceof VcsException)
+      throw (VcsException) e;
+    else
+      throw new VcsException(e);
+  }
+
+
+  private boolean isUseLocalMirrors(AgentRunningBuild build) {
+    String value = build.getSharedConfigParameters().get("teamcity.hg.use.local.mirrors");
+    return "true".equals(value);
+  }
+
+
+  private void initRepository(Settings settings, File workingDir, boolean useLocalMirrors) throws VcsException {
+    try {
+      String defaultPullUrl = getDefaultPullUrl(settings, useLocalMirrors);
+      myLogger.message("Init repository at " + workingDir.getAbsolutePath() + ", remote repository is " +
+              removePrivateData(defaultPullUrl, Collections.singleton(settings.getPassword())));
+      new Init(settings, workingDir, defaultPullUrl).execute();
+    } catch (IOException e) {
+      throw new VcsException("Error while initializing repository at " + workingDir.getAbsolutePath(), e);
+    }
+  }
+
+
+  private void updateRepository(File workingDir) throws VcsException, IOException {
+    if (!Settings.isValidRepository(workingDir)) {
+      initRepository(mySettings, workingDir, myUseLocalMirrors);
+    } else {
+      ensureUseRightRepository(workingDir);
+    }
+    String defaultPullUrl = getDefaultPullUrl(mySettings, myUseLocalMirrors);
+    myLogger.message("Start pulling changes from " + removePrivateData(defaultPullUrl, Collections.singleton(mySettings.getPassword())));
+    new PullCommand(mySettings, workingDir).execute();
+    myLogger.message("Changes successfully pulled");
+  }
+
+
+  private void ensureUseRightRepository(File workingDir) throws VcsException {
+    boolean clonedFromWrongRepository = myUseLocalMirrors && !isClonedFromLocalMirror(workingDir)
+                                     || !myUseLocalMirrors && isClonedFromLocalMirror(workingDir);
+
+    if (clonedFromWrongRepository) {
+      String rightRepository = myUseLocalMirrors ? "local mirror" : "remote repository";
+      String wrongRepository = myUseLocalMirrors ? "remote repository" : "local mirror";
+      myLogger.message("Repository in working directory is cloned from " + wrongRepository + ", clone it from " + rightRepository);
+      FileUtil.delete(workingDir);
+      initRepository(mySettings, workingDir, myUseLocalMirrors);
+    }
+  }
+
+
+  private void updateLocalMirror() throws VcsException, IOException {
+    Settings settings = new Settings(myHgPathProvider, myRoot);
+    File mirrorDir = myMirrorManager.getMirrorDir(settings.getRepositoryUrl());
+    myLogger.message("Update local mirror at " + mirrorDir);
+    if (!Settings.isValidRepository(mirrorDir)) {
+      initRepository(settings, mirrorDir, false);
+    }
+    final String defaultPullUrl = getDefaultPullUrl(settings, true);
+    myLogger.message("Start pulling changes from " + removePrivateData(defaultPullUrl, Collections.singleton(settings.getPassword())));
+    new PullCommand(settings, mirrorDir).execute();
+    myLogger.message("Local mirror changes successfully pulled");
+  }
+
+
+  private void updateWorkingDir(@NotNull final File workingDir) throws VcsException, IOException {
+    String workingDirRevision = getWorkingDirRevision(mySettings, workingDir);
+    if (isInitialClone(workingDirRevision)) {
+      doUpdateWorkingDir(workingDir);
+    } else {
+      Map<String, Pair<String, String>> currentSubrepos = getSubrepositories(workingDir, workingDirRevision);
+      if (currentSubrepos.isEmpty()) {
+        doUpdateWorkingDir(workingDir);
+      } else {
+        Map<String, Pair<String, String>> toVersionSubrepos = getSubrepositories(workingDir, myToVersion);
+        Map<String, Pair<String, String>> subrepositoriesWithChangedUrls = getSubrepositoriesWithChangedUrls(currentSubrepos, toVersionSubrepos);
+        if (subrepositoriesWithChangedUrls.isEmpty()) {
+          doUpdateWorkingDir(workingDir);
+        } else {
+          myLogger.warning("URLs of subrepositories were changed, do clean checkout");
+          FileUtil.delete(workingDir);
+          updateRepository(workingDir);
+          doUpdateWorkingDir(workingDir);
+        }
+      }
+    }
+  }
+
+
+  /*returns map: relative path -> (repository url, repository revision)*/
+  private Map<String, Pair<String, String>> getSubrepositories(@NotNull final File workingDir, @NotNull final String revision) throws VcsException, IOException {
+    CatCommand cc = new CatCommand(mySettings, workingDir);
+    cc.setRevId(revision);
+    try {
+      File parentDir = cc.execute(Arrays.asList(".hgsub", ".hgsubstate"), false);
+      File hgsub = new File(parentDir, ".hgsub");
+      File hgsubstate = new File(parentDir, ".hgsubstate");
+      return readSubrepositories(hgsub, hgsubstate);
+    } catch (VcsException e) {
+      return Collections.emptyMap();
+    }
+  }
+
+
+  private Map<String, Pair<String, String>> readSubrepositories(@NotNull final File hgsub, @NotNull final File hgsubstate) throws IOException {
+    if (hgsub.exists() && hgsubstate.exists()) {
+      Map<String, String> path2repo = readHgsub(hgsub);
+      Map<String, String> path2revision = readHgsubstate(hgsubstate);
+      Map<String, Pair<String, String>> result = new HashMap<String, Pair<String, String>>();
+      for (Map.Entry<String, String> entry : path2repo.entrySet()) {
+        String path = entry.getKey();
+        String repo = entry.getValue();
+        String revision = path2revision.get(path);
+        if (revision != null) {
+          result.put(path, Pair.create(repo, revision));
+        } else {
+          myLogger.warning("Cannot find revision for subrepository at path " + path + " skip it");
+        }
+      }
+      return result;
+    } else {
+      return Collections.emptyMap();
+    }
+  }
+
+
+  /*returns map: relative path -> repository url */
+  private Map<String, String> readHgsub(@NotNull final File hgsub) throws IOException {
+    Map<String, String> result = new HashMap<String, String>();
+    for (String line : FileUtil.readFile(hgsub)) {
+      String[] parts = line.split(" = ");
+      if (parts.length == 2) {
+        result.put(parts[0], parts[1]);
+      } else {
+        myLogger.warning("Cannot parse the line '" + line + "' from .hgsub, skip it");
+      }
+    }
+    return result;
+  }
+
+
+  /*returns map: relative path -> revision */
+  private Map<String, String> readHgsubstate(@NotNull final File hgsubstate) throws IOException {
+    Map<String, String> result = new HashMap<String, String>();
+    for (String line : FileUtil.readFile(hgsubstate)) {
+      String[] parts = line.split(" ");
+      if (parts.length == 2) {
+        result.put(parts[1], parts[0]);
+      } else {
+        myLogger.warning("Cannot parse the line '" + line + "' from .hgsubstate, skip it");
+      }
+    }
+    return result;
+  }
+
+
+  /*returns map repository path -> (old url, new url)*/
+  private Map<String, Pair<String, String>> getSubrepositoriesWithChangedUrls(@NotNull final Map<String, Pair<String, String>> subrepos1, @NotNull final Map<String, Pair<String, String>> subrepos2) {
+    Map<String, Pair<String, String>> result = new HashMap<String, Pair<String, String>>();
+    for (Map.Entry<String, Pair<String, String>> entry : subrepos1.entrySet()) {
+      String path = entry.getKey();
+      String url1 = entry.getValue().first;
+      Pair<String, String> urlRevision = subrepos2.get(path);
+      if (urlRevision != null && !url1.equals(urlRevision.first))
+        result.put(path, Pair.create(url1, urlRevision.first));
+    }
+    return result;
+  }
+
+
+  private boolean isInitialClone(@NotNull final String workingDirRevision) {
+    return "000000000000".equals(workingDirRevision);
+  }
+
+
+  private String getWorkingDirRevision(@NotNull final Settings settings, @NotNull final File workingDir) throws VcsException {
+    IdentifyCommand id = new IdentifyCommand(settings, workingDir);
+    id.setInLocalRepository(true);
+    return id.execute();
+  }
+
+
+  private void doUpdateWorkingDir(@NotNull final File workingDir) throws VcsException {
+    myLogger.message("Updating folder " + workingDir.getAbsolutePath() + " to revision " + myToVersion);
+    UpdateCommand uc = new UpdateCommand(mySettings, workingDir);
+    ChangeSet cs = new ChangeSet(myToVersion);
+    uc.setToId(cs.getId());
+    uc.execute();
+    myLogger.message("Folder successfully updated");
+  }
+
+
+  private String getDefaultPullUrl(Settings settings, boolean useLocalMirror) throws IOException {
+    if (useLocalMirror) {
+      File mirrorDir = myMirrorManager.getMirrorDir(settings.getRepositoryUrl());
+      return mirrorDir.getCanonicalPath();
+    } else {
+      return settings.getRepositoryUrl();
+    }
+  }
+
+
+  private void checkRuleIsValid(IncludeRule includeRule) throws VcsException {
+    if (includeRule.getTo() != null && includeRule.getTo().length() > 0) {
+      if (!".".equals(includeRule.getFrom()) && includeRule.getFrom().length() != 0) {
+        throw new VcsException("Invalid include rule: " + includeRule.toString() + ", Mercurial plugin supports mapping of the form: +:.=>dir only.");
+      }
+    }
+  }
+
+
+  public boolean isClonedFromLocalMirror(@NotNull final File workingDir) {
+    try {
+      File mirrorDir = myMirrorManager.getMirrorDir(mySettings.getRepositoryUrl());
+      File hgrc = new File(workingDir, ".hg" + File.separator + "hgrc");
+      String config = FileUtil.readText(hgrc);
+      return config.contains("default = " + mirrorDir.getCanonicalPath());
+    } catch (Exception e) {
+      return false;
+    }
+  }
+}
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CatCommand.java	Thu Sep 08 13:36:01 2011 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CatCommand.java	Fri Sep 09 12:05:23 2011 +0400
@@ -39,6 +39,10 @@
   }
 
   public File execute(List<String> relPaths) throws VcsException {
+    return execute(relPaths, true);
+  }
+
+  public File execute(List<String> relPaths, boolean checkFailure) throws VcsException {
     File tempDir;
     try {
       tempDir = FileUtil.createTempDirectory("mercurial", "catresult");
@@ -63,7 +67,7 @@
         cmdSize += path.length();
       } while (cmdSize < MAX_CMD_LEN && !paths.isEmpty());
 
-      runCommand(cli);
+      runCommand(cli, checkFailure);
     }
 
     return tempDir;
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommandUtil.java	Thu Sep 08 13:36:01 2011 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommandUtil.java	Fri Sep 09 12:05:23 2011 +0400
@@ -86,6 +86,14 @@
   }
 
   public static ExecResult runCommand(@NotNull GeneralCommandLine cli, final int executionTimeout, @NotNull Set<String> privateData) throws VcsException {
+    return runCommand(cli, executionTimeout, privateData, true);
+  }
+
+  public static ExecResult runCommand(@NotNull GeneralCommandLine cli, @NotNull Set<String> privateData, final boolean checkFailure) throws VcsException {
+    return runCommand(cli, DEFAULT_COMMAND_TIMEOUT_SEC, privateData, checkFailure);
+  }
+
+  public static ExecResult runCommand(@NotNull GeneralCommandLine cli, final int executionTimeout, @NotNull Set<String> privateData, final boolean checkFailure) throws VcsException {
     final String cmdStr = removePrivateData(cli.getCommandLineString(), privateData);
     Loggers.VCS.debug("Run command: " + cmdStr);
     final long start = System.currentTimeMillis();
@@ -103,7 +111,8 @@
 
     removePrivateData(privateData, res);
 
-    CommandUtil.checkCommandFailed(cmdStr, res);
+    if (checkFailure)
+      CommandUtil.checkCommandFailed(cmdStr, res);
     Loggers.VCS.debug("Command " + cmdStr + " output:\n" + res.getStdout());
     return res;
   }
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/IdentifyCommand.java	Thu Sep 08 13:36:01 2011 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/IdentifyCommand.java	Fri Sep 09 12:05:23 2011 +0400
@@ -65,6 +65,7 @@
     }
     ExecResult res = runCommand(cli);
     failIfNotEmptyStdErr(cli, res);
-    return res.getStdout();
+    String output = res.getStdout().trim();
+    return output.contains(" ") ? output.substring(0, output.indexOf(" ")) : output;
   }
 }
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommand.java	Thu Sep 08 13:36:01 2011 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommand.java	Fri Sep 09 12:05:23 2011 +0400
@@ -228,11 +228,6 @@
     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;
-    }
+    return identify.execute();
   }
 }
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/VcsRootCommand.java	Thu Sep 08 13:36:01 2011 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/VcsRootCommand.java	Fri Sep 09 12:05:23 2011 +0400
@@ -41,4 +41,9 @@
   protected ExecResult runCommand(@NotNull GeneralCommandLine cli, int executionTimeout) throws VcsException {
     return CommandUtil.runCommand(cli, executionTimeout, getPrivateData());
   }
+
+
+  protected ExecResult runCommand(@NotNull GeneralCommandLine cli, boolean checkFailure) throws VcsException {
+    return CommandUtil.runCommand(cli, getPrivateData(), checkFailure);
+  }
 }
--- a/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java	Thu Sep 08 13:36:01 2011 +0400
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java	Fri Sep 09 12:05:23 2011 +0400
@@ -449,6 +449,7 @@
   /**
    * Check if changeSet is present in local repository.
    * @param settings root settings
+   * @param workDir where to run a command
    * @param cset change set of interest
    * @return true if changeSet is present in local repository
    */
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/AgentSideCheckoutTest.java	Thu Sep 08 13:36:01 2011 +0400
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/AgentSideCheckoutTest.java	Fri Sep 09 12:05:23 2011 +0400
@@ -18,16 +18,12 @@
 import jetbrains.buildServer.agent.AgentRunningBuild;
 import jetbrains.buildServer.agent.BuildAgentConfiguration;
 import jetbrains.buildServer.agent.BuildProgressLogger;
-import jetbrains.buildServer.parameters.ProcessingResult;
-import jetbrains.buildServer.parameters.ReferencesResolverUtil;
-import jetbrains.buildServer.parameters.ValueResolver;
 import jetbrains.buildServer.util.FileUtil;
 import jetbrains.buildServer.vcs.CheckoutRules;
 import jetbrains.buildServer.vcs.IncludeRule;
 import jetbrains.buildServer.vcs.VcsException;
 import jetbrains.buildServer.vcs.VcsRoot;
 import jetbrains.buildServer.vcs.impl.VcsRootImpl;
-import org.jetbrains.annotations.NotNull;
 import org.jmock.Expectations;
 import org.jmock.Mockery;
 import org.testng.annotations.BeforeMethod;
@@ -46,7 +42,7 @@
 @Test
 public class AgentSideCheckoutTest extends BaseMercurialTestCase {
 
-  private final static String HG_PATH_REFERENCE = "%" + HgDetector.AGENT_HG_PATH_PROPERTY + "%";
+  final static String HG_PATH_REFERENCE = "%" + HgDetector.AGENT_HG_PATH_PROPERTY + "%";
   private MercurialAgentSideVcsSupport myVcsSupport;
   private File myWorkDir;
   private File myMirrorsRootDir;
@@ -66,6 +62,7 @@
     final BuildAgentConfiguration agentConfig = myContext.mock(BuildAgentConfiguration.class);
     myContext.checking(new Expectations() {{
       allowing(agentConfig).getCacheDirectory("mercurial"); will(returnValue(myMirrorsRootDir));
+      allowing(agentConfig).getTempDirectory(); will(returnValue(myTempFiles.createTempDir()));
       allowing(agentConfig).getParametersResolver(); will(returnValue(new HgPathResolver()));
     }});
 
@@ -240,56 +237,4 @@
   }
 
 
-  private static class HgPathResolver implements ValueResolver {
-    @NotNull
-    public ProcessingResult resolve(@NotNull String value) {
-      if (ReferencesResolverUtil.containsReference(value)) {
-        if (value.equals(HG_PATH_REFERENCE)) {
-          try {
-            return new ResolvedPath(Util.getHgPath());
-          } catch (IOException e) {
-            return new Unresolved(value);
-          }
-        } else {
-          throw new IllegalArgumentException("Value resolver is asked to resolve " + value);
-        }
-      } else {
-        return new ResolvedPath(value);
-      }
-    }
-  }
-
-  private static class ResolvedPath implements ProcessingResult {
-    private final String myPath;
-    ResolvedPath(final @NotNull String path) {
-      myPath = path;
-    }
-    public boolean isModified() {
-      return true;
-    }
-    @NotNull
-    public String getResult() {
-      return myPath;
-    }
-    public boolean isFullyResolved() {
-      return true;
-    }
-  }
-
-  private static class Unresolved implements ProcessingResult {
-    private final String myUnresolvedValue;
-    Unresolved(@NotNull final String value) {
-      myUnresolvedValue = value;
-    }
-    public boolean isModified() {
-      return false;
-    }
-    @NotNull
-    public String getResult() {
-      return myUnresolvedValue;
-    }
-    public boolean isFullyResolved() {
-      return false;
-    }
-  }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/AgentSideCheckoutWithSubreposTest.java	Fri Sep 09 12:05:23 2011 +0400
@@ -0,0 +1,105 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import com.intellij.openapi.diagnostic.Logger;
+import jetbrains.buildServer.TempFiles;
+import jetbrains.buildServer.agent.AgentRunningBuild;
+import jetbrains.buildServer.agent.BuildAgentConfiguration;
+import jetbrains.buildServer.agent.BuildProgressLogger;
+import jetbrains.buildServer.agent.vcs.UpdateByIncludeRules2;
+import jetbrains.buildServer.log.Log4jFactory;
+import jetbrains.buildServer.util.FileUtil;
+import jetbrains.buildServer.vcs.CheckoutRules;
+import jetbrains.buildServer.vcs.IncludeRule;
+import jetbrains.buildServer.vcs.VcsException;
+import jetbrains.buildServer.vcs.VcsRoot;
+import jetbrains.buildServer.vcs.impl.VcsRootImpl;
+import org.jetbrains.annotations.NotNull;
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+
+/**
+ * @author dmitry.neverov
+ */
+@Test
+public class AgentSideCheckoutWithSubreposTest {
+
+  private TempFiles myTempFiles = new TempFiles();
+  private File myOriginalRepositoriesParentDir;
+  private File myWorkDir;
+  private Mockery myContext;
+  private BuildProgressLogger myLogger;
+  private UpdateByIncludeRules2 myVcsSupport;
+  private int myBuildCounter = 0;
+  private File myR1Dir;
+
+  static {
+    Logger.setFactory(new Log4jFactory());
+  }
+
+  @BeforeMethod
+  public void setUp() throws Exception {
+    myOriginalRepositoriesParentDir = myTempFiles.createTempDir();
+    myWorkDir = new File(myOriginalRepositoriesParentDir, "agentWorkDir");
+    myContext = new Mockery();
+
+    final BuildAgentConfiguration agentConfig = myContext.mock(BuildAgentConfiguration.class);
+    myContext.checking(new Expectations() {{
+      allowing(agentConfig).getCacheDirectory("mercurial"); will(returnValue(myTempFiles.createTempDir()));
+      allowing(agentConfig).getParametersResolver(); will(returnValue(new HgPathResolver()));
+    }});
+
+    myVcsSupport = new MercurialAgentSideVcsSupport(agentConfig, new AgentHgPathProvider(agentConfig));
+
+    myLogger = myContext.mock(BuildProgressLogger.class);
+    myContext.checking(new Expectations() {{
+      allowing(myLogger).message(with(any(String.class)));
+      allowing(myLogger).warning(with(any(String.class)));
+    }});
+
+    myR1Dir = copy(new File("mercurial-tests/testData/subrepos/r1"));
+    copy(new File("mercurial-tests/testData/subrepos/r2"));
+    copy(new File("mercurial-tests/testData/subrepos/r3"));
+  }
+
+  @AfterMethod
+  public void tearDown() {
+    myTempFiles.cleanup();
+  }
+
+
+  public void subrepository_url_changed() throws Exception {
+    VcsRootImpl root = new VcsRootBuilder()
+            .repository(myR1Dir.getAbsolutePath())
+            .build();
+    doUpdate(root, "34017377d9c3");
+    doUpdate(root, "d350e7209906");
+  }
+
+
+  private void doUpdate(final VcsRoot vcsRoot, final String toVersion) throws VcsException {
+    final AgentRunningBuild build = myContext.mock(AgentRunningBuild.class, "build" + myBuildCounter++);
+    myContext.checking(new Expectations() {{
+      allowing(build).getBuildLogger(); will(returnValue(myLogger));
+      allowing(build).getSharedConfigParameters(); will(returnValue(Collections.emptyMap()));
+    }});
+    myVcsSupport.getUpdater(vcsRoot, CheckoutRules.DEFAULT, toVersion, myWorkDir, build, false).process(IncludeRule.createDefaultInstance(), myWorkDir);
+  }
+
+
+  private File copy(@NotNull File originalRepositoryDir) throws IOException {
+    String dirName = originalRepositoryDir.getName();
+    File copyDir = new File(myOriginalRepositoriesParentDir, dirName);
+    FileUtil.copyDir(originalRepositoryDir, copyDir);
+    if (new File(copyDir, "hg").isDirectory()) {
+      FileUtil.rename(new File(copyDir, "hg"), new File(copyDir, ".hg"));
+    }
+    return copyDir;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/HgPathResolver.java	Fri Sep 09 12:05:23 2011 +0400
@@ -0,0 +1,64 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import jetbrains.buildServer.parameters.ProcessingResult;
+import jetbrains.buildServer.parameters.ReferencesResolverUtil;
+import jetbrains.buildServer.parameters.ValueResolver;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+
+/**
+* @author dmitry.neverov
+*/
+class HgPathResolver implements ValueResolver {
+  @NotNull
+  public ProcessingResult resolve(@NotNull String value) {
+    if (ReferencesResolverUtil.containsReference(value)) {
+      if (value.equals(AgentSideCheckoutTest.HG_PATH_REFERENCE)) {
+        try {
+          return new ResolvedPath(Util.getHgPath());
+        } catch (IOException e) {
+          return new Unresolved(value);
+        }
+      } else {
+        throw new IllegalArgumentException("Value resolver is asked to resolve " + value);
+      }
+    } else {
+      return new ResolvedPath(value);
+    }
+  }
+
+  private static class ResolvedPath implements ProcessingResult {
+    private final String myPath;
+    ResolvedPath(final @NotNull String path) {
+      myPath = path;
+    }
+    public boolean isModified() {
+      return true;
+    }
+    @NotNull
+    public String getResult() {
+      return myPath;
+    }
+    public boolean isFullyResolved() {
+      return true;
+    }
+  }
+
+  private static class Unresolved implements ProcessingResult {
+    private final String myUnresolvedValue;
+    Unresolved(@NotNull final String value) {
+      myUnresolvedValue = value;
+    }
+    public boolean isModified() {
+      return false;
+    }
+    @NotNull
+    public String getResult() {
+      return myUnresolvedValue;
+    }
+    public boolean isFullyResolved() {
+      return false;
+    }
+  }
+}
--- a/mercurial-tests/src/testng.xml	Thu Sep 08 13:36:01 2011 +0400
+++ b/mercurial-tests/src/testng.xml	Fri Sep 09 12:05:23 2011 +0400
@@ -10,6 +10,7 @@
       <class name="jetbrains.buildServer.buildTriggers.vcs.mercurial.command.IdentifyCommandTest"/>
       <class name="jetbrains.buildServer.buildTriggers.vcs.mercurial.MercurialVcsSupportTest"/>
       <class name="jetbrains.buildServer.buildTriggers.vcs.mercurial.AgentSideCheckoutTest"/>
+      <class name="jetbrains.buildServer.buildTriggers.vcs.mercurial.AgentSideCheckoutWithSubreposTest"/>
       <class name="jetbrains.buildServer.buildTriggers.vcs.mercurial.SettingsTest"/>
       <class name="jetbrains.buildServer.buildTriggers.vcs.mercurial.MirrorManagerTest"/>
       <class name="jetbrains.buildServer.buildTriggers.vcs.mercurial.HgVersionTest"/>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/testData/subrepos/README	Fri Sep 09 12:05:23 2011 +0400
@@ -0,0 +1,11 @@
+r1 history:
+3:d350e7209906 Add different subrepository in the same path <- subrepository r2 = ../r3 (9e4a2fef1a1c)
+2:4d7b3db8779f Remove subrepository                         <- subrepository removed
+1:34017377d9c3 Add subrepository                            <- subrepository r2 = ../r2 (916933c1dd8e)
+0:e4eced2b7381 Initial commit
+
+r2 history:
+0:916933c1dd8e Initial commit
+
+r3 history:
+0:9e4a2fef1a1c Initial commit
\ No newline at end of file
Binary file mercurial-tests/testData/subrepos/r1/hg/00changelog.i has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/testData/subrepos/r1/hg/branchheads.cache	Fri Sep 09 12:05:23 2011 +0400
@@ -0,0 +1,2 @@
+4d7b3db8779f75e0ca452fdd9a057a5c69665aa3 2
+4d7b3db8779f75e0ca452fdd9a057a5c69665aa3 default
Binary file mercurial-tests/testData/subrepos/r1/hg/dirstate has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/testData/subrepos/r1/hg/last-message.txt	Fri Sep 09 12:05:23 2011 +0400
@@ -0,0 +1,1 @@
+Add different subrepository in the same path
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/testData/subrepos/r1/hg/requires	Fri Sep 09 12:05:23 2011 +0400
@@ -0,0 +1,3 @@
+revlogv1
+store
+fncache
Binary file mercurial-tests/testData/subrepos/r1/hg/store/00changelog.i has changed
Binary file mercurial-tests/testData/subrepos/r1/hg/store/00manifest.i has changed
Binary file mercurial-tests/testData/subrepos/r1/hg/store/data/.hgsub.i has changed
Binary file mercurial-tests/testData/subrepos/r1/hg/store/data/.hgsubstate.i has changed
Binary file mercurial-tests/testData/subrepos/r1/hg/store/data/a.i has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/testData/subrepos/r1/hg/store/fncache	Fri Sep 09 12:05:23 2011 +0400
@@ -0,0 +1,3 @@
+data/a.i
+data/.hgsub.i
+data/.hgsubstate.i
Binary file mercurial-tests/testData/subrepos/r1/hg/store/undo has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/testData/subrepos/r1/hg/tags.cache	Fri Sep 09 12:05:23 2011 +0400
@@ -0,0 +1,2 @@
+1 34017377d9c3d7bbcf665f845cce1e41c30bf4e9
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/testData/subrepos/r1/hg/undo.branch	Fri Sep 09 12:05:23 2011 +0400
@@ -0,0 +1,1 @@
+default
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/testData/subrepos/r1/hg/undo.desc	Fri Sep 09 12:05:23 2011 +0400
@@ -0,0 +1,2 @@
+3
+commit
Binary file mercurial-tests/testData/subrepos/r1/hg/undo.dirstate has changed
Binary file mercurial-tests/testData/subrepos/r2/hg/00changelog.i has changed
Binary file mercurial-tests/testData/subrepos/r2/hg/dirstate has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/testData/subrepos/r2/hg/last-message.txt	Fri Sep 09 12:05:23 2011 +0400
@@ -0,0 +1,1 @@
+Initial commit
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/testData/subrepos/r2/hg/requires	Fri Sep 09 12:05:23 2011 +0400
@@ -0,0 +1,3 @@
+revlogv1
+store
+fncache
Binary file mercurial-tests/testData/subrepos/r2/hg/store/00changelog.i has changed
Binary file mercurial-tests/testData/subrepos/r2/hg/store/00manifest.i has changed
Binary file mercurial-tests/testData/subrepos/r2/hg/store/data/b.i has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/testData/subrepos/r2/hg/store/fncache	Fri Sep 09 12:05:23 2011 +0400
@@ -0,0 +1,1 @@
+data/b.i
Binary file mercurial-tests/testData/subrepos/r2/hg/store/undo has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/testData/subrepos/r2/hg/undo.branch	Fri Sep 09 12:05:23 2011 +0400
@@ -0,0 +1,1 @@
+default
\ No newline at end of file
Binary file mercurial-tests/testData/subrepos/r2/hg/undo.dirstate has changed
Binary file mercurial-tests/testData/subrepos/r3/hg/00changelog.i has changed
Binary file mercurial-tests/testData/subrepos/r3/hg/dirstate has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/testData/subrepos/r3/hg/last-message.txt	Fri Sep 09 12:05:23 2011 +0400
@@ -0,0 +1,1 @@
+Initial commit
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/testData/subrepos/r3/hg/requires	Fri Sep 09 12:05:23 2011 +0400
@@ -0,0 +1,3 @@
+revlogv1
+store
+fncache
Binary file mercurial-tests/testData/subrepos/r3/hg/store/00changelog.i has changed
Binary file mercurial-tests/testData/subrepos/r3/hg/store/00manifest.i has changed
Binary file mercurial-tests/testData/subrepos/r3/hg/store/data/c.i has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/testData/subrepos/r3/hg/store/fncache	Fri Sep 09 12:05:23 2011 +0400
@@ -0,0 +1,1 @@
+data/c.i
Binary file mercurial-tests/testData/subrepos/r3/hg/store/undo has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/testData/subrepos/r3/hg/tags.cache	Fri Sep 09 12:05:23 2011 +0400
@@ -0,0 +1,2 @@
+0 9e4a2fef1a1c04623a0d89edb4f3ba290cf2666e
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/testData/subrepos/r3/hg/undo.branch	Fri Sep 09 12:05:23 2011 +0400
@@ -0,0 +1,1 @@
+default
\ No newline at end of file
Binary file mercurial-tests/testData/subrepos/r3/hg/undo.dirstate has changed