changeset 181:0ea2ad14ce97

Add local mirrors for agent checkout. To turn them on set agent property teamcity.hg.use.local.mirrors = true.
author Dmitry Neverov <dmitry.neverov@jetbrains.com>
date Tue, 01 Mar 2011 17:55:41 +0300
parents d94b260c4808
children 875e1cb83717
files mercurial-agent/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialAgentSideVcsSupport.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MirrorManager.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/BaseCommand.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/BranchesCommand.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CatCommand.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/ChangedFilesCommand.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CloneCommand.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/IdentifyCommand.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommand.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/PullCommand.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/PushCommand.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/Settings.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/StatusCommand.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/TagCommand.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/UpdateCommand.java mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java mercurial-tests/lib/hamcrest-core-1.1.jar mercurial-tests/lib/hamcrest-library-1.1.jar mercurial-tests/lib/jmock-2.5.1.jar mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/AgentSideCheckoutTest.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MirrorManagerTest.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/SettingsTest.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/BaseCommandTestCase.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommandTest.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/PushCommandTest.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/StatusCommandTest.java mercurial-tests/src/testng.xml mercurial.ipr mercurial.xml
diffstat 30 files changed, 766 insertions(+), 287 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial-agent/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialAgentSideVcsSupport.java	Wed Feb 16 13:35:57 2011 +0300
+++ b/mercurial-agent/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialAgentSideVcsSupport.java	Tue Mar 01 17:55:41 2011 +0300
@@ -15,10 +15,12 @@
  */
 package jetbrains.buildServer.buildTriggers.vcs.mercurial;
 
+import jetbrains.buildServer.agent.AgentRunningBuild;
+import jetbrains.buildServer.agent.BuildAgentConfiguration;
 import jetbrains.buildServer.agent.BuildProgressLogger;
 import jetbrains.buildServer.agent.vcs.AgentVcsSupport;
 import jetbrains.buildServer.agent.vcs.IncludeRuleUpdater;
-import jetbrains.buildServer.agent.vcs.UpdateByIncludeRules;
+import jetbrains.buildServer.agent.vcs.UpdateByIncludeRules2;
 import jetbrains.buildServer.agent.vcs.UpdatePolicy;
 import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.*;
 import jetbrains.buildServer.util.FileUtil;
@@ -29,63 +31,40 @@
 import org.jetbrains.annotations.NotNull;
 
 import java.io.File;
-import java.io.IOException;
 
-public class MercurialAgentSideVcsSupport extends AgentVcsSupport implements UpdateByIncludeRules {
-  private void updateWorkingDir(final Settings settings, final String version, final BuildProgressLogger logger) throws VcsException {
-    logger.message("Updating working directory from the local repository copy");
-    UpdateCommand uc = new UpdateCommand(settings);
-    ChangeSet cs = new ChangeSet(version);
-    uc.setToId(cs.getId());
-    uc.execute();
-    logger.message("Working directory updated successfully");
+public class MercurialAgentSideVcsSupport extends AgentVcsSupport implements UpdateByIncludeRules2 {
+
+  private final MirrorManager myMirrorManager;
+
+  public MercurialAgentSideVcsSupport(BuildAgentConfiguration agentConfiguration) {
+    myMirrorManager = new MirrorManager(agentConfiguration.getCacheDirectory("mercurial"));
   }
 
-  private File cloneRepository(final Settings settings) throws VcsException {
-    File tempDir;
-    try {
-      File parent = FileUtil.createTempDirectory("hg", "clone");
-      parent.deleteOnExit();
-      tempDir = new File(parent, "hg");
-    } catch (IOException e) {
-      throw new VcsException("Failed to create temp directory: " + e.getLocalizedMessage());
-    }
+  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 {
+        checkRuleIsValid(includeRule);
+        Settings settings = new Settings(vcsRoot);
+        if (useLocalMirrors) updateLocalMirror(vcsRoot, logger);
+        if (settings.isValidRepository(workingDir)) {
+          updateRepository(workingDir, settings, logger, useLocalMirrors);
+        } else {
+          cloneRepository(workingDir, settings, logger, useLocalMirrors);
+        }
+        updateWorkingDir(settings, workingDir, toVersion, logger);
+      }
 
-    CloneCommand cc = new CloneCommand(settings);
-    cc.setDestDir(tempDir.getAbsolutePath());
-
-    cc.setUpdateWorkingDir(false);
-    cc.execute();
-    return tempDir;
+      public void dispose() throws VcsException {
+      }
+    };
   }
 
-  /**
-   * Moves files from one directory to another with all subdirectories.
-   * Removes old directory if it became empty.
-   */
-  private static boolean moveDir(File oldDir, File newDir) {
-    // both old and new directories exist
-    boolean moveSuccessful = true;
-    final File[] files = oldDir.listFiles();
-    if (files != null) {
-      for (File file: files) {
-        if (file.isFile()) {
-          File destFile = new File(newDir, file.getName());
-          destFile.getParentFile().mkdirs();
-          if (!file.renameTo(destFile)) {
-            moveSuccessful = false;
-          }
-        } else if (!moveDir(file, new File(newDir, file.getName()))) {
-          moveSuccessful = false;
-        }
-      }
-    }
-
-    if (moveSuccessful) {
-      FileUtil.deleteIfEmpty(oldDir);
-    }
-
-    return moveSuccessful;
+  @NotNull
+  @Override
+  public String getName() {
+    return Constants.VCS_NAME;
   }
 
   @NotNull
@@ -94,50 +73,110 @@
     return this;
   }
 
-  @NotNull
-  @Override
-  public String getName() {
-    return Constants.VCS_NAME;
+  private boolean isUseLocalMirrors(AgentRunningBuild build) {
+    String value = build.getSharedConfigParameters().get("teamcity.hg.use.local.mirrors");
+    return "true".equals(value);
   }
 
-  public IncludeRuleUpdater getUpdater(@NotNull final VcsRoot vcsRoot, @NotNull final CheckoutRules checkoutRules, @NotNull final String toVersion, @NotNull final File checkoutDirectory, @NotNull final BuildProgressLogger logger) throws VcsException {
-    return new IncludeRuleUpdater() {
-      public void process(@NotNull final IncludeRule includeRule, @NotNull final File workingDir) 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.");
-          }
-        }
+  private void cloneRepository(File workingDir, Settings settings, BuildProgressLogger logger, boolean useLocalMirrors) throws VcsException {
+    String message = "No repository in working directory found, start cloning from " + (useLocalMirrors ? "local mirror" : "remote repository");
+    logger.message(message);
+    if (useLocalMirrors) {
+      cloneFromLocalMirror(settings, workingDir);
+    } else {
+      cloneFromRemote(settings, workingDir);
+    }
+    logger.message("Repository successfully cloned to working directory: " + workingDir.getAbsolutePath());
+  }
 
-        Settings settings = new Settings(workingDir, vcsRoot);
-        settings.setWorkingDir(workingDir);
-        if (settings.hasCopyOfRepository()) {
-          // execute pull command
-          logger.message("Repository in working directory found, start pulling changes");
-          PullCommand pc = new PullCommand(settings);
-          pc.execute();
-          logger.message("Changes successfully pulled");
-        } else {
-          // execute clone command
-          logger.message("No repository in working directory found, start cloning repository to temporary folder");
-          File parentDir = cloneRepository(settings);
-          logger.message("Repository successfully cloned to: " + parentDir.getAbsolutePath());
-          logger.message("Moving repository to working directory: " + workingDir.getAbsolutePath());
-          if (!moveDir(parentDir, workingDir)) {
-            File hgDir = new File(workingDir, ".hg");
-            if (hgDir.isDirectory()) {
-              FileUtil.delete(hgDir);
-            }
-            throw new VcsException("Failed to move directory content: " + parentDir.getAbsolutePath() + " to: " + workingDir.getAbsolutePath());
-          }
+  private void updateRepository(File workingDir, Settings settings, BuildProgressLogger logger, boolean useLocalMirrors) throws VcsException {
+    if (useLocalMirrors) {
+      if (isClonedFromLocalMirror(settings, workingDir)) {
+        logger.message("Repository in working directory found, start pulling changes");
+        new PullCommand(settings, workingDir).execute();
+        logger.message("Changes successfully pulled");
+      } else {
+        logger.message("Repository in working directory is cloned from remote repository, clone it from local mirror");
+        FileUtil.delete(workingDir);
+        cloneFromLocalMirror(settings, workingDir);
+      }
+    } else {
+      if (isClonedFromLocalMirror(settings, workingDir)) {
+        logger.message("Repository in working directory is cloned from local mirror, clone it from remote repository");
+        FileUtil.delete(workingDir);
+        cloneFromRemote(settings, workingDir);
+      } else {
+        logger.message("Repository in working directory found, start pulling changes");
+        new PullCommand(settings, workingDir).execute();
+        logger.message("Changes successfully pulled");
+      }
+    }
+  }
 
-          logger.message("Repository successfully moved to working directory: " + workingDir.getAbsolutePath());
-        }
-        updateWorkingDir(settings, toVersion, logger);
+  private void updateLocalMirror(VcsRoot root, BuildProgressLogger logger) throws VcsException {
+    Settings settings = new Settings(root);
+    File mirrorDir = myMirrorManager.getMirrorDir(settings.getRepositoryUrl());
+    if (Settings.isValidRepository(mirrorDir)) {
+      logger.message("Start pulling changes to local mirror at " + mirrorDir);
+      new PullCommand(settings, mirrorDir).execute();
+      logger.message("Local mirror changes successfully pulled");
+    } else {
+      logger.message("No local mirror found for " + settings.getRepositoryUrl() + ", create mirror at " + mirrorDir.getAbsolutePath());
+      logger.message("Clone local mirror at " + mirrorDir);
+      cloneRepository(settings, mirrorDir, settings.getRepositoryUrl());
+      logger.message("Local mirror successfully cloned to " + mirrorDir);
+    }
+  }
+
+  private void updateWorkingDir(final Settings settings, File workingDir, final String version, final BuildProgressLogger logger) throws VcsException {
+    logger.message("Updating working directory from the local repository copy");
+    UpdateCommand uc = new UpdateCommand(settings, workingDir);
+    ChangeSet cs = new ChangeSet(version);
+    uc.setToId(cs.getId());
+    uc.execute();
+    logger.message("Working directory updated successfully");
+  }
+
+  private void cloneFromLocalMirror(final Settings settings, File workingDir) throws VcsException {
+    File mirrorDir = myMirrorManager.getMirrorDir(settings.getRepositoryUrl());
+    try {
+      cloneRepository(settings, workingDir, mirrorDir.getCanonicalPath());
+    } catch (Exception e) {
+      throw new VcsException("Failed to clone from local mirror at " + mirrorDir.getAbsolutePath(), e);
+    }
+  }
+
+  private void cloneFromRemote(final Settings settings, File workingDir) throws VcsException {
+    try {
+      cloneRepository(settings, workingDir, settings.getRepositoryUrl());
+    } catch (Exception e) {
+      throw new VcsException("Failed to clone from remote repository " + settings.getRepositoryUrl(), e);
+    }
+  }
+
+  private void cloneRepository(final Settings settings, File workingDir, String url) throws VcsException {
+    CloneCommand cc = new CloneCommand(settings, workingDir);
+    cc.setRepository(url);
+    cc.setUpdateWorkingDir(false);
+    cc.execute();
+  }
+
+  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 void dispose() throws VcsException {
-      }
-    };
+  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-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MirrorManager.java	Tue Mar 01 17:55:41 2011 +0300
@@ -0,0 +1,237 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import com.intellij.openapi.diagnostic.Logger;
+import jetbrains.buildServer.util.FileUtil;
+import jetbrains.buildServer.util.Hash;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/**
+ * Manages local mirrors of remote repositories.
+ * Each unique url get unique local mirror. Each mirror is used for one url only.
+ * @author dmitry.neverov
+ */
+public final class MirrorManager {
+
+  private static Logger LOG = Logger.getInstance(MirrorManager.class.getName());
+  private static final String MIRROR_DIR_PREFIX = "hg_";
+  private static final String MAPPING_FILE_NAME = "map";
+
+  private final ReadWriteLock myLock = new ReentrantReadWriteLock();
+  private final File myRootDir;
+  /*Only one thread read or write to this file, it is protected by myLock.writeLock()*/
+  private final File myMappingFile;
+  /*Protected by myLock*/
+  private final Map<String, File> myMirrors = new HashMap<String, File>();
+  private HashCalculator myHash = new StandartHash();
+
+  /**
+   * @param rootDir root directory where all mirrors are stored
+   */
+  public MirrorManager(File rootDir) {
+    myRootDir = rootDir;
+    myMappingFile = new File(myRootDir, MAPPING_FILE_NAME);
+    readMappingFromFile();
+  }
+
+
+  /**
+   * Get directory of local mirror repository for specified url, if directory is not exists it is created
+   * @param url url of interest
+   * @return see above
+   */
+  @NotNull
+  public File getMirrorDir(@NotNull final String url) {
+    File result = getMirrorDirWithLock(url);
+    if (result == null) {
+      result = createDirFor(url);
+    }
+    return result;
+  }
+
+
+  /**
+   * Get all local mirror repository dirs
+   * @return see above
+   */
+  @NotNull
+  public List<File> getMirrors() {
+    myLock.readLock().lock();
+    try {
+      return new ArrayList<File>(myMirrors.values());
+    } finally {
+      myLock.readLock().unlock();
+    }
+  }
+
+
+  //for tests only
+  void setHashCalculator(HashCalculator hash) {
+    myHash = hash;
+  }
+
+
+  private File createDirFor(String url) {
+    File result;
+    myLock.writeLock().lock();
+    try {
+      File mirrorDir = getUniqueDir(url);
+      result = saveMappingIfAbsent(url, mirrorDir);
+    } finally {
+      myLock.writeLock().unlock();
+    }
+    if (!result.exists()) {
+      result.mkdirs();
+    }
+    return result;
+  }
+
+
+  private File getMirrorDirWithLock(String url) {
+    myLock.readLock().lock();
+    try {
+      return myMirrors.get(url);
+    } finally {
+      myLock.readLock().unlock();
+    }
+  }
+
+
+  //should be called with myLock.writeLock() held
+  private File saveMappingIfAbsent(String url, File mirrorDir) {
+    File existing = myMirrors.get(url);
+    if (existing != null) {
+      return existing;
+    } else {
+      myMirrors.put(url, mirrorDir);
+      saveMappingToFile();
+      return mirrorDir;
+    }
+  }
+
+
+  private File getUniqueDir(String url) {
+    myLock.readLock().lock();
+    try {
+      String dirName = MIRROR_DIR_PREFIX + hash(normalize(url));
+      File result = PathUtil.getCanonicalFile(new File(myRootDir, dirName));
+      while (isUsedForOtherUrl(result, url)) {
+        dirName = MIRROR_DIR_PREFIX + hash(result.getName());
+        result = PathUtil.getCanonicalFile(new File(myRootDir, dirName));
+      }
+      return result;
+    } finally {
+      myLock.readLock().unlock();
+    }
+  }
+
+
+  private boolean isUsedForOtherUrl(File repositoryDir, String url) {
+    myLock.readLock().lock();
+    try {
+      for (Map.Entry<String, File> mirror : myMirrors.entrySet()) {
+        String mirrorUrl = mirror.getKey();
+        File mirrorDir = mirror.getValue();
+        if (mirrorDir.equals(repositoryDir) && !mirrorUrl.equals(url)) {
+          return true;
+        }
+      }
+      return false;
+    } finally {
+      myLock.readLock().unlock();
+    }
+  }
+
+
+  private String hash(String value) {
+    return String.valueOf(myHash.calc(value));
+  }
+
+
+  private static String normalize(final String path) {
+    String normalized = PathUtil.normalizeSeparator(path);
+    if (path.endsWith("/")) {
+      return normalized.substring(0, normalized.length()-1);
+    }
+    return normalized;
+  }
+
+
+  private void readMappingFromFile() {
+    myLock.writeLock().lock();
+    try {
+      LOG.debug("Parse mapping file " + myMappingFile.getAbsolutePath());
+      for (String line : readLines()) {
+        int separatorIndex = line.lastIndexOf(" = ");
+        if (separatorIndex == -1) {
+          LOG.error("Cannot parse mapping '" + line + "', skip it.");
+        } else {
+          String url = line.substring(0, separatorIndex);
+          String dirName = line.substring(separatorIndex + 3);
+          File repositoryDir = PathUtil.getCanonicalFile(new File(myRootDir, dirName));
+          if (isUsedForOtherUrl(repositoryDir, url)) {
+            LOG.error("Skip mapping " + line + ": " + dirName + " is used for url other than " + url);
+          } else {
+            myMirrors.put(url, PathUtil.getCanonicalFile(new File(myRootDir, dirName)));
+          }
+        }
+      }
+    } finally {
+      myLock.writeLock().unlock();
+    }
+  }
+
+  /*Should be called with myLock.writeLock() held*/
+  private List<String> readLines() {
+    if (myMappingFile.exists()) {
+      try {
+        return FileUtil.readFile(myMappingFile);
+      } catch (IOException e) {
+        LOG.error("Error while reading a mapping file at " + myMappingFile.getAbsolutePath() + " starting with empty mapping", e);
+        return new ArrayList<String>();
+      }
+    } else {
+      LOG.info("No mapping file found at " + myMappingFile.getAbsolutePath() + " starting with empty mapping");
+      File parentDir = myMappingFile.getParentFile();
+      if (!parentDir.exists() && !parentDir.mkdirs()) {
+        LOG.error("Cannot create local mirrors dir at " + parentDir.getAbsolutePath());
+      }
+      return new ArrayList<String>();
+    }
+  }
+
+
+  private void saveMappingToFile() {
+    myLock.writeLock().lock();
+    try {
+      StringBuilder sb = new StringBuilder();
+      for (Map.Entry<String, File> mirror : myMirrors.entrySet()) {
+        String url = mirror.getKey();
+        String dir = mirror.getValue().getName();
+        sb.append(url).append(" = ").append(dir).append("\n");
+      }
+      FileUtil.writeFile(myMappingFile, sb.toString());
+    } finally {
+      myLock.writeLock().unlock();
+    }
+  }
+
+
+  final static class StandartHash implements HashCalculator {
+    public long calc(String value) {
+      return Hash.calc(value);
+    }
+  }
+
+  public static interface HashCalculator {
+    long calc(String value);
+  }
+}
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/BaseCommand.java	Wed Feb 16 13:35:57 2011 +0300
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/BaseCommand.java	Tue Mar 01 17:55:41 2011 +0300
@@ -22,6 +22,7 @@
 import jetbrains.buildServer.vcs.VcsException;
 import org.jetbrains.annotations.NotNull;
 
+import java.io.File;
 import java.util.Collections;
 import java.util.Set;
 
@@ -32,11 +33,12 @@
   private Settings mySettings;
   private String myWorkDirectory;
 
-  public BaseCommand(@NotNull final Settings settings) {
+  public BaseCommand(@NotNull final Settings settings, @NotNull File workingDir) {
     mySettings = settings;
-    myWorkDirectory = settings.getLocalRepositoryDir().getAbsolutePath();
+    myWorkDirectory = workingDir.getAbsolutePath();
   }
 
+
   public Settings getSettings() {
     return mySettings;
   }
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/BranchesCommand.java	Wed Feb 16 13:35:57 2011 +0300
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/BranchesCommand.java	Tue Mar 01 17:55:41 2011 +0300
@@ -20,18 +20,20 @@
 import jetbrains.buildServer.vcs.VcsException;
 import org.jetbrains.annotations.NotNull;
 
+import java.io.File;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import java.util.regex.Matcher;
 
 /**
  * @author Pavel.Sher
  *         Date: 26.10.2008
  */
 public class BranchesCommand extends BaseCommand {
-  public BranchesCommand(@NotNull final Settings settings) {
-    super(settings);
+
+  public BranchesCommand(@NotNull Settings settings, @NotNull File workingDir) {
+    super(settings, workingDir);
   }
 
   /**
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CatCommand.java	Wed Feb 16 13:35:57 2011 +0300
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CatCommand.java	Tue Mar 01 17:55:41 2011 +0300
@@ -30,8 +30,8 @@
   private String myRevId;
   private final static int MAX_CMD_LEN = 900;
 
-  public CatCommand(@NotNull final Settings settings) {
-    super(settings);
+  public CatCommand(@NotNull Settings settings, @NotNull File workingDir) {
+    super(settings, workingDir);
   }
 
   public void setRevId(final String revId) {
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/ChangedFilesCommand.java	Wed Feb 16 13:35:57 2011 +0300
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/ChangedFilesCommand.java	Tue Mar 01 17:55:41 2011 +0300
@@ -31,8 +31,8 @@
 public class ChangedFilesCommand extends BaseCommand {
   private String myRevId;
 
-  public ChangedFilesCommand(@NotNull final Settings settings) {
-    super(settings);
+  public ChangedFilesCommand(@NotNull Settings settings, @NotNull File workingDir) {
+    super(settings, workingDir);
   }
 
   public void setRevId(@NotNull final String revId) {
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CloneCommand.java	Wed Feb 16 13:35:57 2011 +0300
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CloneCommand.java	Tue Mar 01 17:55:41 2011 +0300
@@ -23,13 +23,14 @@
 import java.io.File;
 
 public class CloneCommand extends BaseCommand{
-  private String myDestDir;
   private String myToId;
   private boolean myUpdateWorkingDir = true;
   private String myRepository;
+  private File myWorkingDir;
 
-  public CloneCommand(@NotNull final Settings settings) {
-    super(settings);
+  public CloneCommand(@NotNull Settings settings, @NotNull File workingDir) {
+    super(settings, workingDir);
+    myWorkingDir = workingDir;
     myRepository = getSettings().getRepositoryUrl();
   }
 
@@ -41,10 +42,6 @@
     myRepository = repo;
   }
 
-  public void setDestDir(@NotNull String destDir) {
-    myDestDir = destDir;
-  }
-
   public void setToId(final String toId) {
     myToId = toId;
   }
@@ -54,10 +51,8 @@
   }
 
   public void execute() throws VcsException {
-    if (myDestDir == null) throw new IllegalStateException("Destination dir must be specified");
     GeneralCommandLine cli = createCommandLine();
-    File dir = new File(myDestDir);
-    File parent = dir.getParentFile();
+    File parent = myWorkingDir.getParentFile();
     cli.setWorkDirectory(parent.getAbsolutePath());
     cli.addParameter("clone");
     if (myToId != null) {
@@ -72,7 +67,7 @@
       cli.addParameter("--uncompressed");
     }
     cli.addParameter(myRepository);
-    cli.addParameter(dir.getName());
+    cli.addParameter(myWorkingDir.getName());
 
     ExecResult res = runCommand(cli, 24*3600); // some repositories are quite large, we set timeout to 24 hours
     failIfNonZeroExitCode(cli, res);
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/IdentifyCommand.java	Wed Feb 16 13:35:57 2011 +0300
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/IdentifyCommand.java	Tue Mar 01 17:55:41 2011 +0300
@@ -20,13 +20,16 @@
 import jetbrains.buildServer.vcs.VcsException;
 import org.jetbrains.annotations.NotNull;
 
+import java.io.File;
+
 /**
  * @author Pavel.Sher
  *         Date: 16.07.2008
  */
 public class IdentifyCommand extends BaseCommand {
-  public IdentifyCommand(@NotNull final Settings settings) {
-    super(settings);
+
+  public IdentifyCommand(@NotNull Settings settings, @NotNull File workingDir) {
+    super(settings, workingDir);
   }
 
   public String execute() throws VcsException {
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommand.java	Wed Feb 16 13:35:57 2011 +0300
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommand.java	Tue Mar 01 17:55:41 2011 +0300
@@ -21,6 +21,7 @@
 import jetbrains.buildServer.vcs.VcsException;
 import org.jetbrains.annotations.NotNull;
 
+import java.io.File;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
@@ -42,8 +43,8 @@
   private static final String DESCRIPTION_PREFIX = "description:";
   private static final String FILES_PREFIX = "files:";
 
-  public LogCommand(@NotNull Settings settings) {
-    super(settings);
+  public LogCommand(@NotNull Settings settings, @NotNull File workingDir) {
+    super(settings, workingDir);
   }
 
   public void setFromRevId(String id) {
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/PullCommand.java	Wed Feb 16 13:35:57 2011 +0300
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/PullCommand.java	Tue Mar 01 17:55:41 2011 +0300
@@ -19,13 +19,16 @@
 import jetbrains.buildServer.vcs.VcsException;
 import org.jetbrains.annotations.NotNull;
 
+import java.io.File;
+
 /**
  * @author Pavel.Sher
  *         Date: 14.07.2008
  */
 public class PullCommand extends BaseCommand {
-  public PullCommand(@NotNull final Settings settings) {
-    super(settings);
+
+  public PullCommand(@NotNull Settings settings, @NotNull File workingDir) {
+    super(settings, workingDir);
   }
 
   public void execute() throws VcsException {
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/PushCommand.java	Wed Feb 16 13:35:57 2011 +0300
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/PushCommand.java	Tue Mar 01 17:55:41 2011 +0300
@@ -20,14 +20,16 @@
 import jetbrains.buildServer.vcs.VcsException;
 import org.jetbrains.annotations.NotNull;
 
+import java.io.File;
+
 /**
  * @author pavel
  */
 public class PushCommand extends BaseCommand {
   private boolean myForced;
 
-  public PushCommand(@NotNull final Settings settings) {
-    super(settings);
+  public PushCommand(@NotNull Settings settings, @NotNull File workingDir) {
+    super(settings, workingDir);
   }
 
   public void setForce(boolean force) {
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/Settings.java	Wed Feb 16 13:35:57 2011 +0300
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/Settings.java	Tue Mar 01 17:55:41 2011 +0300
@@ -18,10 +18,10 @@
 import jetbrains.buildServer.buildTriggers.vcs.mercurial.Constants;
 import jetbrains.buildServer.buildTriggers.vcs.mercurial.PathUtil;
 import jetbrains.buildServer.log.Loggers;
-import jetbrains.buildServer.util.Hash;
 import jetbrains.buildServer.util.StringUtil;
 import jetbrains.buildServer.vcs.VcsRoot;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 import java.io.File;
 import java.net.MalformedURLException;
@@ -37,30 +37,26 @@
 public class Settings {
   private String myRepository;
   private String myHgCommandPath;
-  private File myWorkingDir;
-  private File myWorkFolderParentDir;
+  private File myCustomWorkingDir;
   private String myUsername;
   private String myPassword;
   private String myBranchName;
   private boolean myUncompressedTransfer = false;
   private static final String DEFAULT_BRANCH_NAME = "default";
+  private String myCustomClonePath;
 
-  public Settings(@NotNull File workFolderParentDir, @NotNull VcsRoot vcsRoot) {
-    myWorkFolderParentDir = workFolderParentDir;
-    setRepository(vcsRoot.getProperty(Constants.REPOSITORY_PROP));
-    setHgCommandPath(vcsRoot.getProperty(Constants.HG_COMMAND_PATH_PROP));
+  public Settings(@NotNull VcsRoot vcsRoot) {
+    myRepository = vcsRoot.getProperty(Constants.REPOSITORY_PROP);
+    myHgCommandPath = vcsRoot.getProperty(Constants.HG_COMMAND_PATH_PROP);
     myBranchName = vcsRoot.getProperty(Constants.BRANCH_NAME_PROP);
-
+    myCustomClonePath = vcsRoot.getProperty(Constants.SERVER_CLONE_PATH_PROP);
     myUsername = vcsRoot.getProperty(Constants.USERNAME);
     myPassword = vcsRoot.getProperty(Constants.PASSWORD);
     myUncompressedTransfer = "true".equals(vcsRoot.getProperty(Constants.UNCOMPRESSED_TRANSFER));
   }
 
-  public Settings() {
-  }
-
-  public void setRepository(@NotNull final String repository) {
-    myRepository = repository;
+  public String getCustomClonePath() {
+    return myCustomClonePath;
   }
 
   /**
@@ -184,48 +180,26 @@
     return userInfo;
   }
 
-  public void setHgCommandPath(@NotNull final String hgCommandPath) {
-    myHgCommandPath = hgCommandPath;
-  }
-
-  public void setWorkingDir(@NotNull final File workingDir) {
-    myWorkingDir = PathUtil.getCanonicalFile(workingDir);
-  }
-
   /**
-   * Returns directory where repository is supposed to be cloned, i.e. working directory of cloned repository
-   * @return repository working directory
+   * Set custom working dir for vcs root. This option make sence only for server-side checkout
+   * @param customWorkingDir custom working dir
    */
-  @NotNull
-  public File getLocalRepositoryDir() {
-    if (myWorkingDir != null) {
-      return myWorkingDir;
-    }
-
-    return getDefaultWorkDir(myWorkFolderParentDir, myRepository);
+  public void setCustomWorkingDir(@NotNull final File customWorkingDir) {
+    myCustomWorkingDir = PathUtil.getCanonicalFile(customWorkingDir);
   }
 
   /**
-   * Returns true if current working directory contains copy of repository (contains .hg directory)
+   * Returns custom working dir for root or null if default working dir should be used.
+   * This options make sence only with server-side checkout.
    * @return see above
    */
-  public boolean hasCopyOfRepository() {
-    // need better way to check that repository copy is ok
-    return getLocalRepositoryDir().isDirectory() && new File(getLocalRepositoryDir(), ".hg").isDirectory();
+  @Nullable
+  public File getCustomWorkingDir() {
+    return myCustomWorkingDir;
   }
 
-  public static String DEFAULT_WORK_DIR_PREFIX = "hg_";
-
-  private static File getDefaultWorkDir(@NotNull File workFolderParentDir, @NotNull String repPath) {
-    String workingDirname = DEFAULT_WORK_DIR_PREFIX + String.valueOf(Hash.calc(normalize(repPath)));
-    return PathUtil.getCanonicalFile(new File(workFolderParentDir, workingDirname));
-  }
-
-  private static String normalize(final String path) {
-    String normalized = PathUtil.normalizeSeparator(path);
-    if (path.endsWith("/")) {
-      return normalized.substring(0, normalized.length()-1);
-    }
-    return normalized;
+  public static boolean isValidRepository(File dir) {
+    // need better way to check that repository copy is ok
+    return dir.isDirectory() && new File(dir, ".hg").isDirectory();
   }
 }
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/StatusCommand.java	Wed Feb 16 13:35:57 2011 +0300
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/StatusCommand.java	Tue Mar 01 17:55:41 2011 +0300
@@ -20,6 +20,7 @@
 import jetbrains.buildServer.vcs.VcsException;
 import org.jetbrains.annotations.NotNull;
 
+import java.io.File;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -27,8 +28,8 @@
   private String myFromId;
   private String myToId;
 
-  public StatusCommand(@NotNull final Settings settings) {
-    super(settings);
+  public StatusCommand(@NotNull Settings settings, @NotNull File workingDir) {
+    super(settings, workingDir);
   }
 
   public void setFromRevId(final String fromId) {
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/TagCommand.java	Wed Feb 16 13:35:57 2011 +0300
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/TagCommand.java	Tue Mar 01 17:55:41 2011 +0300
@@ -15,16 +15,18 @@
  */
 package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
 
-import org.jetbrains.annotations.NotNull;
 import com.intellij.execution.configurations.GeneralCommandLine;
 import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
 
 public class TagCommand extends BaseCommand {
   private String myTag;
   private String myRevId;
 
-  public TagCommand(@NotNull Settings settings) {
-    super(settings);
+  public TagCommand(@NotNull Settings settings, @NotNull File workingDir) {
+    super(settings, workingDir);
   }
 
   public void setTag(@NotNull final String tag) {
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/UpdateCommand.java	Wed Feb 16 13:35:57 2011 +0300
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/UpdateCommand.java	Tue Mar 01 17:55:41 2011 +0300
@@ -19,14 +19,16 @@
 import jetbrains.buildServer.vcs.VcsException;
 import org.jetbrains.annotations.NotNull;
 
+import java.io.File;
+
 public class UpdateCommand extends BaseCommand {
 
   private static final int UPDATE_TIMEOUT_SECONDS = 8 * 3600;//8 hours
 
   private String myToId;
 
-  public UpdateCommand(@NotNull final Settings settings) {
-    super(settings);
+  public UpdateCommand(@NotNull Settings settings, @NotNull File workingDir) {
+    super(settings, workingDir);
   }
 
   public void setToId(final String toId) {
--- a/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java	Wed Feb 16 13:35:57 2011 +0300
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java	Tue Mar 01 17:55:41 2011 +0300
@@ -56,6 +56,7 @@
   private ConcurrentMap<String, Lock> myWorkDirLocks= new ConcurrentHashMap<String, Lock>();
   private VcsManager myVcsManager;
   private File myDefaultWorkFolderParent;
+  private MirrorManager myMirrorManager;
 
   public MercurialVcsSupport(@NotNull final VcsManager vcsManager,
                              @NotNull ServerPaths paths,
@@ -63,6 +64,7 @@
                              @NotNull EventDispatcher<BuildServerListener> dispatcher) {
     myVcsManager = vcsManager;
     myDefaultWorkFolderParent = new File(paths.getCachesDir(), "mercurial");
+    myMirrorManager = new MirrorManager(myDefaultWorkFolderParent);
     dispatcher.addListener(new BuildServerAdapter() {
       @Override
       public void cleanupFinished() {
@@ -79,24 +81,31 @@
         super.sourcesVersionReleased(agent);
         server.getExecutor().submit(new Runnable() {
           public void run() {
-            Set<File> clonedRepos = getAllClonedRepos();
-            if (clonedRepos == null) return;
-            for (File f: clonedRepos) {
-              lockWorkDir(f);
-              try {
-                FileUtil.delete(f);
-              } finally {
-                unlockWorkDir(f);
-              }
-            }
+            deleteWithLocking(myMirrorManager.getMirrors());
           }
         });
       }
     });
   }
 
+  public MirrorManager getMirrorManager() {
+    return myMirrorManager;
+  }
+
+  private void deleteWithLocking(Collection<File> filesForDelete) {
+    for (File f : filesForDelete) {
+      lockWorkDir(f);
+      try {
+        FileUtil.delete(f);
+      } finally {
+        unlockWorkDir(f);
+      }
+    }
+  }
+
   private Collection<ModifiedFile> computeModifiedFilesForMergeCommit(final Settings settings, final ChangeSet cur) throws VcsException {
-    ChangedFilesCommand cfc = new ChangedFilesCommand(settings);
+    File workingDir = myMirrorManager.getMirrorDir(settings.getRepositoryUrl());
+    ChangedFilesCommand cfc = new ChangedFilesCommand(settings, workingDir);
     cfc.setRevId(cur.getId());
     return cfc.execute();
   }
@@ -138,7 +147,8 @@
   public byte[] getContent(@NotNull final String filePath, @NotNull final VcsRoot vcsRoot, @NotNull final String version) throws VcsException {
     syncClonedRepository(vcsRoot);
     Settings settings = createSettings(vcsRoot);
-    CatCommand cc = new CatCommand(settings);
+    File workingDir = myMirrorManager.getMirrorDir(settings.getRepositoryUrl());
+    CatCommand cc = new CatCommand(settings, workingDir);
     cc.setRevId(new ChangeSet(version).getId());
     File parentDir = cc.execute(Collections.singletonList(filePath));
     try {
@@ -195,7 +205,8 @@
     // we will return full version of the most recent change as current version
     syncClonedRepository(root);
     Settings settings = createSettings(root);
-    BranchesCommand branches = new BranchesCommand(settings);
+    File workingDir = myMirrorManager.getMirrorDir(settings.getRepositoryUrl());
+    BranchesCommand branches = new BranchesCommand(settings, workingDir);
     Map<String, ChangeSet> result = branches.execute();
     if (!result.containsKey(settings.getBranchName())) {
       throw new VcsException("Unable to find current version for the branch: " + settings.getBranchName());
@@ -218,7 +229,8 @@
     return new TestConnectionSupport() {
       public String testConnection(@NotNull final VcsRoot vcsRoot) throws VcsException {
         Settings settings = createSettings(vcsRoot);
-        IdentifyCommand id = new IdentifyCommand(settings);
+        File workingDir = myMirrorManager.getMirrorDir(settings.getRepositoryUrl());
+        IdentifyCommand id = new IdentifyCommand(settings, workingDir);
         StringBuilder res = new StringBuilder();
         res.append(quoteIfNeeded(settings.getHgCommandPath()));
         res.append(" identify ");
@@ -269,7 +281,8 @@
   // builds patch from version to version
   private void buildIncrementalPatch(final Settings settings, @NotNull final ChangeSet fromVer, @NotNull final ChangeSet toVer, final PatchBuilder builder, final CheckoutRules checkoutRules)
     throws VcsException, IOException {
-    StatusCommand st = new StatusCommand(settings);
+    File workingDir = myMirrorManager.getMirrorDir(settings.getRepositoryUrl());
+    StatusCommand st = new StatusCommand(settings, workingDir);
     st.setFromRevId(fromVer.getId());
     st.setToRevId(toVer.getId());
     List<ModifiedFile> modifiedFiles = st.execute();
@@ -282,7 +295,7 @@
 
     if (notDeletedFiles.isEmpty()) return;
 
-    CatCommand cc = new CatCommand(settings);
+    CatCommand cc = new CatCommand(settings, workingDir);
     cc.setRevId(toVer.getId());
     File parentDir = cc.execute(notDeletedFiles);
 
@@ -318,19 +331,19 @@
   // builds patch by exporting files using specified version
   private void buildFullPatch(final Settings settings, @NotNull final ChangeSet toVer, final PatchBuilder builder, final CheckoutRules checkoutRules)
     throws IOException, VcsException {
-    CloneCommand cl = new CloneCommand(settings);
-    // clone from the local repository
-    cl.setRepository(settings.getLocalRepositoryDir().getAbsolutePath());
-    cl.setToId(toVer.getId());
-    cl.setUpdateWorkingDir(false);
     File tempDir = FileUtil.createTempDirectory("mercurial", toVer.getId());
     try {
+      File customRepositoryDir = settings.getCustomWorkingDir();
+      File mirrorDir = customRepositoryDir != null ? customRepositoryDir : myMirrorManager.getMirrorDir(settings.getRepositoryUrl());
       final File repRoot = new File(tempDir, "rep");
-      cl.setDestDir(repRoot.getAbsolutePath());
+      CloneCommand cl = new CloneCommand(settings, repRoot);
+      // clone from the local repository
+      cl.setRepository(mirrorDir.getAbsolutePath());
+      cl.setToId(toVer.getId());
+      cl.setUpdateWorkingDir(false);
       cl.execute();
 
-      UpdateCommand up = new UpdateCommand(settings);
-      up.setWorkDirectory(repRoot.getAbsolutePath());
+      UpdateCommand up = new UpdateCommand(settings, repRoot);
       up.setToId(toVer.getId());
       up.execute();
 
@@ -379,22 +392,20 @@
   // updates current working copy of repository by pulling changes from the repository specified in VCS root
   private void syncClonedRepository(final VcsRoot root) throws VcsException {
     Settings settings = createSettings(root);
-    File workDir = settings.getLocalRepositoryDir();
-    lockWorkDir(workDir);
+    File customRepositoryDir = settings.getCustomWorkingDir();
+    File workingDir = customRepositoryDir != null ? customRepositoryDir : myMirrorManager.getMirrorDir(settings.getRepositoryUrl());
+    lockWorkDir(workingDir);
     try {
-      if (settings.hasCopyOfRepository()) {
-        // update
-        PullCommand pull = new PullCommand(settings);
+      if (Settings.isValidRepository(workingDir)) {
+        PullCommand pull = new PullCommand(settings, workingDir);
         pull.execute();
       } else {
-        // clone
-        CloneCommand cl = new CloneCommand(settings);
-        cl.setDestDir(workDir.getAbsolutePath());
+        CloneCommand cl = new CloneCommand(settings, workingDir);
         cl.setUpdateWorkingDir(false);
         cl.execute();
       }
     } finally {
-      unlockWorkDir(workDir);
+      unlockWorkDir(workingDir);
     }
   }
 
@@ -417,7 +428,8 @@
   public Map<String, String> getBranchesRevisions(@NotNull VcsRoot root) throws VcsException {
     syncClonedRepository(root);
     Settings settings = createSettings(root);
-    BranchesCommand branches = new BranchesCommand(settings);
+    File workingDir = myMirrorManager.getMirrorDir(settings.getRepositoryUrl());
+    BranchesCommand branches = new BranchesCommand(settings, workingDir);
     Map<String, String> result = new HashMap<String, String>();
     for (Map.Entry<String, ChangeSet> entry : branches.execute().entrySet()) {
       result.put(entry.getKey(), entry.getValue().getId());
@@ -443,7 +455,8 @@
 
   private String getBranchPoint(@NotNull VcsRoot root, String branchOneRev, String branchTwoRev) throws VcsException {
     Settings settings = createSettings(root);
-    LogCommand lc = new LogCommand(settings);
+    File workingDir = myMirrorManager.getMirrorDir(settings.getRepositoryUrl());
+    LogCommand lc = new LogCommand(settings, workingDir);
     lc.setFromRevId(new ChangeSetRevision(branchOneRev).getId());
     lc.setToRevId(new ChangeSetRevision(branchTwoRev).getId());
     lc.setLimit(1);
@@ -467,7 +480,8 @@
         if (currentVersion == null) return result;
 
         Settings settings = createSettings(root);
-        LogCommand lc = new LogCommand(settings);
+        File workingDir = myMirrorManager.getMirrorDir(settings.getRepositoryUrl());
+        LogCommand lc = new LogCommand(settings, workingDir);
         String fromId = new ChangeSetRevision(fromVersion).getId();
         lc.setFromRevId(fromId);
         lc.setToRevId(new ChangeSetRevision(currentVersion).getId());
@@ -477,7 +491,7 @@
         }
 
         // invoke status command for each changeset and determine what files were modified in these changesets
-        StatusCommand st = new StatusCommand(settings);
+        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
@@ -563,26 +577,20 @@
   }
 
   private void removeOldWorkFolders() {
-    Set<File> workDirs = getAllClonedRepos();
-    if (workDirs == null) return;
+    Set<File> workDirs = new HashSet<File>(myMirrorManager.getMirrors());
 
     for (VcsRoot vcsRoot: getMercurialVcsRoots()) {
       try {
         Settings s = createSettings(vcsRoot);
-        workDirs.remove(PathUtil.getCanonicalFile(s.getLocalRepositoryDir()));
+        File customWorkingDir = s.getCustomWorkingDir();
+        File workingDir = customWorkingDir != null ? customWorkingDir : myMirrorManager.getMirrorDir(s.getRepositoryUrl());
+        workDirs.remove(PathUtil.getCanonicalFile(workingDir));
       } catch (VcsException e) {
         Loggers.VCS.error(e);
       }
     }
 
-    for (File f: workDirs) {
-      lockWorkDir(f);
-      try {
-        FileUtil.delete(f);
-      } finally {
-        unlockWorkDir(f);
-      }
-    }
+    deleteWithLocking(workDirs);
   }
 
   private Collection<VcsRoot> getMercurialVcsRoots() {
@@ -595,44 +603,25 @@
     return res;
   }
 
-  @Nullable
-  private Set<File> getAllClonedRepos() {
-    File workFoldersParent = myDefaultWorkFolderParent;
-    if (!workFoldersParent.isDirectory()) return null;
-
-    Set<File> workDirs = new HashSet<File>();
-    File[] files = workFoldersParent.listFiles(new FileFilter() {
-      public boolean accept(final File file) {
-        return file.isDirectory() && file.getName().startsWith(Settings.DEFAULT_WORK_DIR_PREFIX);
-      }
-    });
-    if (files != null) {
-      for (File f: files) {
-        workDirs.add(PathUtil.getCanonicalFile(f));
-      }
-    }
-    return workDirs;
-  }
-
   public String label(@NotNull String label, @NotNull String version, @NotNull VcsRoot root, @NotNull CheckoutRules checkoutRules) throws VcsException {
     syncClonedRepository(root);
 
     Settings settings = createSettings(root);
-
+    File workingDir = myMirrorManager.getMirrorDir(settings.getRepositoryUrl());
     // I do not know why but hg tag does not work correctly if
     // update command was not invoked for the current repo
     // in such case if there were no tags before Mercurial attempts to
     // create new head when tag is pushed to the parent repository
-    UpdateCommand uc = new UpdateCommand(settings);
+    UpdateCommand uc = new UpdateCommand(settings, workingDir);
     uc.execute();
 
     String fixedTagname = fixTagName(label);
-    TagCommand tc = new TagCommand(settings);
+    TagCommand tc = new TagCommand(settings, workingDir);
     tc.setRevId(new ChangeSet(version).getId());
     tc.setTag(fixedTagname);
     tc.execute();
 
-    PushCommand pc = new PushCommand(settings);
+    PushCommand pc = new PushCommand(settings, workingDir);
 //    pc.setForce(true);
     pc.execute();
     return fixedTagname;
@@ -649,8 +638,8 @@
   }
 
   private Settings createSettings(final VcsRoot root) throws VcsException {
-    Settings settings = new Settings(myDefaultWorkFolderParent, root);
-    String customClonePath = root.getProperty(Constants.SERVER_CLONE_PATH_PROP);
+    Settings settings = new Settings(root);
+    String customClonePath = settings.getCustomClonePath();
     if (!StringUtil.isEmptyOrSpaces(customClonePath) && !myDefaultWorkFolderParent.equals(new File(customClonePath).getAbsoluteFile())) {
       File parentDir = new File(customClonePath);
       createClonedRepositoryParentDir(parentDir);
@@ -663,9 +652,7 @@
       }
 
       File customWorkingDir = new File(parentDir, repPath);
-      settings.setWorkingDir(customWorkingDir);
-    } else {
-      createClonedRepositoryParentDir(myDefaultWorkFolderParent);
+      settings.setCustomWorkingDir(customWorkingDir);
     }
     return settings;
   }
Binary file mercurial-tests/lib/hamcrest-core-1.1.jar has changed
Binary file mercurial-tests/lib/hamcrest-library-1.1.jar has changed
Binary file mercurial-tests/lib/jmock-2.5.1.jar has changed
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/AgentSideCheckoutTest.java	Wed Feb 16 13:35:57 2011 +0300
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/AgentSideCheckoutTest.java	Tue Mar 01 17:55:41 2011 +0300
@@ -15,18 +15,24 @@
  */
 package jetbrains.buildServer.buildTriggers.vcs.mercurial;
 
+import jetbrains.buildServer.agent.AgentRunningBuild;
+import jetbrains.buildServer.agent.BuildAgentConfiguration;
 import jetbrains.buildServer.agent.BuildProgressLogger;
 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 org.jmock.Mock;
+import org.jmock.Expectations;
+import org.jmock.Mockery;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import java.io.File;
 import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 /**
  * @author Pavel.Sher
@@ -35,18 +41,35 @@
 @Test
 public class AgentSideCheckoutTest extends BaseMercurialTestCase {
   private MercurialAgentSideVcsSupport myVcsSupport;
-  private Mock myProgressLoggerMock;
   private File myWorkDir;
+  private File myMirrorsRootDir;
+  private Mockery myContext;
+  private BuildProgressLogger myLogger;
+  private int myBuildCounter = 0;
 
   @Override
   @BeforeMethod
   protected void setUp() throws Exception {
     super.setUp();
 
-    myVcsSupport = new MercurialAgentSideVcsSupport();
-    myProgressLoggerMock = new Mock(BuildProgressLogger.class);
+    myContext = new Mockery();
+
+    myMirrorsRootDir = myTempFiles.createTempDir();
+
+    final BuildAgentConfiguration agentConfig = myContext.mock(BuildAgentConfiguration.class);
+    myContext.checking(new Expectations() {{
+      allowing(agentConfig).getCacheDirectory("mercurial"); will(returnValue(myMirrorsRootDir));
+    }});
+
+    myVcsSupport = new MercurialAgentSideVcsSupport(agentConfig);
+
+    myLogger = myContext.mock(BuildProgressLogger.class);
+    myContext.checking(new Expectations() {{
+      allowing(myLogger).message(with(any(String.class)));
+    }});
+
     myWorkDir = myTempFiles.createTempDir();
-    myProgressLoggerMock.stubs().method("message");
+
   }
 
   public void checkout_on_agent() throws IOException, VcsException {
@@ -69,8 +92,19 @@
   }
 
   private File doUpdate(final VcsRoot vcsRoot, final String version, final IncludeRule includeRule) throws VcsException {
+    return doUpdate(vcsRoot, version, includeRule, false);
+  }
+
+  private File doUpdate(final VcsRoot vcsRoot, final String version, final IncludeRule includeRule, boolean useLocalMirrors) throws VcsException {
     File actualWorkDir = new File(myWorkDir, includeRule.getTo());
-    myVcsSupport.getUpdater(vcsRoot, new CheckoutRules(""), version, myWorkDir, (BuildProgressLogger) myProgressLoggerMock.proxy()).process(includeRule, actualWorkDir);
+    final Map<String, String> sharedConfigParameters = new HashMap<String, String>();
+    sharedConfigParameters.put("teamcity.hg.use.local.mirrors", String.valueOf(useLocalMirrors));
+    final AgentRunningBuild build = myContext.mock(AgentRunningBuild.class, "build" + myBuildCounter++);
+    myContext.checking(new Expectations() {{
+      allowing(build).getBuildLogger(); will(returnValue(myLogger));
+      allowing(build).getSharedConfigParameters(); will(returnValue(sharedConfigParameters));
+    }});
+    myVcsSupport.getUpdater(vcsRoot, new CheckoutRules(""), version, myWorkDir, build, false).process(includeRule, actualWorkDir);
 
     File hgDir = new File(actualWorkDir, ".hg");
     assertTrue(hgDir.isDirectory());
@@ -105,6 +139,66 @@
     checkWorkingDir("patch4/after", workDir);
   }
 
+  public void by_default_local_mirror_not_created() throws IOException, VcsException {
+    List<File> mirrors = FileUtil.getSubDirectories(myMirrorsRootDir);
+    assertTrue(mirrors.isEmpty());
+    VcsRoot root = createVcsRoot(simpleRepo());
+    doUpdate(root, "3:9522278aa38d", new IncludeRule(".", ".", null));
+    mirrors = FileUtil.getSubDirectories(myMirrorsRootDir);
+    assertTrue(mirrors.isEmpty());
+  }
+
+  public void local_mirror_is_created() throws IOException, VcsException {
+    List<File> mirrors = FileUtil.getSubDirectories(myMirrorsRootDir);
+    assertTrue(mirrors.isEmpty());
+    VcsRoot root = createVcsRoot(simpleRepo());
+    doUpdate(root, "3:9522278aa38d", new IncludeRule(".", ".", null), true);
+    mirrors = FileUtil.getSubDirectories(myMirrorsRootDir);
+    assertEquals(1, mirrors.size());
+    File mirror = mirrors.get(0);
+    File dotHg = new File(mirror, ".hg");
+    assertTrue(dotHg.exists());
+    File hgrc = new File(dotHg, "hgrc");
+    String hgrcContent = FileUtil.readText(hgrc);
+    assertTrue(hgrcContent.contains("default = " + root.getProperty(Constants.REPOSITORY_PROP)));
+  }
+
+  public void new_repository_is_cloned_from_local_mirror() throws IOException, VcsException {
+    VcsRoot root = createVcsRoot(simpleRepo());
+    File workingDir = doUpdate(root, "3:9522278aa38d", new IncludeRule(".", ".", null), true);
+    File mirrorDir = FileUtil.getSubDirectories(myMirrorsRootDir).get(0);
+    File hgrc = new File(workingDir, ".hg" + File.separator + "hgrc");
+    String hgrcContent = FileUtil.readText(hgrc);
+    assertTrue(hgrcContent.contains("default = " + mirrorDir.getCanonicalPath()));
+  }
+
+  public void repository_cloned_from_remote_start_cloning_from_local_mirror() throws IOException, VcsException {
+    VcsRoot root = createVcsRoot(simpleRepo());
+    //clone from remote repository
+    File workingDir = doUpdate(root, "3:9522278aa38d", new IncludeRule(".", ".", null));
+    String hgrcContent = FileUtil.readText(new File(workingDir, ".hg" + File.separator + "hgrc"));
+
+    File workingDir2 = doUpdate(root, "3:9522278aa38d", new IncludeRule(".", ".", null), true);
+    File newMirrorDir = FileUtil.getSubDirectories(myMirrorsRootDir).get(0);
+    String hgrcContent2 = FileUtil.readText(new File(workingDir2, ".hg" + File.separator + "hgrc"));
+    assertFalse(hgrcContent2.equals(hgrcContent));//repository settings are changed
+    assertTrue(hgrcContent2.contains("default = " + newMirrorDir.getCanonicalPath()));//now it clones from local mirror
+  }
+
+  public void repository_cloned_from_local_mirror_start_cloning_from_remote() throws IOException, VcsException {
+    VcsRoot root = createVcsRoot(simpleRepo());
+    //clone from remote repository
+    File workingDir = doUpdate(root, "3:9522278aa38d", new IncludeRule(".", ".", null), true);
+    String hgrcContent = FileUtil.readText(new File(workingDir, ".hg" + File.separator + "hgrc"));
+    File newMirrorDir = FileUtil.getSubDirectories(myMirrorsRootDir).get(0);
+    assertTrue(hgrcContent.contains("default = " + newMirrorDir.getCanonicalPath()));//now it clones from local mirror
+
+    File workingDir2 = doUpdate(root, "3:9522278aa38d", new IncludeRule(".", ".", null));
+    String hgrcContent2 = FileUtil.readText(new File(workingDir2, ".hg" + File.separator + "hgrc"));
+    assertFalse(hgrcContent2.equals(hgrcContent));//repository settings are changed
+    assertTrue(hgrcContent2.contains("default = " + root.getProperty(Constants.REPOSITORY_PROP)));//now it clones from remote
+  }
+
   protected String getTestDataPath() {
     return "mercurial-tests/testData";
   }
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java	Wed Feb 16 13:35:57 2011 +0300
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java	Tue Mar 01 17:55:41 2011 +0300
@@ -312,15 +312,15 @@
     VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
     String repPath = vcsRoot.getProperty(Constants.REPOSITORY_PROP);
     vcsRoot.addProperty(Constants.REPOSITORY_PROP, repPath + "#test_branch");
-    Settings settings = new Settings(new File(myServerPaths.getCachesDir()), vcsRoot);
+    Settings settings = new Settings(vcsRoot);
     assertEquals("test_branch", settings.getBranchName());
 
     vcsRoot.addProperty(Constants.REPOSITORY_PROP, repPath + "#");
-    settings = new Settings(new File(myServerPaths.getCachesDir()), vcsRoot);
+    settings = new Settings(vcsRoot);
     assertEquals("default", settings.getBranchName());
 
     vcsRoot.addProperty(Constants.REPOSITORY_PROP, repPath);
-    settings = new Settings(new File(myServerPaths.getCachesDir()), vcsRoot);
+    settings = new Settings(vcsRoot);
     assertEquals("default", settings.getBranchName());
   }
 
@@ -404,7 +404,7 @@
     VcsRootImpl root = new VcsRootImpl(1, Constants.VCS_NAME);
     root.addAllProperties(myVcs.getDefaultVcsProperties());
     root.addProperty(Constants.REPOSITORY_PROP, "http://host.com/path");
-    Settings settings = new Settings(new File("."), root);
+    Settings settings = new Settings(root);
     assertFalse(settings.isUncompressedTransfer());
   }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MirrorManagerTest.java	Tue Mar 01 17:55:41 2011 +0300
@@ -0,0 +1,106 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import jetbrains.buildServer.TempFiles;
+import jetbrains.buildServer.util.Hash;
+import junit.framework.TestCase;
+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.List;
+
+/**
+ * @author dmitry.neverov
+ */
+@Test
+public class MirrorManagerTest extends TestCase {
+
+  private TempFiles myTempFiles;
+  private File myRootDir;
+  private MirrorManager myManager;
+
+
+  @BeforeMethod
+  public void setUp() throws Exception {
+    myTempFiles = new TempFiles();
+    myRootDir = myTempFiles.createTempDir();
+    myManager = new MirrorManager(myRootDir);
+  }
+
+  @AfterMethod
+  public void tearDown() {
+    myTempFiles.cleanup();
+  }
+
+
+  public void getMirrorDir_returns_dir_under_root() {
+    File mirrorDir = myManager.getMirrorDir("hg://some.com/repository.hg");
+    assertEquals(myRootDir, mirrorDir.getParentFile());
+    assertTrue(mirrorDir.exists());
+  }
+
+
+  public void getMirrorDir_returns_same_result_for_same_url() {
+    String url = "hg://some.com/repository.hg";
+    assertEquals(myManager.getMirrorDir(url), myManager.getMirrorDir(url));
+  }
+
+
+  public void getMirrors_remember_created_repositories() {
+    File mirrorDir1 = myManager.getMirrorDir("hg://some.com/repository.hg");
+    File mirrorDir2 = myManager.getMirrorDir("hg://other.com/repository.hg");
+    List<File> mirrors = myManager.getMirrors();
+    assertEquals(2, mirrors.size());
+    assertTrue(mirrors.contains(mirrorDir1));
+    assertTrue(mirrors.contains(mirrorDir2));
+  }
+
+
+  public void should_handle_url_collisions() throws IOException {
+    final String url1 = "hg://some.com/repository.hg";
+    final String url2 = "hg://other.com/repository.hg";
+
+    MirrorManager.HashCalculator hashWithCollision = new MirrorManager.HashCalculator() {
+      public long calc(String value) {
+        if (value.equals(url1) || value.equals(url2)) {
+          return 0;//emulate collision
+        } else {
+          return Hash.calc(value);
+        }
+      }
+    };
+
+    //alone they get dir with the same name:
+    MirrorManager mm1 = new MirrorManager(myTempFiles.createTempDir());
+    mm1.setHashCalculator(hashWithCollision);
+    File separateMirrorDir1 = mm1.getMirrorDir(url1);
+
+    MirrorManager mm2 = new MirrorManager(myTempFiles.createTempDir());
+    mm2.setHashCalculator(hashWithCollision);
+    File separateMirrorDir2 = mm2.getMirrorDir(url2);
+
+    assertEquals(separateMirrorDir1.getName(), separateMirrorDir2.getName());
+
+    myManager.setHashCalculator(hashWithCollision);
+    File mirrorDir1 = myManager.getMirrorDir(url1);
+    File mirrorDir2 = myManager.getMirrorDir(url2);
+    assertFalse(mirrorDir1.equals(mirrorDir2));
+  }
+
+
+  public void should_survive_restart() throws IOException {
+    String url1 = "hg://some.com/repository.hg";
+    String url2 = "hg://other.com/repository.hg?param = 1";
+    File mirrorDir1 = myManager.getMirrorDir(url1);
+    File mirrorDir2 = myManager.getMirrorDir(url2);
+
+    //emulate restart by creating a new manager for the same rootDir
+    MirrorManager manager = new MirrorManager(myRootDir);
+
+    assertEquals(2, manager.getMirrors().size());
+    assertTrue(manager.getMirrors().contains(mirrorDir1));
+    assertTrue(manager.getMirrors().contains(mirrorDir2));
+  }
+}
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/SettingsTest.java	Wed Feb 16 13:35:57 2011 +0300
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/SettingsTest.java	Tue Mar 01 17:55:41 2011 +0300
@@ -15,78 +15,95 @@
  */
 package jetbrains.buildServer.buildTriggers.vcs.mercurial;
 
+import jetbrains.buildServer.TempFiles;
 import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.Settings;
 import jetbrains.buildServer.vcs.impl.VcsRootImpl;
-import junit.framework.Assert;
+import junit.framework.TestCase;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
-import java.io.File;
-
 /**
  * @author Pavel.Sher
  */
 @Test
-public class SettingsTest extends Assert {
+public class SettingsTest extends TestCase {
+
+  private TempFiles myTempFiles = new TempFiles();
+  private MirrorManager myMirrorManager;
+
+  @Override
+  @BeforeMethod
+  public void setUp() throws Exception {
+    myMirrorManager = new MirrorManager(myTempFiles.createTempDir());
+  }
+
+  @Override
+  @AfterMethod
+  public void tearDown() throws Exception {
+    myTempFiles.cleanup();
+  }
+
   public void test_url_without_credentials() {
     VcsRootImpl vcsRoot = createVcsRoot("http://host.com/path");
-    Settings settings = new Settings(new File("."), vcsRoot);
+    Settings settings = new Settings(vcsRoot);
     assertEquals("http://user:pwd@host.com/path", settings.getRepositoryUrl());
   }
 
   public void test_url_with_credentials() {
     VcsRootImpl vcsRoot = createVcsRoot("http://user:pwd@host.com/path");
-    Settings settings = new Settings(new File("."), vcsRoot);
+    Settings settings = new Settings(vcsRoot);
     assertEquals("http://user:pwd@host.com/path", settings.getRepositoryUrl());
   }
 
   public void test_url_with_username() {
     VcsRootImpl vcsRoot = createVcsRoot("http://user@host.com/path");
-    Settings settings = new Settings(new File("."), vcsRoot);
+    Settings settings = new Settings(vcsRoot);
     assertEquals("http://user:pwd@host.com/path", settings.getRepositoryUrl());
   }
 
   public void test_url_with_at_after_slash() {
     VcsRootImpl vcsRoot = createVcsRoot("http://host.com/path@");
-    Settings settings = new Settings(new File("."), vcsRoot);
+    Settings settings = new Settings(vcsRoot);
     assertEquals("http://user:pwd@host.com/path@", settings.getRepositoryUrl());
   }
 
   public void test_url_with_at_in_username() {
     VcsRootImpl vcsRoot = createVcsRoot("http://host.com/path", "my.name@gmail.com", "1234");
-    Settings settings = new Settings(new File("."), vcsRoot);
+    Settings settings = new Settings(vcsRoot);
     assertEquals("http://my.name%40gmail.com:1234@host.com/path", settings.getRepositoryUrl());
   }
 
   /** TW-13768 */
   public void test_underscore_in_host() {
 		VcsRootImpl vcsRoot = createVcsRoot("http://Klekovkin.SDK_GARANT:8000/", "my.name@gmail.com", "1234");
-		Settings settings = new Settings(new File("."), vcsRoot);
+    Settings settings = new Settings(vcsRoot);
 		assertEquals("http://my.name%40gmail.com:1234@Klekovkin.SDK_GARANT:8000/", settings.getRepositoryUrl());
 	}
 
   /** TW-13768 */
   public void test_underscore_in_host_with_credentials_in_url() {
     VcsRootImpl vcsRoot = createVcsRoot("http://me:mypass@Klekovkin.SDK_GARANT:8000/");
-		Settings settings = new Settings(new File("."), vcsRoot);
+    Settings settings = new Settings(vcsRoot);
 		assertEquals("http://me:mypass@Klekovkin.SDK_GARANT:8000/", settings.getRepositoryUrl());
   }
 
   public void test_windows_path() throws Exception {
     VcsRootImpl vcsRoot = createVcsRoot("c:\\windows\\path");
-    Settings settings = new Settings(new File("."), vcsRoot);
+    Settings settings = new Settings(vcsRoot);
     assertEquals("c:\\windows\\path", settings.getRepositoryUrl());
   }
 
   public void test_file_scheme_has_no_credentials() {
     VcsRootImpl vcsRoot = createVcsRoot("file:///path/to/repo", "my.name@gmail.com", "1234");
-    Settings settings = new Settings(new File("."), vcsRoot);
+    Settings settings = new Settings(vcsRoot);
     assertEquals("file:///path/to/repo", settings.getRepositoryUrl());
   }
 
   public void uncompressed_transfer() {
     VcsRootImpl root = createVcsRoot("http://host.com/path");
     root.addProperty(Constants.UNCOMPRESSED_TRANSFER, "true");
-    Settings settings = new Settings(new File("."), root);
+    Settings settings = new Settings(root);
     assertTrue(settings.isUncompressedTransfer());
   }
 
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/BaseCommandTestCase.java	Wed Feb 16 13:35:57 2011 +0300
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/BaseCommandTestCase.java	Tue Mar 01 17:55:41 2011 +0300
@@ -19,6 +19,7 @@
 import jetbrains.buildServer.TempFiles;
 import jetbrains.buildServer.buildTriggers.vcs.mercurial.Constants;
 import jetbrains.buildServer.buildTriggers.vcs.mercurial.LocalRepositoryUtil;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.MirrorManager;
 import jetbrains.buildServer.buildTriggers.vcs.mercurial.Util;
 import jetbrains.buildServer.vcs.VcsException;
 import jetbrains.buildServer.vcs.VcsRoot;
@@ -36,6 +37,9 @@
   private String myPassword;
   private boolean myCloneRequired;
 
+  public BaseCommandTestCase() {
+  }
+
   protected void setRepository(final String repository, boolean cloneRequired) {
     myRepository = repository;
     myCloneRequired = cloneRequired;
@@ -69,25 +73,24 @@
 
     TempFiles tf = new TempFiles();
     File parentDir = tf.createTempDir();
-    final File workingDir = new File(parentDir, "rep").getAbsoluteFile();
 
+    MirrorManager mirrorManager = new MirrorManager(parentDir);
     VcsRoot vcsRoot = new VcsRootImpl(1, vcsRootProps);
-    Settings settings = new Settings(workingDir.getParentFile(), vcsRoot);
-    settings.setWorkingDir(workingDir);
+    Settings settings = new Settings(vcsRoot);
+    final File workingDir = mirrorManager.getMirrorDir(settings.getRepositoryUrl());
+    settings.setCustomWorkingDir(workingDir);
     try {
       if (myCloneRequired) {
-        CloneCommand cl = new CloneCommand(settings);
-        cl.setDestDir(settings.getLocalRepositoryDir().getAbsolutePath());
-        cl.execute();
+        new CloneCommand(settings, workingDir).execute();
       }
 
-      return executor.execute(settings);
+      return executor.execute(settings, workingDir);
     } finally {
       tf.cleanup();
     }
   }
 
   public interface CommandExecutor<T> {
-    T execute(@NotNull Settings settings) throws VcsException;
+    T execute(@NotNull Settings settings, File workingDir) throws VcsException;
   }
 }
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommandTest.java	Wed Feb 16 13:35:57 2011 +0300
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommandTest.java	Tue Mar 01 17:55:41 2011 +0300
@@ -20,6 +20,7 @@
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import java.io.File;
 import java.io.IOException;
 import java.util.List;
 
@@ -85,8 +86,8 @@
 
   private List<ChangeSet> runLog(final String fromId, final String toId) throws IOException, VcsException {
     return runCommand(new CommandExecutor<List<ChangeSet>>() {
-      public List<ChangeSet> execute(@NotNull final Settings settings) throws VcsException {
-        LogCommand lc = new LogCommand(settings);
+      public List<ChangeSet> execute(@NotNull final Settings settings, @NotNull File workingDir) throws VcsException {
+        LogCommand lc = new LogCommand(settings, workingDir);
         lc.setFromRevId(fromId);
         lc.setToRevId(toId);
         return lc.execute();
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/PushCommandTest.java	Wed Feb 16 13:35:57 2011 +0300
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/PushCommandTest.java	Tue Mar 01 17:55:41 2011 +0300
@@ -19,6 +19,7 @@
 import org.jetbrains.annotations.NotNull;
 import org.testng.annotations.Test;
 
+import java.io.File;
 import java.io.IOException;
 
 /**
@@ -34,8 +35,8 @@
 
     try {
       runCommand(new CommandExecutor<Boolean>() {
-        public Boolean execute(@NotNull final Settings settings) throws VcsException {
-          PushCommand cmd = new PushCommand(settings);
+        public Boolean execute(@NotNull final Settings settings, @NotNull File workingDir) throws VcsException {
+          PushCommand cmd = new PushCommand(settings, workingDir);
           cmd.execute();
           return null;
         }
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/StatusCommandTest.java	Wed Feb 16 13:35:57 2011 +0300
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/StatusCommandTest.java	Tue Mar 01 17:55:41 2011 +0300
@@ -54,8 +54,8 @@
 
   private List<ModifiedFile> runStatus(final String fromId, final String toId) throws IOException, VcsException {
     return runCommand(new CommandExecutor<List<ModifiedFile>>() {
-      public List<ModifiedFile> execute(@NotNull final Settings settings) throws VcsException {
-        StatusCommand st = new StatusCommand(settings);
+      public List<ModifiedFile> execute(@NotNull final Settings settings, @NotNull final File workingDir) throws VcsException {
+        StatusCommand st = new StatusCommand(settings, workingDir);
         st.setFromRevId(fromId);
         st.setToRevId(toId);
         return st.execute();
--- a/mercurial-tests/src/testng.xml	Wed Feb 16 13:35:57 2011 +0300
+++ b/mercurial-tests/src/testng.xml	Tue Mar 01 17:55:41 2011 +0300
@@ -8,6 +8,7 @@
       <class name="jetbrains.buildServer.buildTriggers.vcs.mercurial.MercurialVcsSupportTest"/>
       <class name="jetbrains.buildServer.buildTriggers.vcs.mercurial.AgentSideCheckoutTest"/>
       <class name="jetbrains.buildServer.buildTriggers.vcs.mercurial.SettingsTest"/>
+      <class name="jetbrains.buildServer.buildTriggers.vcs.mercurial.MirrorManagerTest"/>
     </classes>
   </test>
 </suite>
--- a/mercurial.ipr	Wed Feb 16 13:35:57 2011 +0300
+++ b/mercurial.ipr	Tue Mar 01 17:55:41 2011 +0300
@@ -388,6 +388,9 @@
     </library>
     <library name="JMock">
       <CLASSES>
+        <root url="jar://$PROJECT_DIR$/mercurial-tests/lib/hamcrest-library-1.1.jar!/" />
+        <root url="jar://$PROJECT_DIR$/mercurial-tests/lib/hamcrest-core-1.1.jar!/" />
+        <root url="jar://$PROJECT_DIR$/mercurial-tests/lib/jmock-2.5.1.jar!/" />
         <root url="jar://$PROJECT_DIR$/mercurial-tests/lib/jmock-SNAPSHOT.jar!/" />
       </CLASSES>
       <JAVADOC />
--- a/mercurial.xml	Wed Feb 16 13:35:57 2011 +0300
+++ b/mercurial.xml	Tue Mar 01 17:55:41 2011 +0300
@@ -93,6 +93,9 @@
   </path>
   
   <path id="library.jmock.classpath">
+    <pathelement location="${basedir}/mercurial-tests/lib/hamcrest-core-1.1.jar"/>
+    <pathelement location="${basedir}/mercurial-tests/lib/hamcrest-library-1.1.jar"/>
+    <pathelement location="${basedir}/mercurial-tests/lib/jmock-2.5.1.jar"/>
     <pathelement location="${basedir}/mercurial-tests/lib/jmock-SNAPSHOT.jar"/>
   </path>