changeset 380:3bc8125671b8

Merge branch Eluru-6.5.x
author Dmitry Neverov <dmitry.neverov@jetbrains.com>
date Wed, 15 Feb 2012 13:23:53 +0400
parents a4829fde54f5 (diff) 55c2c88a2d82 (current diff)
children 44a16c27bed6
files mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/BaseCommand.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommandResult.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommand.java mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/CommandFactoryImpl.java mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java
diffstat 100 files changed, 6267 insertions(+), 3268 deletions(-) [+]
line wrap: on
line diff
--- a/build.xml	Wed Feb 15 12:07:12 2012 +0400
+++ b/build.xml	Wed Feb 15 13:23:53 2012 +0400
@@ -1,72 +1,72 @@
-<project name="Mercurial VCS Support" default="dist" basedir=".">
-  <property file="mercurial.properties"/>
-  <import file="mercurial.xml"/>
-
-  <property name="distPath" value="${basedir}/dist"/>
-
-  <property name="plugin.name" value="mercurial"/>
-
-  <property name="build.number" value=""/>
-  <tstamp>
-    <format property="timestamp" pattern="yyyyMMddhhmmss"/>
-  </tstamp>
-  <property name="snapshot.build.number" value="SNAPSHOT-${timestamp}"/>
-  <property name="build.vcs.number" value=""/>
-
-  <condition property="plugin.version" value="${snapshot.build.number}" else="${build.number}">
-    <matches pattern="snapshot-.*" string="${build.number}" casesensitive="false"/>
-  </condition>
-
-  <import file="teamcity-common.xml"/>
-
-  <target name="package" depends="define.version">
-    <package.teamcity.plugin name="${plugin.name}"
-                             server.output="${mercurial-server.output.dir}"
-                             agent.output="${mercurial-agent.output.dir}"
-                             common.output="${mercurial-common.output.dir}"
-                             plugin.descriptor.file="${basedir}/teamcity-plugin.xml"
-                             plugin.version="${plugin.version}"/>
-  </target>
-
-  <target name="define.version" depends="define.version.if.under.teamcity">
-    <tstamp>
-      <format property="current.time" pattern="yyyyMMddHHmm"/>
-    </tstamp>
-    <property name="plugin.version" value="SNAPSHOT-${current.time}"/>
-  </target>
-
-  <target name="define.version.if.under.teamcity" if="build.number">
-    <property name="plugin.version" value="${build.number}"/>
-  </target>
-
-  <target name="dist" depends="check.teamcitydistribution,all,package"/>
-
-  <target name="deploy" depends="dist">
-    <deploy.teamcity.plugin name="${plugin.name}"/>
-  </target>
-
-  <taskdef name="testng" classname="org.testng.TestNGAntTask" classpath="${basedir}/mercurial-tests/lib/testng-5.7-jdk15.jar"/>
-
-  <path id="tests_classpath">
-    <pathelement location="${agent.home.dir}/lib/runtime-util.jar"/>
-    <pathelement location="${agent.home.dir}/lib/buildServerRuntimeUtil.jar"/>
-    <path refid="mercurial-tests.runtime.module.classpath"/>
-  </path>
-
-  <target name="run-tests" depends="clean, init, compile.module.mercurial-tests">
-    <property name="suspend" value="n"/>
-
-    <testng haltonfailure="no" failureProperty="failure_found" listener="org.testng.reporters.TestHTMLReporter"
-            outputdir="${basedir}/test-output" classpathref="tests_classpath" dumpcommand="true" workingDir="${basedir}">
-
-      <jvmarg value="-ea"/>
-      <!--<jvmarg value="-Xrunjdwp:transport=dt_socket,server=y,suspend=${suspend},address=5555"/>-->
-
-      <sysproperty key="java.awt.headless" value="true"/>
-
-      <xmlfileset dir="${basedir}/mercurial-tests/src">
-        <include name="testng.xml"/>
-      </xmlfileset>
-    </testng>
-  </target>
-</project>
+<project name="Mercurial VCS Support" default="dist" basedir=".">
+  <property file="mercurial.properties"/>
+  <import file="mercurial.xml"/>
+
+  <property name="distPath" value="${basedir}/dist"/>
+
+  <property name="plugin.name" value="mercurial"/>
+
+  <property name="build.number" value=""/>
+  <tstamp>
+    <format property="timestamp" pattern="yyyyMMddhhmmss"/>
+  </tstamp>
+  <property name="snapshot.build.number" value="SNAPSHOT-${timestamp}"/>
+  <property name="build.vcs.number" value=""/>
+
+  <condition property="plugin.version" value="${snapshot.build.number}" else="${build.number}">
+    <matches pattern="snapshot-.*" string="${build.number}" casesensitive="false"/>
+  </condition>
+
+  <import file="teamcity-common.xml"/>
+
+  <target name="package" depends="define.version">
+    <package.teamcity.plugin name="${plugin.name}"
+                             server.output="${mercurial-server.output.dir}"
+                             agent.output="${mercurial-agent.output.dir}"
+                             common.output="${mercurial-common.output.dir}"
+                             plugin.descriptor.file="${basedir}/teamcity-plugin.xml"
+                             plugin.version="${plugin.version}"/>
+  </target>
+
+  <target name="define.version" depends="define.version.if.under.teamcity">
+    <tstamp>
+      <format property="current.time" pattern="yyyyMMddHHmm"/>
+    </tstamp>
+    <property name="plugin.version" value="SNAPSHOT-${current.time}"/>
+  </target>
+
+  <target name="define.version.if.under.teamcity" if="build.number">
+    <property name="plugin.version" value="${build.number}"/>
+  </target>
+
+  <target name="dist" depends="check.teamcitydistribution,all,package"/>
+
+  <target name="deploy" depends="dist">
+    <deploy.teamcity.plugin name="${plugin.name}"/>
+  </target>
+
+  <taskdef name="testng" classname="org.testng.TestNGAntTask" classpath="${basedir}/mercurial-tests/lib/testng-5.7-jdk15.jar"/>
+
+  <path id="tests_classpath">
+    <pathelement location="${agent.home.dir}/lib/runtime-util.jar"/>
+    <pathelement location="${agent.home.dir}/lib/buildServerRuntimeUtil.jar"/>
+    <path refid="mercurial-tests.runtime.module.classpath"/>
+  </path>
+
+  <target name="run-tests" depends="clean, init, compile.module.mercurial-tests">
+    <property name="suspend" value="n"/>
+
+    <testng haltonfailure="no" failureProperty="failure_found" listener="org.testng.reporters.TestHTMLReporter"
+            outputdir="${basedir}/test-output" classpathref="tests_classpath" dumpcommand="true" workingDir="${basedir}">
+
+      <jvmarg value="-ea"/>
+      <!--<jvmarg value="-Xrunjdwp:transport=dt_socket,server=y,suspend=${suspend},address=5555"/>-->
+
+      <sysproperty key="java.awt.headless" value="true"/>
+
+      <xmlfileset dir="${basedir}/mercurial-tests/src">
+        <include name="testng.xml"/>
+      </xmlfileset>
+    </testng>
+  </target>
+</project>
--- a/mercurial-agent/src/META-INF/build-agent-plugin-mercurial.xml	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-agent/src/META-INF/build-agent-plugin-mercurial.xml	Wed Feb 15 13:23:53 2012 +0400
@@ -1,6 +1,10 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
-
-<beans default-autowire="constructor">
-  <bean id="mercurialAgent" class="jetbrains.buildServer.buildTriggers.vcs.mercurial.MercurialAgentSideVcsSupport" />
-</beans>
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
+
+<beans default-autowire="constructor">
+  <bean id="mercurialAgent" class="jetbrains.buildServer.buildTriggers.vcs.mercurial.MercurialAgentSideVcsSupport" />
+  <bean id="hgPathProvider" class="jetbrains.buildServer.buildTriggers.vcs.mercurial.AgentHgPathProvider" />
+  <bean id="hgDetector" class="jetbrains.buildServer.buildTriggers.vcs.mercurial.HgDetector" />
+  <bean id="pluginConfig" class="jetbrains.buildServer.buildTriggers.vcs.mercurial.AgentPluginConfigImpl"/>
+  <bean id="mirrorManager" class="jetbrains.buildServer.buildTriggers.vcs.mercurial.MirrorManagerImpl" />
+</beans>
--- a/mercurial-agent/src/build-agent-plugin.xml	Wed Feb 15 12:07:12 2012 +0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
-<!--
-PicoContainer configuration for old fashioned TeamCity agent plugins.
-In TeamCity 4.0 Spring configuration must be created instead.
--->
-<container>
-  <component class="jetbrains.buildServer.buildTriggers.vcs.mercurial.MercurialAgentSideVcsSupport" />
-</container>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-agent/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/AgentHgPathProvider.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,32 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import jetbrains.buildServer.agent.BuildAgentConfiguration;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.Settings;
+import jetbrains.buildServer.parameters.ProcessingResult;
+import jetbrains.buildServer.parameters.ValueResolver;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author dmitry.neverov
+ */
+public class AgentHgPathProvider implements HgPathProvider {
+
+  private final ValueResolver myResolver;
+
+
+  public AgentHgPathProvider(@NotNull final BuildAgentConfiguration agentConfig) {
+    myResolver = agentConfig.getParametersResolver();
+  }
+
+
+  public String getHgPath(@NotNull final Settings settings) {
+    String pathFromRoot = settings.getHgPath();
+    return resolve(pathFromRoot);
+  }
+
+
+  private String resolve(@NotNull final String value) {
+    ProcessingResult result = myResolver.resolve(value);
+    return result.getResult();
+  }
+}
--- a/mercurial-agent/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/AgentPluginConfig.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-agent/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/AgentPluginConfig.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,10 +1,14 @@
 package jetbrains.buildServer.buildTriggers.vcs.mercurial;
 
+import jetbrains.buildServer.agent.AgentRunningBuild;
+import org.jetbrains.annotations.NotNull;
+
 /**
  * @author dmitry.neverov
  */
 public interface AgentPluginConfig extends PluginConfig {
 
-  boolean isUseLocalMirrors();
+  boolean isUseLocalMirrors(@NotNull AgentRunningBuild build);
 
+  int getPullTimeout(@NotNull AgentRunningBuild build);
 }
--- a/mercurial-agent/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/AgentPluginConfigImpl.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-agent/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/AgentPluginConfigImpl.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,9 +1,12 @@
 package jetbrains.buildServer.buildTriggers.vcs.mercurial;
 
 import jetbrains.buildServer.agent.AgentRunningBuild;
+import jetbrains.buildServer.agent.BuildAgentConfiguration;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import java.io.File;
+
 /**
  * @author dmitry.neverov
  */
@@ -12,18 +15,23 @@
   private static final String PULL_TIMEOUT_SECONDS = "teamcity.hg.pull.timeout.seconds";
   private final int DEFAULT_PULL_TIMEOUT_SECONDS = 3600;
 
-  private final AgentRunningBuild myBuild;
+  private final BuildAgentConfiguration myAgentConfig;
 
-  public AgentPluginConfigImpl(@NotNull AgentRunningBuild build) {
-    myBuild = build;
+  public AgentPluginConfigImpl(@NotNull BuildAgentConfiguration agentConfig) {
+    myAgentConfig = agentConfig;
   }
 
-  public boolean isUseLocalMirrors() {
-    return "true".equals(myBuild.getSharedConfigParameters().get("teamcity.hg.use.local.mirrors"));
+  public boolean isUseLocalMirrors(@NotNull AgentRunningBuild build) {
+    return "true".equals(build.getSharedConfigParameters().get("teamcity.hg.use.local.mirrors"));
   }
 
-  public int getPullTimeout() {
-    Integer timeout = parseTimeout(myBuild.getSharedConfigParameters().get(PULL_TIMEOUT_SECONDS));
+  @NotNull
+  public File getCachesDir() {
+    return myAgentConfig.getCacheDirectory("mercurial");
+  }
+
+  public int getPullTimeout(@NotNull AgentRunningBuild build) {
+    Integer timeout = parseTimeout(build.getSharedConfigParameters().get(PULL_TIMEOUT_SECONDS));
     if (timeout != null)
       return timeout;
     return DEFAULT_PULL_TIMEOUT_SECONDS;
@@ -38,7 +46,7 @@
       if (timeout > 0)
         return timeout;
       else
-        return null;
+         return null;
     } catch (NumberFormatException e) {
       return null;
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-agent/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/HgDetector.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,90 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import com.intellij.openapi.diagnostic.Logger;
+import jetbrains.buildServer.agent.AgentLifeCycleAdapter;
+import jetbrains.buildServer.agent.AgentLifeCycleListener;
+import jetbrains.buildServer.agent.BuildAgent;
+import jetbrains.buildServer.agent.BuildAgentConfiguration;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.VersionCommand;
+import jetbrains.buildServer.util.EventDispatcher;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author dmitry.neverov
+ */
+public class HgDetector extends AgentLifeCycleAdapter {
+
+  final static String AGENT_HG_PATH_PROPERTY = "teamcity.hg.agent.path";
+  private final static Logger LOG = Logger.getInstance(HgDetector.class.getName());
+  private final static HgVersion LEAST_SUPPORTED_VERSION = new HgVersion(1, 5, 2);
+  private final List<String> myHgPaths = Arrays.asList("hg");
+
+
+  public HgDetector(@NotNull final EventDispatcher<AgentLifeCycleListener> dispatcher) {
+    dispatcher.addListener(this);
+  }
+
+
+  @Override
+  public void beforeAgentConfigurationLoaded(@NotNull final BuildAgent agent) {
+    BuildAgentConfiguration config = agent.getConfiguration();
+    String agentHgPath = config.getConfigurationParameters().get(AGENT_HG_PATH_PROPERTY);
+    File workDir = config.getTempDirectory();
+    if (agentHgPath == null) {
+      String detectedHg = detectHg(workDir);
+      if (detectedHg != null) {
+        LOG.info("Detect installed mercurial at path " + detectedHg + ", provide it as a property " + AGENT_HG_PATH_PROPERTY);
+        config.addConfigurationParameter(AGENT_HG_PATH_PROPERTY, "hg");
+      } else {
+        LOG.info("Cannot detect installed mercurial");
+      }
+    } else {
+      if (!canRunHg(agentHgPath, workDir, true))
+        LOG.warn("Mercurial executable at path " + agentHgPath + " cannot be run or not compatible with TeamCity");
+    }
+  }
+
+
+  @Nullable
+  private String detectHg(@NotNull final File workDir) {
+    for (String path : myHgPaths) {
+      if (canRunHg(path, workDir))
+        return path;
+    }
+    return null;
+  }
+
+
+  private boolean canRunHg(@NotNull final String hgPath, @NotNull final File workDir) {
+    return canRunHg(hgPath, workDir, false);
+  }
+
+  private boolean canRunHg(@NotNull final String hgPath, @NotNull final File workDir, boolean logWarnings) {
+    VersionCommand versionCommand = new VersionCommand(hgPath, workDir);
+    try {
+      HgVersion version = versionCommand.execute();
+      if (isCompatible(version)) {
+        return true;
+      } else {
+        if (logWarnings)
+          LOG.warn("Mercurial version at path " + hgPath + " is " + version + ", required version is " + LEAST_SUPPORTED_VERSION + "+");
+        return false;
+      }
+    } catch (VcsException e) {
+      if (logWarnings)
+        LOG.warn("Error while trying to get hg version, hg path " + hgPath + ", error: " + e.getMessage());
+      return false;
+    }
+  }
+
+
+  private boolean isCompatible(@NotNull final HgVersion version) {
+    return version.isEqualsOrGreaterThan(LEAST_SUPPORTED_VERSION);
+  }
+}
--- a/mercurial-agent/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialAgentSideVcsSupport.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-agent/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialAgentSideVcsSupport.java	Wed Feb 15 13:23:53 2012 +0400
@@ -16,58 +16,33 @@
 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.UpdateByIncludeRules2;
 import jetbrains.buildServer.agent.vcs.UpdatePolicy;
-import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.*;
-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.jetbrains.annotations.NotNull;
 
 import java.io.File;
-import java.io.IOException;
-import java.util.Collections;
-
-import static jetbrains.buildServer.buildTriggers.vcs.mercurial.command.CommandUtil.removePrivateData;
 
 public class MercurialAgentSideVcsSupport extends AgentVcsSupport implements UpdateByIncludeRules2 {
 
+  private final AgentPluginConfig myConfig;
+  private final HgPathProvider myHgPathProvider;
   private final MirrorManager myMirrorManager;
 
-  public MercurialAgentSideVcsSupport(BuildAgentConfiguration agentConfiguration) {
-    myMirrorManager = new MirrorManager(agentConfiguration.getCacheDirectory("mercurial"));
+  public MercurialAgentSideVcsSupport(@NotNull final AgentPluginConfig pluginConfig,
+                                      @NotNull final HgPathProvider hgPathProvider,
+                                      @NotNull final MirrorManager mirrorManager) {
+    myConfig = pluginConfig;
+    myHgPathProvider = hgPathProvider;
+    myMirrorManager = mirrorManager;
   }
 
   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 AgentPluginConfig config = new AgentPluginConfigImpl(build);
-    final BuildProgressLogger logger = build.getBuildLogger();
-    return new IncludeRuleUpdater() {
-      public void process(@NotNull final IncludeRule includeRule, @NotNull final File workingDir) throws VcsException {
-        try {
-          checkRuleIsValid(includeRule);
-          Settings settings = new Settings(vcsRoot);
-          if (config.isUseLocalMirrors()) {
-            updateLocalMirror(vcsRoot, logger, config);
-          }
-          updateRepository(workingDir, settings, logger, config);
-          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(myConfig, myMirrorManager, myHgPathProvider, vcsRoot, toVersion, build);
   }
 
   @NotNull
@@ -81,90 +56,4 @@
   public UpdatePolicy getUpdatePolicy() {
     return this;
   }
-
-  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, AgentPluginConfig config) throws VcsException, IOException {
-    if (!Settings.isValidRepository(workingDir)) {
-      initRepository(workingDir, settings, logger, config.isUseLocalMirrors());
-    } else {
-      ensureUseRightRepository(workingDir, settings, logger, config.isUseLocalMirrors());
-    }
-    String defaultPullUrl = getDefaultPullUrl(settings, config.isUseLocalMirrors());
-    logger.message("Start pulling changes from " + removePrivateData(defaultPullUrl, Collections.singleton(settings.getPassword())));
-    new PullCommand(settings, workingDir, defaultPullUrl).execute(config.getPullTimeout());
-    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, AgentPluginConfig config) throws VcsException, IOException {
-    Settings settings = new Settings(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(config.getPullTimeout());
-    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	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,335 @@
+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 com.intellij.openapi.util.io.FileUtil.delete;
+import static jetbrains.buildServer.buildTriggers.vcs.mercurial.command.CommandUtil.removePrivateData;
+import static jetbrains.buildServer.util.FileUtil.isEmptyDir;
+
+/**
+ * @author dmitry.neverov
+ */
+public class MercurialIncludeRuleUpdater implements IncludeRuleUpdater {
+
+  private final AgentPluginConfig myConfig;
+  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;
+  private int myPullTimeout;
+
+  public MercurialIncludeRuleUpdater(@NotNull final AgentPluginConfig pluginConfig,
+                                     @NotNull final MirrorManager mirrorManager,
+                                     @NotNull final HgPathProvider hgPathProvider,
+                                     @NotNull final VcsRoot root,
+                                     @NotNull final String toVersion,
+                                     @NotNull final AgentRunningBuild build) {
+    myConfig = pluginConfig;
+    myMirrorManager = mirrorManager;
+    myHgPathProvider = hgPathProvider;
+    myRoot = root;
+    mySettings = new Settings(myHgPathProvider, myRoot);
+    myToVersion = toVersion;
+    myLogger = build.getBuildLogger();
+    myUseLocalMirrors = myConfig.isUseLocalMirrors(build);
+    myPullTimeout = myConfig.getPullTimeout(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 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 {
+    String defaultPullUrl = getDefaultPullUrl(mySettings, myUseLocalMirrors);
+    if (isEmptyDir(workingDir)) {
+      workingDir.mkdirs();
+      myLogger.message("Start cloning from " + removePrivateData(defaultPullUrl, Collections.singleton(mySettings.getPassword())));
+      CloneCommand clone = new CloneCommand(mySettings, workingDir);
+      clone.setRepository(defaultPullUrl);
+      clone.setUsePullProtocol(false);
+      clone.setUpdateWorkingDir(false);
+      clone.execute();
+      myLogger.message("Repository successfully cloned");
+    } else {
+      if (!Settings.isValidRepository(workingDir)) {
+        initRepository(mySettings, workingDir, myUseLocalMirrors);
+      } else {
+        ensureUseRightRepository(workingDir);
+      }
+      myLogger.message("Start pulling changes from " + removePrivateData(defaultPullUrl, Collections.singleton(mySettings.getPassword())));
+      new PullCommand(mySettings, workingDir, defaultPullUrl).execute(myPullTimeout);
+      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.getRepository());
+    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(myPullTimeout);
+    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 {
+          logSubrepositoriesUrlsChanged(workingDirRevision, myToVersion, subrepositoriesWithChangedUrls);
+          FileUtil.delete(workingDir);
+          updateRepository(workingDir);
+          doUpdateWorkingDir(workingDir);
+        }
+      }
+    }
+  }
+
+
+  private void logSubrepositoriesUrlsChanged(@NotNull final String fromVersion,
+                                             @NotNull final String toVersion,
+                                             @NotNull final Map<String, Pair<String, String>> subrepositoriesWithChangedUrls) {
+    StringBuilder sb = new StringBuilder();
+    sb.append("Subrepositories' URLs were changed between revisions ").append(fromVersion).append("..").append(toVersion).append(":\n");
+    for (Map.Entry<String, Pair<String, String>> entry : subrepositoriesWithChangedUrls.entrySet()) {
+      String path = entry.getKey();
+      String oldUrl = entry.getValue().first;
+      String newUrl = entry.getValue().second;
+      sb.append("path: \"").append(path).append("\", old URL: \"").append(oldUrl).append("\", new URL: \"").append(newUrl).append("\";\n");
+    }
+    sb.append("do clean checkout.");
+    myLogger.warning(sb.toString());
+  }
+
+
+  /*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);
+    File catDir = null;
+    try {
+      catDir = cc.execute(Arrays.asList(".hgsub", ".hgsubstate"), false);
+      File hgsub = new File(catDir, ".hgsub");
+      File hgsubstate = new File(catDir, ".hgsubstate");
+      return readSubrepositories(hgsub, hgsubstate);
+    } catch (VcsException e) {
+      return Collections.emptyMap();
+    } finally {
+      if (catDir != null)
+        delete(catDir);
+    }
+  }
+
+
+  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 update = new UpdateCommand(mySettings, workingDir);
+    addUpdateAuthConfigParams(update);
+    update.setToId(new ChangeSet(myToVersion).getId());
+    update.execute();
+    myLogger.message("Folder successfully updated");
+  }
+
+
+  private void addUpdateAuthConfigParams(@NotNull UpdateCommand update) {
+    String username = mySettings.getUsername();
+    String password = mySettings.getPassword();
+    if (username == null || password == null)
+      return;
+    update.withConfig("auth.tc.prefix", "*")
+          .withConfig("auth.tc.username", username)
+          .withConfig("auth.tc.password", password)
+          .withConfig("auth.tc.schemes", "http https");
+  }
+
+
+  private String getDefaultPullUrl(Settings settings, boolean useLocalMirror) throws IOException {
+    if (useLocalMirror) {
+      File mirrorDir = myMirrorManager.getMirrorDir(settings.getRepository());
+      return mirrorDir.getCanonicalPath();
+    } else {
+      return settings.getRepositoryUrlWithCredentials();
+    }
+  }
+
+
+  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.getRepository());
+      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/Constants.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/Constants.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,30 +1,31 @@
-/*
- * Copyright 2000-2011 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package jetbrains.buildServer.buildTriggers.vcs.mercurial;
-
-import jetbrains.buildServer.vcs.VcsRoot;
-
-public interface Constants {
-  String VCS_NAME = "mercurial";
-  String REPOSITORY_PROP = "repositoryPath";
-  String BRANCH_NAME_PROP = "branchName";
-  String HG_COMMAND_PATH_PROP = "hgCommandPath";
-  String HG_PATH_ENV = "TEAMCITY_HG_PATH";
-  String SERVER_CLONE_PATH_PROP = "serverClonePath";
-  String USERNAME = "username";
-  String PASSWORD = VcsRoot.SECURE_PROPERTY_PREFIX + "password";
-  String UNCOMPRESSED_TRANSFER = "uncompressedTransfer";
-}
+/*
+ * Copyright 2000-2011 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import jetbrains.buildServer.vcs.VcsRoot;
+
+public interface Constants {
+  String VCS_NAME = "mercurial";
+  String REPOSITORY_PROP = "repositoryPath";
+  String BRANCH_NAME_PROP = "branchName";
+  String HG_COMMAND_PATH_PROP = "hgCommandPath";
+  String HG_PATH_ENV = "TEAMCITY_HG_PATH";
+  String SERVER_CLONE_PATH_PROP = "serverClonePath";
+  String USERNAME = "username";
+  String PASSWORD = VcsRoot.SECURE_PROPERTY_PREFIX + "password";
+  String UNCOMPRESSED_TRANSFER = "uncompressedTransfer";
+  String USER_FOR_TAG = "tagUsername";
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/HgPathProvider.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,13 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.Settings;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author dmitry.neverov
+ */
+public interface HgPathProvider {
+
+  String getHgPath(@NotNull Settings settings);
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/HgVersion.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,104 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author dmitry.neverov
+ */
+public class HgVersion implements Comparable<HgVersion> {
+
+  private static final String PREFIX = "Mercurial Distributed SCM (version ";
+
+  private final int myMajor;
+  private final int myMinor;
+  private final int myThird;
+
+
+  public HgVersion(int major, int minor, int third) {
+    myMajor = major;
+    myMinor = minor;
+    myThird = third;
+  }
+
+
+  public static HgVersion parse(@NotNull final String version) {
+    Parser p = new Parser(version);
+    if (!p.skipString(PREFIX))
+      throw new IllegalArgumentException("Incorrect version format: " + version);
+    int major = p.readInt();
+    p.skipString(".");
+    int minor = p.readInt();
+    int third = p.skipString(".") ? p.readInt() : 0;
+    return new HgVersion(major, minor, third);
+  }
+
+
+  public boolean isEqualsOrGreaterThan(HgVersion other) {
+    return compareTo(other) >= 0;
+  }
+
+
+  @Override
+  public String toString() {
+    return myMajor + "." + myMinor + "." + myThird;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    return (o instanceof HgVersion) && this.compareTo((HgVersion) o) == 0;
+  }
+
+  @Override
+  public int hashCode() {
+    int result = myMajor;
+    result = 31 * result + myMinor;
+    result = 31 * result + myThird;
+    return result;
+  }
+
+  public int compareTo(HgVersion other) {
+    int d = myMajor - other.myMajor;
+    if (d != 0)
+      return d;
+
+    d = myMinor - other.myMinor;
+    if (d != 0)
+      return d;
+
+    return myThird - other.myThird;
+  }
+
+
+  private static final class Parser {
+
+    private final String myString;
+    private int myIndex = 0;
+
+    Parser(@NotNull String string) {
+      myString = string;
+    }
+
+
+    boolean skipString(String str) {
+      if (myIndex == myString.length() || myIndex + str.length() > myString.length())
+        return false;
+      String substr = myString.substring(myIndex, myIndex + str.length());
+      if (substr.equals(str)) {
+        myIndex = myIndex + str.length();
+        return true;
+      } else {
+        return false;
+      }
+    }
+
+
+    int readInt() {
+      int result = 0;
+      while (myIndex < myString.length() && Character.isDigit(myString.codePointAt(myIndex))) {
+        result = result * 10 + Character.digit(myString.codePointAt(myIndex), 10);
+        myIndex++;
+      }
+      return result;
+    }
+  }
+}
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MirrorManager.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MirrorManager.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,47 +1,15 @@
 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();
-  }
-
+public interface MirrorManager {
 
   /**
    * Get directory of local mirror repository for specified url, if directory is not exists it is created
@@ -49,197 +17,29 @@
    * @return see above
    */
   @NotNull
-  public File getMirrorDir(@NotNull final String url) {
-    File result = getMirrorDirWithLock(url);
-    if (result == null) {
-      result = createDirFor(url);
-    }
-    return result;
-  }
-
+  public File getMirrorDir(@NotNull final String url);
 
   /**
    * 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();
-    }
-  }
-
+  public List<File> getMirrors();
 
-  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) {
-          if (!line.equals(""))
-            LOG.warn("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();
-    }
-  }
+  /**
+   * Forget specified dir. After call to this method with non-empty dir,
+   * all urls which were mapped to this dir will be mapped to another.
+   * If dir is empty, subsequent call getMirrorDir(dir) will return the
+   * same dir.
+   *
+   * @param dir dir of interest
+   */
+  public void forgetDir(@NotNull final File dir);
 
-  /*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.debug("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());
-      } else {
-        try {
-          if (!myMappingFile.createNewFile())
-            LOG.warn("Someone else creates a mapping file " + myMappingFile.getAbsolutePath() + ", will use it");
-        } catch (IOException e) {
-          LOG.error("Cannot create a mapping file at " + myMappingFile.getAbsolutePath(), e);
-        }
-      }
-      return new ArrayList<String>();
-    }
-  }
+  @NotNull
+  public Map<String, File> getMappings();
 
+  public void lockDir(@NotNull File dir);
 
-  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);
-  }
+  public void unlockDir(@NotNull File dir);
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MirrorManagerImpl.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,331 @@
+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.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import static jetbrains.buildServer.util.FileUtil.isEmptyDir;
+
+/**
+ * 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 MirrorManagerImpl implements MirrorManager {
+
+  private static Logger LOG = Logger.getInstance(MirrorManagerImpl.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();
+
+  private final ConcurrentMap<String, Lock> myDirLocks = new ConcurrentHashMap<String, Lock>();
+
+  public MirrorManagerImpl(@NotNull PluginConfig config) {
+    myRootDir = config.getCachesDir();
+    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();
+    }
+  }
+
+
+  @NotNull
+  public Map<String, File> getMappings() {
+    myLock.readLock().lock();
+    try {
+      return new HashMap<String, File>(myMirrors);
+    } finally {
+      myLock.readLock().unlock();
+    }
+  }
+
+  public void lockDir(@NotNull final File dir) {
+    lockFor(dir).lock();
+  }
+
+  public void unlockDir(@NotNull final File dir) {
+    lockFor(dir).unlock();
+  }
+
+  private Lock lockFor(final File dir) {
+    String path = dir.getAbsolutePath();
+    Lock lock = myDirLocks.get(path);
+    if (lock == null) {
+      lock = new ReentrantLock();
+      Lock curLock = myDirLocks.putIfAbsent(path, lock);
+      if (curLock != null)
+        lock = curLock;
+    }
+    return lock;
+  }
+
+  /**
+   * Forget specified dir. After call to this method with non-empty dir,
+   * all urls which were mapped to this dir will be mapped to another.
+   * If dir is empty, subsequent call getMirrorDir(dir) will return the
+   * same dir.
+   *
+   * @param dir dir of interest
+   */
+  public void forgetDir(@NotNull final File dir) {
+    myLock.writeLock().lock();
+    try {
+      removeMappingsToDir(dir);
+      saveMappingToFile();
+    } finally {
+      myLock.writeLock().unlock();
+    }
+  }
+
+  private void removeMappingsToDir(@NotNull final File dir) {
+    Set<String> keysToRemove = getUrlsMappedToDir(dir);
+    for (String key : keysToRemove) {
+      myMirrors.remove(key);
+    }
+  }
+
+  private Set<String> getUrlsMappedToDir(@NotNull final File dir) {
+    Set<String> urlsMappedToDir = new HashSet<String>();
+    for (Map.Entry<String, File> entry : myMirrors.entrySet()) {
+      File f = entry.getValue();
+      if (f.equals(dir))
+        urlsMappedToDir.add(entry.getKey());
+    }
+    return urlsMappedToDir;
+  }
+
+
+  //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) || !isEmptyDir(result)) {
+        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) {
+          if (!line.equals(""))
+            LOG.warn("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.debug("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());
+      } else {
+        try {
+          if (!myMappingFile.createNewFile())
+            LOG.warn("Someone else creates a mapping file " + myMappingFile.getAbsolutePath() + ", will use it");
+        } catch (IOException e) {
+          LOG.error("Cannot create a mapping file at " + myMappingFile.getAbsolutePath(), e);
+        }
+      }
+      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();
+    }
+  }
+
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    myLock.readLock().lock();
+    try {
+      Iterator<Map.Entry<String, File>> iter = myMirrors.entrySet().iterator();
+      while (iter.hasNext()) {
+        Map.Entry<String, File> entry = iter.next();
+        sb.append("[").append(entry.getKey()).append("]").append("->").append(entry.getValue().getAbsolutePath());
+        if (iter.hasNext())
+          sb.append("\n");
+      }
+    } finally {
+      myLock.readLock().unlock();
+    }
+    return sb.toString();
+  }
+
+  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/PathUtil.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/PathUtil.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,37 +1,37 @@
-/*
- * Copyright 2000-2011 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package jetbrains.buildServer.buildTriggers.vcs.mercurial;
-
-import org.jetbrains.annotations.NotNull;
-
-import java.io.File;
-import java.io.IOException;
-
-public class PathUtil {
-  @NotNull
-  public static String normalizeSeparator(@NotNull String repPath) {
-    return repPath.replace('\\', '/');
-  }
-
-  @NotNull
-  public static File getCanonicalFile(@NotNull File file) {
-    try {
-      return file.getCanonicalFile();
-    } catch (IOException e) {
-      return file.getAbsoluteFile();
-    }
-  }
-}
+/*
+ * Copyright 2000-2011 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.IOException;
+
+public class PathUtil {
+  @NotNull
+  public static String normalizeSeparator(@NotNull String repPath) {
+    return repPath.replace('\\', '/');
+  }
+
+  @NotNull
+  public static File getCanonicalFile(@NotNull File file) {
+    try {
+      return file.getCanonicalFile();
+    } catch (IOException e) {
+      return file.getAbsoluteFile();
+    }
+  }
+}
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/PluginConfig.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/PluginConfig.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,10 +1,15 @@
 package jetbrains.buildServer.buildTriggers.vcs.mercurial;
 
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+
 /**
  * @author dmitry.neverov
  */
 public interface PluginConfig {
 
-  int getPullTimeout();
+  @NotNull
+  File getCachesDir();
 
 }
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/ArchiveCommand.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/ArchiveCommand.java	Wed Feb 15 13:23:53 2012 +0400
@@ -20,7 +20,9 @@
 
 import java.io.File;
 
-public class ArchiveCommand extends BaseCommand {
+import static jetbrains.buildServer.buildTriggers.vcs.mercurial.command.CommandExecutionSettingsBuilder.with;
+
+public class ArchiveCommand extends VcsRootCommand {
   private File myDestDir;
   private String myToId;
 
@@ -45,8 +47,7 @@
     setRevision(cli);
     setDestination(cli);
 
-    CommandResult res = runCommand(cli);
-    failIfNotEmptyStdErr(cli, res);
+    runCommand(cli, with().failureWhenStderrNotEmpty());
     deleteHgArchival();
   }
 
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/BaseCommand.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/BaseCommand.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,91 +1,60 @@
-/*
- * Copyright 2000-2011 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
-
-import com.intellij.execution.configurations.GeneralCommandLine;
-import jetbrains.buildServer.util.StringUtil;
-import jetbrains.buildServer.vcs.VcsException;
-import org.jetbrains.annotations.NotNull;
-
-import java.io.File;
-import java.util.Collections;
-import java.util.Set;
-
-/**
- * @author pavel
- */
-public class BaseCommand {
-  private Settings mySettings;
-  private String myWorkDirectory;
-
-  public BaseCommand(@NotNull final Settings settings, @NotNull File workingDir) {
-    mySettings = settings;
-    myWorkDirectory = workingDir.getAbsolutePath();
-  }
-
-
-  public Settings getSettings() {
-    return mySettings;
-  }
-
-  /**
-   * Sets new working directory, by default working directory is taken from the Settings#getLocalRepositoryDir
-   * @param workDirectory work dir
-   */
-  public void setWorkDirectory(final String workDirectory) {
-    myWorkDirectory = workDirectory;
-  }
-
-  public String getWorkDirectory() {
-    return myWorkDirectory;
-  }
-
-  protected GeneralCommandLine createCommandLine() {
-    GeneralCommandLine cli = createCL();
-    cli.setWorkDirectory(myWorkDirectory);
-    cli.setPassParentEnvs(true);
-    return cli;
-  }
-
-  protected GeneralCommandLine createCL() {
-    GeneralCommandLine cl = new MercurialCommandLine(getPrivateData());
-    cl.setExePath(getSettings().getHgCommandPath());
-    return cl;
-  }
-
-  protected CommandResult runCommand(@NotNull GeneralCommandLine cli) throws VcsException {
-    return CommandUtil.runCommand(cli, getPrivateData());
-  }
-
-  protected CommandResult runCommand(@NotNull GeneralCommandLine cli, boolean logErrorsInDebug) throws VcsException {
-    return CommandUtil.runCommand(cli, CommandUtil.DEFAULT_COMMAND_TIMEOUT_SEC, getPrivateData(), logErrorsInDebug);
-  }
-
-  protected CommandResult runCommand(@NotNull GeneralCommandLine cli, int executionTimeout) throws VcsException {
-    return CommandUtil.runCommand(cli, executionTimeout, getPrivateData());
-  }
-
-  protected void failIfNotEmptyStdErr(@NotNull GeneralCommandLine cli, @NotNull CommandResult res) throws VcsException {
-    if (!StringUtil.isEmpty(res.getStderr())) {
-      CommandUtil.commandFailed(cli.getCommandLineString(), res);
-    }
-  }
-
-  public Set<String> getPrivateData() {
-    String password = mySettings.getPassword();
-    return password != null ? Collections.singleton(password) : Collections.<String>emptySet();
-  }
-}
+/*
+ * Copyright 2000-2011 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import com.intellij.execution.configurations.GeneralCommandLine;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.Set;
+
+import static java.util.Collections.emptySet;
+
+/**
+ * @author pavel
+ */
+public class BaseCommand {
+
+  private final String myHgPath;
+  private final File myWorkDirectory;
+
+  public BaseCommand(@NotNull final String hgPath, @NotNull File workingDir) {
+    myHgPath = hgPath;
+    myWorkDirectory = workingDir;
+  }
+
+
+  public File getWorkDirectory() {
+    return myWorkDirectory;
+  }
+
+  protected GeneralCommandLine createCommandLine() {
+    GeneralCommandLine cli = createCL();
+    cli.setWorkDirectory(myWorkDirectory.getAbsolutePath());
+    cli.setPassParentEnvs(true);
+    return cli;
+  }
+
+  protected GeneralCommandLine createCL() {
+    GeneralCommandLine cl = new MercurialCommandLine(getPrivateData());
+    cl.setExePath(myHgPath);
+    return cl;
+  }
+
+  protected Set<String> getPrivateData() {
+    return emptySet();
+  }
+}
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/BranchesCommand.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/BranchesCommand.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,60 +1,60 @@
-/*
- * Copyright 2000-2011 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
-
-import com.intellij.execution.configurations.GeneralCommandLine;
-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;
-
-/**
- * @author Pavel.Sher
- *         Date: 26.10.2008
- */
-public class BranchesCommand extends BaseCommand {
-
-  public BranchesCommand(@NotNull Settings settings, @NotNull File workingDir) {
-    super(settings, workingDir);
-  }
-
-  /**
-   * Returns map of branch name to latest changeset in that branch
-   * @return see above
-   * @throws jetbrains.buildServer.vcs.VcsException if error occurs
-   */
-  public Map<String, ChangeSet> execute() throws VcsException {
-    GeneralCommandLine cli = createCommandLine();
-    cli.addParameter("branches");
-    CommandResult res = runCommand(cli);
-    String stdout = res.getStdout();
-    Map<String, ChangeSet> result = new HashMap<String, ChangeSet>();
-    Pattern branchPattern = Pattern.compile("(.*)[\\s]+([0-9]+:[A-Za-z0-9]+).*");
-    for (String line: stdout.split("[\r\n]+")) {
-      Matcher matcher = branchPattern.matcher(line);
-      if (matcher.matches()) {
-        String branchName = matcher.group(1).trim();
-        String changeId = matcher.group(2).trim();
-        result.put(branchName, new ChangeSet(changeId));
-      }
-    }
-    return result;
-  }
-}
+/*
+ * Copyright 2000-2011 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import com.intellij.execution.configurations.GeneralCommandLine;
+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;
+
+/**
+ * @author Pavel.Sher
+ *         Date: 26.10.2008
+ */
+public class BranchesCommand extends VcsRootCommand {
+
+  public BranchesCommand(@NotNull Settings settings, @NotNull File workingDir) {
+    super(settings, workingDir);
+  }
+
+  /**
+   * Returns map of branch name to latest changeset in that branch
+   * @return see above
+   * @throws jetbrains.buildServer.vcs.VcsException if error occurs
+   */
+  public Map<String, ChangeSet> execute() throws VcsException {
+    GeneralCommandLine cli = createCommandLine();
+    cli.addParameter("branches");
+    CommandResult res = runCommand(cli);
+    String stdout = res.getStdout();
+    Map<String, ChangeSet> result = new HashMap<String, ChangeSet>();
+    Pattern branchPattern = Pattern.compile("(.*)[\\s]+([0-9]+:[A-Za-z0-9]+).*");
+    for (String line: stdout.split("[\r\n]+")) {
+      Matcher matcher = branchPattern.matcher(line);
+      if (matcher.matches()) {
+        String branchName = matcher.group(1).trim();
+        String changeId = matcher.group(2).trim();
+        result.put(branchName, new ChangeSet(changeId));
+      }
+    }
+    return result;
+  }
+}
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CatCommand.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CatCommand.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,88 +1,105 @@
-/*
- * Copyright 2000-2011 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
-
-import com.intellij.execution.configurations.GeneralCommandLine;
-import jetbrains.buildServer.util.FileUtil;
-import jetbrains.buildServer.vcs.VcsException;
-import org.jetbrains.annotations.NotNull;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Queue;
-
-public class CatCommand extends BaseCommand {
-  private String myRevId;
-  private final static int MAX_CMD_LEN = 900;
-  private boolean myLogErrorsInDebug = false;
-
-  public CatCommand(@NotNull Settings settings, @NotNull File workingDir) {
-    super(settings, workingDir);
-  }
-
-  public void setRevId(final String revId) {
-    myRevId = revId;
-  }
-
-  public void setLogErrorsInDebug(boolean doLogErrorsInDebug) {
-    myLogErrorsInDebug = doLogErrorsInDebug;
-  }
-
-  public File execute(List<String> relPaths) throws VcsException {
-    File tempDir;
-    try {
-      tempDir = FileUtil.createTempDirectory("mercurial", "catresult");
-    } catch (IOException e) {
-      throw new VcsException("Unable to create temporary directory");
-    }
-    for (String path: relPaths) {
-      final File parentFile = new File(tempDir, path).getParentFile();
-      if (!parentFile.isDirectory() && !parentFile.mkdirs()) {
-        throw new VcsException("Failed to create directory: " + parentFile.getAbsolutePath());
-      }
-    }
-
-    Queue<String> paths = new LinkedList<String>(relPaths);
-    while (!paths.isEmpty()) {
-      GeneralCommandLine cli = createCommandLine(tempDir);
-      int cmdSize = cli.getCommandLineString().length();
-
-      do {
-        String path = paths.poll();
-        cli.addParameter(path);
-        cmdSize += path.length();
-      } while (cmdSize < MAX_CMD_LEN && !paths.isEmpty());
-
-      runCommand(cli, myLogErrorsInDebug);
-    }
-
-    return tempDir;
-  }
-
-  private GeneralCommandLine createCommandLine(final File tempDir) {
-    GeneralCommandLine cli = createCommandLine();
-    cli.addParameter("cat");
-    cli.addParameter("-o");
-    cli.addParameter(tempDir.getAbsolutePath() + File.separator + "%p");
-    if (myRevId != null) {
-      cli.addParameter("-r");
-      cli.addParameter(myRevId);
-    }
-    return cli;
-  }
-}
+/*
+ * Copyright 2000-2011 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import com.intellij.execution.configurations.GeneralCommandLine;
+import jetbrains.buildServer.util.FileUtil;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+
+import static com.intellij.openapi.util.io.FileUtil.delete;
+import static jetbrains.buildServer.buildTriggers.vcs.mercurial.command.CommandExecutionSettingsBuilder.with;
+
+public class CatCommand extends VcsRootCommand {
+  private String myRevId;
+  private final static int MAX_CMD_LEN = 900;
+
+  public CatCommand(@NotNull Settings settings, @NotNull File workingDir) {
+    super(settings, workingDir);
+  }
+
+  public void setRevId(final String revId) {
+    myRevId = revId;
+  }
+
+  public File execute(List<String> relPaths) throws VcsException {
+    return execute(relPaths, true);
+  }
+
+  public File execute(List<String> relPaths, boolean checkFailure) throws VcsException {
+    File tempDir = null;
+    try {
+      tempDir = createTmpDir();
+      createDirectories(relPaths, tempDir);
+      catFiles(relPaths, checkFailure, tempDir);
+      return tempDir;
+    } catch (VcsException e) {
+      if (tempDir != null)
+        delete(tempDir);
+      throw e;
+    }
+  }
+
+  private File createTmpDir() throws VcsException {
+    try {
+      return FileUtil.createTempDirectory("mercurial", "catresult");
+    } catch (IOException e) {
+      throw new VcsException("Unable to create temporary directory");
+    }
+  }
+
+  private void createDirectories(List<String> relPaths, File tempDir) throws VcsException {
+    for (String path: relPaths) {
+      File parentFile = new File(tempDir, path).getParentFile();
+      if (!parentFile.isDirectory() && !parentFile.mkdirs())
+        throw new VcsException("Failed to create directory: " + parentFile.getAbsolutePath());
+    }
+  }
+
+  private void catFiles(List<String> relPaths, boolean checkFailure, File tempDir) throws VcsException {
+    Queue<String> paths = new LinkedList<String>(relPaths);
+    while (!paths.isEmpty()) {
+      GeneralCommandLine cli = createCommandLine(tempDir);
+      int cmdSize = cli.getCommandLineString().length();
+
+      do {
+        String path = paths.poll();
+        cli.addParameter(path);
+        cmdSize += path.length();
+      } while (cmdSize < MAX_CMD_LEN && !paths.isEmpty());
+
+      runCommand(cli, with().checkForFailure(checkFailure));
+    }
+  }
+
+  private GeneralCommandLine createCommandLine(final File tempDir) {
+    GeneralCommandLine cli = createCommandLine();
+    cli.addParameter("cat");
+    cli.addParameter("-o");
+    cli.addParameter(tempDir.getAbsolutePath() + File.separator + "%p");
+    if (myRevId != null) {
+      cli.addParameter("-r");
+      cli.addParameter(myRevId);
+    }
+    return cli;
+  }
+}
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/ChangeSet.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/ChangeSet.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,115 +1,113 @@
-/*
- * Copyright 2000-2011 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
-
-import org.jetbrains.annotations.NotNull;
-
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-
-/**
- * Represents Mercurial change set
- */
-public class ChangeSet extends ChangeSetRevision {
-  @NotNull private String myUser;
-  @NotNull private Date myTimestamp;
-  private String myDescription;
-  private List<ChangeSetRevision> myParents = new ArrayList<ChangeSetRevision>();
-  private List<ModifiedFile> myModifiedFiles = new ArrayList<ModifiedFile>();
-
-
-  public ChangeSet(final int revNumber, @NotNull final String id) {
-    super(revNumber, id);
-  }
-
-  /**
-   * Constructor for version in the form revnum:changeset_id or just changeset_id (in this case rev number is set to -1)
-   * @param fullVersion full changeset version as reported by hg log command
-   */
-  public ChangeSet(@NotNull final String fullVersion) {
-    super(fullVersion);
-  }
-
-  public void setUser(@NotNull final String user) {
-    myUser = user;
-  }
-
-  public void setTimestamp(@NotNull final Date timestamp) {
-    myTimestamp = timestamp;
-  }
-
-  public void setDescription(final String description) {
-    myDescription = description;
-  }
-
-  public void addParent(@NotNull ChangeSetRevision rev) {
-    myParents.add(rev);
-  }
-
-  /**
-   * Returns user who made changeset
-   * @return user who made changeset
-   */
-  @NotNull
-  public String getUser() {
-    return myUser;
-  }
-
-  /**
-   * Returns changeset timestamp
-   * @return changeset timestamp
-   */
-  @NotNull
-  public Date getTimestamp() {
-    return myTimestamp;
-  }
-
-  /**
-   * Returns changeset summary specified by user
-   * @return changeset summary
-   */
-  public String getDescription() {
-    return myDescription;
-  }
-
-  /**
-   * Returns parrents of this change set
-   * @return see above
-   */
-  @NotNull
-  public List<ChangeSetRevision> getParents() {
-    return myParents;
-  }
-
-  /**
-   * Check if changeset is initial changeset (has no parents)
-   * @return true if changeset is initial changeset
-   */
-  public boolean isInitial() {
-    return getParents().isEmpty();
-  }
-
-  public void setModifiedFiles(@NotNull final List<ModifiedFile> files) {
-    myModifiedFiles = files;
-  }
-
-  @NotNull
-  public List<ModifiedFile> getModifiedFiles() {
-    return myModifiedFiles;
-  }
-
-}
+/*
+ * Copyright 2000-2011 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Represents Mercurial change set
+ */
+public class ChangeSet extends ChangeSetRevision {
+  @NotNull private String myUser;
+  @NotNull private Date myTimestamp;
+  private String myDescription;
+  private List<ChangeSetRevision> myParents = new ArrayList<ChangeSetRevision>();
+  private List<ModifiedFile> myModifiedFiles = new ArrayList<ModifiedFile>();
+
+  public ChangeSet(final int revNumber, @NotNull final String id) {
+    super(revNumber, id);
+  }
+
+  /**
+   * Constructor for version in the form revnum:changeset_id or just changeset_id (in this case rev number is set to -1)
+   * @param fullVersion full changeset version as reported by hg log command
+   */
+  public ChangeSet(@NotNull final String fullVersion) {
+    super(fullVersion);
+  }
+
+  public void setUser(@NotNull final String user) {
+    myUser = user;
+  }
+
+  public void setTimestamp(@NotNull final Date timestamp) {
+    myTimestamp = timestamp;
+  }
+
+  public void setDescription(final String description) {
+    myDescription = description;
+  }
+
+  public void addParent(@NotNull ChangeSetRevision rev) {
+    myParents.add(rev);
+  }
+
+  /**
+   * Returns user who made changeset
+   * @return user who made changeset
+   */
+  @NotNull
+  public String getUser() {
+    return myUser;
+  }
+
+  /**
+   * Returns changeset timestamp
+   * @return changeset timestamp
+   */
+  @NotNull
+  public Date getTimestamp() {
+    return myTimestamp;
+  }
+
+  /**
+   * Returns changeset summary specified by user
+   * @return changeset summary
+   */
+  public String getDescription() {
+    return myDescription;
+  }
+
+  /**
+   * Returns parrents of this change set
+   * @return see above
+   */
+  @NotNull
+  public List<ChangeSetRevision> getParents() {
+    return myParents;
+  }
+
+  /**
+   * Check if changeset is initial changeset (has no parents)
+   * @return true if changeset is initial changeset
+   */
+  public boolean isInitial() {
+    return getParents().isEmpty();
+  }
+
+  public void setModifiedFiles(@NotNull final List<ModifiedFile> files) {
+    myModifiedFiles = files;
+  }
+
+  @NotNull
+  public List<ModifiedFile> getModifiedFiles() {
+    return myModifiedFiles;
+  }
+}
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/ChangeSetRevision.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/ChangeSetRevision.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,99 +1,99 @@
-/*
- * Copyright 2000-2011 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
-
-import org.jetbrains.annotations.NotNull;
-
-/**
- * @author Pavel.Sher
- */
-public class ChangeSetRevision {
-  private final int myRevNumber;
-  @NotNull
-  private final String myId;
-
-  public ChangeSetRevision(final int revNumber, @NotNull final String id) {
-    myRevNumber = revNumber;
-    myId = id;
-  }
-
-  /**
-   * Constructor for version in the form revnum:changeset_id or just changeset_id (in this case rev number is set to -1)
-   * @param fullVersion full changeset version as reported by hg log command
-   */
-  public ChangeSetRevision(@NotNull final String fullVersion) {
-    try {
-      int colon = fullVersion.indexOf(":");
-      if (colon != -1) {
-        myRevNumber = Integer.parseInt(fullVersion.substring(0, colon));
-        myId = fullVersion.substring(colon+1);
-      } else {
-        myRevNumber = -1;
-        myId = fullVersion;
-      }
-    } catch (Throwable e) {
-      throw new IllegalArgumentException(e);
-    }
-  }
-
-  /**
-   * Returns changeset revision id
-   * @return changeset revision id
-   */
-  @NotNull
-  public String getId() {
-    return myId;
-  }
-
-  /**
-   * Returns changeset revision number
-   * @return changeset revision number
-   */
-  public int getRevNumber() {
-    return myRevNumber;
-  }
-
-  /**
-   * Returns full changeset version as reported by hg log command: revnum:revid
-   * @return full changeset version as reported by hg log
-   */
-  @NotNull
-  public String getFullVersion() {
-    return myRevNumber + ":" + myId;
-  }
-
-  @Override
-  public boolean equals(final Object o) {
-    if (this == o) return true;
-    if (o == null || getClass() != o.getClass()) return false;
-
-    final ChangeSetRevision that = (ChangeSetRevision) o;
-
-    return myRevNumber == that.myRevNumber && myId.equals(that.myId);
-  }
-
-  @Override
-  public int hashCode() {
-    int result = myRevNumber;
-    result = 31 * result + myId.hashCode();
-    return result;
-  }
-
-  @Override
-  public String toString() {
-    return "change:" + myId;
-  }
-}
+/*
+ * Copyright 2000-2011 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Pavel.Sher
+ */
+public class ChangeSetRevision {
+  private final int myRevNumber;
+  @NotNull
+  private final String myId;
+
+  public ChangeSetRevision(final int revNumber, @NotNull final String id) {
+    myRevNumber = revNumber;
+    myId = id;
+  }
+
+  /**
+   * Constructor for version in the form revnum:changeset_id or just changeset_id (in this case rev number is set to -1)
+   * @param fullVersion full changeset version as reported by hg log command
+   */
+  public ChangeSetRevision(@NotNull final String fullVersion) {
+    try {
+      int colon = fullVersion.indexOf(":");
+      if (colon != -1) {
+        myRevNumber = Integer.parseInt(fullVersion.substring(0, colon));
+        myId = fullVersion.substring(colon+1);
+      } else {
+        myRevNumber = -1;
+        myId = fullVersion;
+      }
+    } catch (Throwable e) {
+      throw new IllegalArgumentException(e);
+    }
+  }
+
+  /**
+   * Returns changeset revision id
+   * @return changeset revision id
+   */
+  @NotNull
+  public String getId() {
+    return myId;
+  }
+
+  /**
+   * Returns changeset revision number
+   * @return changeset revision number
+   */
+  public int getRevNumber() {
+    return myRevNumber;
+  }
+
+  /**
+   * Returns full changeset version as reported by hg log command: revnum:revid
+   * @return full changeset version as reported by hg log
+   */
+  @NotNull
+  public String getFullVersion() {
+    return myRevNumber + ":" + myId;
+  }
+
+  @Override
+  public boolean equals(final Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+
+    final ChangeSetRevision that = (ChangeSetRevision) o;
+
+    return myRevNumber == that.myRevNumber && myId.equals(that.myId);
+  }
+
+  @Override
+  public int hashCode() {
+    int result = myRevNumber;
+    result = 31 * result + myId.hashCode();
+    return result;
+  }
+
+  @Override
+  public String toString() {
+    return "change:" + myId;
+  }
+}
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/ChangedFilesCommand.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/ChangedFilesCommand.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,75 +1,75 @@
-/*
- * Copyright 2000-2011 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
-
-import com.intellij.execution.configurations.GeneralCommandLine;
-import jetbrains.buildServer.util.FileUtil;
-import jetbrains.buildServer.vcs.VcsException;
-import org.jetbrains.annotations.NotNull;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.List;
-
-/**
- * @author Pavel.Sher
- */
-public class ChangedFilesCommand extends BaseCommand {
-  private String myRevId;
-
-  public ChangedFilesCommand(@NotNull Settings settings, @NotNull File workingDir) {
-    super(settings, workingDir);
-  }
-
-  public void setRevId(@NotNull final String revId) {
-    myRevId = revId;
-  }
-
-  public List<ModifiedFile> execute() throws VcsException {
-    File styleFile;
-    try {
-      styleFile = getStyleFile();
-    } catch (IOException e) {
-      throw new VcsException("Unable to create style file: " + e.toString(), e);
-    }
-    try {
-      GeneralCommandLine cli = createCommandLine();
-      cli.addParameter("log");
-      cli.addParameter("-r");
-      cli.addParameter(myRevId + ":" + myRevId);
-      cli.addParameter("--style=" + styleFile.getAbsolutePath());
-
-      CommandResult res = runCommand(cli);
-      return parseFiles(res.getStdout());
-    } finally {
-      FileUtil.delete(styleFile);
-    }
-  }
-
-  private File getStyleFile() throws IOException {
-    File styleFile = FileUtil.createTempFile("hg", "style");
-    FileUtil.writeFile(styleFile,
-            "changeset = \"{file_mods}{file_adds}{file_dels}\"\n" +
-            "file_add = \"A {file_add}\\n\"\n" +
-            "file_del = \"R {file_del}\\n\"\n" +
-            "file_mod = \"M {file_mod}\\n\"");
-    return styleFile;
-  }
-
-  private List<ModifiedFile> parseFiles(final String stdout) {
-    return StatusCommand.parseFiles(stdout);
-  }
-}
+/*
+ * Copyright 2000-2011 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import com.intellij.execution.configurations.GeneralCommandLine;
+import jetbrains.buildServer.util.FileUtil;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * @author Pavel.Sher
+ */
+public class ChangedFilesCommand extends VcsRootCommand {
+  private String myRevId;
+
+  public ChangedFilesCommand(@NotNull Settings settings, @NotNull File workingDir) {
+    super(settings, workingDir);
+  }
+
+  public void setRevId(@NotNull final String revId) {
+    myRevId = revId;
+  }
+
+  public List<ModifiedFile> execute() throws VcsException {
+    File styleFile;
+    try {
+      styleFile = getStyleFile();
+    } catch (IOException e) {
+      throw new VcsException("Unable to create style file: " + e.toString(), e);
+    }
+    try {
+      GeneralCommandLine cli = createCommandLine();
+      cli.addParameter("log");
+      cli.addParameter("-r");
+      cli.addParameter(myRevId + ":" + myRevId);
+      cli.addParameter("--style=" + styleFile.getAbsolutePath());
+
+      CommandResult res = runCommand(cli);
+      return parseFiles(res.getStdout());
+    } finally {
+      FileUtil.delete(styleFile);
+    }
+  }
+
+  private File getStyleFile() throws IOException {
+    File styleFile = FileUtil.createTempFile("hg", "style");
+    FileUtil.writeFile(styleFile,
+            "changeset = \"{file_mods}{file_adds}{file_dels}\"\n" +
+            "file_add = \"A {file_add}\\n\"\n" +
+            "file_del = \"R {file_del}\\n\"\n" +
+            "file_mod = \"M {file_mod}\\n\"");
+    return styleFile;
+  }
+
+  private List<ModifiedFile> parseFiles(final String stdout) {
+    return StatusCommand.parseFiles(stdout);
+  }
+}
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CloneCommand.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CloneCommand.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,79 +1,81 @@
-/*
- * Copyright 2000-2011 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
-
-import com.intellij.execution.configurations.GeneralCommandLine;
-import jetbrains.buildServer.vcs.VcsException;
-import org.jetbrains.annotations.NotNull;
-
-import java.io.File;
-
-public class CloneCommand extends BaseCommand{
-  private String myToId;
-  private boolean myUpdateWorkingDir = true;
-  private String myRepository;
-  private File myWorkingDir;
-  private boolean myUsePullProtocol = true;
-
-  public CloneCommand(@NotNull Settings settings, @NotNull File workingDir) {
-    super(settings, workingDir);
-    myWorkingDir = workingDir;
-    myRepository = getSettings().getRepositoryUrl();
-  }
-
-  /**
-   * Sets repository to clone, by default uses repository from the specified settings
-   * @param repo repository path
-   */
-  public void setRepository(@NotNull String repo) {
-    myRepository = repo;
-  }
-
-  public void setToId(final String toId) {
-    myToId = toId;
-  }
-
-  public void setUpdateWorkingDir(final boolean updateWorkingDir) {
-    myUpdateWorkingDir = updateWorkingDir;
-  }
-
-  public void setUsePullProtocol(boolean usePullProtocol) {
-    myUsePullProtocol = usePullProtocol;
-  }
-
-  public void execute() throws VcsException {
-    GeneralCommandLine cli = createCommandLine();
-    File parent = myWorkingDir.getParentFile();
-    cli.setWorkDirectory(parent.getAbsolutePath());
-    cli.addParameter("clone");
-    if (myToId != null) {
-      cli.addParameter("-r");
-      cli.addParameter(myToId);
-    }
-    if (myUsePullProtocol)
-      cli.addParameter("--pull");
-    if (!myUpdateWorkingDir) {
-      cli.addParameter("-U");
-    }
-    if (getSettings().isUncompressedTransfer()) {
-      cli.addParameter("--uncompressed");
-    }
-    cli.addParameter(myRepository);
-    cli.addParameter(myWorkingDir.getName());
-
-    runCommand(cli, 24*3600); // some repositories are quite large, we set timeout to 24 hours
-  }
-}
+/*
+ * Copyright 2000-2011 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import com.intellij.execution.configurations.GeneralCommandLine;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+
+import static jetbrains.buildServer.buildTriggers.vcs.mercurial.command.CommandExecutionSettingsBuilder.with;
+
+public class CloneCommand extends VcsRootCommand {
+  private String myToId;
+  private boolean myUpdateWorkingDir = true;
+  private String myRepository;
+  private File myWorkingDir;
+  private boolean myUsePullProtocol = true;
+
+  public CloneCommand(@NotNull Settings settings, @NotNull File workingDir) {
+    super(settings, workingDir);
+    myWorkingDir = workingDir;
+    myRepository = getSettings().getRepositoryUrlWithCredentials();
+  }
+
+  /**
+   * Sets repository to clone, by default uses repository from the specified settings
+   * @param repo repository path
+   */
+  public void setRepository(@NotNull String repo) {
+    myRepository = repo;
+  }
+
+  public void setToId(final String toId) {
+    myToId = toId;
+  }
+
+  public void setUpdateWorkingDir(final boolean updateWorkingDir) {
+    myUpdateWorkingDir = updateWorkingDir;
+  }
+
+  public void setUsePullProtocol(boolean usePullProtocol) {
+    myUsePullProtocol = usePullProtocol;
+  }
+
+  public void execute() throws VcsException {
+    GeneralCommandLine cli = createCommandLine();
+    File parent = myWorkingDir.getParentFile();
+    cli.setWorkDirectory(parent.getAbsolutePath());
+    cli.addParameter("clone");
+    if (myToId != null) {
+      cli.addParameter("-r");
+      cli.addParameter(myToId);
+    }
+    if (myUsePullProtocol)
+      cli.addParameter("--pull");
+    if (!myUpdateWorkingDir) {
+      cli.addParameter("-U");
+    }
+    if (getSettings().isUncompressedTransfer()) {
+      cli.addParameter("--uncompressed");
+    }
+    cli.addParameter(myRepository);
+    cli.addParameter(myWorkingDir.getName());
+
+    runCommand(cli, with().timeout(24 * 3600)); // some repositories are quite large, we set timeout to 24 hours
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommandExecutionSettings.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,40 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Set;
+
+/**
+ * @author dmitry.neverov
+ */
+public class CommandExecutionSettings {
+
+  private final int myTimeout;
+  private final Set<String> myPrivateData;
+  private final boolean myCheckForFailure;
+  private final boolean myFailWhenStderrNotEmpty;
+
+  CommandExecutionSettings(int timeout, @NotNull Set<String> privateData, boolean checkForFailure, boolean failWhenStderrNotEmpty) {
+    myTimeout = timeout;
+    myPrivateData = privateData;
+    myCheckForFailure = checkForFailure;
+    myFailWhenStderrNotEmpty = failWhenStderrNotEmpty;
+  }
+
+  public int timeout() {
+    return myTimeout;
+  }
+
+  @NotNull
+  public Set<String> privateData() {
+    return myPrivateData;
+  }
+
+  public boolean shouldCheckForFailure() {
+    return myCheckForFailure;
+  }
+
+  public boolean shouldFailWithNonEmptyStderr() {
+    return myFailWhenStderrNotEmpty;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommandExecutionSettingsBuilder.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,47 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * @author dmitry.neverov
+ */
+public class CommandExecutionSettingsBuilder {
+
+  private static final int DEFAULT_COMMAND_TIMEOUT_SEC = 3600;
+
+  private int myTimeout = DEFAULT_COMMAND_TIMEOUT_SEC;
+  private Set<String> myPrivateData = Collections.emptySet();
+  private boolean myCheckForFailure = true;
+  private boolean myFailWhenStderrNotEmpty = false;
+
+  public static CommandExecutionSettingsBuilder with() {
+    return new CommandExecutionSettingsBuilder();
+  }
+
+  public CommandExecutionSettings build() {
+    return new CommandExecutionSettings(myTimeout, myPrivateData, myCheckForFailure, myFailWhenStderrNotEmpty);
+  }
+
+  public CommandExecutionSettingsBuilder timeout(int timeout) {
+    myTimeout = timeout;
+    return this;
+  }
+
+  public CommandExecutionSettingsBuilder privateData(@NotNull Set<String> privateData) {
+    myPrivateData = privateData;
+    return this;
+  }
+
+  public CommandExecutionSettingsBuilder checkForFailure(boolean checkForFailure) {
+    myCheckForFailure = checkForFailure;
+    return this;
+  }
+
+  public CommandExecutionSettingsBuilder failureWhenStderrNotEmpty() {
+    myFailWhenStderrNotEmpty = true;
+    return this;
+  }
+}
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommandResult.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommandResult.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,24 +1,37 @@
 package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
 
+import com.intellij.openapi.diagnostic.Logger;
 import jetbrains.buildServer.ExecResult;
+import jetbrains.buildServer.util.StringUtil;
+import jetbrains.buildServer.vcs.VcsException;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import java.util.Collections;
 import java.util.Set;
 
+import static com.intellij.openapi.util.text.StringUtil.isEmpty;
 import static jetbrains.buildServer.buildTriggers.vcs.mercurial.command.CommandUtil.removePrivateData;
 
 /**
- * Decorator for ExecResult that filters out private data from stdout and strerr.
+ * Mercurial command result. Filters out private data from stdout and detects errors.
  *
  * @author dmitry.neverov
  */
 public class CommandResult {
 
+  private final Logger myLogger;
+  private final String myCommand;
   private final ExecResult myDelegate;
   private final Set<String> myPrivateData;
 
-  public CommandResult(@NotNull final ExecResult execResult, @NotNull final Set<String> privateData) {
+  public CommandResult(@NotNull Logger logger, @NotNull String command, @NotNull ExecResult execResult) {
+    this(logger, command, execResult, Collections.<String>emptySet());
+  }
+
+  public CommandResult(@NotNull Logger logger, @NotNull String command, @NotNull ExecResult execResult, @NotNull Set<String> privateData) {
+    myLogger = logger;
+    myCommand = command;
     myDelegate = execResult;
     myPrivateData = privateData;
   }
@@ -28,18 +41,125 @@
     return removePrivateData(myDelegate.getStdout(), myPrivateData);
   }
 
+  public void checkCommandFailed() throws VcsException {
+    checkFailure(false);
+  }
+
+  public void checkFailure(boolean failWhenStderrIsNonEmpty) throws VcsException {
+    rethrowDetectedError();
+    if (isFailure())
+      logAndThrowError();
+    String stderr = getStderr();
+    if (!isEmpty(stderr)) {
+      if (failWhenStderrIsNonEmpty)
+        logAndThrowError();
+      else
+        logStderr(stderr);
+    }
+  }
+
+  private void logAndThrowError() throws VcsException {
+    String message = createCommandLogMessage();
+    myLogger.warn(message);
+    if (hasImportantException())
+      myLogger.error("Error during executing '" + getCommand() + "'", getException());
+    throw new VcsException(message);
+  }
+
+  private void logStderr(String stderr) {
+    myLogger.warn("Error output produced by: " + getCommand());
+    myLogger.warn(stderr);
+  }
+
   @NotNull
-  public String getStderr() {
+  private String getStderr() {
     return removePrivateData(myDelegate.getStderr(), myPrivateData);
   }
 
   @Nullable
-  public Throwable getException() {
+  private Throwable getException() {
     return myDelegate.getException();
   }
 
-  public int getExitCode() {
-    return myDelegate.getExitCode();
+  private boolean isFailure() {
+    //A non-zero exit code is not an error:
+    //http://mercurial.selenic.com/bts/issue186
+    //http://mercurial.selenic.com/bts/issue2189
+    //E.g. pull command in hg 2.1 exits with 1 if no new changes were pulled
+    return getException() != null;
+  }
+
+  private boolean shouldDetectErrors() {
+    return isFailure() || myDelegate.getExitCode() != 0;
+  }
+
+  @NotNull
+  private String getCommand() {
+    return removePrivateData(myCommand, myPrivateData);
+  }
+
+  private boolean hasImportantException() {
+    Throwable exception = getException();
+    return exception instanceof NullPointerException;
+  }
+
+  private String createCommandLogMessage() {
+    String stderr = getStderr();
+    String stdout = getStdout();
+    String exceptionMessage = getExceptionMessage();
+    return "'" + getCommand() + "' command failed.\n" +
+            (!StringUtil.isEmpty(stdout) ? "stdout: " + stdout + "\n" : "") +
+            (!StringUtil.isEmpty(stderr) ? "stderr: " + stderr + "\n" : "") +
+            (exceptionMessage != null ? "exception: " + exceptionMessage : "");
+  }
+
+  @Nullable
+  private String getExceptionMessage() {
+    Throwable exception = getException();
+    if (exception == null)
+      return null;
+    String message = exception.getMessage();
+    if (message == null)
+      message = exception.getClass().getName();
+    return message;
+  }
+
+  private void rethrowDetectedError() throws VcsException {
+    if (!shouldDetectErrors())
+      return;
+    String stderr = getStderr().trim();
+    checkUnrelatedRepository(stderr);
+    checkUnknownRevision(stderr);
+    checkFileNotUnderTheRoot(stderr);
+  }
+
+  private void checkUnrelatedRepository(@NotNull final String stderr) throws UnrelatedRepositoryException {
+    if (stderr.contains("abort: repository is unrelated"))
+      throw new UnrelatedRepositoryException();
+  }
+
+  private void checkUnknownRevision(@NotNull final String stderr) throws UnknownRevisionException {
+    final String message = "abort: unknown revision '";
+    int idx = stderr.indexOf(message);
+    if (idx != -1) {
+      int startIdx = idx + message.length();
+      int endIdx = stderr.indexOf("'", startIdx);
+      String revision = stderr.substring(startIdx, endIdx);
+      throw new UnknownRevisionException(revision);
+    }
+  }
+
+  private void checkFileNotUnderTheRoot(@NotNull final String stderr) throws VcsException {
+    final String prefix = "abort: ";
+    int idx = stderr.indexOf("abort: ");
+    if (idx != -1) {
+      int startIdx = idx + prefix.length();
+      int endIdx = stderr.indexOf(" not under root");
+      if (endIdx != -1) {
+        String path = stderr.substring(startIdx, endIdx);
+        throw new UnknownFileException(path);
+      }
+    }
   }
 
   public byte[] getByteOut() {
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommandUtil.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommandUtil.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,138 +1,73 @@
-/*
- * Copyright 2000-2011 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
-
-import com.intellij.execution.configurations.GeneralCommandLine;
-import jetbrains.buildServer.ExecResult;
-import jetbrains.buildServer.SimpleCommandLineProcessRunner;
-import jetbrains.buildServer.log.Loggers;
-import jetbrains.buildServer.util.StringUtil;
-import jetbrains.buildServer.vcs.VcsException;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-import java.util.Collections;
-import java.util.Set;
-
-public class CommandUtil {
-  public static final int DEFAULT_COMMAND_TIMEOUT_SEC = 3600;
-
-  public static void checkCommandFailed(@NotNull String cmdName, @NotNull CommandResult res) throws VcsException {
-    checkCommandFailed(cmdName, res, false);
-  }
-
-  private static void checkCommandFailed(@NotNull String cmdName, @NotNull CommandResult res, boolean logErrorsInDebug) throws VcsException {
-    if (logErrorsInDebug) {
-      if (res.getExitCode() != 0 || res.getException() != null)
-        Loggers.VCS.debug(createCommandLogMessage(cmdName, res));
-    } else {
-      if (res.getExitCode() != 0 || res.getException() != null)
-        commandFailed(cmdName, res);
-      if (res.getStderr().length() > 0) {
-        Loggers.VCS.warn("Error output produced by: " + cmdName);
-        Loggers.VCS.warn(res.getStderr());
-      }
-    }
-  }
-
-  public static void commandFailed(final String cmdName, final CommandResult res) throws VcsException {
-    final String message = createCommandLogMessage(cmdName, res);
-    Loggers.VCS.warn(message);
-    if (hasImportantException(res)) {
-      Loggers.VCS.error("Error during executing '" + cmdName + "'", res.getException());
-    }
-    throw new VcsException(message);
-  }
-
-  private static String createCommandLogMessage(final String cmdName, final CommandResult res) {
-    String stderr = res.getStderr();
-    String stdout = res.getStdout();
-    String exceptionMessage = getExceptionMessage(res);
-    return "'" + cmdName + "' command failed.\n" +
-            (!StringUtil.isEmpty(stdout) ? "stdout: " + stdout + "\n" : "") +
-            (!StringUtil.isEmpty(stderr) ? "stderr: " + stderr + "\n" : "") +
-            (exceptionMessage != null ? "exception: " + exceptionMessage : "");
-  }
-
-  @Nullable
-  private static String getExceptionMessage(CommandResult result) {
-    Throwable exception = result.getException();
-    String message = null;
-    if (exception != null) {
-      message = exception.getMessage();
-      if (message == null) {
-        message = exception.getClass().getName();
-      }
-    }
-    return message;
-  }
-
-  private static boolean hasImportantException(CommandResult result) {
-    Throwable exception = result.getException();
-    if (exception != null) {
-      return exception instanceof NullPointerException;
-    } else {
-      return false;
-    }
-  }
-
-  public static CommandResult runCommand(@NotNull GeneralCommandLine cli) throws VcsException {
-    return runCommand(cli, DEFAULT_COMMAND_TIMEOUT_SEC, Collections.<String>emptySet());
-  }
-
-  public static CommandResult runCommand(@NotNull GeneralCommandLine cli, @NotNull Set<String> privateData) throws VcsException {
-    return runCommand(cli, DEFAULT_COMMAND_TIMEOUT_SEC, privateData);
-  }
-
-  public static CommandResult runCommand(@NotNull GeneralCommandLine cli, final int executionTimeout, @NotNull Set<String> privateData) throws VcsException {
-    return runCommand(cli, executionTimeout, privateData, false);
-  }
-
-  public static CommandResult runCommand(@NotNull GeneralCommandLine cli, final int executionTimeout, @NotNull Set<String> privateData, boolean logErrorsInDebug) throws VcsException {
-    final String cmdStr = removePrivateData(cli.getCommandLineString(), privateData);
-    Loggers.VCS.debug("Run command: " + cmdStr);
-    CommandResult res = run(cli, executionTimeout, cmdStr, privateData);
-    CommandUtil.checkCommandFailed(cmdStr, res, logErrorsInDebug);
-    Loggers.VCS.debug("Command " + cmdStr + " output:\n" + res.getStdout());
-    return res;
-  }
-
-  private static CommandResult run(@NotNull final GeneralCommandLine cli, final int executionTimeout, @NotNull final String cmdStr, @NotNull final Set<String> privateData) {
-    final long start = System.currentTimeMillis();
-    ExecResult res = SimpleCommandLineProcessRunner.runCommand(cli, null, new SimpleCommandLineProcessRunner.RunCommandEventsAdapter() {
-      @Override
-      public Integer getOutputIdleSecondsTimeout() {
-        return executionTimeout;
-      }
-      @Override
-      public void onProcessFinished(Process ps) {
-        long duration = System.currentTimeMillis() - start;
-        Loggers.VCS.debug("Command " + cmdStr + " took " + duration + "ms");
-      }
-    });
-    return new CommandResult(res, privateData);
-  }
-
-  public static String removePrivateData(final String str, final Set<String> privateData) {
-    String result = str;
-    for (String data: privateData) {
-      if (data == null || data.length() == 0) continue;
-      result = result.replace(data, "******");
-    }
-
-    return result;
-  }
-}
+/*
+ * Copyright 2000-2011 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import com.intellij.execution.configurations.GeneralCommandLine;
+import jetbrains.buildServer.ExecResult;
+import jetbrains.buildServer.SimpleCommandLineProcessRunner;
+import jetbrains.buildServer.log.Loggers;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Set;
+
+import static jetbrains.buildServer.buildTriggers.vcs.mercurial.command.CommandExecutionSettingsBuilder.with;
+
+public class CommandUtil {
+
+  public static CommandResult runCommand(@NotNull GeneralCommandLine cli) throws VcsException {
+    return runCommand(cli, with());
+  }
+
+  public static CommandResult runCommand(@NotNull GeneralCommandLine cli, @NotNull CommandExecutionSettingsBuilder executionSettingsBuilder) throws VcsException {
+    return runCommand(cli, executionSettingsBuilder.build());
+  }
+
+  private static CommandResult runCommand(@NotNull GeneralCommandLine cli, @NotNull CommandExecutionSettings executionSettings) throws VcsException {
+    final String command = removePrivateData(cli.getCommandLineString(), executionSettings.privateData());
+    Loggers.VCS.debug("Run command: " + command);
+    CommandResult res = run(cli, executionSettings.timeout(), command, executionSettings.privateData());
+    if (executionSettings.shouldCheckForFailure() || executionSettings.shouldFailWithNonEmptyStderr())
+      res.checkFailure(executionSettings.shouldFailWithNonEmptyStderr());
+    Loggers.VCS.debug("Command " + command + " output:\n" + res.getStdout());
+    return res;
+  }
+
+  private static CommandResult run(@NotNull final GeneralCommandLine cli, final int executionTimeout, @NotNull final String command, @NotNull final Set<String> privateData) {
+    final long start = System.currentTimeMillis();
+    ExecResult res = SimpleCommandLineProcessRunner.runCommand(cli, null, new SimpleCommandLineProcessRunner.RunCommandEventsAdapter() {
+      @Override
+      public Integer getOutputIdleSecondsTimeout() {
+        return executionTimeout;
+      }
+      @Override
+      public void onProcessFinished(Process ps) {
+        long duration = System.currentTimeMillis() - start;
+        Loggers.VCS.debug("Command " + command + " took " + duration + "ms");
+      }
+    });
+    return new CommandResult(Loggers.VCS, command, res, privateData);
+  }
+
+  public static String removePrivateData(final String str, final Set<String> privateData) {
+    String result = str;
+    for (String data: privateData) {
+      if (data == null || data.length() == 0) continue;
+      result = result.replace(data, "******");
+    }
+    return result;
+  }
+}
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/IdentifyCommand.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/IdentifyCommand.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,61 +1,71 @@
-/*
- * Copyright 2000-2011 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
-
-import com.intellij.execution.configurations.GeneralCommandLine;
-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 {
-
-  private boolean myInLocalRepository = false;
-  private ChangeSet myChangeSet;
-
-  public IdentifyCommand(@NotNull Settings settings, @NotNull File workingDir) {
-    super(settings, workingDir);
-  }
-
-  public void setInLocalRepository(boolean inLocalRepository) {
-    myInLocalRepository = inLocalRepository;
-  }
-
-  public void setChangeSet(ChangeSet changeSet) {
-    myChangeSet = changeSet;
-  }
-
-  public String execute() throws VcsException {
-    GeneralCommandLine cli = createCL();
-    cli.addParameter("identify");
-    if (myInLocalRepository) {
-      cli.setWorkDirectory(this.getWorkDirectory());
-    } else {
-      cli.addParameter(getSettings().getRepositoryUrl());
-    }
-    if (myChangeSet != null) {
-      cli.addParameter("--rev");
-      cli.addParameter(myChangeSet.getId());
-    }
-    CommandResult res = runCommand(cli);
-    failIfNotEmptyStdErr(cli, res);
-    return res.getStdout();
-  }
-}
+/*
+ * Copyright 2000-2011 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import com.intellij.execution.configurations.GeneralCommandLine;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+
+import static jetbrains.buildServer.buildTriggers.vcs.mercurial.command.CommandExecutionSettingsBuilder.with;
+
+/**
+ * @author Pavel.Sher
+ *         Date: 16.07.2008
+ */
+public class IdentifyCommand extends VcsRootCommand {
+
+  private boolean myInLocalRepository = false;
+  private ChangeSet myChangeSet;
+  private Integer myRevisionNumber;
+
+  public IdentifyCommand(@NotNull Settings settings, @NotNull File workingDir) {
+    super(settings, workingDir);
+  }
+
+  public void setInLocalRepository(boolean inLocalRepository) {
+    myInLocalRepository = inLocalRepository;
+  }
+
+  public void setChangeSet(ChangeSet changeSet) {
+    myChangeSet = changeSet;
+  }
+
+  public void setRevisionNumber(int revisionNumber) {
+    myRevisionNumber = revisionNumber;
+  }
+
+  public String execute() throws VcsException {
+    GeneralCommandLine cli = createCL();
+    cli.addParameter("identify");
+    if (myInLocalRepository) {
+      cli.setWorkDirectory(this.getWorkDirectory().getAbsolutePath());
+    } else {
+      cli.addParameter(getSettings().getRepositoryUrlWithCredentials());
+    }
+    if (myChangeSet != null) {
+      cli.addParameter("--rev");
+      cli.addParameter(myChangeSet.getId());
+    } else if (myRevisionNumber != null) {
+      cli.addParameter("--rev");
+      cli.addParameter(myRevisionNumber.toString());
+    }
+    CommandResult res = runCommand(cli, with().failureWhenStderrNotEmpty());
+    String output = res.getStdout().trim();
+    return output.contains(" ") ? output.substring(0, output.indexOf(" ")) : output;
+  }
+}
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/Init.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/Init.java	Wed Feb 15 13:23:53 2012 +0400
@@ -10,7 +10,7 @@
 /**
  * @author dmitry.neverov
  */
-public class Init extends BaseCommand {
+public class Init extends VcsRootCommand {
 
   private final String myDefaultPullUrl;
 
@@ -20,7 +20,7 @@
   }
 
   public void execute() throws VcsException {
-    new File(getWorkDirectory()).mkdirs();
+    getWorkDirectory().mkdirs();
     GeneralCommandLine cli = createCommandLine();
     cli.addParameter("init");
     runCommand(cli);
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommand.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommand.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,220 +1,255 @@
-/*
- * Copyright 2000-2011 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
-
-import com.intellij.execution.configurations.GeneralCommandLine;
-import com.intellij.openapi.util.JDOMUtil;
-import jetbrains.buildServer.vcs.VcsException;
-import org.jdom.Document;
-import org.jdom.Element;
-import org.jdom.JDOMException;
-import org.jetbrains.annotations.NotNull;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.*;
-
-public class LogCommand extends BaseCommand {
-
-  private static final String DATE_FORMAT = "EEE MMM d HH:mm:ss yyyy Z";
-
-  private String myFromId;
-  private String myToId;
-  private Integer myLimit = null;
-  private String myBranchName;
-  private final File myTemplate;
-
-  public LogCommand(@NotNull Settings settings, @NotNull File workingDir, @NotNull final File template) {
-    super(settings, workingDir);
-    myTemplate = template;
-    myBranchName = settings.getBranchName();
-  }
-
-  public void setFromRevId(String id) {
-    myFromId = id;
-  }
-
-  public void setToRevId(String id) {
-    myToId = id;
-  }
-
-  public void setLimit(final int limit) {
-    myLimit = limit;
-  }
-
-  public void setBranchName(String branchName) {
-    myBranchName = branchName;
-  }
-
-  public List<ChangeSet> execute() throws VcsException {
-    GeneralCommandLine cli = createCommandLine();
-    cli.addParameters("--encoding", "UTF-8");
-    cli.addParameter("log");
-    cli.addParameter("--style=" + myTemplate.getAbsolutePath());
-    if (myBranchName != null) {
-      cli.addParameter("-b");
-      cli.addParameter(getSettings().getBranchName());
-    }
-    cli.addParameter("-r");
-    String from = myFromId;
-    if (from == null) from = "0";
-    String to = myToId;
-    if (to == null) to = "tip";
-    cli.addParameter(from + ":" + to);
-    if (myLimit != null) {
-      cli.addParameter("--limit");
-      cli.addParameter(myLimit.toString());
-    }
-    CommandResult res = runCommand(cli);
-    String output = getStdout(res);
-    try {
-      return parseChangeSetsXml(output);
-    } catch (Exception e) {
-      throw new VcsException("Error while parsing log output:\n" + output, e);
-    }
-  }
-
-  private String getStdout(CommandResult res) throws VcsException {
-    try {
-      return new String(res.getByteOut(), "UTF-8");
-    } catch (UnsupportedEncodingException e) {
-      throw new VcsException("Error while reading output", e);
-    }
-  }
-
-
-  private List<ChangeSet> parseChangeSetsXml(@NotNull final String xml) throws JDOMException, IOException, ParseException {
-    if ("".equals(xml))
-      return Collections.emptyList();
-    Document doc = loadDocument(xml);
-    Element log = doc.getRootElement();
-    return parseLog(log);
-  }
-
-  private Document loadDocument(@NotNull final String xml) throws IOException, JDOMException {
-    String validXml = makeValidXml(xml);
-    return JDOMUtil.loadDocument(validXml);
-  }
-
-
-  private String makeValidXml(@NotNull final String xml) {
-    String trimmed = xml.trim();
-    if (trimmed.endsWith("</log>"))
-      return trimmed;
-    else
-      return trimmed + "</log>";
-  }
-
-
-  private List<ChangeSet> parseLog(@NotNull final Element logElement) throws ParseException {
-    List<ChangeSet> result = new ArrayList<ChangeSet>();
-    for (Object o : logElement.getChildren("logentry")) {
-      Element entry = (Element) o;
-      result.add(parseLogEntry(entry));
-    }
-    return result;
-  }
-
-
-
-  private ChangeSet parseLogEntry(@NotNull final Element logEntry) throws ParseException {
-    ChangeSet cset = new ChangeSet(getRevision(logEntry), getId(logEntry));
-    addParents(cset, logEntry);
-    cset.setUser(getAuthor(logEntry));
-    cset.setDescription(getDescription(logEntry));
-    cset.setTimestamp(getDate(logEntry));
-    cset.setModifiedFiles(getModifiedFiles(logEntry));
-    return cset;
-  }
-
-
-  private int getRevision(@NotNull final Element logEntry) {
-    return Integer.parseInt(logEntry.getAttribute("revision").getValue());
-  }
-
-
-  private String getId(@NotNull final Element logEntry) {
-    return logEntry.getAttribute("shortnode").getValue();
-  }
-
-
-  private void addParents(@NotNull final ChangeSet cset, @NotNull final Element logEntry) {
-    List parents = logEntry.getChildren("parent");
-    for (Object p : parents) {
-      Element parent = (Element) p;
-      ChangeSetRevision parentCset = getParent(parent);
-      cset.addParent(parentCset);
-    }
-  }
-
-
-  private ChangeSetRevision getParent(@NotNull final Element parent) {
-    return new ChangeSetRevision(getRevision(parent), getId(parent));
-  }
-
-
-  private String getAuthor(@NotNull final Element logEntry) {
-    Element author = logEntry.getChild("author");
-    return author.getAttribute("original").getValue();
-  }
-
-
-  private String getDescription(@NotNull final Element logEntry) {
-    Element msg = logEntry.getChild("msg");
-    return msg.getText();
-  }
-
-
-  private Date getDate(@NotNull final Element logEntry) throws ParseException {
-    Element date = logEntry.getChild("date");
-    return new SimpleDateFormat(DATE_FORMAT, Locale.ENGLISH).parse(date.getText());
-  }
-
-
-  private List<ModifiedFile> getModifiedFiles(@NotNull final Element logEntry) {
-    List<ModifiedFile> result = new ArrayList<ModifiedFile>();
-    Element paths = logEntry.getChild("paths");
-    for (Object o : paths.getChildren("path")) {
-      Element path = (Element) o;
-      result.add(getModifiedFile(path));
-    }
-    return result;
-  }
-
-
-  private ModifiedFile getModifiedFile(@NotNull final Element path) {
-    String filePath = path.getText();
-    ModifiedFile.Status status = getStatus(path);
-    return new ModifiedFile(status, filePath);
-  }
-
-
-  private ModifiedFile.Status getStatus(@NotNull final Element path) {
-    String action = path.getAttribute("action").getValue();
-    if (action.equals("A")) {
-      return ModifiedFile.Status.ADDED;
-    } else if (action.equals("M")) {
-      return ModifiedFile.Status.MODIFIED;
-    } else if (action.equals("R")) {
-      return ModifiedFile.Status.REMOVED;
-    } else {
-      return ModifiedFile.Status.UNKNOWN;
-    }
-  }
-}
+/*
+ * Copyright 2000-2011 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import com.intellij.execution.configurations.GeneralCommandLine;
+import com.intellij.openapi.util.JDOMUtil;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jdom.Document;
+import org.jdom.Element;
+import org.jdom.JDOMException;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+public class LogCommand extends VcsRootCommand {
+
+  private final static String ZERO_PARENT_ID = "0000000000000000000000000000000000000000";
+  private static final String DATE_FORMAT = "EEE MMM d HH:mm:ss yyyy Z";
+
+  private String myFromId;
+  private String myToId;
+  private Integer myLimit = null;
+  private String myBranchName;
+  private boolean myCalculateParents = true;
+  private String myRevsets;
+  private final File myTemplate;
+
+  public LogCommand(@NotNull Settings settings, @NotNull File workingDir, @NotNull final File template) {
+    super(settings, workingDir);
+    myTemplate = template;
+    myBranchName = settings.getBranchName();
+  }
+
+  public void setFromRevId(String id) {
+    myFromId = id;
+  }
+
+  public void setToRevId(String id) {
+    myToId = id;
+  }
+
+  public void setLimit(final int limit) {
+    myLimit = limit;
+  }
+
+  public void showCommitsFromAllBranches() {
+    myBranchName = null;
+  }
+
+  public void setCalculateParents(boolean doCalculate) {
+    myCalculateParents = doCalculate;
+  }
+
+  public void setRevsets(String revsets) {
+    myRevsets = revsets;
+  }
+
+  public List<ChangeSet> execute() throws VcsException {
+    GeneralCommandLine cli = createCommandLine();
+    cli.addParameters("--encoding", "UTF-8");
+    cli.addParameter("log");
+    cli.addParameter("-v");
+    cli.addParameter("--style=" + myTemplate.getAbsolutePath());
+    if (myBranchName != null) {
+      cli.addParameter("-b");
+      cli.addParameter(getSettings().getBranchName());
+    }
+    cli.addParameter("-r");
+    if (myRevsets != null) {
+      cli.addParameter(myRevsets);
+    } else {
+      String from = myFromId != null ? myFromId : "0";
+      String to = myToId != null ? myToId : "tip";
+      cli.addParameter(from + ":" + to);
+    }
+    if (myLimit != null) {
+      cli.addParameter("--limit");
+      cli.addParameter(myLimit.toString());
+    }
+
+    CommandResult res = runCommand(cli);
+    String output = getStdout(res);
+    try {
+      List<ChangeSet> changes = parseChangeSetsXml(output);
+      if (myCalculateParents)
+        assignTrivialParents(changes);
+      return changes;
+    } catch (Exception e) {
+      throw new VcsException("Error while parsing log output:\n" + output, e);
+    }
+  }
+
+  private String getStdout(CommandResult res) throws VcsException {
+    try {
+      return new String(res.getByteOut(), "UTF-8");
+    } catch (UnsupportedEncodingException e) {
+      throw new VcsException("Error while reading output", e);
+    }
+  }
+
+
+  private List<ChangeSet> parseChangeSetsXml(@NotNull final String xml) throws JDOMException, IOException, ParseException {
+    if ("".equals(xml))
+      return Collections.emptyList();
+    Document doc = loadDocument(xml);
+    Element log = doc.getRootElement();
+    return parseLog(log);
+  }
+
+  private Document loadDocument(@NotNull final String xml) throws IOException, JDOMException {
+    String validXml = makeValidXml(xml);
+    return JDOMUtil.loadDocument(validXml);
+  }
+
+  private String makeValidXml(@NotNull final String xml) {
+    String trimmed = xml.trim();
+    if (trimmed.endsWith("</log>"))
+      return xml;
+    else
+      return xml + "</log>";
+  }
+
+
+  private List<ChangeSet> parseLog(@NotNull final Element logElement) throws ParseException {
+    List<ChangeSet> result = new ArrayList<ChangeSet>();
+    for (Object o : logElement.getChildren("logentry")) {
+      Element entry = (Element) o;
+      result.add(parseLogEntry(entry));
+    }
+    return result;
+  }
+
+
+  private ChangeSet parseLogEntry(@NotNull final Element logEntry) throws ParseException {
+    ChangeSet cset = new ChangeSet(getRevision(logEntry), getId(logEntry));
+    addParents(cset, logEntry);
+    cset.setUser(getAuthor(logEntry));
+    cset.setDescription(getDescription(logEntry));
+    cset.setTimestamp(getDate(logEntry));
+    cset.setModifiedFiles(getModifiedFiles(logEntry));
+    return cset;
+  }
+
+
+  private int getRevision(@NotNull final Element logEntry) {
+    return Integer.parseInt(logEntry.getAttribute("revision").getValue());
+  }
+
+
+  private String getId(@NotNull final Element logEntry) {
+    return logEntry.getAttribute("shortnode").getValue();
+  }
+
+
+  private void addParents(@NotNull final ChangeSet cset, @NotNull final Element logEntry) {
+    List parents = logEntry.getChildren("parent");
+    for (Object p : parents) {
+      Element parent = (Element) p;
+      ChangeSetRevision parentCset = getParent(parent);
+      cset.addParent(parentCset);
+    }
+  }
+
+
+  private ChangeSetRevision getParent(@NotNull final Element parent) {
+    return new ChangeSetRevision(getRevision(parent), getId(parent));
+  }
+
+
+  private String getAuthor(@NotNull final Element logEntry) {
+    Element author = logEntry.getChild("author");
+    return author.getAttribute("original").getValue();
+  }
+
+
+  private String getDescription(@NotNull final Element logEntry) {
+    Element msg = logEntry.getChild("msg");
+    return msg.getText();
+  }
+
+
+  private Date getDate(@NotNull final Element logEntry) throws ParseException {
+    Element date = logEntry.getChild("date");
+    return new SimpleDateFormat(DATE_FORMAT, Locale.ENGLISH).parse(date.getText());
+  }
+
+
+  private List<ModifiedFile> getModifiedFiles(@NotNull final Element logEntry) {
+    List<ModifiedFile> result = new ArrayList<ModifiedFile>();
+    Element paths = logEntry.getChild("paths");
+    for (Object o : paths.getChildren("path")) {
+      Element path = (Element) o;
+      result.add(getModifiedFile(path));
+    }
+    return result;
+  }
+
+
+  private ModifiedFile getModifiedFile(@NotNull final Element path) {
+    String filePath = path.getText();
+    ModifiedFile.Status status = getStatus(path);
+    return new ModifiedFile(status, filePath);
+  }
+
+
+  private ModifiedFile.Status getStatus(@NotNull final Element path) {
+    String action = path.getAttribute("action").getValue();
+    if (action.equals("A")) {
+      return ModifiedFile.Status.ADDED;
+    } else if (action.equals("M")) {
+      return ModifiedFile.Status.MODIFIED;
+    } else if (action.equals("R")) {
+      return ModifiedFile.Status.REMOVED;
+    } else {
+      return ModifiedFile.Status.UNKNOWN;
+    }
+  }
+
+  private void assignTrivialParents(final @NotNull List<ChangeSet> csets) throws VcsException {
+    for (ChangeSet cset : csets) {
+      if (cset.getParents().isEmpty()) {
+        int parentRevNumber = cset.getRevNumber() - 1;
+        String parentId = getIdOf(parentRevNumber);
+        cset.addParent(new ChangeSetRevision(parentRevNumber, parentId));
+      }
+    }
+  }
+
+  private String getIdOf(int revNumber) throws VcsException {
+    if (revNumber < 0)
+      return ZERO_PARENT_ID;
+    IdentifyCommand identify = new IdentifyCommand(getSettings(), getWorkDirectory());
+    identify.setInLocalRepository(true);
+    identify.setRevisionNumber(revNumber);
+    return identify.execute();
+  }
+}
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/MercurialCommandLine.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/MercurialCommandLine.java	Wed Feb 15 13:23:53 2012 +0400
@@ -4,12 +4,17 @@
 import jetbrains.buildServer.util.StringUtil;
 import org.jetbrains.annotations.NotNull;
 
+import java.util.Collections;
 import java.util.Set;
 
 public class MercurialCommandLine extends GeneralCommandLine {
 
   private final Set<String> myPrivateData;
 
+  public MercurialCommandLine() {
+    this(Collections.<String>emptySet());
+  }
+
   public MercurialCommandLine(@NotNull Set<String> privateData) {
     myPrivateData = privateData;
   }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/MergeBaseCommand.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,25 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Analog of git merge-base. It returns a last common ancestor between two revisions.
+ *
+ * @author dmitry.neverov
+ */
+public interface MergeBaseCommand {
+
+  /**
+   * Returns hash of least common ancestor between two revisions or null
+   * if common ancestor is not found
+   * @param revision1 first revision
+   * @param revision2 second revision
+   * @return see above
+   * @throws VcsException if some commands fail
+   */
+  @Nullable
+  String execute(@NotNull String revision1, @NotNull String revision2) throws VcsException;
+
+}
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/ModifiedFile.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/ModifiedFile.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,86 +1,86 @@
-/*
- * Copyright 2000-2011 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
-
-import org.jetbrains.annotations.NotNull;
-
-/**
- * Represents repository modified file
- */
-public class ModifiedFile {
-  /**
-   * Type of modification
-   */
-  public static enum Status {
-    ADDED("added"),
-    MODIFIED("modified"),
-    REMOVED("removed"),
-    UNKNOWN("unknown");
-    private String myName;
-
-    Status(@NotNull final String name) {
-      myName = name;
-    }
-
-    @NotNull
-    public String getName() {
-      return myName;
-    }
-  }
-
-  @NotNull private Status myStatus;
-  @NotNull private String myPath;
-
-  public ModifiedFile(@NotNull final Status status, @NotNull final String path) {
-    myStatus = status;
-    myPath = path;
-  }
-
-  /**
-   * Returns type of modification
-   * @return type of modification
-   */
-  @NotNull
-  public Status getStatus() {
-    return myStatus;
-  }
-
-  /**
-   * Returns file path
-   * @return file path
-   */
-  @NotNull
-  public String getPath() {
-    return myPath;
-  }
-
-  @Override
-  public boolean equals(final Object o) {
-    if (this == o) return true;
-    if (o == null || getClass() != o.getClass()) return false;
-
-    final ModifiedFile that = (ModifiedFile) o;
-
-    return myPath.equals(that.myPath) && myStatus == that.myStatus;
-  }
-
-  @Override
-  public int hashCode() {
-    int result = myStatus.hashCode();
-    result = 31 * result + myPath.hashCode();
-    return result;
-  }
-}
+/*
+ * Copyright 2000-2011 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Represents repository modified file
+ */
+public class ModifiedFile {
+  /**
+   * Type of modification
+   */
+  public static enum Status {
+    ADDED("added"),
+    MODIFIED("modified"),
+    REMOVED("removed"),
+    UNKNOWN("unknown");
+    private String myName;
+
+    Status(@NotNull final String name) {
+      myName = name;
+    }
+
+    @NotNull
+    public String getName() {
+      return myName;
+    }
+  }
+
+  @NotNull private Status myStatus;
+  @NotNull private String myPath;
+
+  public ModifiedFile(@NotNull final Status status, @NotNull final String path) {
+    myStatus = status;
+    myPath = path;
+  }
+
+  /**
+   * Returns type of modification
+   * @return type of modification
+   */
+  @NotNull
+  public Status getStatus() {
+    return myStatus;
+  }
+
+  /**
+   * Returns file path
+   * @return file path
+   */
+  @NotNull
+  public String getPath() {
+    return myPath;
+  }
+
+  @Override
+  public boolean equals(final Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+
+    final ModifiedFile that = (ModifiedFile) o;
+
+    return myPath.equals(that.myPath) && myStatus == that.myStatus;
+  }
+
+  @Override
+  public int hashCode() {
+    int result = myStatus.hashCode();
+    result = 31 * result + myPath.hashCode();
+    return result;
+  }
+}
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/PullCommand.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/PullCommand.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,47 +1,62 @@
-/*
- * Copyright 2000-2011 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
-
-import com.intellij.execution.configurations.GeneralCommandLine;
-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 {
-
-  private final String myPullUrl;
-
-  public PullCommand(@NotNull Settings settings, @NotNull File workingDir) {
-    this(settings, workingDir, settings.getRepository());
-  }
-
-  public PullCommand(@NotNull Settings settings, @NotNull File workingDir, @NotNull String pullUrl) {
-    super(settings, workingDir);
-    myPullUrl = pullUrl;
-  }
-
-  public void execute(int timeout) throws VcsException {
-    GeneralCommandLine cli = createCommandLine();
-    cli.addParameter("pull");
-    cli.addParameter(myPullUrl);
-    runCommand(cli, timeout);
-  }
-}
+/*
+ * Copyright 2000-2011 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import com.intellij.execution.configurations.GeneralCommandLine;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+
+import static com.intellij.openapi.util.io.FileUtil.delete;
+import static jetbrains.buildServer.buildTriggers.vcs.mercurial.command.CommandExecutionSettingsBuilder.with;
+
+/**
+ * @author Pavel.Sher
+ *         Date: 14.07.2008
+ */
+public class PullCommand extends VcsRootCommand {
+
+  private final String myPullUrl;
+
+  public PullCommand(@NotNull Settings settings, @NotNull File workingDir) {
+    this(settings, workingDir, settings.getRepositoryUrlWithCredentials());
+  }
+
+  public PullCommand(@NotNull Settings settings, @NotNull File workingDir, @NotNull String pullUrl) {
+    super(settings, workingDir);
+    myPullUrl = pullUrl;
+  }
+
+  public void execute(int timeout) throws VcsException {
+    ensureRepositoryIsNotLocked();
+    GeneralCommandLine cli = createCommandLine();
+    cli.addParameter("pull");
+    cli.addParameter(myPullUrl);
+    runCommand(cli, with().timeout(timeout));
+  }
+
+  private void ensureRepositoryIsNotLocked() {
+    File lock = getRepositoryLock();
+    if (lock.exists())
+      delete(lock);
+  }
+
+  @NotNull
+  private File getRepositoryLock() {
+    return new File(getWorkDirectory(), ".hg" + File.separator + "store" + File.separator + "lock");
+  }
+}
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/PushCommand.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/PushCommand.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,48 +1,49 @@
-/*
- * Copyright 2000-2011 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
-
-import com.intellij.execution.configurations.GeneralCommandLine;
-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 Settings settings, @NotNull File workingDir) {
-    super(settings, workingDir);
-  }
-
-  public void setForce(boolean force) {
-    myForced = force;
-  }
-
-  public void execute() throws VcsException {
-    GeneralCommandLine cli = createCommandLine();
-    cli.addParameter("push");
-    if (myForced) {
-      cli.addParameter("-f");
-    }
-    cli.addParameter(getSettings().getRepositoryUrl());
-    CommandResult res = runCommand(cli);
-    failIfNotEmptyStdErr(cli, res);
-  }
-}
+/*
+ * Copyright 2000-2011 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import com.intellij.execution.configurations.GeneralCommandLine;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+
+import static jetbrains.buildServer.buildTriggers.vcs.mercurial.command.CommandExecutionSettingsBuilder.with;
+
+/**
+ * @author pavel
+ */
+public class PushCommand extends VcsRootCommand {
+  private boolean myForced;
+
+  public PushCommand(@NotNull Settings settings, @NotNull File workingDir) {
+    super(settings, workingDir);
+  }
+
+  public void setForce(boolean force) {
+    myForced = force;
+  }
+
+  public void execute() throws VcsException {
+    GeneralCommandLine cli = createCommandLine();
+    cli.addParameter("push");
+    if (myForced) {
+      cli.addParameter("-f");
+    }
+    cli.addParameter(getSettings().getRepositoryUrlWithCredentials());
+    runCommand(cli, with().failureWhenStderrNotEmpty());
+  }
+}
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/Settings.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/Settings.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,216 +1,230 @@
-/*
- * Copyright 2000-2011 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
-
-import jetbrains.buildServer.buildTriggers.vcs.mercurial.Constants;
-import jetbrains.buildServer.buildTriggers.vcs.mercurial.PathUtil;
-import jetbrains.buildServer.log.Loggers;
-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.io.IOException;
-import java.net.*;
-import java.util.HashSet;
-import java.util.Set;
-
-/**
- * Represents Mercurial repository settings
- */
-public class Settings {
-  private String myRepository;
-  private String myHgCommandPath;
-  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 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 String getCustomClonePath() {
-    return myCustomClonePath;
-  }
-
-  public String getRepository() {
-    return myRepository;
-  }
-
-  /**
-   * Returns name of the branch to use (returns 'default' if no branch specified)
-   * @return see above
-   */
-  @NotNull
-  public String getBranchName() {
-    return StringUtil.isEmpty(myBranchName) ? DEFAULT_BRANCH_NAME : myBranchName;
-  }
-
-  /**
-   * Returns true if current branch is default branch
-   * @return see above
-   */
-  public boolean isDefaultBranch() {
-    return getBranchName().equals(DEFAULT_BRANCH_NAME);
-  }
-
-  public boolean isUncompressedTransfer() {
-    return myUncompressedTransfer;
-  }
-
-  /**
-   * Returns path to hg command
-   * @return path to hg command
-   */
-  @NotNull
-  public String getHgCommandPath() {
-    return myHgCommandPath;
-  }
-
-  public String getUsername() {
-    return myUsername;
-  }
-
-  public String getPassword() {
-    return myPassword;
-  }
-
-  private final static Set<String> AUTH_PROTOS = new HashSet<String>();
-  static {
-    AUTH_PROTOS.add("http");
-    AUTH_PROTOS.add("https");
-    AUTH_PROTOS.add("ssh");
-  }
-
-  /**
-   * Returns URL to use for push command
-   * @return URL to use for push command
-   */
-  public String getRepositoryUrl() {
-    if (isRequireCredentials()) {
-      if (containsCredentials(myRepository)) return myRepository;
-      try {
-        return createURLWithCredentials(myRepository);
-      } catch (MalformedURLException e) {
-        Loggers.VCS.warn("Error while parsing url " + myRepository, e);
-      }
-      return myRepository;
-    } else {
-      return myRepository;
-    }
-  }
-
-  private boolean containsCredentials(final String repository) {
-    try {
-      URL url = new URL(null, repository, new FakeStreamHandler());
-      String userInfo = url.getUserInfo();
-      return userInfo != null && userInfo.contains(":");
-    } catch (MalformedURLException e) {
-      return false;
-    }
-  }
-
-  private String createURLWithCredentials(final String originalUrl) throws MalformedURLException {
-    String userInfo = createUserInfo();
-    if (!"".equals(userInfo)) {
-      URL url = new URL(null, originalUrl, new FakeStreamHandler());
-      return url.getProtocol() + "://"
-              + userInfo + "@"
-              + url.getHost()
-              + (url.getPort() != -1 ? ":" + url.getPort() : "")
-              + url.getFile()
-              + (url.getRef() != null ? url.getRef() : "");
-    } else {
-      return originalUrl;
-    }
-  }
-
-  private boolean isRequireCredentials() {
-    for (String scheme : AUTH_PROTOS) {
-      if (myRepository.startsWith(scheme + ":")) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  private String createUserInfo() {
-    String userInfo = "";
-    if (!StringUtil.isEmpty(myUsername)) {
-      userInfo += myUsername;
-      if (!StringUtil.isEmpty(myPassword)) {
-        userInfo += ":" + myPassword;
-      }
-    }
-    return getEscapedUserInfo(userInfo);
-  }
-
-  private static String getEscapedUserInfo(String userInfo) {
-    try {
-      URI uri = new URI("http", userInfo, "somewhere.com", 80, "", "", "");
-      String escapedURI = uri.toASCIIString();
-      int from = "http://".length();
-      int to = escapedURI.indexOf("somewhere.com") - 1;
-      String escapedUserInfo = escapedURI.substring(from, to);
-      escapedUserInfo = escapedUserInfo.replaceAll("&", "%26");
-      return escapedUserInfo;
-    } catch (URISyntaxException e) {
-      assert false;
-    }
-    return userInfo;
-  }
-
-  /**
-   * Set custom working dir for vcs root. This option make sence only for server-side checkout
-   * @param customWorkingDir custom working dir
-   */
-  public void setCustomWorkingDir(@NotNull final File customWorkingDir) {
-    myCustomWorkingDir = PathUtil.getCanonicalFile(customWorkingDir);
-  }
-
-  /**
-   * 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
-   */
-  @Nullable
-  public File getCustomWorkingDir() {
-    return myCustomWorkingDir;
-  }
-
-  public static boolean isValidRepository(File dir) {
-    // need better way to check that repository copy is ok
-    return dir.isDirectory() && new File(dir, ".hg").isDirectory();
-  }
-
-  private class FakeStreamHandler extends URLStreamHandler {
-     @Override
-     protected URLConnection openConnection(URL u) throws IOException {
-       throw new UnsupportedOperationException();
-     }
-  }
-}
+/*
+ * Copyright 2000-2011 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.Constants;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.HgPathProvider;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.PathUtil;
+import jetbrains.buildServer.log.Loggers;
+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.io.IOException;
+import java.net.*;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Represents Mercurial repository settings
+ */
+public class Settings {
+
+  private final HgPathProvider myHgPathProvider;
+  private String myRepository;
+  private String myHgCommandPath;
+  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;
+  private final String myUserForTag;
+
+  public Settings(@NotNull final HgPathProvider hgPathProvider, @NotNull final VcsRoot vcsRoot) {
+    myHgPathProvider = hgPathProvider;
+    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));
+    myUserForTag = vcsRoot.getProperty(Constants.USER_FOR_TAG);
+  }
+
+  public String getCustomClonePath() {
+    return myCustomClonePath;
+  }
+
+  public String getRepository() {
+    return myRepository;
+  }
+
+  /**
+   * Returns name of the branch to use (returns 'default' if no branch specified)
+   * @return see above
+   */
+  @NotNull
+  public String getBranchName() {
+    return StringUtil.isEmpty(myBranchName) ? DEFAULT_BRANCH_NAME : myBranchName;
+  }
+
+  /**
+   * Returns true if current branch is default branch
+   * @return see above
+   */
+  public boolean isDefaultBranch() {
+    return getBranchName().equals(DEFAULT_BRANCH_NAME);
+  }
+
+  public boolean isUncompressedTransfer() {
+    return myUncompressedTransfer;
+  }
+
+  /**
+   * @return path to hg command taking into account server-wide/agent-wide settings
+   */
+  @NotNull
+  public String getHgCommandPath() {
+    return myHgPathProvider.getHgPath(this);
+  }
+
+  /**
+   * @return path to hg command as it is set in VCS root settings
+   */
+  public String getHgPath() {
+    return myHgCommandPath;
+  }
+
+  public String getUsername() {
+    return myUsername;
+  }
+
+  public String getPassword() {
+    return myPassword;
+  }
+
+  @Nullable
+  public String getUserForTag() {
+    return myUserForTag;
+  }
+
+  private final static Set<String> AUTH_PROTOS = new HashSet<String>();
+  static {
+    AUTH_PROTOS.add("http");
+    AUTH_PROTOS.add("https");
+    AUTH_PROTOS.add("ssh");
+  }
+
+  public String getRepositoryUrlWithCredentials() {
+    if (isRequireCredentials()) {
+      if (containsCredentials(myRepository))
+        return myRepository;
+      try {
+        return createURLWithCredentials(myRepository);
+      } catch (MalformedURLException e) {
+        Loggers.VCS.warn("Error while parsing url " + myRepository, e);
+      }
+      return myRepository;
+    } else {
+      return myRepository;
+    }
+  }
+
+  private boolean containsCredentials(final String repository) {
+    try {
+      URL url = new URL(null, repository, new FakeStreamHandler());
+      String userInfo = url.getUserInfo();
+      return userInfo != null && userInfo.contains(":");
+    } catch (MalformedURLException e) {
+      return false;
+    }
+  }
+
+  private String createURLWithCredentials(final String originalUrl) throws MalformedURLException {
+    String userInfo = createUserInfo();
+    if (!"".equals(userInfo)) {
+      URL url = new URL(null, originalUrl, new FakeStreamHandler());
+      return url.getProtocol() + "://"
+              + userInfo + "@"
+              + url.getHost()
+              + (url.getPort() != -1 ? ":" + url.getPort() : "")
+              + url.getFile()
+              + (url.getRef() != null ? url.getRef() : "");
+    } else {
+      return originalUrl;
+    }
+  }
+
+  private boolean isRequireCredentials() {
+    for (String scheme : AUTH_PROTOS) {
+      if (myRepository.startsWith(scheme + ":")) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private String createUserInfo() {
+    String userInfo = "";
+    if (!StringUtil.isEmpty(myUsername)) {
+      userInfo += myUsername;
+      if (!StringUtil.isEmpty(myPassword)) {
+        userInfo += ":" + myPassword;
+      }
+    }
+    return getEscapedUserInfo(userInfo);
+  }
+
+  private static String getEscapedUserInfo(String userInfo) {
+    try {
+      URI uri = new URI("http", userInfo, "somewhere.com", 80, "", "", "");
+      String escapedURI = uri.toASCIIString();
+      int from = "http://".length();
+      int to = escapedURI.indexOf("somewhere.com") - 1;
+      String escapedUserInfo = escapedURI.substring(from, to);
+      escapedUserInfo = escapedUserInfo.replaceAll("&", "%26");
+      return escapedUserInfo;
+   } catch (URISyntaxException e) {
+      assert false;
+    }
+    return userInfo;
+  }
+
+  /**
+   * Set custom working dir for vcs root. This option make sence only for server-side checkout
+   * @param customWorkingDir custom working dir
+   */
+  public void setCustomWorkingDir(@NotNull final File customWorkingDir) {
+    myCustomWorkingDir = PathUtil.getCanonicalFile(customWorkingDir);
+  }
+
+  /**
+   * 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
+   */
+  @Nullable
+  public File getCustomWorkingDir() {
+    return myCustomWorkingDir;
+  }
+
+  public static boolean isValidRepository(File dir) {
+    // need better way to check that repository copy is ok
+    return dir.isDirectory() && new File(dir, ".hg").isDirectory();
+  }
+
+  private class FakeStreamHandler extends URLStreamHandler {
+    @Override
+    protected URLConnection openConnection(URL u) throws IOException {
+      throw new UnsupportedOperationException();
+    }
+  }
+}
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/StatusCommand.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/StatusCommand.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,77 +1,77 @@
-/*
- * Copyright 2000-2011 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
-
-import com.intellij.execution.configurations.GeneralCommandLine;
-import jetbrains.buildServer.vcs.VcsException;
-import org.jetbrains.annotations.NotNull;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-
-public class StatusCommand extends BaseCommand {
-  private String myFromId;
-  private String myToId;
-
-  public StatusCommand(@NotNull Settings settings, @NotNull File workingDir) {
-    super(settings, workingDir);
-  }
-
-  public void setFromRevId(final String fromId) {
-    myFromId = fromId;
-  }
-
-  public void setToRevId(final String toId) {
-    myToId = toId;
-  }
-
-  public List<ModifiedFile> execute() throws VcsException {
-    GeneralCommandLine cli = createCommandLine();
-    cli.addParameter("status");
-    cli.addParameter("--rev");
-    String from = myFromId;
-    if (from == null) from = "0";
-    String to = myToId;
-    if (to == null) to = "0";
-    cli.addParameter(from + ":" + to);
-    CommandResult res = runCommand(cli);
-    return parseFiles(res.getStdout());
-  }
-
-  public static List<ModifiedFile> parseFiles(final String stdout) {
-    List<ModifiedFile> result = new ArrayList<ModifiedFile>();
-    String[] lines = stdout.split("\n");
-    for (String line: lines) {
-      if (line.length() == 0) continue;
-      char modifier = line.charAt(0);
-      String path = line.substring(2);
-      ModifiedFile.Status status = toStatus(modifier);
-      if (status == ModifiedFile.Status.UNKNOWN) continue;
-      result.add(new ModifiedFile(status, path));
-    }
-    return result;
-  }
-
-  public static ModifiedFile.Status toStatus(final char modifier) {
-    switch (modifier) {
-      case 'A': return ModifiedFile.Status.ADDED;
-      case 'M': return ModifiedFile.Status.MODIFIED;
-      case 'R': return ModifiedFile.Status.REMOVED;
-      default: return ModifiedFile.Status.UNKNOWN;
-    }
-  }
-}
+/*
+ * Copyright 2000-2011 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import com.intellij.execution.configurations.GeneralCommandLine;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+public class StatusCommand extends VcsRootCommand {
+  private String myFromId;
+  private String myToId;
+
+  public StatusCommand(@NotNull Settings settings, @NotNull File workingDir) {
+    super(settings, workingDir);
+  }
+
+  public void setFromRevId(final String fromId) {
+    myFromId = fromId;
+  }
+
+  public void setToRevId(final String toId) {
+    myToId = toId;
+  }
+
+  public List<ModifiedFile> execute() throws VcsException {
+    GeneralCommandLine cli = createCommandLine();
+    cli.addParameter("status");
+    cli.addParameter("--rev");
+    String from = myFromId;
+    if (from == null) from = "0";
+    String to = myToId;
+    if (to == null) to = "0";
+    cli.addParameter(from + ":" + to);
+    CommandResult res = runCommand(cli);
+    return parseFiles(res.getStdout());
+  }
+
+  public static List<ModifiedFile> parseFiles(final String stdout) {
+    List<ModifiedFile> result = new ArrayList<ModifiedFile>();
+    String[] lines = stdout.split("\n");
+    for (String line: lines) {
+      if (line.length() == 0) continue;
+      char modifier = line.charAt(0);
+      String path = line.substring(2);
+      ModifiedFile.Status status = toStatus(modifier);
+      if (status == ModifiedFile.Status.UNKNOWN) continue;
+      result.add(new ModifiedFile(status, path));
+    }
+    return result;
+  }
+
+  public static ModifiedFile.Status toStatus(final char modifier) {
+    switch (modifier) {
+      case 'A': return ModifiedFile.Status.ADDED;
+      case 'M': return ModifiedFile.Status.MODIFIED;
+      case 'R': return ModifiedFile.Status.REMOVED;
+      default: return ModifiedFile.Status.UNKNOWN;
+    }
+  }
+}
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/TagCommand.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/TagCommand.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,48 +1,59 @@
-/*
- * Copyright 2000-2011 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
-
-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, @NotNull File workingDir) {
-    super(settings, workingDir);
-  }
-
-  public void setTag(@NotNull final String tag) {
-    myTag = tag;
-  }
-
-  public void setRevId(@NotNull final String revId) {
-    myRevId = revId;
-  }
-
-  public void execute() throws VcsException {
-    GeneralCommandLine cli = createCommandLine();
-    cli.addParameter("tag");
-    cli.addParameter("-r");
-    cli.addParameter(myRevId);
-    cli.addParameter(myTag);
-    runCommand(cli);
-  }
-}
+/*
+ * Copyright 2000-2011 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import com.intellij.execution.configurations.GeneralCommandLine;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+
+public class TagCommand extends VcsRootCommand {
+  private String myTag;
+  private String myRevId;
+  private String myUsername;
+
+  public TagCommand(@NotNull Settings settings, @NotNull File workingDir) {
+    super(settings, workingDir);
+  }
+
+  public void setTag(@NotNull final String tag) {
+    myTag = tag;
+  }
+
+  public void setRevId(@NotNull final String revId) {
+    myRevId = revId;
+  }
+
+  public void setUser(@NotNull final String username) {
+    myUsername = username;
+  }
+
+  public void execute() throws VcsException {
+    GeneralCommandLine cli = createCommandLine();
+    cli.addParameter("tag");
+    setUser(cli);
+    cli.addParameter("-r");
+    cli.addParameter(myRevId);
+    cli.addParameter(myTag);
+    runCommand(cli);
+  }
+
+  private void setUser(GeneralCommandLine cli) {
+    if (myUsername != null)
+      cli.addParameters("--user", myUsername);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/UnknownFileException.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,22 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author dmitry.neverov
+ */
+public class UnknownFileException extends VcsException {
+
+  private final String myPath;
+
+  public UnknownFileException(@NotNull String path) {
+    super("Unknown file " + path);
+    myPath = path;
+  }
+
+  public String getPath() {
+    return myPath;
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/UnknownRevisionException.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,23 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author dmitry.neverov
+ */
+public class UnknownRevisionException extends VcsException {
+
+  private final String myRevision;
+
+  public UnknownRevisionException(@NotNull final String revision) {
+    super("Unknown revision " + revision);
+    myRevision = revision;
+  }
+
+  @NotNull
+  public String getRevision() {
+    return myRevision;
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/UnrelatedRepositoryException.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,10 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import jetbrains.buildServer.vcs.VcsException;
+
+/**
+ * @author dmitry.neverov
+ */
+public class UnrelatedRepositoryException extends VcsException {
+
+}
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/UpdateCommand.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/UpdateCommand.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,50 +1,81 @@
-/*
- * Copyright 2000-2011 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
-
-import com.intellij.execution.configurations.GeneralCommandLine;
-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 Settings settings, @NotNull File workingDir) {
-    super(settings, workingDir);
-  }
-
-  public void setToId(final String toId) {
-    myToId = toId;
-  }
-
-  public void execute() throws VcsException {
-    GeneralCommandLine cli = createCommandLine();
-    cli.addParameter("update");
-    cli.addParameter("-C");
-    cli.addParameter("-r");
-    if (myToId != null) {
-      cli.addParameter(myToId);
-    } else {
-      cli.addParameter(getSettings().getBranchName());
-    }
-    runCommand(cli, UPDATE_TIMEOUT_SECONDS);
-  }
-}
+/*
+ * Copyright 2000-2011 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import com.intellij.execution.configurations.GeneralCommandLine;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.intellij.openapi.util.io.FileUtil.delete;
+import static jetbrains.buildServer.buildTriggers.vcs.mercurial.command.CommandExecutionSettingsBuilder.with;
+
+public class UpdateCommand extends VcsRootCommand {
+
+  private static final int UPDATE_TIMEOUT_SECONDS = 8 * 3600;//8 hours
+
+  private String myToId;
+  private final Map<String, String> myConfigParams = new HashMap<String, String>();
+
+  public UpdateCommand(@NotNull Settings settings, @NotNull File workingDir) {
+    super(settings, workingDir);
+  }
+
+  public void setToId(final String toId) {
+    myToId = toId;
+  }
+
+  public UpdateCommand withConfig(@NotNull String paramName, @NotNull String paramValue) {
+    myConfigParams.put(paramName, paramValue);
+    return this;
+  }
+
+  public void execute() throws VcsException {
+    ensureWorkingDirIsNotLocked();
+    GeneralCommandLine cli = createCommandLine();
+    cli.addParameter("update");
+    addConfigParams(cli);
+    cli.addParameter("-C");
+    cli.addParameter("-r");
+    if (myToId != null) {
+      cli.addParameter(myToId);
+    } else {
+      cli.addParameter(getSettings().getBranchName());
+    }
+    runCommand(cli, with().timeout(UPDATE_TIMEOUT_SECONDS));
+  }
+
+  private void ensureWorkingDirIsNotLocked() {
+    File lock = getWorkingDirLock();
+    if (lock.exists())
+      delete(lock);
+  }
+
+  private void addConfigParams(GeneralCommandLine cmd) {
+    for (Map.Entry<String, String> entry : myConfigParams.entrySet()) {
+      cmd.addParameter("--config");
+      cmd.addParameter(entry.getKey() + "=" + entry.getValue());
+    }
+  }
+
+  @NotNull
+  private File getWorkingDirLock() {
+    return new File(getWorkDirectory(), ".hg" + File.separator + "wlock");
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/VcsRootCommand.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,42 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import com.intellij.execution.configurations.GeneralCommandLine;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.Set;
+
+import static jetbrains.buildServer.buildTriggers.vcs.mercurial.command.CommandExecutionSettingsBuilder.with;
+
+/**
+ * @author dmitry.neverov
+ */
+public class VcsRootCommand extends BaseCommand {
+
+  private final Settings mySettings;
+
+  public VcsRootCommand(@NotNull final Settings settings, @NotNull final File workDir) {
+    super(settings.getHgCommandPath(), workDir);
+    mySettings = settings;
+  }
+
+
+  public Settings getSettings() {
+    return mySettings;
+  }
+
+  protected CommandResult runCommand(@NotNull GeneralCommandLine cli) throws VcsException {
+    return CommandUtil.runCommand(cli, with());
+  }
+
+  protected CommandResult runCommand(@NotNull GeneralCommandLine cli, @NotNull CommandExecutionSettingsBuilder with) throws VcsException {
+    return CommandUtil.runCommand(cli, with.privateData(getPrivateData()));
+  }
+
+  protected Set<String> getPrivateData() {
+    String password = mySettings.getPassword();
+    return password != null ? Collections.singleton(password) : Collections.<String>emptySet();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/VersionCommand.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,33 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import com.intellij.execution.configurations.GeneralCommandLine;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.HgVersion;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+
+/**
+ * @author dmitry.neverov
+ */
+public class VersionCommand extends BaseCommand {
+
+  public VersionCommand(@NotNull final Settings settings, @NotNull File workingDir) {
+    super(settings.getHgCommandPath(), workingDir);
+  }
+
+
+  public VersionCommand(@NotNull final String hgPath, @NotNull File workingDir) {
+    super(hgPath, workingDir);
+  }
+
+
+  public HgVersion execute() throws VcsException {
+    GeneralCommandLine cli = createCommandLine();
+    cli.addParameter("version");
+    cli.addParameter("--quiet");
+    CommandResult result = CommandUtil.runCommand(cli);
+    return HgVersion.parse(result.getStdout());
+  }
+
+}
--- a/mercurial-server/resources/buildServerResources/mercurialSettings.jsp	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-server/resources/buildServerResources/mercurialSettings.jsp	Wed Feb 15 13:23:53 2012 +0400
@@ -32,6 +32,10 @@
     </td>
   </tr>
   <tr>
+    <th><label for="tagUsername">Username for tags: </label></th>
+    <td><props:textProperty name="tagUsername"/></td>
+  </tr>
+  <tr>
     <th><label for="uncompressedTransfer">Use uncompressed transfer: </label></th>
     <td><props:checkboxProperty name="uncompressedTransfer"/>
       <div class="smallNote" style="margin: 0;">Uncompressed transfer is faster for repositories in the LAN.</div>
@@ -40,9 +44,6 @@
   </l:settingsGroup>
   <l:settingsGroup title="Authorization settings">
   <tr>
-    <td colspan="2">Authorization settings can be required if you need to tag / label sources in the remote repository.</td>
-  </tr>
-  <tr>
     <th><label for="username">User name:</label></th>
     <td><props:textProperty name="username"/></td>
   </tr>
@@ -51,5 +52,5 @@
     <td><props:passwordProperty name="secure:password"/></td>
   </tr>
   </l:settingsGroup>
-  
+
 </table>
--- a/mercurial-server/src/META-INF/build-server-plugin-mercurial.xml	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-server/src/META-INF/build-server-plugin-mercurial.xml	Wed Feb 15 13:23:53 2012 +0400
@@ -1,7 +1,10 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
-
-<beans default-autowire="constructor">
-  <bean id="mercurialServer" class="jetbrains.buildServer.buildTriggers.vcs.mercurial.MercurialVcsSupport" />
-  <bean id="config" class="jetbrains.buildServer.buildTriggers.vcs.mercurial.ServerPluginConfigImpl" />
-</beans>
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
+
+<beans default-autowire="constructor">
+  <bean id="mercurialServer" class="jetbrains.buildServer.buildTriggers.vcs.mercurial.MercurialVcsSupport" />
+  <bean id="config" class="jetbrains.buildServer.buildTriggers.vcs.mercurial.ServerPluginConfigImpl" />
+  <bean id="commandFactory" class="jetbrains.buildServer.buildTriggers.vcs.mercurial.CommandFactoryImpl" />
+  <bean id="hgPathProvider" class="jetbrains.buildServer.buildTriggers.vcs.mercurial.ServerHgPathProvider"/>
+  <bean id="mirrorManager" class="jetbrains.buildServer.buildTriggers.vcs.mercurial.MirrorManagerImpl" />
+</beans>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/Cleanup.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,135 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import com.intellij.openapi.diagnostic.Logger;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.Settings;
+import jetbrains.buildServer.serverSide.impl.LogUtil;
+import jetbrains.buildServer.util.FileUtil;
+import jetbrains.buildServer.vcs.VcsManager;
+import jetbrains.buildServer.vcs.VcsRoot;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.util.*;
+
+import static java.util.Arrays.asList;
+
+/**
+ * @author dmitry.neverov
+ */
+public class Cleanup implements Runnable {
+
+  private static Logger LOG = Logger.getInstance(Cleanup.class.getName());
+
+  private final VcsManager myVcsManager;
+  private final MirrorManager myMirrorManager;
+  private final PluginConfig myConfig;
+  private final HgPathProvider myHgPathProvider;
+
+  public Cleanup(@NotNull final VcsManager vcsManager,
+                 @NotNull final MirrorManager mirrorManager,
+                 @NotNull final PluginConfig config,
+                 @NotNull final HgPathProvider hgPathProvider) {
+    myVcsManager = vcsManager;
+    myMirrorManager = mirrorManager;
+    myConfig = config;
+    myHgPathProvider = hgPathProvider;
+  }
+
+  public void run() {
+    delete(unusedDirs());
+  }
+
+  private Collection<File> unusedDirs() {
+    List<File> existingDirs = existingDirs();
+    List<File> mirrorsInUse = mirrorDirsOfRootsInUse();
+    existingDirs.removeAll(mirrorsInUse);
+    return existingDirs;
+  }
+
+  private List<File> existingDirs() {
+    File[] files = listDirs();
+    if (files != null)
+      return new ArrayList<File>(asList(files));
+    if (myConfig.getCachesDir().isDirectory())
+      LOG.warn("Cannot list files in " + myConfig.getCachesDir());
+    return Collections.emptyList();
+  }
+
+  private File[] listDirs() {
+    return myConfig.getCachesDir().listFiles(new FileFilter() {
+      public boolean accept(File f) {
+        return f.isDirectory();
+      }
+    });
+  }
+
+  private List<File> mirrorDirsOfRootsInUse() {
+    Map<String, File> mirrorMap = myMirrorManager.getMappings();
+    List<File> result = new ArrayList<File>();
+    for (VcsRoot root : mercurialVcsRoots()) {
+      File mirrorDir = mirrorMap.get(urlOf(root));
+      if (mirrorDir != null)
+        result.add(mirrorDir);
+    }
+    return result;
+  }
+
+  private String urlOf(VcsRoot root) {
+    Settings s = new Settings(myHgPathProvider, root);
+    return s.getRepository();
+  }
+
+  private Collection<VcsRoot> mercurialVcsRoots() {
+    List<VcsRoot> mercurialRoots = new ArrayList<VcsRoot>();
+    for (VcsRoot root : myVcsManager.getAllRegisteredVcsRoots()) {
+      if (isMercurialRoot(root))
+        mercurialRoots.add(root);
+    }
+    logRegisteredMercurialRoots(mercurialRoots);
+    return mercurialRoots;
+  }
+
+  private boolean isMercurialRoot(VcsRoot root) {
+    return Constants.VCS_NAME.equals(root.getVcsName());
+  }
+
+  private void delete(Collection<File> dirs) {
+    logUnusedLocalClones(dirs);
+    for (File dir : dirs) {
+      myMirrorManager.lockDir(dir);
+      try {
+        myMirrorManager.forgetDir(dir);
+        FileUtil.delete(dir);
+      } finally {
+        myMirrorManager.unlockDir(dir);
+      }
+    }
+  }
+
+  private void logRegisteredMercurialRoots(@NotNull List<VcsRoot> roots) {
+    StringBuilder sb = new StringBuilder();
+    sb.append("Registered mercurial roots: ");
+    Iterator<VcsRoot> iter = roots.iterator();
+    while (iter.hasNext()) {
+      sb.append(LogUtil.describe(iter.next()));
+      if (iter.hasNext())
+        sb.append(", ");
+    }
+    LOG.debug(sb.toString());
+  }
+
+  private void logUnusedLocalClones(@NotNull Collection<File> dirs) {
+    if (dirs.isEmpty())
+      return;
+    StringBuilder sb = new StringBuilder();
+    sb.append("Unused local clones: ");
+    Iterator<File> iter = dirs.iterator();
+    while (iter.hasNext()) {
+      sb.append(iter.next().getAbsolutePath());
+      if (iter.hasNext())
+        sb.append(", ");
+    }
+    LOG.info(sb.toString());
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/CollectChangesCommand.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,17 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.ChangeSet;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * @author dmitry.neverov
+ */
+public interface CollectChangesCommand {
+
+  @NotNull
+  public List<ChangeSet> execute(@NotNull String fromCommit, @NotNull String toCommit) throws VcsException;
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/CollectChangesNoRevsets.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,83 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import com.intellij.openapi.util.Pair;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.*;
+import jetbrains.buildServer.util.graph.DAG;
+import jetbrains.buildServer.util.graph.DAGIterator;
+import jetbrains.buildServer.util.graph.DAGs;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.*;
+
+/**
+ * @author dmitry.neverov
+ */
+public class CollectChangesNoRevsets implements CollectChangesCommand {
+
+  private final Settings mySettings;
+  private final File myWorkingDir;
+  private final File myTemplate;
+
+  public CollectChangesNoRevsets(@NotNull final Settings settings,
+                                 @NotNull final File workingDir,
+                                 @NotNull final File template) {
+    mySettings = settings;
+    myWorkingDir = workingDir;
+    myTemplate = template;
+  }
+
+
+  @NotNull
+  public List<ChangeSet> execute(@NotNull final String fromCommit, @NotNull final String toCommit) throws VcsException {
+    List<ChangeSet> csets = getRevisionsReachableFrom(toCommit);
+    Map<String, ChangeSet> csetsMap = getChangesetMap(csets);
+    if (csetsMap.containsKey(fromCommit)) {
+      DAG<String> dag = DAGs.createFromEdges(getEdges(csets));
+      DAGIterator<String> iter = dag.iterator(toCommit);
+      iter.markUninteresting(fromCommit);
+      List<ChangeSet> result = new ArrayList<ChangeSet>();
+      while (iter.hasNext()) {
+        String commit = iter.next();
+        ChangeSet cset = csetsMap.get(commit);
+        if (cset == null)
+          throw new IllegalStateException("Cannot find cset for id " + commit + ", csets map: " + csetsMap);
+        result.add(cset);
+      }
+      Collections.reverse(result);
+      return result;
+    } else {
+      return Collections.emptyList();
+    }
+  }
+
+
+  private Map<String, ChangeSet> getChangesetMap(@NotNull final List<ChangeSet> csets) {
+    Map<String, ChangeSet> result = new HashMap<String, ChangeSet>();
+    for (ChangeSet cset : csets) {
+      result.put(cset.getId(), cset);
+    }
+    return result;
+  }
+
+
+  private List<ChangeSet> getRevisionsReachableFrom(@NotNull final String revision) throws VcsException {
+    LogCommand log = new LogCommand(mySettings, myWorkingDir, myTemplate);
+    log.setFromRevId(new ChangeSetRevision(revision).getId());
+    log.showCommitsFromAllBranches();
+    log.setToRevId("0");
+    return log.execute();
+  }
+
+
+  private List<Pair<String, String>> getEdges(List<ChangeSet> csets) {
+    List<Pair<String, String>> result = new ArrayList<Pair<String, String>>();
+    for (ChangeSet cset : csets) {
+      for (ChangeSetRevision parent : cset.getParents()) {
+        result.add(Pair.create(cset.getId(), parent.getId()));
+      }
+    }
+    return result;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/CollectChangesWithRevsets.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,34 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.ChangeSet;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.LogCommand;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.Settings;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * @author dmitry.neverov
+ */
+public class CollectChangesWithRevsets implements CollectChangesCommand {
+
+  private final Settings mySettings;
+  private final File myWorkingDir;
+  private final File myTemplate;
+
+  public CollectChangesWithRevsets(@NotNull final Settings settings, @NotNull final File workingDir, @NotNull final File template) {
+    mySettings = settings;
+    myWorkingDir = workingDir;
+    myTemplate = template;
+  }
+
+  @NotNull
+  public List<ChangeSet> execute(@NotNull final String fromCommit, @NotNull final String toCommit) throws VcsException {
+    LogCommand log = new LogCommand(mySettings, myWorkingDir, myTemplate);
+    log.showCommitsFromAllBranches();
+    log.setRevsets("ancestors(" + toCommit + ") - ancestors(" + fromCommit + ")");
+    return log.execute();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/CommandFactory.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,25 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.LogCommand;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.MergeBaseCommand;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.Settings;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+
+/**
+ * @author dmitry.neverov
+ */
+public interface CommandFactory {
+
+  @NotNull
+  public MergeBaseCommand createMergeBase(@NotNull Settings settings, @NotNull File workingDir) throws VcsException;
+
+  @NotNull
+  public LogCommand createLog(@NotNull final Settings settings, @NotNull final File workingDir);
+
+  @NotNull
+  public CollectChangesCommand getCollectChangesCommand(@NotNull final Settings settings, @NotNull final File workingDir) throws VcsException;
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/CommandFactoryImpl.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,68 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.LogCommand;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.MergeBaseCommand;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.Settings;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.VersionCommand;
+import jetbrains.buildServer.util.FileUtil;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * @author dmitry.neverov
+ */
+public final class CommandFactoryImpl implements CommandFactory {
+
+  //hg version which supports revsets
+  private final static HgVersion REVSET_HG_VERSION = new HgVersion(1, 7, 0);
+  private final static String LOG_TEMPLATE_NAME = "log.template";
+
+  private final File myDefaultWorkingDir;
+  private final File myLogTemplate;
+
+
+  public CommandFactoryImpl(@NotNull final ServerPluginConfig config) throws IOException {
+    myDefaultWorkingDir = config.getCachesDir();
+    myLogTemplate = createLogTemplate(config.getPluginDataDir());
+  }
+
+
+  @NotNull
+  public MergeBaseCommand createMergeBase(@NotNull Settings settings, @NotNull File workingDir) throws VcsException {
+    HgVersion hgVersion = getHgVersion(settings);
+    if (hgVersion.isEqualsOrGreaterThan(REVSET_HG_VERSION))
+      return new MergeBaseWithRevsets(settings, workingDir, this);
+    else
+      return new MergeBaseNoRevsets(settings, workingDir, this);
+  }
+
+
+  @NotNull
+  public LogCommand createLog(@NotNull final Settings settings, @NotNull final File workingDir) {
+    return new LogCommand(settings, workingDir, myLogTemplate);
+  }
+
+  @NotNull
+  public CollectChangesCommand getCollectChangesCommand(@NotNull final Settings settings, @NotNull final File workingDir) throws VcsException {
+    HgVersion hgVersion = getHgVersion(settings);
+    if (hgVersion.isEqualsOrGreaterThan(REVSET_HG_VERSION)) {
+      return new CollectChangesWithRevsets(settings, workingDir, myLogTemplate);
+    } else {
+      return new CollectChangesNoRevsets(settings, workingDir, myLogTemplate);
+    }
+  }
+
+  private File createLogTemplate(@NotNull final File templateFileDir) throws IOException {
+    File template = new File(templateFileDir, LOG_TEMPLATE_NAME);
+    FileUtil.copyResource(CommandFactoryImpl.class, "/buildServerResources/log.template", template);
+    return template;
+  }
+
+  private HgVersion getHgVersion(@NotNull final Settings settings) throws VcsException {
+    VersionCommand versionCommand = new VersionCommand(settings, myDefaultWorkingDir);
+    return versionCommand.execute();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialResetCacheHandler.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,88 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import com.intellij.openapi.diagnostic.Logger;
+import jetbrains.buildServer.util.cache.ResetCacheHandler;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static com.intellij.openapi.util.io.FileUtil.delete;
+import static java.util.Collections.singletonList;
+
+/**
+ * @author dmitry.neverov
+ */
+public class MercurialResetCacheHandler implements ResetCacheHandler {
+
+  private static Logger LOG = Logger.getInstance(MercurialResetCacheHandler.class.getName());
+  private static final String MERCURIAL_CACHE_NAME = "mercurial";
+
+  private final MirrorManager myMirrorManager;
+  private AtomicBoolean myResetRunning = new AtomicBoolean(false);
+
+  public MercurialResetCacheHandler(@NotNull MirrorManager mirrorManager) {
+    myMirrorManager = mirrorManager;
+  }
+
+  @NotNull
+  public List<String> listCaches() {
+    return singletonList(MERCURIAL_CACHE_NAME);
+  }
+
+  public boolean isEmpty(@NotNull final String cache) {
+    return myMirrorManager.getMappings().isEmpty();
+  }
+
+  public void resetCache(@NotNull final String cache) {
+    boolean started = startReset();
+    if (!started) {
+      LOG.info("Mercurial mirrors reset is already running");
+      return;
+    }
+    resetAllMirrors();
+    finishReset();
+  }
+
+  private boolean startReset() {
+    return myResetRunning.compareAndSet(false, true);
+  }
+
+  private void finishReset() {
+    myResetRunning.set(false);
+  }
+
+  private void resetAllMirrors() {
+    LOG.info("Start reseting mercurial caches");
+    for (Map.Entry<String, File> entry : myMirrorManager.getMappings().entrySet()) {
+      String url = entry.getKey();
+      File mirror = entry.getValue();
+      try {
+        lockMirror(url, mirror);
+        resetMirror(mirror);
+      } finally {
+        unlockMirror(url, mirror);
+      }
+    }
+    LOG.info("Mercurial caches reset");
+  }
+
+  private void lockMirror(@NotNull final String url, @NotNull final File mirror) {
+    LOG.debug("Lock mirror of " + url);
+    myMirrorManager.lockDir(mirror);
+    LOG.debug("Mirror of " + url + " is locked");
+  }
+
+  private void resetMirror(@NotNull final File mirror) {
+    LOG.debug("Reset mercurial mirror "  + mirror.getAbsolutePath());
+    myMirrorManager.forgetDir(mirror);
+    delete(mirror);
+    LOG.debug("Mercurial mirror "  + mirror.getAbsolutePath() + " reset");
+  }
+
+  private void unlockMirror(@NotNull final String url, @NotNull final File mirror) {
+    myMirrorManager.unlockDir(mirror);
+    LOG.debug("Mirror of " + url + " is unlocked");
+  }
+}
--- a/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java	Wed Feb 15 13:23:53 2012 +0400
@@ -24,8 +24,7 @@
 import jetbrains.buildServer.util.EventDispatcher;
 import jetbrains.buildServer.util.FileUtil;
 import jetbrains.buildServer.util.StringUtil;
-import jetbrains.buildServer.util.filters.Filter;
-import jetbrains.buildServer.util.filters.FilterUtil;
+import jetbrains.buildServer.util.cache.ResetCacheRegister;
 import jetbrains.buildServer.vcs.*;
 import jetbrains.buildServer.vcs.impl.VcsRootImpl;
 import jetbrains.buildServer.vcs.patches.PatchBuilder;
@@ -37,10 +36,6 @@
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.util.*;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
 
 import static com.intellij.openapi.util.text.StringUtil.isEmptyOrSpaces;
 
@@ -55,37 +50,36 @@
  * <p>Working copy of repository is created in the $TEAMCITY_DATA_PATH/system/caches/hg_&lt;hash code> folder.
  * <p>Personal builds (remote runs) are not yet supported, they require corresponding functionality from the IDE.
  */
-public class MercurialVcsSupport extends ServerVcsSupport implements LabelingSupport, VcsFileContentProvider, BranchSupport {
-
-  private final String LOG_TEMPLATE_NAME = "log.template";
-  private ConcurrentMap<String, Lock> myWorkDirLocks= new ConcurrentHashMap<String, Lock>();
-  private VcsManager myVcsManager;
-  private File myDefaultWorkFolderParent;
-  private MirrorManager myMirrorManager;
+public class MercurialVcsSupport extends ServerVcsSupport implements LabelingSupport, VcsFileContentProvider, BranchSupport,
+        CollectChangesBetweenRoots {
+  private final VcsManager myVcsManager;
+  private final File myDefaultWorkFolderParent;
+  private final MirrorManager myMirrorManager;
   private final ServerPluginConfig myConfig;
-  private File myLogTemplate;
+  private final HgPathProvider myHgPathProvider;
+  private final CommandFactory myCommandFactory;
   private final FileFilter myIgnoreDotHgFilter = new IgnoreDotHgFilter();
   private final FileFilter myAcceptAllFilter = new AcceptAllFilter();
 
   public MercurialVcsSupport(@NotNull final VcsManager vcsManager,
-                             @NotNull final ServerPaths paths,
                              @NotNull final SBuildServer server,
                              @NotNull final EventDispatcher<BuildServerListener> dispatcher,
-                             @NotNull final ServerPluginConfig config) throws IOException {
-    myLogTemplate = createLogTemplate(paths.getPluginDataDirectory());
+                             @NotNull final ResetCacheRegister resetCacheHandlerManager,
+                             @NotNull final ServerPluginConfig config,
+                             @NotNull final HgPathProvider hgPathProvider,
+                             @NotNull final CommandFactory commandFactory,
+                             @NotNull final MirrorManager mirrorManager) {
     myVcsManager = vcsManager;
-    myDefaultWorkFolderParent = new File(paths.getCachesDir(), "mercurial");
-    myMirrorManager = new MirrorManager(myDefaultWorkFolderParent);
     myConfig = config;
+    myDefaultWorkFolderParent = myConfig.getCachesDir();
+    myMirrorManager = mirrorManager;
+    myHgPathProvider = hgPathProvider;
+    myCommandFactory = commandFactory;
+    resetCacheHandlerManager.registerHandler(new MercurialResetCacheHandler(myMirrorManager));
     dispatcher.addListener(new BuildServerAdapter() {
       @Override
       public void cleanupFinished() {
-        super.cleanupFinished();
-        server.getExecutor().submit(new Runnable() {
-          public void run() {
-            removeOldWorkFolders();
-          }
-        });
+        server.getExecutor().submit(new Cleanup(myVcsManager, myMirrorManager, myConfig, myHgPathProvider));
       }
 
       @Override
@@ -98,6 +92,15 @@
         });
       }
     });
+    logUsedHg();
+  }
+
+  private void logUsedHg() {
+    String hgPath = myConfig.getHgPath();
+    if (hgPath != null)
+      Loggers.VCS.info("Use server-wide hg path " + hgPath + ", path in the VCS root settings will be ignored");
+    else
+      Loggers.VCS.info("Server-wide hg path is not set, will use path from the VCS root settings");
   }
 
   private void deleteWithLocking(Collection<File> filesForDelete) {
@@ -111,19 +114,6 @@
     }
   }
 
-  private Collection<ModifiedFile> computeModifiedFilesForMergeCommit(final Settings settings, final ChangeSet cur) throws VcsException {
-    File workingDir = getWorkingDir(settings);
-    ChangedFilesCommand cfc = new ChangedFilesCommand(settings, workingDir);
-    cfc.setRevId(cur.getId());
-    return cfc.execute();
-  }
-
-  private File createLogTemplate(@NotNull final File templateFileDir) throws IOException {
-    File template = new File(templateFileDir, LOG_TEMPLATE_NAME);
-    FileUtil.copyResource(MercurialVcsSupport.class, "/buildServerResources/log.template", template);
-    return template;
-  }
-
   private List<VcsChange> toVcsChanges(final List<ModifiedFile> modifiedFiles, String prevVer, String curVer, CheckoutRules rules) {
     List<VcsChange> files = new ArrayList<VcsChange>();
     for (ModifiedFile mf: modifiedFiles) {
@@ -192,35 +182,6 @@
     return new byte[0];
   }
 
-  /**
-   * Returns file's content or empty string if it doesn't exist.
-   * @param path path of the file of interest
-   * @param settings root settings
-   * @param cset repository cset (should be present in the repository)
-   * @return see above
-   */
-  @NotNull
-  private String getFileContent(@NotNull final String path, @NotNull final Settings settings, @NotNull final ChangeSet cset) throws VcsException {
-    File dir = getWorkingDir(settings);
-    CatCommand cat = new CatCommand(settings, dir);
-    cat.setRevId(cset.getId());
-    cat.setLogErrorsInDebug(true);
-    File parentDir = null;
-    try {
-      parentDir = cat.execute(Collections.singletonList(path));
-      File f = new File(parentDir, path);
-      if (f.isFile())
-        return FileUtil.readText(f);
-      else
-        return "";
-    } catch (Exception e) {
-      return "";
-    } finally {
-      if (parentDir != null)
-        deleteTmpDir(parentDir);
-    }
-  }
-
   @NotNull
   public String getName() {
     return Constants.VCS_NAME;
@@ -286,7 +247,7 @@
         StringBuilder res = new StringBuilder();
         res.append(quoteIfNeeded(settings.getHgCommandPath()));
         res.append(" identify ");
-        final String obfuscatedUrl = CommandUtil.removePrivateData(settings.getRepositoryUrl(), Collections.singleton(settings.getPassword()));
+        final String obfuscatedUrl = CommandUtil.removePrivateData(settings.getRepositoryUrlWithCredentials(), Collections.singleton(settings.getPassword()));
         res.append(quoteIfNeeded(obfuscatedUrl));
         res.append('\n').append(id.execute());
         return res.toString();
@@ -418,6 +379,33 @@
     return !isEmptyOrSpaces(hgsub);
   }
 
+  /**
+   * Returns file's content or empty string if it doesn't exist.
+   * @param path path of the file of interest
+   * @param settings root settings
+   * @param cset repository cset (should be present in the repository)
+   * @return see above
+   */
+  @NotNull
+  private String getFileContent(@NotNull final String path, @NotNull final Settings settings, @NotNull final ChangeSet cset) throws VcsException {
+    File dir = getWorkingDir(settings);
+    CatCommand cat = new CatCommand(settings, dir);
+    cat.setRevId(cset.getId());
+    File parentDir = cat.execute(Collections.singletonList(path), false);
+    try {
+      File f = new File(parentDir, path);
+      if (f.isFile())
+        return FileUtil.readText(f);
+      else
+        return "";
+    } catch (Exception e) {
+      return "";
+    } finally {
+      deleteTmpDir(parentDir);
+    }
+  }
+
+
   private void buildPatchFromDirectory(final PatchBuilder builder, final File repRoot, final CheckoutRules checkoutRules, @NotNull final FileFilter filter) throws IOException {
     buildPatchFromDirectory(repRoot, builder, repRoot, checkoutRules, filter);
   }
@@ -459,8 +447,14 @@
     try {
       if (Settings.isValidRepository(workingDir)) {
         if (!isChangeSetExist(settings, workingDir, cset)) {
-          PullCommand pull = new PullCommand(settings, workingDir);
-          pull.execute(myConfig.getPullTimeout());
+          try {
+            PullCommand pull = new PullCommand(settings, workingDir);
+            pull.execute(myConfig.getPullTimeout());
+          } catch (UnrelatedRepositoryException e) {
+            Loggers.VCS.warn("Repository at " + settings.getRepository() + " is unrelated, clone it again");
+            myMirrorManager.forgetDir(workingDir);
+            syncRepository(settings, cset);
+          }
         }
       } else {
         CloneCommand cl = new CloneCommand(settings, workingDir);
@@ -477,8 +471,14 @@
     lockWorkDir(workingDir);
     try {
       if (Settings.isValidRepository(workingDir)) {
-        PullCommand pull = new PullCommand(settings, workingDir);
-        pull.execute(myConfig.getPullTimeout());
+        try {
+          PullCommand pull = new PullCommand(settings, workingDir);
+          pull.execute(myConfig.getPullTimeout());
+        } catch (UnrelatedRepositoryException e) {
+          Loggers.VCS.warn("Repository at " + settings.getRepository() + " is unrelated, clone it again");
+          myMirrorManager.forgetDir(workingDir);
+          syncRepository(settings);
+        }
       } else {
         CloneCommand cl = new CloneCommand(settings, workingDir);
         cl.setUpdateWorkingDir(false);
@@ -492,6 +492,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
    */
@@ -523,7 +524,12 @@
   }
 
   @NotNull
-  public Map<String, String> getBranchesRevisions(@NotNull VcsRoot root) throws VcsException {
+  public RepositoryState getCurrentState(@NotNull VcsRoot root) throws VcsException {
+    return RepositoryStateFactory.createRepositoryState(getBranchesRevisions(root));
+  }
+
+  @NotNull
+  private Map<String, String> getBranchesRevisions(@NotNull VcsRoot root) throws VcsException {
     Settings settings = createSettings(root);
     syncRepository(settings);
     File workingDir = getWorkingDir(settings);
@@ -549,12 +555,15 @@
     VcsRoot branchRoot = createBranchRoot(root, branchName);
     String baseVersion = getCurrentVersion(root);
     String branchVersion = getCurrentVersion(branchRoot);
-    String branchPoint = getBranchPoint(settings, baseVersion, branchVersion);
+    String mergeBase = getMergeBase(settings, baseVersion, branchVersion);
 
-    LogCommand lc = new LogCommand(settings, getWorkingDir(settings), myLogTemplate);
-    lc.setFromRevId(new ChangeSetRevision(branchPoint).getId());
+    if (mergeBase == null)
+      return null;
+
+    LogCommand lc = myCommandFactory.createLog(settings, getWorkingDir(settings));
+    lc.setFromRevId(new ChangeSetRevision(mergeBase).getId());
     lc.setToRevId(new ChangeSetRevision(branchVersion).getId());
-    lc.setBranchName(null);//do not limit output to particular branch, return all commits
+    lc.showCommitsFromAllBranches();
     List<ChangeSet> changeSets = lc.execute();
     if (changeSets.size() > 1) {//when branch points to the commit in original branch we get 1 cset
       String branchId = changeSets.get(1).getId();
@@ -576,76 +585,93 @@
   public List<ModificationData> collectChanges(@NotNull VcsRoot fromRoot, @NotNull String fromRootRevision,
                                                @NotNull VcsRoot toRoot, @Nullable String toRootRevision,
                                                @NotNull CheckoutRules checkoutRules) throws VcsException {
-    //we get all branches while clone, if vcs roots are related it is doesn't matter in which one search for branch point
-    Settings settings = createSettings(fromRoot);
+    Settings settings = createSettings(toRoot);
     syncRepository(settings);
-    String branchPoint = getBranchPoint(settings, fromRootRevision, toRootRevision);
-    return ((CollectChangesByCheckoutRules) getCollectChangesPolicy()).collectChanges(toRoot, branchPoint, toRootRevision, checkoutRules);
+    String toRevision = toRootRevision != null ? toRootRevision : getCurrentVersion(toRoot);
+    String mergeBase = getMergeBase(settings, fromRootRevision, toRevision);
+    if (mergeBase == null)
+      return Collections.emptyList();
+    return collectChanges(toRoot, mergeBase, toRootRevision, checkoutRules);
   }
 
 
-  private String getBranchPoint(@NotNull Settings settings, String branchOneRev, String branchTwoRev) throws VcsException {
-    if (branchOneRev.equals(branchTwoRev))
-      return branchOneRev;
-    File workingDir = getWorkingDir(settings);
-    LogCommand lc = new LogCommand(settings, workingDir, myLogTemplate);
-    lc.setFromRevId(new ChangeSetRevision(branchOneRev).getId());
-    lc.setToRevId(new ChangeSetRevision(branchTwoRev).getId());
-    lc.setLimit(1);
-    List<ChangeSet> changeSets = lc.execute();
-    ChangeSet cs = changeSets.get(0);
-    if (cs.isInitial()) {
-      return cs.getId();
-    } else {
-      return cs.getParents().get(0).getId();
-    }
+  @Nullable
+  private String getMergeBase(@NotNull Settings settings, @NotNull String revision1, @NotNull String revision2) throws VcsException {
+    String result = myCommandFactory.createMergeBase(settings, getWorkingDir(settings)).execute(revision1, revision2);
+    if (result == null)
+      result = getMinusNthCommit(settings, 10);
+    return result;
   }
 
+
+  @Nullable
+  private String getMinusNthCommit(@NotNull Settings settings, int n) throws VcsException {
+    LogCommand log = myCommandFactory.createLog(settings, getWorkingDir(settings));
+    log.setToRevId(settings.getBranchName());
+    if (n > 0)
+      log.setLimit(n);
+    List<ChangeSet> changeSets = log.execute();
+    if (changeSets.isEmpty())
+      return null;
+    return changeSets.get(0).getId();
+  }
+
+
   @NotNull
   public CollectChangesPolicy getCollectChangesPolicy() {
-    return new CollectChangesByCheckoutRules() {
-      @NotNull
-      public List<ModificationData> collectChanges(@NotNull VcsRoot root, @NotNull String fromVersion, @Nullable String currentVersion, @NotNull CheckoutRules checkoutRules) throws VcsException {
-        Settings settings = createSettings(root);
-        syncRepository(settings);
-
-        // first obtain changes between specified versions
-        List<ModificationData> result = new ArrayList<ModificationData>();
-        if (currentVersion == null)
-          return result;
-
-        File workingDir = getWorkingDir(settings);
-        LogCommand lc = new LogCommand(settings, workingDir, myLogTemplate);
-        String fromId = new ChangeSetRevision(fromVersion).getId();
-        lc.setFromRevId(fromId);
-        lc.setToRevId(new ChangeSetRevision(currentVersion).getId());
-        List<ChangeSet> changeSets = lc.execute();
-        if (changeSets.isEmpty()) {
-          return result;
-        }
+    return this;
+  }
 
-        ChangeSet prev = new ChangeSet(fromVersion);
-        for (ChangeSet cur : changeSets) {
-          if (cur.getId().equals(fromId))
-            continue; // skip already reported changeset
+  public List<ModificationData> collectChanges(@NotNull VcsRoot root, @NotNull String fromVersion, @Nullable String currentVersion, @NotNull CheckoutRules checkoutRules) throws VcsException {
+    Settings settings = createSettings(root);
+    syncRepository(settings);
+    List<ModificationData> result = new ArrayList<ModificationData>();
+    for (ChangeSet cset : getChangesets(settings, fromVersion, currentVersion)) {
+      result.add(createModificationData(cset, root, checkoutRules));
+    }
+    return result;
+  }
 
-          boolean merge = cur.getParents().size() > 1;
-          List<ModifiedFile> modifiedFiles = cur.getModifiedFiles();
-          List<VcsChange> files = toVcsChanges(modifiedFiles, prev.getFullVersion(), cur.getFullVersion(), checkoutRules);
-          if (files.isEmpty() && !merge)
-            continue;
-          ModificationData md = new ModificationData(cur.getTimestamp(), files, cur.getDescription(), cur.getUser(), root, cur.getFullVersion(), cur.getId());
-          if (merge)
-            md.setCanBeIgnored(false);
-          result.add(md);
-          prev = cur;
-        }
 
-        return result;
+  private ModificationData createModificationData(@NotNull final ChangeSet cset, @NotNull final VcsRoot root, @NotNull final CheckoutRules checkoutRules) {
+    List<ChangeSetRevision> parents = cset.getParents();
+    if (parents.isEmpty())
+      throw new IllegalStateException("Commit " + cset.getId() + " has no parents");
+    List<VcsChange> files = toVcsChanges(cset.getModifiedFiles(), parents.get(0).getFullVersion(), cset.getFullVersion(), checkoutRules);
+    final ModificationData result = new ModificationData(cset.getTimestamp(), files, cset.getDescription(), cset.getUser(), root, cset.getFullVersion(), cset.getId());
+    for (ChangeSetRevision parent : parents) {
+      result.addParentRevision(parent.getFullVersion());
+    }
+    if (result.getParentRevisions().size() > 1)
+      result.setCanBeIgnored(false);
+    return result;
+  }
+
+
+  @NotNull
+  private List<ChangeSet> getChangesets(@NotNull final Settings settings, @NotNull final String fromVersion, @Nullable final String toVersion) throws VcsException {
+    if (toVersion == null)
+      return Collections.emptyList();
+    String fromCommit = new ChangeSetRevision(fromVersion).getId();
+    String toCommit = new ChangeSetRevision(toVersion).getId();
+    File workingDir = getWorkingDir(settings);
+    CollectChangesCommand log = myCommandFactory.getCollectChangesCommand(settings, workingDir);
+    try {
+      List<ChangeSet> changesets = log.execute(fromCommit, toCommit);
+      Iterator<ChangeSet> iter = changesets.iterator();
+      while (iter.hasNext()) {
+        ChangeSet cset = iter.next();
+        if (cset.getId().equals(fromCommit))
+          iter.remove();//skip already reported changes
       }
-    };
+      return changesets;
+    } catch (UnknownRevisionException e) {
+      Loggers.VCS.warn("Revision '" + e.getRevision() + "' is unknown, will return no changes");
+      return Collections.emptyList();
+    }
   }
 
+
   @NotNull
   public BuildPatchPolicy getBuildPatchPolicy() {
     return new BuildPatchByCheckoutRules() {
@@ -666,11 +692,11 @@
   }
 
   private void lockWorkDir(@NotNull File workDir) {
-    getWorkDirLock(workDir).lock();
+    myMirrorManager.lockDir(workDir);
   }
 
   private void unlockWorkDir(@NotNull File workDir) {
-    getWorkDirLock(workDir).unlock();
+    myMirrorManager.unlockDir(workDir);
   }
 
   @Override
@@ -680,50 +706,11 @@
     return false;
   }
 
-  private Lock getWorkDirLock(final File workDir) {
-    String path = workDir.getAbsolutePath();
-    Lock lock = myWorkDirLocks.get(path);
-    if (lock == null) {
-      lock = new ReentrantLock();
-      Lock curLock = myWorkDirLocks.putIfAbsent(path, lock);
-      if (curLock != null) {
-        lock = curLock;
-      }
-    }
-    return lock;
-  }
-
-  private void removeOldWorkFolders() {
-    Set<File> workDirs = new HashSet<File>(myMirrorManager.getMirrors());
-
-    for (VcsRoot vcsRoot: getMercurialVcsRoots()) {
-      try {
-        Settings s = createSettings(vcsRoot);
-        File workingDir = getWorkingDir(s);
-        workDirs.remove(PathUtil.getCanonicalFile(workingDir));
-      } catch (VcsException e) {
-        Loggers.VCS.error(e);
-      }
-    }
-
-    deleteWithLocking(workDirs);
-  }
-
-  private Collection<VcsRoot> getMercurialVcsRoots() {
-    List<VcsRoot> res = new ArrayList<VcsRoot>(myVcsManager.getAllRegisteredVcsRoots());
-    FilterUtil.filterCollection(res, new Filter<VcsRoot>() {
-      public boolean accept(@NotNull final VcsRoot data) {
-        return getName().equals(data.getVcsName());
-      }
-    });
-    return res;
-  }
-
   public String label(@NotNull String label, @NotNull String version, @NotNull VcsRoot root, @NotNull CheckoutRules checkoutRules) throws VcsException {
     File tmpDir = null;
     try {
       tmpDir = createLabelingTmpDir();
-      final Settings settings = createSettings(root);
+      Settings settings = createSettings(root);
       settings.setCustomWorkingDir(tmpDir);
       syncRepository(settings);
       File workingDir = getWorkingDir(settings);
@@ -733,6 +720,9 @@
       TagCommand tc = new TagCommand(settings, workingDir);
       tc.setRevId(new ChangeSet(version).getId());
       tc.setTag(fixedTagname);
+      String user = settings.getUserForTag();
+      if (user != null)
+        tc.setUser(user);
       tc.execute();
 
       PushCommand pc = new PushCommand(settings, workingDir);
@@ -756,18 +746,18 @@
 
   private File getWorkingDir(Settings s) {
     File customDir = s.getCustomWorkingDir();
-    return customDir != null ? customDir : myMirrorManager.getMirrorDir(s.getRepositoryUrl());
+    return customDir != null ? customDir : myMirrorManager.getMirrorDir(s.getRepository());
   }
 
   private Settings createSettings(final VcsRoot root) throws VcsException {
-    Settings settings = new Settings(root);
+    Settings settings = new Settings(myHgPathProvider, root);
     String customClonePath = settings.getCustomClonePath();
     if (!StringUtil.isEmptyOrSpaces(customClonePath) && !myDefaultWorkFolderParent.equals(new File(customClonePath).getAbsoluteFile())) {
       File parentDir = new File(customClonePath);
       createClonedRepositoryParentDir(parentDir);
 
       // take last part of repository path
-      String repPath = settings.getRepositoryUrl();
+      String repPath = settings.getRepositoryUrlWithCredentials();
       String[] splitted = repPath.split("[/\\\\]");
       if (splitted.length > 0) {
         repPath = splitted[splitted.length-1];
@@ -805,6 +795,11 @@
   }
 
 
+  @Override
+  public boolean isDAGBasedVcs() {
+    return true;
+  }
+
   private static class IgnoreDotHgFilter implements FileFilter {
     public boolean accept(final File file) {
       return !(file.isDirectory() && ".hg".equals(file.getName()));
@@ -816,4 +811,14 @@
       return true;
     }
   }
+
+  @NotNull
+  public String getBranchName(@NotNull final VcsRoot root) {
+    try {
+      Settings s = createSettings(root);
+      return s.getBranchName();
+    } catch (VcsException e) {
+      return "default";
+    }
+  }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MergeBaseNoRevsets.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,64 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import com.intellij.openapi.util.Pair;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.*;
+import jetbrains.buildServer.util.graph.DAG;
+import jetbrains.buildServer.util.graph.DAGs;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.*;
+
+/**
+ * Implementation of merge-base for hg versions which don't have revsets
+ * @author dmitry.neverov
+ */
+public final class MergeBaseNoRevsets implements MergeBaseCommand {
+
+  private final Settings mySettings;
+  private final File myWorkingDir;
+  private final CommandFactoryImpl myCommandFactory;
+
+  public MergeBaseNoRevsets(@NotNull final Settings settings, @NotNull final File workingDir, @NotNull final CommandFactoryImpl commandFactory) {
+    mySettings = settings;
+    myWorkingDir = workingDir;
+    myCommandFactory = commandFactory;
+  }
+
+
+  @Nullable
+  public String execute(@NotNull final String revision1, @NotNull final String revision2) {
+    if (revision1.equals(revision2))
+      return revision1;
+    try {
+      List<Pair<String, String>> edges = new ArrayList<Pair<String, String>>();
+      fillEdges(edges, getRevisionsReachableFrom(revision1));
+      fillEdges(edges, getRevisionsReachableFrom(revision2));
+      DAG<String> dag = DAGs.createFromEdges(edges);
+      List<String> commonAncestors = dag.getCommonAncestors(new ChangeSetRevision(revision1).getId(), new ChangeSetRevision(revision2).getId());
+      return commonAncestors.isEmpty() ? null : commonAncestors.get(0);
+    } catch (VcsException e) {
+      return null;
+    }
+  }
+
+
+  private List<ChangeSet> getRevisionsReachableFrom(@NotNull final String revision) throws VcsException {
+    LogCommand log = myCommandFactory.createLog(mySettings, myWorkingDir);
+    log.setFromRevId(new ChangeSetRevision(revision).getId());
+    log.showCommitsFromAllBranches();
+    log.setToRevId("0");
+    return log.execute();
+  }
+
+
+  private void fillEdges(List<Pair<String, String>> edges, List<ChangeSet> csets) {
+    for (ChangeSet cset : csets) {
+      for (ChangeSetRevision parent : cset.getParents()) {
+        edges.add(Pair.create(cset.getId(), parent.getId()));
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MergeBaseWithRevsets.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,38 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.*;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * Implementation of merge-base using hg revsets
+ * @author dmitry.neverov
+ */
+public final class MergeBaseWithRevsets implements MergeBaseCommand {
+
+  private final Settings mySettings;
+  private final File myWorkingDir;
+  private final CommandFactoryImpl myCommandFactory;
+
+  public MergeBaseWithRevsets(@NotNull final Settings settings, @NotNull final File workingDir, @NotNull final CommandFactoryImpl commandFactory) {
+    mySettings = settings;
+    myWorkingDir = workingDir;
+    myCommandFactory = commandFactory;
+  }
+
+  public String execute(@NotNull final String revision1, @NotNull final String revision2) throws VcsException {
+    try {
+      LogCommand log = myCommandFactory.createLog(mySettings, myWorkingDir);
+      log.setRevsets("ancestor(" + new ChangeSetRevision(revision1).getId() + ", " + new ChangeSetRevision(revision2).getId() + ")");
+      log.showCommitsFromAllBranches();
+      log.setCalculateParents(false);
+      List<ChangeSet> csets = log.execute();
+      return csets.isEmpty() ? null : csets.get(0).getId();
+    } catch (VcsException e) {
+      return null;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/ServerHgPathProvider.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,39 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.Settings;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author dmitry.neverov
+ */
+public class ServerHgPathProvider implements HgPathProvider {
+
+  private final ServerPluginConfig myConfig;
+
+
+  public ServerHgPathProvider(@NotNull final ServerPluginConfig config) {
+    myConfig = config;
+  }
+
+
+  public String getHgPath(@NotNull final Settings settings) {
+    String serverWideHgPath = myConfig.getHgPath();
+    if (serverWideHgPath != null) {
+      return serverWideHgPath;
+    } else {
+      String pathFromRoot = settings.getHgPath();
+      if (pathFromRoot.equals(unresolvedAgentHgPath())) {
+        //try to use hg from the PATH:
+        return "hg";
+      } else {
+        return pathFromRoot;
+      }
+    }
+  }
+
+
+  private String unresolvedAgentHgPath() {
+    //Use hard-coded value here in order to not add dependency on agent part of plugin:
+    return "%teamcity.hg.agent.path%";
+  }
+}
--- a/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/ServerPluginConfig.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/ServerPluginConfig.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,10 +1,22 @@
 package jetbrains.buildServer.buildTriggers.vcs.mercurial;
 
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+
 /**
  * @author dmitry.neverov
  */
 public interface ServerPluginConfig extends PluginConfig {
 
+  @Nullable
+  String getHgPath();
+
   public boolean isUsePullProtocol();
 
+  @NotNull
+  public File getPluginDataDir();
+
+  int getPullTimeout();
 }
--- a/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/ServerPluginConfigImpl.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/ServerPluginConfigImpl.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,6 +1,11 @@
 package jetbrains.buildServer.buildTriggers.vcs.mercurial;
 
+import jetbrains.buildServer.serverSide.ServerPaths;
 import jetbrains.buildServer.serverSide.TeamCityProperties;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
 
 /**
  * @author dmitry.neverov
@@ -8,12 +13,35 @@
 public class ServerPluginConfigImpl implements ServerPluginConfig {
 
   private static final String PULL_TIMEOUT_SECONDS = "teamcity.hg.pull.timeout.seconds";
-  private final int DEFAULT_PULL_TIMEOUT_SECONDS = 3600;
+  public static final int DEFAULT_PULL_TIMEOUT_SECONDS = 3600;
+
+  private final File myCachesDir;
+  private final File myPluginDataDir;
+
+  public ServerPluginConfigImpl(@NotNull final ServerPaths paths) {
+    myCachesDir = new File(paths.getCachesDir(), "mercurial");
+    myPluginDataDir = paths.getPluginDataDirectory();
+  }
 
   public boolean isUsePullProtocol() {
     return TeamCityProperties.getBooleanOrTrue("teamcity.hg.use.pull.protocol");
   }
 
+  @Nullable
+  public String getHgPath() {
+    return TeamCityProperties.getPropertyOrNull("teamcity.hg.server.path");
+  }
+
+  @NotNull
+  public File getCachesDir() {
+    return myCachesDir;
+  }
+
+  @NotNull
+  public File getPluginDataDir() {
+    return myPluginDataDir;
+  }
+
   public int getPullTimeout() {
     int timeout = TeamCityProperties.getInteger(PULL_TIMEOUT_SECONDS, DEFAULT_PULL_TIMEOUT_SECONDS);
     return timeout > 0 ? timeout : DEFAULT_PULL_TIMEOUT_SECONDS;
Binary file mercurial-tests/lib/hamcrest-integration-1.1.jar has changed
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/AgentSideCheckoutTest.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/AgentSideCheckoutTest.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,225 +1,285 @@
-/*
- * Copyright 2000-2011 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-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.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
- *         Date: 30.07.2008
- */
-@Test
-public class AgentSideCheckoutTest extends BaseMercurialTestCase {
-  private MercurialAgentSideVcsSupport myVcsSupport;
-  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();
-
-    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();
-
-  }
-
-  public void checkout_on_agent() throws IOException, VcsException {
-    testUpdate(createVcsRoot(simpleRepo()), "4:b06a290a363b", "cleanPatch1/after", new IncludeRule(".", ".", null));
-  }
-
-  public void checkout_on_agent_include_rule_with_mapping() throws IOException, VcsException {
-    testUpdate(createVcsRoot(simpleRepo()), "4:b06a290a363b", "cleanPatch1/after", new IncludeRule("+:.", "subdir", null));
-  }
-
-  private void testUpdate(final VcsRoot vcsRoot, String version, String expected, final IncludeRule includeRule) throws VcsException, IOException {
-    File workDir = doUpdate(vcsRoot, version, includeRule);
-
-    checkWorkingDir(expected, workDir);
-  }
-
-  private void checkWorkingDir(final String expected, final File workDir) throws IOException {
-    FileUtil.delete(new File(workDir, ".hg"));
-    checkDirectoriesAreEqual(new File(getTestDataPath(), expected), workDir);
-  }
-
-  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());
-    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());
-    return actualWorkDir;
-  }
-
-  public void checkout_on_agent_from_branch() throws IOException, VcsException {
-    testUpdate(createVcsRoot(simpleRepo(), "test_branch"), "7:376dcf05cd2a", "patch3/after", new IncludeRule(".", ".", null));
-  }
-
-  public void update_on_agent() throws IOException, VcsException {
-    VcsRoot vcsRoot = createVcsRoot(simpleRepo());
-    doUpdate(vcsRoot, "3:9522278aa38d", new IncludeRule(".", ".", null));
-    File workDir = doUpdate(vcsRoot, "4:b06a290a363b", new IncludeRule(".", ".", null));
-
-    checkWorkingDir("patch1/after", workDir);
-  }
-
-  public void update_on_agent_with_include_rule() throws IOException, VcsException {
-    VcsRoot vcsRoot = createVcsRoot(simpleRepo());
-    doUpdate(vcsRoot, "3:9522278aa38d", new IncludeRule(".", "subdir", null));
-    File workDir = doUpdate(vcsRoot, "4:b06a290a363b", new IncludeRule(".", "subdir", null));
-
-    checkWorkingDir("patch1/after", workDir);
-  }
-
-  public void update_on_agent_from_branch() throws IOException, VcsException {
-    VcsRoot vcsRoot = createVcsRoot(simpleRepo(), "test_branch");
-    doUpdate(vcsRoot, "7:376dcf05cd2a", new IncludeRule(".", ".", null));
-    File workDir = doUpdate(vcsRoot, "8:04c3ae4c6312", new IncludeRule(".", ".", null));
-
-    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);
-    //though some dirs are created - they are empty => there were no clones into local mirrors
-    for (File mirror : mirrors) {
-      assertTrue(FileUtil.getSubDirectories(mirror).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
-  }
-
-  /**
-   * TW-15984
-   */
-  public void should_be_able_to_clone_into_non_empty_dir() throws IOException, VcsException {
-    VcsRoot vcsRoot = createVcsRoot(simpleRepo());
-    doUpdate(vcsRoot, "3:9522278aa38d", new IncludeRule(".", "subdir", null));
-    doUpdate(vcsRoot, "4:b06a290a363b", new IncludeRule(".", ".", null));
-  }
-
-  public void cloned_repo_should_contains_default_parameter_in_hgrc() throws VcsException, IOException {
-    VcsRoot root = createVcsRoot(simpleRepo());
-    File workingDir = doUpdate(root, "4:b06a290a363b", new IncludeRule(".", ".", null));
-    File hgrc = new File(workingDir, ".hg" + File.separator + "hgrc");
-    String hgrcContent = FileUtil.readText(hgrc);
-    assertTrue(hgrcContent.contains("default = " + root.getProperty(Constants.REPOSITORY_PROP)));
-  }
-
-  protected String getTestDataPath() {
-    return "mercurial-tests/testData";
-  }
-}
+/*
+ * Copyright 2000-2011 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+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 jetbrains.buildServer.vcs.impl.VcsRootImpl;
+import org.jetbrains.annotations.NotNull;
+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;
+import java.util.concurrent.*;
+
+/**
+ * @author Pavel.Sher
+ *         Date: 30.07.2008
+ */
+@Test
+public class AgentSideCheckoutTest extends BaseMercurialTestCase {
+
+  final static String HG_PATH_REFERENCE = "%" + HgDetector.AGENT_HG_PATH_PROPERTY + "%";
+  private MercurialAgentSideVcsSupport myVcsSupport;
+  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();
+
+    myContext = new Mockery();
+
+    myMirrorsRootDir = myTempFiles.createTempDir();
+
+    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()));
+    }});
+
+    final AgentPluginConfigImpl pluginConfig = new AgentPluginConfigImpl(agentConfig);
+    myVcsSupport = new MercurialAgentSideVcsSupport(pluginConfig, new AgentHgPathProvider(agentConfig), new MirrorManagerImpl(pluginConfig));
+
+    myLogger = myContext.mock(BuildProgressLogger.class);
+    myContext.checking(new Expectations() {{
+      allowing(myLogger).message(with(any(String.class)));
+    }});
+
+    myWorkDir = myTempFiles.createTempDir();
+
+  }
+
+  public void should_work_when_path_to_hg_is_property() throws Exception {
+    VcsRootImpl root = new VcsRootBuilder()
+            .withUrl(LocalRepositoryUtil.prepareRepository(simpleRepo()).getAbsolutePath())
+            .withHgPath(HG_PATH_REFERENCE).build();
+    testUpdate(root, "4:b06a290a363b", "cleanPatch1/after", new IncludeRule(".", ".", null));
+  }
+
+
+  public void checkout_on_agent() throws IOException, VcsException {
+    testUpdate(createVcsRoot(simpleRepo()), "4:b06a290a363b", "cleanPatch1/after", new IncludeRule(".", ".", null));
+  }
+
+  public void checkout_on_agent_include_rule_with_mapping() throws IOException, VcsException {
+    testUpdate(createVcsRoot(simpleRepo()), "4:b06a290a363b", "cleanPatch1/after", new IncludeRule("+:.", "subdir", null));
+  }
+
+  private void testUpdate(final VcsRoot vcsRoot, String version, String expected, final IncludeRule includeRule) throws VcsException, IOException {
+    File workDir = doUpdate(vcsRoot, version, includeRule);
+
+    checkWorkingDir(expected, workDir);
+  }
+
+  private void checkWorkingDir(final String expected, final File workDir) throws IOException {
+    FileUtil.delete(new File(workDir, ".hg"));
+    checkDirectoriesAreEqual(new File(getTestDataPath(), expected), workDir);
+  }
+
+  private File doUpdate(@NotNull final VcsRoot root, @NotNull final String version, int timeoutSeconds) throws Exception {
+    ExecutorService executor = Executors.newSingleThreadExecutor();
+    Future<File> future = executor.submit(new Callable<File>() {
+      public File call() throws Exception {
+        return doUpdate(root, version);
+      }
+    });
+    executor.shutdown();
+    executor.awaitTermination(timeoutSeconds, TimeUnit.SECONDS);
+    if (!future.isDone())
+      fail("Update failed due to timeout");
+    return future.get();
+  }
+
+  private File doUpdate(@NotNull VcsRoot root, @NotNull String version) throws VcsException {
+    return doUpdate(root, version, IncludeRule.createDefaultInstance());
+  }
+
+  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());
+    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());
+    return actualWorkDir;
+  }
+
+  public void checkout_on_agent_from_branch() throws IOException, VcsException {
+    testUpdate(createVcsRoot(simpleRepo(), "test_branch"), "7:376dcf05cd2a", "patch3/after", new IncludeRule(".", ".", null));
+  }
+
+  public void update_on_agent() throws IOException, VcsException {
+    VcsRoot vcsRoot = createVcsRoot(simpleRepo());
+    doUpdate(vcsRoot, "3:9522278aa38d", new IncludeRule(".", ".", null));
+    File workDir = doUpdate(vcsRoot, "4:b06a290a363b", new IncludeRule(".", ".", null));
+
+    checkWorkingDir("patch1/after", workDir);
+  }
+
+  public void update_on_agent_with_include_rule() throws IOException, VcsException {
+    VcsRoot vcsRoot = createVcsRoot(simpleRepo());
+    doUpdate(vcsRoot, "3:9522278aa38d", new IncludeRule(".", "subdir", null));
+    File workDir = doUpdate(vcsRoot, "4:b06a290a363b", new IncludeRule(".", "subdir", null));
+
+    checkWorkingDir("patch1/after", workDir);
+  }
+
+  public void update_on_agent_from_branch() throws IOException, VcsException {
+    VcsRoot vcsRoot = createVcsRoot(simpleRepo(), "test_branch");
+    doUpdate(vcsRoot, "7:376dcf05cd2a", new IncludeRule(".", ".", null));
+    File workDir = doUpdate(vcsRoot, "8:04c3ae4c6312", new IncludeRule(".", ".", null));
+
+    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);
+    //though some dirs are created - they are empty => there were no clones into local mirrors
+    for (File mirror : mirrors) {
+      assertTrue(FileUtil.getSubDirectories(mirror).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
+  }
+
+  /**
+   * TW-15984
+   */
+  public void should_be_able_to_clone_into_non_empty_dir() throws IOException, VcsException {
+    VcsRoot vcsRoot = createVcsRoot(simpleRepo());
+    doUpdate(vcsRoot, "3:9522278aa38d", new IncludeRule(".", "subdir", null));
+    doUpdate(vcsRoot, "4:b06a290a363b", new IncludeRule(".", ".", null));
+  }
+
+  public void cloned_repo_should_contains_default_parameter_in_hgrc() throws VcsException, IOException {
+    VcsRoot root = createVcsRoot(simpleRepo());
+    File workingDir = doUpdate(root, "4:b06a290a363b", new IncludeRule(".", ".", null));
+    File hgrc = new File(workingDir, ".hg" + File.separator + "hgrc");
+    String hgrcContent = FileUtil.readText(hgrc);
+    assertTrue(hgrcContent.contains("default = " + root.getProperty(Constants.REPOSITORY_PROP)));
+  }
+
+  //TW-19703
+  public void should_work_when_repository_is_locked() throws Exception{
+    VcsRootImpl root = new VcsRootBuilder()
+            .withUrl(LocalRepositoryUtil.prepareRepository(simpleRepo()).getAbsolutePath())
+            .withHgPath(HG_PATH_REFERENCE).build();
+    File workingDir = doUpdate(root, "4:b06a290a363b");
+
+    lockRepository(workingDir);
+    doUpdate(root, "6:b9deb9a1c6f4", 10);
+
+    lockWorkingDir(workingDir);
+    doUpdate(root, "10:9c6a6b4aede0", 10);
+  }
+
+  private void lockRepository(@NotNull File workingDir) {
+    File lock = new File(workingDir, ".hg" + File.separator + "store" + File.separator + "lock");
+    FileUtil.writeFile(lock, "");
+  }
+
+  private void lockWorkingDir(@NotNull File workingDir) {
+    File lock = new File(workingDir, ".hg" + File.separator + "wlock");
+    FileUtil.writeFile(lock, "");
+  }
+
+  protected String getTestDataPath() {
+    return "mercurial-tests/testData";
+  }
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/AgentSideCheckoutWithSubreposTest.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,106 @@
+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()));
+    }});
+
+    final AgentPluginConfigImpl pluginConfig = new AgentPluginConfigImpl(agentConfig);
+    myVcsSupport = new MercurialAgentSideVcsSupport(pluginConfig, new AgentHgPathProvider(agentConfig), new MirrorManagerImpl(pluginConfig));
+
+    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()
+            .withUrl(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;
+  }
+}
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/BaseMercurialTestCase.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/BaseMercurialTestCase.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,74 +1,64 @@
-/*
- * Copyright 2000-2011 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package jetbrains.buildServer.buildTriggers.vcs.mercurial;
-
-import jetbrains.buildServer.MockSupport;
-import jetbrains.buildServer.TempFiles;
-import jetbrains.buildServer.vcs.impl.VcsRootImpl;
-import jetbrains.buildServer.vcs.patches.PatchTestCase;
-import org.jetbrains.annotations.NotNull;
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeMethod;
-
-import java.io.File;
-import java.io.IOException;
-
-/**
- * @author Pavel.Sher
- *         Date: 31.07.2008
- */
-public abstract class BaseMercurialTestCase extends PatchTestCase {
-  protected TempFiles myTempFiles;
-  protected MockSupport myMockSupport;
-
-  @Override
-  @BeforeMethod
-  protected void setUp() throws Exception {
-    super.setUp();
-
-    myMockSupport = new MockSupport();
-    myMockSupport.setUpMocks();
-    myTempFiles = new TempFiles();
-  }
-
-  @AfterMethod
-  protected void tearDown() throws Exception {
-    myMockSupport.tearDownMocks();
-    myTempFiles.cleanup();
-  }
-
-  protected VcsRootImpl createVcsRoot(@NotNull String repPath) throws IOException {
-    VcsRootImpl vcsRoot = new VcsRootImpl(1, Constants.VCS_NAME);
-    vcsRoot.addProperty(Constants.HG_COMMAND_PATH_PROP, new File(Util.getHgPath()).getAbsolutePath());
-    File repository = LocalRepositoryUtil.prepareRepository(repPath);
-    vcsRoot.addProperty(Constants.REPOSITORY_PROP, repository.getAbsolutePath());
-    return vcsRoot;
-  }
-
-  protected VcsRootImpl createVcsRoot(@NotNull String repPath, @NotNull String branchName) throws IOException {
-    VcsRootImpl vcsRoot = createVcsRoot(repPath);
-    vcsRoot.addProperty(Constants.BRANCH_NAME_PROP, branchName);
-    return vcsRoot;
-  }
-
-  protected void cleanRepositoryAfterTest(@NotNull String repPath) {
-    LocalRepositoryUtil.forgetRepository(repPath);
-  }
-
-  protected String simpleRepo() {
-    return new File("mercurial-tests/testData/rep1").getAbsolutePath();
-  }
-}
+/*
+ * Copyright 2000-2011 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import jetbrains.buildServer.TempFiles;
+import jetbrains.buildServer.vcs.impl.VcsRootImpl;
+import jetbrains.buildServer.vcs.patches.PatchTestCase;
+import org.jetbrains.annotations.NotNull;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * @author Pavel.Sher
+ *         Date: 31.07.2008
+ */
+public abstract class BaseMercurialTestCase extends PatchTestCase {
+  protected TempFiles myTempFiles;
+
+  @Override
+  @BeforeMethod
+  protected void setUp() throws Exception {
+    super.setUp();
+    myTempFiles = new TempFiles();
+  }
+
+  @AfterMethod
+  protected void tearDown() throws Exception {
+    myTempFiles.cleanup();
+  }
+
+  protected VcsRootImpl createVcsRoot(@NotNull String repPath) throws IOException {
+    File repository = LocalRepositoryUtil.prepareRepository(repPath);
+    return new VcsRootBuilder().withUrl(repository.getAbsolutePath()).build();
+  }
+
+  protected VcsRootImpl createVcsRoot(@NotNull String repPath, @NotNull String branchName) throws IOException {
+    File repository = LocalRepositoryUtil.prepareRepository(repPath);
+    return new VcsRootBuilder().withUrl(repository.getAbsolutePath()).withBranch(branchName).build();
+  }
+
+  protected void cleanRepositoryAfterTest(@NotNull String repPath) {
+    LocalRepositoryUtil.forgetRepository(repPath);
+  }
+
+  protected String simpleRepo() {
+    return new File("mercurial-tests/testData/rep1").getAbsolutePath();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/CleanupTest.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,163 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import jetbrains.buildServer.TempFiles;
+import jetbrains.buildServer.util.FileUtil;
+import jetbrains.buildServer.vcs.SVcsRoot;
+import jetbrains.buildServer.vcs.VcsManager;
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+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.FileFilter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import static java.util.Arrays.asList;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.testng.AssertJUnit.assertEquals;
+
+/**
+ * @author dmitry.neverov
+ */
+@Test
+public class CleanupTest {
+
+  private Mockery myContext;
+  private TempFiles myTempFiles;
+  private File myCachesDir;
+  private Cleanup myCleanup;
+  private MirrorManager myMirrorManager;
+  private VcsManager myVcsManager;
+  private long myRootId = 0;
+
+  @BeforeMethod
+  public void setUp() throws Exception {
+    myContext = new Mockery();
+    myTempFiles = new TempFiles();
+    myCachesDir = myTempFiles.createTempDir();
+
+    ServerPluginConfig config = new ServerPluginConfigBuilder().cachesDir(myCachesDir).build();
+    myMirrorManager = new MirrorManagerImpl(config);
+
+    myVcsManager = myContext.mock(VcsManager.class);
+    myCleanup = new Cleanup(myVcsManager, myMirrorManager, config, new ServerHgPathProvider(config));
+  }
+
+  @AfterMethod
+  public void tearDown() {
+    myTempFiles.cleanup();
+  }
+
+
+  public void cleanup_should_remove_all_directories_that_are_not_mirrors() throws IOException {
+    final String url1 = "http://some.org/repository1";
+    final String url2 = "http://some.org/repository2";
+    final String url3 = "http://some.org/repository3";
+    createDirFor(url1);
+    createDirFor(url2);
+    createDirFor(url3);
+    createUnusedDir();
+    myContext.checking(new Expectations() {{
+      atLeast(1).of(myVcsManager).getAllRegisteredVcsRoots();
+      will(returnValue(asList(build(vcsRoot().withUrl(url1)), build(vcsRoot().withUrl(url2)))));
+    }});
+
+    myCleanup.run();
+
+    myContext.assertIsSatisfied();
+    assertEquals(2, directoriesInside(myCachesDir).size());
+    assertThat(myMirrorManager, knowsAboutAll(directoriesInside(myCachesDir)));
+    assertThat(mappingsFile(), containsOnly(directoriesInside(myCachesDir)));
+  }
+
+  private void createUnusedDir() {
+    new File(myCachesDir, "some unused dir").mkdirs();
+  }
+
+
+  private void createDirFor(@NotNull String url) {
+    myMirrorManager.getMirrorDir(url);
+  }
+
+  private VcsRootBuilder vcsRoot() {
+    return new VcsRootBuilder().withId(myRootId++);
+  }
+
+  private SVcsRoot build(@NotNull VcsRootBuilder builder) {
+    return builder.build(myContext);
+  }
+
+  private List<File> directoriesInside(@NotNull File dir) {
+    return asList(dir.listFiles(new FileFilter() {
+      public boolean accept(File f) {
+        return f.isDirectory();
+      }
+    }));
+  }
+
+  private List<File> mappingsFile() throws IOException {
+    File mappingFile = new File(myCachesDir, "map");
+    List<File> files = new ArrayList<File>();
+    for (String line : FileUtil.readFile(mappingFile)) {
+      String[] fields = line.split("=");
+      files.add(new File(mappingFile.getParentFile(), fields[1].trim()));
+    }
+    return files;
+  }
+
+
+  static MirrorManagerKnowsAboutDirectories knowsAboutAll(@NotNull List<File> dirs) {
+    return new MirrorManagerKnowsAboutDirectories(dirs);
+  }
+
+  static class MirrorManagerKnowsAboutDirectories extends TypeSafeMatcher<MirrorManager> {
+    private final List<File> myActualMirrorDirs;
+
+    MirrorManagerKnowsAboutDirectories(@NotNull List<File> actualMirrorDirs) {
+      myActualMirrorDirs = actualMirrorDirs;
+    }
+
+    @Override
+    public boolean matchesSafely(MirrorManager mirrorManager) {
+      List<File> knownMirrors = mirrorManager.getMirrors();
+      return knownMirrors.size() == myActualMirrorDirs.size() && knownMirrors.containsAll(myActualMirrorDirs);
+    }
+
+    public void describeTo(Description description) {
+      description.appendText("mirrorManager knows following directories: ");
+      for (File dir : myActualMirrorDirs) {
+        description.appendText(dir.getAbsolutePath()).appendText("\n");
+      }
+    }
+  }
+
+
+  private static <T> ContainsOnly<T> containsOnly(Collection<T> expected) {
+    return new ContainsOnly<T>(expected);
+  }
+
+  static class ContainsOnly<T> extends TypeSafeMatcher<Collection<T>> {
+    private final Collection<T> myExpected;
+
+    ContainsOnly(@NotNull Collection<T> expected) {
+      myExpected = expected;
+    }
+
+    @Override
+    public boolean matchesSafely(Collection<T> col) {
+      return col.size() == myExpected.size() && col.containsAll(myExpected);
+    }
+
+    public void describeTo(Description description) {
+      description.appendText(myExpected.toString());
+    }
+  }
+}
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/CommandResultTest.java	Wed Feb 15 12:07:12 2012 +0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,41 +0,0 @@
-package jetbrains.buildServer.buildTriggers.vcs.mercurial;
-
-import jetbrains.buildServer.ExecResult;
-import jetbrains.buildServer.StreamGobbler;
-import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.CommandResult;
-import org.jetbrains.annotations.NotNull;
-import org.testng.annotations.Test;
-
-import java.io.ByteArrayInputStream;
-import java.util.Collections;
-
-import static org.testng.AssertJUnit.assertFalse;
-
-/**
- * @author dmitry.neverov
- */
-@Test
-public class CommandResultTest {
-
-  public void output_should_not_contain_private_data() {
-    String password = "pass";
-    ExecResult result = createExecResult(password, password);
-    CommandResult commandResult = new CommandResult(result, Collections.singleton(password));
-    assertFalse(commandResult.getStdout().contains(password));
-    assertFalse(commandResult.getStderr().contains(password));
-  }
-
-  private ExecResult createExecResult(@NotNull final String output, @NotNull final String error) {
-    ExecResult result = new ExecResult();
-    result.setOutputGobbler(createStringGobbler(output));
-    result.setErrorGobbler(createStringGobbler(error));
-    return result;
-  }
-
-  private StreamGobbler createStringGobbler(@NotNull final String str) {
-    StreamGobbler gobbler = new StreamGobbler(new ByteArrayInputStream(str.getBytes()));
-    gobbler.start();
-    return gobbler;
-  }
-
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/DagFeaturesTest.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,92 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import com.intellij.openapi.diagnostic.Logger;
+import jetbrains.buildServer.TempFiles;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.ChangeSetRevision;
+import jetbrains.buildServer.log.Log4jFactory;
+import jetbrains.buildServer.vcs.CheckoutRules;
+import jetbrains.buildServer.vcs.ModificationData;
+import jetbrains.buildServer.vcs.impl.VcsRootImpl;
+import org.jetbrains.annotations.NotNull;
+import org.jmock.Mockery;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.util.List;
+
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertFalse;
+import static org.testng.AssertJUnit.assertTrue;
+
+/**
+ * @author dmitry.neverov
+ */
+@Test
+public class DagFeaturesTest {
+
+  static {
+    Logger.setFactory(new Log4jFactory());
+  }
+
+  private TempFiles myTempFiles = new TempFiles();
+  private MercurialVcsSupport myHg;
+  private String myRepository;
+
+  @BeforeMethod
+  public void setUp() throws Exception {
+    ServerPluginConfig config = new ServerPluginConfigBuilder()
+            .cachesDir(myTempFiles.createTempDir())
+            .pluginDataDir(myTempFiles.createTempDir())
+            .build();
+    myHg = Util.createMercurialServerSupport(new Mockery(), config);
+
+    File original = new File("mercurial-tests/testData/rep2");
+    File copy = new File(myTempFiles.createTempDir(), "rep2");
+    LocalRepositoryUtil.copyRepository(original, copy);
+    myRepository = copy.getAbsolutePath();
+  }
+
+  public void tearDown() {
+    myTempFiles.cleanup();
+  }
+
+
+  //TW-17882
+  public void should_detect_changes_from_named_branches() throws Exception {
+    VcsRootImpl root = new VcsRootBuilder().withUrl(myRepository).build();
+
+    List<ModificationData> changes = myHg.collectChanges(root, "8:b6e2d176fe8e", "12:1e620196c4b6", CheckoutRules.DEFAULT);
+    assertEquals(4, changes.size());
+    for (ModificationData change : changes) {
+      assertFalse(change.getParentRevisions().isEmpty());
+      checkVersionsHaveNumbers(change.getParentRevisions());
+    }
+
+    changes = myHg.collectChanges(root, "12:1e620196c4b6", "18:df04faa7575a", CheckoutRules.DEFAULT);
+    assertEquals(6, changes.size());
+    for (ModificationData change : changes) {
+      assertFalse(change.getParentRevisions().isEmpty());
+      checkVersionsHaveNumbers(change.getParentRevisions());
+    }
+  }
+
+
+  //TW-17882
+  public void should_report_changes_only_from_merged_named_branches() throws Exception {
+    VcsRootImpl root = new VcsRootBuilder().withUrl(myRepository).build();
+    List<ModificationData> changes = myHg.collectChanges(root, "1e620196c4b6", "505c5b9d01e6", CheckoutRules.DEFAULT);
+    assertEquals(2, changes.size());
+    for (ModificationData change : changes) {
+      checkVersionsHaveNumbers(change.getParentRevisions());
+    }
+  }
+
+
+  private void checkVersionsHaveNumbers(@NotNull List<String> versions) {
+    for (String version : versions) {
+      ChangeSetRevision rev = new ChangeSetRevision(version);
+      assertTrue(rev.getRevNumber() != -1);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/HgPathResolver.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,64 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import jetbrains.buildServer.parameters.NullValueResolver;
+import jetbrains.buildServer.parameters.ProcessingResult;
+import jetbrains.buildServer.parameters.ReferencesResolverUtil;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+
+/**
+* @author dmitry.neverov
+*/
+class HgPathResolver extends NullValueResolver {
+  @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;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/HgVersionTest.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,24 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import junit.framework.TestCase;
+import org.jetbrains.annotations.NotNull;
+import org.testng.annotations.Test;
+
+/**
+ * @author dmitry.neverov
+ */
+@Test
+public class HgVersionTest extends TestCase {
+
+  public void test_parse() {
+    checkParseCorrectly("Mercurial Distributed SCM (version 1.7.1+11-cc4e13c92dfa)", "1.7.1");
+    checkParseCorrectly("Mercurial Distributed SCM (version 1.7)", "1.7.0");
+    checkParseCorrectly("Mercurial Distributed SCM (version 1.5.2)", "1.5.2");
+  }
+
+
+  private void checkParseCorrectly(@NotNull String versionToParse, @NotNull String expected) {
+    assertEquals(expected, HgVersion.parse(versionToParse).toString());
+  }
+
+}
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/LocalRepositoryUtil.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/LocalRepositoryUtil.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,57 +1,61 @@
-/*
- * Copyright 2000-2011 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package jetbrains.buildServer.buildTriggers.vcs.mercurial;
-
-import jetbrains.buildServer.TempFiles;
-import jetbrains.buildServer.util.FileUtil;
-import org.jetbrains.annotations.NotNull;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * @author Pavel.Sher
- *         Date: 14.07.2008
- */
-public class LocalRepositoryUtil {
-  private final static TempFiles myTempFiles = new TempFiles();
-  private final static Map<String, File> myRepositories = new HashMap<String, File>();
-  static {
-    Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
-      public void run() {
-        myTempFiles.cleanup();
-      }
-    }));
-  }
-
-  public static File prepareRepository(@NotNull String repPath) throws IOException {
-    File repository = myRepositories.get(repPath);
-    if (repository != null) return repository;
-    final File tempDir = myTempFiles.createTempDir();
-    FileUtil.copyDir(new File(repPath), tempDir);
-    if (new File(tempDir, "hg").isDirectory()) {
-      FileUtil.rename(new File(tempDir, "hg"), new File(tempDir, ".hg"));
-    }
-    myRepositories.put(repPath, tempDir);
-    return tempDir;
-  }
-
-  public static void forgetRepository(@NotNull String repPath) {
-    myRepositories.remove(repPath);
-  }
-}
+/*
+ * Copyright 2000-2011 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import jetbrains.buildServer.TempFiles;
+import jetbrains.buildServer.util.FileUtil;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Pavel.Sher
+ *         Date: 14.07.2008
+ */
+public class LocalRepositoryUtil {
+  private final static TempFiles myTempFiles = new TempFiles();
+  private final static Map<String, File> myRepositories = new HashMap<String, File>();
+  static {
+    Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
+      public void run() {
+        myTempFiles.cleanup();
+      }
+    }));
+  }
+
+  public static File prepareRepository(@NotNull String repPath) throws IOException {
+    File repository = myRepositories.get(repPath);
+    if (repository != null) return repository;
+    final File tempDir = myTempFiles.createTempDir();
+    copyRepository(new File(repPath), tempDir);
+    myRepositories.put(repPath, tempDir);
+    return tempDir;
+  }
+
+  public static void forgetRepository(@NotNull String repPath) {
+    myRepositories.remove(repPath);
+  }
+
+
+  public static void copyRepository(File src, File dst) throws IOException {
+    FileUtil.copyDir(src, dst);
+    if (new File(dst, "hg").isDirectory())
+      FileUtil.rename(new File(dst, "hg"), new File(dst, ".hg"));
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialResetCacheHandlerTest.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,107 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import jetbrains.buildServer.TempFiles;
+import jetbrains.buildServer.util.cache.ResetCacheHandler;
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.jmock.States;
+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.HashMap;
+import java.util.Map;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyMap;
+import static org.testng.AssertJUnit.*;
+
+/**
+ * @author dmitry.neverov
+ */
+@Test
+public class MercurialResetCacheHandlerTest {
+
+  private Mockery myContext;
+  private TempFiles myTempFiles;
+  private File myCachesDir;
+  private MirrorManager myMirrorManager;
+  private ResetCacheHandler myCacheHandler;
+
+  @BeforeMethod
+  public void setUp() throws IOException {
+    myContext = new Mockery();
+    myTempFiles = new TempFiles();
+    myCachesDir = myTempFiles.createTempDir();
+    myMirrorManager = myContext.mock(MirrorManager.class);
+    myCacheHandler = new MercurialResetCacheHandler(myMirrorManager);
+  }
+
+  @AfterMethod
+  public void tearDown() {
+    myTempFiles.cleanup();
+  }
+
+
+  public void cache_list_should_contains_entry_for_mercurial_in_general() {
+    assertEquals(asList("mercurial"), myCacheHandler.listCaches());
+  }
+
+
+  public void mercurial_cache_is_empty_when_there_are_no_mirrors() {
+    myContext.checking(new Expectations() {{
+      atLeast(1).of(myMirrorManager).getMappings(); will(returnValue(emptyMap()));
+    }});
+
+    assertTrue(myCacheHandler.isEmpty("mercurial"));
+    myContext.assertIsSatisfied();
+  }
+
+
+  public void mercurial_cache_is_not_empty_when_mirrors_exist() {
+    final Map<String, File> mapping = new HashMap<String, File>() {{
+      put("http://some.org/repository1", new File(myCachesDir, "a"));
+      put("http://some.org/repository2", new File(myCachesDir, "b"));
+    }};
+    myContext.checking(new Expectations() {{
+      atLeast(1).of(myMirrorManager).getMappings(); will(returnValue(mapping));
+    }});
+
+    assertFalse(myCacheHandler.isEmpty("mercurial"));
+    myContext.assertIsSatisfied();
+  }
+
+
+  public void reset_cache_should_reset_caches_for_all_mirrors() {
+    final String url1 = "http://some.org/repository1";
+    final String url2 = "http://some.org/repository2";
+    final File mirror1 = new File(myCachesDir, "a");
+    final File mirror2 = new File(myCachesDir, "b");
+    mirror1.mkdirs();
+    mirror2.mkdirs();
+    final Map<String, File> mapping = new HashMap<String, File>() {{
+      put(url1, mirror1);
+      put(url2, mirror2);
+    }};
+    final States stateOfMirror1 = myContext.states("mirror1").startsAs("initial");
+    final States stateOfMirror2 = myContext.states("mirror2").startsAs("initial");
+    myContext.checking(new Expectations() {{
+      atLeast(1).of(myMirrorManager).getMappings(); will(returnValue(mapping));
+
+      one(myMirrorManager).lockDir(mirror1); when(stateOfMirror1.is("initial")); then(stateOfMirror1.is("locked"));
+      one(myMirrorManager).lockDir(mirror2); when(stateOfMirror2.is("initial")); then(stateOfMirror2.is("locked"));
+
+      one(myMirrorManager).forgetDir(mirror1); when(stateOfMirror1.is("locked")); then(stateOfMirror1.is("reset"));
+      one(myMirrorManager).forgetDir(mirror2); when(stateOfMirror2.is("locked")); then(stateOfMirror2.is("reset"));
+
+      one(myMirrorManager).unlockDir(mirror1); when(stateOfMirror1.is("reset"));
+      one(myMirrorManager).unlockDir(mirror2); when(stateOfMirror2.is("reset"));
+    }});
+
+    myCacheHandler.resetCache("mercurial");
+    myContext.assertIsSatisfied();
+    assertTrue(myCachesDir.listFiles().length == 0);
+  }
+}
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java	Wed Feb 15 13:23:53 2012 +0400
@@ -19,16 +19,12 @@
 import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.CommandResult;
 import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.CommandUtil;
 import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.Settings;
-import jetbrains.buildServer.serverSide.BuildServerListener;
-import jetbrains.buildServer.serverSide.SBuildServer;
-import jetbrains.buildServer.serverSide.ServerPaths;
-import jetbrains.buildServer.util.EventDispatcher;
 import jetbrains.buildServer.vcs.*;
 import jetbrains.buildServer.vcs.impl.VcsRootImpl;
 import jetbrains.buildServer.vcs.patches.PatchBuilderImpl;
 import junit.framework.Assert;
 import org.jetbrains.annotations.NotNull;
-import org.jmock.Mock;
+import org.jmock.Mockery;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
@@ -37,33 +33,27 @@
 import java.io.FilenameFilter;
 import java.io.IOException;
 import java.util.*;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
 
 import static com.intellij.openapi.util.io.FileUtil.copyDir;
 import static com.intellij.openapi.util.io.FileUtil.moveDirWithContent;
+import static jetbrains.buildServer.buildTriggers.vcs.mercurial.VcsRootBuilder.vcsRoot;
 
 @Test
 public class MercurialVcsSupportTest extends BaseMercurialTestCase {
+
   private MercurialVcsSupport myVcs;
-  private ServerPaths myServerPaths;
+  private String myRep2Path = new File("mercurial-tests/testData/rep2").getAbsolutePath();
+  private ServerPluginConfig myPluginConfig;
 
   @BeforeMethod
   protected void setUp() throws Exception {
     super.setUp();
-
-    Mock vcsManagerMock = new Mock(VcsManager.class);
-    vcsManagerMock.stubs().method("registerVcsSupport");
-    Mock serverMock = new Mock(SBuildServer.class);
-    ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
-    serverMock.stubs().method("getExecutor").will(myMockSupport.returnValue(executor));
-
-    EventDispatcher<BuildServerListener> dispatcher = EventDispatcher.create(BuildServerListener.class);
-
-    File systemDir = myTempFiles.createTempDir();
-    myServerPaths = new ServerPaths(systemDir.getAbsolutePath(), systemDir.getAbsolutePath(), systemDir.getAbsolutePath());
-    assertTrue(new File(myServerPaths.getCachesDir()).mkdirs());
-    myVcs = new MercurialVcsSupport((VcsManager)vcsManagerMock.proxy(), myServerPaths, (SBuildServer)serverMock.proxy(), dispatcher, createPluginConfig());
+    Mockery context = new Mockery();
+    myPluginConfig = new ServerPluginConfigBuilder()
+            .cachesDir(myTempFiles.createTempDir())
+            .pluginDataDir(myTempFiles.createTempDir())
+            .build();
+    myVcs = Util.createMercurialServerSupport(context, myPluginConfig);
   }
 
   protected String getTestDataPath() {
@@ -82,7 +72,7 @@
   }
 
   private List<ModificationData> collectChanges(@NotNull VcsRoot vcsRoot, @NotNull String from, @NotNull String to, @NotNull CheckoutRules rules) throws VcsException {
-    return ((CollectChangesByCheckoutRules) myVcs.getCollectChangesPolicy()).collectChanges(vcsRoot, from, to, rules);
+    return myVcs.collectChanges(vcsRoot, from, to, rules);
   }
 
   public void test_collect_changes_between_two_same_roots() throws Exception {
@@ -92,6 +82,13 @@
     do_check_for_collect_changes(changes);
   }
 
+  public void test_collect_changes_from_non_existing_revision() throws Exception {
+    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
+    VcsRootImpl sameVcsRoot = createVcsRoot(simpleRepo());
+    List<ModificationData> changes = myVcs.collectChanges(vcsRoot, "0:9875b412a789", sameVcsRoot, "3:9522278aa38d", new CheckoutRules(""));
+    assertFalse(changes.isEmpty());//should return some changes from the toRoot
+  }
+
   public void test_collect_changes() throws Exception {
     VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
     List<ModificationData> changes = collectChanges(vcsRoot, "0:9875b412a788", "3:9522278aa38d", new CheckoutRules(""));
@@ -143,7 +140,7 @@
     ByteArrayOutputStream output = buildPatch(vcsRoot, null, "4:b06a290a363b", new CheckoutRules(""));
     checkPatchResult(output.toByteArray());
 
-    File clonedReposParentDir = new File(myServerPaths.getCachesDir(), "mercurial");
+    File clonedReposParentDir = myPluginConfig.getCachesDir();
     assertTrue(clonedReposParentDir.isDirectory());
     assertTrue(1 == clonedReposParentDir.list(new FilenameFilter() {
       public boolean accept(final File dir, final String name) {
@@ -189,8 +186,8 @@
   }
 
   public void test_clean_patch_with_subrepositories() throws Exception {
-    File r1 = new File(myServerPaths.getCachesDir() + File.separator + "mercurial", "r1");
-    File r3 = new File(myServerPaths.getCachesDir() + File.separator + "mercurial", "r3");
+    File r1 = new File(myPluginConfig.getCachesDir(), "r1");
+    File r3 = new File(myPluginConfig.getCachesDir(), "r3");
     copyDir(new File("mercurial-tests/testData/subrepos/r1"), r1);
     copyDir(new File("mercurial-tests/testData/subrepos/r3"), r3);
     moveDirWithContent(new File(r1, "hg"), new File(r1, ".hg"));
@@ -263,6 +260,21 @@
   }
 
 
+  public void test_tag_with_specified_username() throws IOException, VcsException {
+    final String customUserForTag = "John Doe <john@some.org>";
+    File repository = LocalRepositoryUtil.prepareRepository(simpleRepo());
+    VcsRoot root = vcsRoot().withUrl(repository.getAbsolutePath()).withUserForTag(customUserForTag).build();
+    cleanRepositoryAfterTest(simpleRepo());
+
+    myVcs.label("tag_by_specified_user", "10:9c6a6b4aede0", root, CheckoutRules.DEFAULT);
+
+    String currentVersion = myVcs.getCurrentVersion(root);
+    List<ModificationData> changes = myVcs.collectChanges(root, "10:9c6a6b4aede0", currentVersion, CheckoutRules.DEFAULT);
+    assertEquals(changes.size(), 1);
+    assertEquals(changes.get(0).getUserName(), customUserForTag);
+  }
+
+
   public void labeling_should_not_populate_files_in_local_mirror() throws Exception {
     VcsRootImpl root = createVcsRoot(simpleRepo());
     cleanRepositoryAfterTest(simpleRepo());
@@ -344,15 +356,15 @@
     VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
     String repPath = vcsRoot.getProperty(Constants.REPOSITORY_PROP);
     vcsRoot.addProperty(Constants.REPOSITORY_PROP, repPath + "#test_branch");
-    Settings settings = new Settings(vcsRoot);
+    Settings settings = new Settings(new ServerHgPathProvider(myPluginConfig), vcsRoot);
     assertEquals("test_branch", settings.getBranchName());
 
     vcsRoot.addProperty(Constants.REPOSITORY_PROP, repPath + "#");
-    settings = new Settings(vcsRoot);
+    settings = new Settings(new ServerHgPathProvider(myPluginConfig), vcsRoot);
     assertEquals("default", settings.getBranchName());
 
     vcsRoot.addProperty(Constants.REPOSITORY_PROP, repPath);
-    settings = new Settings(vcsRoot);
+    settings = new Settings(new ServerHgPathProvider(myPluginConfig), vcsRoot);
     assertEquals("default", settings.getBranchName());
   }
 
@@ -383,6 +395,13 @@
     assertEquals("10:fc524efc2bc4", changes.get(1).getVersion());
   }
 
+  public void collectChanges_should_return_all_changes_from_branch() throws Exception {
+    VcsRootImpl defaultBranchRoot = createVcsRoot(myRep2Path, "default");
+    VcsRootImpl personalBranchRoot = createVcsRoot(myRep2Path, "personal-branch");
+    List<ModificationData> modifications = myVcs.collectChanges(defaultBranchRoot, "16:505c5b9d01e6", personalBranchRoot, "17:9ec402c74298", CheckoutRules.DEFAULT);
+    assertEquals(3, modifications.size());
+  }
+
   public void test_collect_changes_merge() throws Exception {
     VcsRootImpl vcsRoot = createVcsRoot(mergeCommittsRepo());
 
@@ -412,10 +431,10 @@
     VcsRootImpl vcsRoot = createVcsRoot(mergeCommittsRepo());
 
     List<ModificationData> changes = collectChanges(vcsRoot, "8:b6e2d176fe8e", "12:1e620196c4b6", CheckoutRules.DEFAULT);
-    assertEquals(changes.size(), 2);
+    assertEquals(changes.size(), 4);
 
-    assertFiles(Arrays.asList("A dir6/file6.txt"), changes.get(0));
-    assertFiles(Arrays.asList("M dir6/file6.txt", "A dir5/file5.txt"), changes.get(1));
+    assertFiles(Arrays.asList("A dir6/file6.txt"), changes.get(2));
+    assertFiles(Arrays.asList("M dir6/file6.txt", "A dir5/file5.txt"), changes.get(3));
   }
 
   //TW-17530
@@ -443,7 +462,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(root);
+    Settings settings = new Settings(new ServerHgPathProvider(myPluginConfig), root);
     assertFalse(settings.isUncompressedTransfer());
   }
 
@@ -510,16 +529,5 @@
   public void test_collect_changes_using_checkout_rules() {
     assertTrue(myVcs.getCollectChangesPolicy() instanceof CollectChangesByCheckoutRules);
   }
-
-  private ServerPluginConfig createPluginConfig() {
-    return new ServerPluginConfig() {
-      public boolean isUsePullProtocol() {
-        return true;
-      }
-      public int getPullTimeout() {
-        return CommandUtil.DEFAULT_COMMAND_TIMEOUT_SEC;
-      }
-    };
-  }
 }
 
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MirrorManagerTest.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MirrorManagerTest.java	Wed Feb 15 13:23:53 2012 +0400
@@ -3,6 +3,7 @@
 import jetbrains.buildServer.TempFiles;
 import jetbrains.buildServer.util.Hash;
 import junit.framework.TestCase;
+import org.jetbrains.annotations.NotNull;
 import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
@@ -19,14 +20,19 @@
 
   private TempFiles myTempFiles;
   private File myRootDir;
-  private MirrorManager myManager;
+  private MirrorManagerImpl myManager;
 
 
   @BeforeMethod
   public void setUp() throws Exception {
     myTempFiles = new TempFiles();
     myRootDir = myTempFiles.createTempDir();
-    myManager = new MirrorManager(myRootDir);
+    myManager = new MirrorManagerImpl(new PluginConfig() {
+      @NotNull
+      public File getCachesDir() {
+        return myRootDir;
+      }
+    });
   }
 
   @AfterMethod
@@ -67,7 +73,7 @@
     final String url1 = "hg://some.com/repository.hg";
     final String url2 = "hg://other.com/repository.hg";
 
-    MirrorManager.HashCalculator hashWithCollision = new MirrorManager.HashCalculator() {
+    MirrorManagerImpl.HashCalculator hashWithCollision = new MirrorManagerImpl.HashCalculator() {
       public long calc(String value) {
         if (value.equals(url1) || value.equals(url2)) {
           return 0;//emulate collision
@@ -78,11 +84,23 @@
     };
 
     //alone they get dir with the same name:
-    MirrorManager mm1 = new MirrorManager(myTempFiles.createTempDir());
+    final File dir1 = myTempFiles.createTempDir();
+    MirrorManagerImpl mm1 = new MirrorManagerImpl(new PluginConfig() {
+      @NotNull
+      public File getCachesDir() {
+        return dir1;
+      }
+    });
     mm1.setHashCalculator(hashWithCollision);
     File separateMirrorDir1 = mm1.getMirrorDir(url1);
 
-    MirrorManager mm2 = new MirrorManager(myTempFiles.createTempDir());
+    final File dir2 = myTempFiles.createTempDir();
+    MirrorManagerImpl mm2 = new MirrorManagerImpl(new PluginConfig() {
+      @NotNull
+      public File getCachesDir() {
+        return dir2;
+      }
+    });
     mm2.setHashCalculator(hashWithCollision);
     File separateMirrorDir2 = mm2.getMirrorDir(url2);
 
@@ -102,10 +120,26 @@
     File mirrorDir2 = myManager.getMirrorDir(url2);
 
     //emulate restart by creating a new manager for the same rootDir
-    MirrorManager manager = new MirrorManager(myRootDir);
+    MirrorManagerImpl manager = new MirrorManagerImpl(new PluginConfig() {
+      @NotNull
+      public File getCachesDir() {
+        return myRootDir;
+      }
+    });
 
     assertEquals(2, manager.getMirrors().size());
     assertTrue(manager.getMirrors().contains(mirrorDir1));
     assertTrue(manager.getMirrors().contains(mirrorDir2));
   }
+
+
+  public void should_be_able_to_forget_directory() throws Exception {
+    String url = "hg://some.org/repository";
+    File mirror1 = myManager.getMirrorDir(url);
+    File dotHg = new File(mirror1, ".hg");
+    dotHg.mkdirs();
+    myManager.forgetDir(mirror1);
+    File mirror2 = myManager.getMirrorDir(url);
+    assertFalse(mirror2.equals(mirror1));
+  }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/ServerHgPathProviderTest.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,54 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.Settings;
+import jetbrains.buildServer.vcs.impl.VcsRootImpl;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import static org.testng.AssertJUnit.assertEquals;
+
+/**
+ * @author dmitry.neverov
+ */
+@Test
+public class ServerHgPathProviderTest {
+
+  private String myServerWideHgPath;
+  private String myVcsRootHgPath;
+
+
+  @BeforeMethod
+  public void setUp() {
+    myServerWideHgPath = null;
+    myVcsRootHgPath = "/vcs/root/hg/path";
+  }
+
+
+  public void server_should_use_settings_from_vcs_root_if_server_wide_path_is_not_set() throws Exception {
+    myServerWideHgPath = null;
+    HgPathProvider provider = createHgPathProvider();
+    Settings settings = createSettings(provider);
+    assertEquals(myVcsRootHgPath, provider.getHgPath(settings));
+  }
+
+
+  public void server_should_use_server_wide_path_if_it_is_set() throws Exception {
+    myServerWideHgPath = "/server-wide/hg/path";
+    HgPathProvider provider = createHgPathProvider();
+    Settings settings = createSettings(provider);
+    assertEquals(myServerWideHgPath, provider.getHgPath(settings));
+  }
+
+
+  private ServerHgPathProvider createHgPathProvider() {
+    ServerPluginConfig config = new ServerPluginConfigBuilder().hgPath(myServerWideHgPath).build();
+    return new ServerHgPathProvider(config);
+  }
+
+
+  private Settings createSettings(HgPathProvider hgPathProvider) throws Exception {
+    VcsRootImpl root = new VcsRootBuilder().withHgPath(myVcsRootHgPath).build();
+    return new Settings(hgPathProvider, root);
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/ServerPluginConfigBuilder.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,70 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+
+/**
+ * @author dmitry.neverov
+ */
+public class ServerPluginConfigBuilder {
+
+  private boolean myUsePullProtocol = true;
+  private String myHgPath;
+  private File myCachesDir;
+  private File myPluginDataDir;
+
+
+  @NotNull
+  public ServerPluginConfig build() {
+    return new ServerPluginConfig() {
+
+      public boolean isUsePullProtocol() {
+        return myUsePullProtocol;
+      }
+
+      public String getHgPath() {
+        return myHgPath;
+      }
+
+      @NotNull
+      public File getCachesDir() {
+        if (myCachesDir == null)
+          throw new IllegalStateException("Caches dir is not set");
+        return myCachesDir;
+      }
+
+      @NotNull
+      public File getPluginDataDir() {
+        if (myPluginDataDir == null)
+          throw new IllegalStateException("Plugin data dir is not set");
+        return myPluginDataDir;
+      }
+
+      public int getPullTimeout() {
+        return ServerPluginConfigImpl.DEFAULT_PULL_TIMEOUT_SECONDS;
+      }
+    };
+  }
+
+
+  public ServerPluginConfigBuilder userPullProtocol(boolean doUse) {
+    myUsePullProtocol = doUse;
+    return this;
+  }
+
+  public ServerPluginConfigBuilder hgPath(String hgPath) {
+    myHgPath = hgPath;
+    return this;
+  }
+
+  public ServerPluginConfigBuilder cachesDir(File cachesDir) {
+    myCachesDir = cachesDir;
+    return this;
+  }
+
+  public ServerPluginConfigBuilder pluginDataDir(File pluginDataDir) {
+    myPluginDataDir = pluginDataDir;
+    return this;
+  }
+}
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/SettingsTest.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/SettingsTest.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,136 +1,125 @@
-/*
- * Copyright 2000-2011 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-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.TestCase;
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-/**
- * @author Pavel.Sher
- */
-@Test
-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(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(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(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(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(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(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(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(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(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(root);
-    assertTrue(settings.isUncompressedTransfer());
-  }
-
-  //TW-18262
-  public void ampersand_in_password() {
-    VcsRootImpl vcsRoot = createVcsRoot("http://some.org/path", "user", "m&n");
-    Settings settings = new Settings(vcsRoot);
-		assertEquals("http://user:m%26n@some.org/path", settings.getRepositoryUrl());
-  }
-
-  //TW-18835
-  public void test_ssh() {
-    VcsRootImpl vcsRoot = createVcsRoot("ssh://ourserver.com/mercurialrepo/", "user", "pwd");
-    Settings settings = new Settings(vcsRoot);
-    assertEquals("ssh://user:pwd@ourserver.com/mercurialrepo/", settings.getRepositoryUrl());
-  }
-
-  private VcsRootImpl createVcsRoot(String url) {
-    return createVcsRoot(url, "user", "pwd");
-  }
-
-  private VcsRootImpl createVcsRoot(String url, String userName, String password) {
-    VcsRootImpl vcsRoot = new VcsRootImpl(1, Constants.VCS_NAME);
-    vcsRoot.addProperty(Constants.HG_COMMAND_PATH_PROP, "hg.exe");
-    vcsRoot.addProperty(Constants.REPOSITORY_PROP, url);
-    vcsRoot.addProperty(Constants.USERNAME, userName);
-    vcsRoot.addProperty(Constants.PASSWORD, password);
-    return vcsRoot;
-  }
-}
+/*
+ * Copyright 2000-2011 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.Settings;
+import jetbrains.buildServer.vcs.VcsRoot;
+import jetbrains.buildServer.vcs.impl.VcsRootImpl;
+import junit.framework.TestCase;
+import org.jetbrains.annotations.NotNull;
+import org.testng.annotations.Test;
+
+/**
+ * @author Pavel.Sher
+ */
+@Test
+public class SettingsTest extends TestCase {
+
+  public void test_url_without_credentials() {
+    VcsRootImpl vcsRoot = createVcsRoot("http://host.com/path");
+    Settings settings = createSettings(vcsRoot);
+    assertEquals("http://user:pwd@host.com/path", settings.getRepositoryUrlWithCredentials());
+  }
+
+  public void test_url_with_credentials() {
+    VcsRootImpl vcsRoot = createVcsRoot("http://user:pwd@host.com/path");
+    Settings settings = createSettings(vcsRoot);
+    assertEquals("http://user:pwd@host.com/path", settings.getRepositoryUrlWithCredentials());
+  }
+
+  public void test_url_with_username() {
+    VcsRootImpl vcsRoot = createVcsRoot("http://user@host.com/path");
+    Settings settings = createSettings(vcsRoot);
+    assertEquals("http://user:pwd@host.com/path", settings.getRepositoryUrlWithCredentials());
+  }
+
+  public void test_url_with_at_after_slash() {
+    VcsRootImpl vcsRoot = createVcsRoot("http://host.com/path@");
+    Settings settings = createSettings(vcsRoot);
+    assertEquals("http://user:pwd@host.com/path@", settings.getRepositoryUrlWithCredentials());
+  }
+
+  public void test_url_with_at_in_username() {
+    VcsRootImpl vcsRoot = createVcsRoot("http://host.com/path", "my.name@gmail.com", "1234");
+    Settings settings = createSettings(vcsRoot);
+    assertEquals("http://my.name%40gmail.com:1234@host.com/path", settings.getRepositoryUrlWithCredentials());
+  }
+
+  /** TW-13768 */
+  public void test_underscore_in_host() {
+		VcsRootImpl vcsRoot = createVcsRoot("http://Klekovkin.SDK_GARANT:8000/", "my.name@gmail.com", "1234");
+    Settings settings = createSettings(vcsRoot);
+		assertEquals("http://my.name%40gmail.com:1234@Klekovkin.SDK_GARANT:8000/", settings.getRepositoryUrlWithCredentials());
+	}
+
+  /** TW-13768 */
+  public void test_underscore_in_host_with_credentials_in_url() {
+    VcsRootImpl vcsRoot = createVcsRoot("http://me:mypass@Klekovkin.SDK_GARANT:8000/");
+    Settings settings = createSettings(vcsRoot);
+		assertEquals("http://me:mypass@Klekovkin.SDK_GARANT:8000/", settings.getRepositoryUrlWithCredentials());
+  }
+
+  public void test_windows_path() throws Exception {
+    VcsRootImpl vcsRoot = createVcsRoot("c:\\windows\\path");
+    Settings settings = createSettings(vcsRoot);
+    assertEquals("c:\\windows\\path", settings.getRepositoryUrlWithCredentials());
+  }
+
+  public void test_file_scheme_has_no_credentials() {
+    VcsRootImpl vcsRoot = createVcsRoot("file:///path/to/repo", "my.name@gmail.com", "1234");
+    Settings settings = createSettings(vcsRoot);
+    assertEquals("file:///path/to/repo", settings.getRepositoryUrlWithCredentials());
+  }
+
+  public void uncompressed_transfer() {
+    VcsRootImpl root = createVcsRoot("http://host.com/path");
+    root.addProperty(Constants.UNCOMPRESSED_TRANSFER, "true");
+    Settings settings = createSettings(root);
+    assertTrue(settings.isUncompressedTransfer());
+  }
+
+  //TW-18262
+  public void ampersand_in_password() {
+    VcsRootImpl vcsRoot = createVcsRoot("http://some.org/path", "user", "m&n");
+    Settings settings = createSettings(vcsRoot);
+    assertEquals("http://user:m%26n@some.org/path", settings.getRepositoryUrlWithCredentials());
+  }
+
+  //TW-18835
+  public void test_ssh() {
+    VcsRootImpl vcsRoot = createVcsRoot("ssh://ourserver.com/mercurialrepo/", "user", "pwd");
+    Settings settings = createSettings(vcsRoot);
+    assertEquals("ssh://user:pwd@ourserver.com/mercurialrepo/", settings.getRepositoryUrlWithCredentials());
+  }
+
+  private VcsRootImpl createVcsRoot(String url) {
+    return createVcsRoot(url, "user", "pwd");
+  }
+
+  private VcsRootImpl createVcsRoot(String url, String userName, String password) {
+    VcsRootImpl vcsRoot = new VcsRootImpl(1, Constants.VCS_NAME);
+    vcsRoot.addProperty(Constants.HG_COMMAND_PATH_PROP, "hg.exe");
+    vcsRoot.addProperty(Constants.REPOSITORY_PROP, url);
+    vcsRoot.addProperty(Constants.USERNAME, userName);
+    vcsRoot.addProperty(Constants.PASSWORD, password);
+    return vcsRoot;
+  }
+
+  private Settings createSettings(@NotNull final VcsRoot root) {
+    ServerPluginConfig config = new ServerPluginConfigBuilder().build();
+    return new Settings(new ServerHgPathProvider(config), root);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/UnrelatedResitoriesTest.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,104 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import jetbrains.buildServer.TempFiles;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.Settings;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.UnknownRevisionException;
+import jetbrains.buildServer.util.FileUtil;
+import jetbrains.buildServer.vcs.CheckoutRules;
+import jetbrains.buildServer.vcs.VcsException;
+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 static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertTrue;
+
+/**
+ * @author dmitry.neverov
+ */
+@Test
+public class UnrelatedResitoriesTest {
+
+  private final static String CURRENT_VERSION_OF_NEW_REPO = "18:df04faa7575a";
+  private MercurialVcsSupport myVcs;
+  private TempFiles myTempFiles;
+  private File myRepositoryLocation;
+  private VcsRootImpl myRoot;
+  private Mockery myContext;
+  private ServerPluginConfig myPluginConfig;
+
+  @BeforeMethod
+  public void setUp() throws Exception {
+    myContext = new Mockery();
+    myTempFiles = new TempFiles();
+    myPluginConfig = new ServerPluginConfigBuilder()
+            .cachesDir(myTempFiles.createTempDir())
+            .pluginDataDir(myTempFiles.createTempDir())
+            .build();
+
+    myRepositoryLocation = myTempFiles.createTempDir();
+    copyRepository(new File("mercurial-tests/testData/rep1"));
+    myRoot = new VcsRootBuilder().withUrl(myRepositoryLocation.getCanonicalPath()).build();
+  }
+
+  @AfterMethod
+  public void tearDown() {
+    myTempFiles.cleanup();
+  }
+
+
+  public void should_be_able_to_sync_when_repository_became_unrelated() throws Exception {
+    myVcs = createVcs();
+    syncRepository();
+    repositoryBecamesUnrelated();
+    String currentVersion = syncRepository();
+    assertEquals(CURRENT_VERSION_OF_NEW_REPO, currentVersion);
+  }
+
+
+  public void should_return_no_changes_when_fromRevision_is_from_unrelated_repository() throws Exception {
+    final CommandFactory factory = myContext.mock(CommandFactory.class);
+    final CollectChangesCommand commandExecutedWithException = myContext.mock(CollectChangesCommand.class);
+    myVcs = createVcs(factory);
+    myContext.checking(new Expectations(){{
+      allowing(factory).getCollectChangesCommand(with(any(Settings.class)), with(any(File.class)));
+      will(returnValue(commandExecutedWithException));
+      allowing(commandExecutedWithException).execute(with(any(String.class)), with(any(String.class)));
+      will(throwException(new UnknownRevisionException("1234")));
+    }});
+
+    String currentVersionOfOldRepo = syncRepository();
+    repositoryBecamesUnrelated();
+    String currentVersionOfNewRepo = syncRepository();
+    assertTrue(myVcs.collectChanges(myRoot, currentVersionOfOldRepo, currentVersionOfNewRepo, CheckoutRules.DEFAULT).isEmpty());
+  }
+
+
+  private String syncRepository() throws VcsException {
+    return myVcs.getCurrentVersion(myRoot);
+  }
+
+  private void repositoryBecamesUnrelated() throws IOException {
+    FileUtil.delete(myRepositoryLocation);
+    copyRepository(new File("mercurial-tests/testData/rep2"));
+  }
+
+  private void copyRepository(@NotNull final File src) throws IOException {
+    LocalRepositoryUtil.copyRepository(src, myRepositoryLocation);
+  }
+
+  private MercurialVcsSupport createVcs() throws IOException {
+    return Util.createMercurialServerSupport(myContext, myPluginConfig);
+  }
+
+  private MercurialVcsSupport createVcs(@NotNull final CommandFactory factory) throws IOException {
+    return Util.createMercurialServerSupport(myContext, myPluginConfig, factory);
+  }
+}
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/Util.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/Util.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,6 +1,18 @@
 package jetbrains.buildServer.buildTriggers.vcs.mercurial;
 
+import jetbrains.buildServer.serverSide.BuildServerListener;
+import jetbrains.buildServer.serverSide.SBuildServer;
+import jetbrains.buildServer.util.EventDispatcher;
+import jetbrains.buildServer.util.cache.ResetCacheRegister;
+import jetbrains.buildServer.vcs.VcsManager;
+import org.jetbrains.annotations.NotNull;
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+
+import java.io.File;
 import java.io.IOException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
 
 /**
  * @author dmitry.neverov
@@ -14,8 +26,24 @@
     if (providedHg != null) {
       return providedHg;
     } else {
-      return "mercurial-tests/testData/bin/hg.exe";
+      return new File("mercurial-tests/testData/bin/hg.exe").getAbsolutePath();
     }
   }
 
+
+  public static MercurialVcsSupport createMercurialServerSupport(@NotNull Mockery context, ServerPluginConfig config) throws IOException {
+    return createMercurialServerSupport(context, config, new CommandFactoryImpl(config));
+  }
+
+
+  public static MercurialVcsSupport createMercurialServerSupport(@NotNull Mockery context, @NotNull ServerPluginConfig config, @NotNull CommandFactory commandFactory) throws IOException {
+    VcsManager vcsManager = context.mock(VcsManager.class);
+    final SBuildServer server = context.mock(SBuildServer.class);
+    final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
+    context.checking(new Expectations() {{
+      allowing(server).getExecutor(); will(returnValue(executor));
+    }});
+    EventDispatcher<BuildServerListener> dispatcher = EventDispatcher.create(BuildServerListener.class);
+    return new MercurialVcsSupport(vcsManager, server, dispatcher, new ResetCacheRegister(), config, new ServerHgPathProvider(config), commandFactory, new MirrorManagerImpl(config));
+  }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/VcsRootBuilder.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,97 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import jetbrains.buildServer.vcs.SVcsRoot;
+import jetbrains.buildServer.vcs.impl.VcsRootImpl;
+import org.jetbrains.annotations.NotNull;
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+
+import java.io.IOException;
+
+/**
+ * @author dmitry.neverov
+ */
+public class VcsRootBuilder {
+
+  private String myRepository;
+  private String myUsername;
+  private String myPassword;
+  private String myBranch;
+  private long myRootId = 1L;
+  private String myHgPath;
+  private String myUserForTag;
+
+  public static VcsRootBuilder vcsRoot() {
+    return new VcsRootBuilder();
+  }
+
+  public VcsRootImpl build() throws IOException {
+    VcsRootImpl vcsRoot = new VcsRootImpl(myRootId, Constants.VCS_NAME);
+    vcsRoot.addProperty(Constants.REPOSITORY_PROP, myRepository);
+    vcsRoot.addProperty(Constants.HG_COMMAND_PATH_PROP, myHgPath != null ? myHgPath : Util.getHgPath());
+    vcsRoot.addProperty(Constants.USERNAME, myUsername);
+    vcsRoot.addProperty(Constants.PASSWORD, myPassword);
+    vcsRoot.addProperty(Constants.BRANCH_NAME_PROP, myBranch);
+    vcsRoot.addProperty(Constants.USER_FOR_TAG, myUserForTag);
+    return vcsRoot;
+  }
+
+
+  public SVcsRoot build(Mockery context) {
+    final SVcsRoot root = context.mock(SVcsRoot.class, "SVcsRoot" + myRootId);
+    context.checking(new Expectations() {{
+      allowing(root).getVcsName(); will(returnValue(Constants.VCS_NAME));
+      allowing(root).getProperty(with(Constants.REPOSITORY_PROP)); will(returnValue(myRepository));
+      allowing(root).getProperty(with(Constants.HG_COMMAND_PATH_PROP)); will(returnValue(myHgPath));
+      allowing(root).getProperty(with(Constants.BRANCH_NAME_PROP)); will(returnValue(myBranch));
+      allowing(root).getProperty(with(Constants.SERVER_CLONE_PATH_PROP)); will(returnValue(null));
+      allowing(root).getProperty(with(Constants.USERNAME)); will(returnValue(myUsername));
+      allowing(root).getProperty(with(Constants.PASSWORD)); will(returnValue(myPassword));
+      allowing(root).getProperty(with(Constants.UNCOMPRESSED_TRANSFER)); will(returnValue(null));
+      allowing(root).getProperty(with(Constants.USER_FOR_TAG)); will(returnValue(myUserForTag));
+    }});
+    return root;
+  }
+
+
+  public VcsRootBuilder withUrl(@NotNull String repository) {
+    myRepository = repository;
+    return this;
+  }
+
+
+  public VcsRootBuilder withUserName(@NotNull String username) {
+    myUsername = username;
+    return this;
+  }
+
+
+  public VcsRootBuilder withPassword(@NotNull String password) {
+    myPassword = password;
+    return this;
+  }
+
+
+  public VcsRootBuilder withBranch(@NotNull String branch) {
+    myBranch = branch;
+    return this;
+  }
+
+
+  public VcsRootBuilder withId(long rootId) {
+    myRootId = rootId;
+    return this;
+  }
+
+
+  public VcsRootBuilder withHgPath(String hgPath) {
+    myHgPath = hgPath;
+    return this;
+  }
+
+
+  public VcsRootBuilder withUserForTag(String username) {
+    myUserForTag = username;
+    return this;
+  }
+}
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/BaseCommandTest.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/BaseCommandTest.java	Wed Feb 15 13:23:53 2012 +0400
@@ -2,9 +2,6 @@
 
 import com.intellij.execution.configurations.GeneralCommandLine;
 import com.intellij.openapi.util.SystemInfo;
-import jetbrains.buildServer.buildTriggers.vcs.mercurial.Constants;
-import jetbrains.buildServer.buildTriggers.vcs.mercurial.Util;
-import jetbrains.buildServer.vcs.impl.VcsRootImpl;
 import junit.framework.TestCase;
 import org.testng.annotations.Test;
 
@@ -18,13 +15,8 @@
 public class BaseCommandTest extends TestCase {
 
   public void should_quote_command_line_arguments() throws IOException {
-    VcsRootImpl root = new VcsRootImpl(1, "rootForTest");
-    root.addProperty(Constants.REPOSITORY_PROP, "http://some.org/repo.hg");
-    root.addProperty(Constants.HG_COMMAND_PATH_PROP, Util.getHgPath());
     File workingDir = new File("some dir");
-    Settings settings = new Settings(root);
-
-    BaseCommand command = new BaseCommand(settings, workingDir);
+    BaseCommand command = new BaseCommand("/path/to/hg", workingDir);
     GeneralCommandLine cl = command.createCommandLine();
     cl.addParameter("param with spaces");
     cl.addParameter("param with quote \" rm -rf /");
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/BaseCommandTestCase.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/BaseCommandTestCase.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,96 +1,92 @@
-/*
- * Copyright 2000-2011 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
-
-import jetbrains.buildServer.BaseTestCase;
-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;
-import jetbrains.buildServer.vcs.impl.VcsRootImpl;
-import org.jetbrains.annotations.NotNull;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-
-public class BaseCommandTestCase extends BaseTestCase {
-  private String myRepository;
-  private String myUsername;
-  private String myPassword;
-  private boolean myCloneRequired;
-
-  public BaseCommandTestCase() {
-  }
-
-  protected void setRepository(final String repository, boolean cloneRequired) {
-    myRepository = repository;
-    myCloneRequired = cloneRequired;
-  }
-
-  protected void setUsername(final String username) {
-    myUsername = username;
-  }
-
-  protected void setPassword(final String password) {
-    myPassword = password;
-  }
-
-  protected <T> T runCommand(CommandExecutor<T> executor) throws IOException, VcsException {
-    Map<String, String> vcsRootProps = new HashMap<String, String>();
-
-    vcsRootProps.put(Constants.REPOSITORY_PROP, myRepository);
-
-    if (myCloneRequired) {
-      File repository = LocalRepositoryUtil.prepareRepository(new File(myRepository).getAbsolutePath());
-      vcsRootProps.put(Constants.REPOSITORY_PROP, repository.getAbsolutePath());
-    }
-
-    vcsRootProps.put(Constants.HG_COMMAND_PATH_PROP, Util.getHgPath());
-    if (myUsername != null) {
-      vcsRootProps.put(Constants.USERNAME, myUsername);
-    }
-    if (myPassword != null) {
-      vcsRootProps.put(Constants.PASSWORD, myPassword);
-    }
-
-    TempFiles tf = new TempFiles();
-    File parentDir = tf.createTempDir();
-
-    MirrorManager mirrorManager = new MirrorManager(parentDir);
-    VcsRoot vcsRoot = new VcsRootImpl(1, vcsRootProps);
-    Settings settings = new Settings(vcsRoot);
-    final File workingDir = mirrorManager.getMirrorDir(settings.getRepositoryUrl());
-    settings.setCustomWorkingDir(workingDir);
-    try {
-      if (myCloneRequired) {
-        new CloneCommand(settings, workingDir).execute();
-      }
-
-      return executor.execute(settings, workingDir);
-    } finally {
-      tf.cleanup();
-    }
-  }
-
-  public interface CommandExecutor<T> {
-    T execute(@NotNull Settings settings, File workingDir) throws VcsException;
-  }
-}
+/*
+ * Copyright 2000-2011 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import jetbrains.buildServer.BaseTestCase;
+import jetbrains.buildServer.TempFiles;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.*;
+import jetbrains.buildServer.vcs.VcsException;
+import jetbrains.buildServer.vcs.VcsRoot;
+import jetbrains.buildServer.vcs.impl.VcsRootImpl;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+public class BaseCommandTestCase extends BaseTestCase {
+  private String myRepository;
+  private String myUsername;
+  private String myPassword;
+  private boolean myCloneRequired;
+
+  public BaseCommandTestCase() {
+  }
+
+  protected void setRepository(final String repository, boolean cloneRequired) {
+    myRepository = repository;
+    myCloneRequired = cloneRequired;
+  }
+
+  protected void setUsername(final String username) {
+    myUsername = username;
+  }
+
+  protected void setPassword(final String password) {
+    myPassword = password;
+  }
+
+  protected <T> T runCommand(CommandExecutor<T> executor) throws IOException, VcsException {
+    Map<String, String> vcsRootProps = new HashMap<String, String>();
+
+    vcsRootProps.put(Constants.REPOSITORY_PROP, myRepository);
+
+    if (myCloneRequired) {
+      File repository = LocalRepositoryUtil.prepareRepository(new File(myRepository).getAbsolutePath());
+      vcsRootProps.put(Constants.REPOSITORY_PROP, repository.getAbsolutePath());
+    }
+
+    vcsRootProps.put(Constants.HG_COMMAND_PATH_PROP, Util.getHgPath());
+    if (myUsername != null) {
+      vcsRootProps.put(Constants.USERNAME, myUsername);
+    }
+    if (myPassword != null) {
+      vcsRootProps.put(Constants.PASSWORD, myPassword);
+    }
+
+    TempFiles tf = new TempFiles();
+    VcsRoot vcsRoot = new VcsRootImpl(1, vcsRootProps);
+    ServerPluginConfig config = new ServerPluginConfigBuilder().cachesDir(tf.createTempDir()).build();
+    MirrorManager mirrorManager = new MirrorManagerImpl(config);
+    Settings settings = new Settings(new ServerHgPathProvider(config), vcsRoot);
+    final File workingDir = mirrorManager.getMirrorDir(settings.getRepository());
+    settings.setCustomWorkingDir(workingDir);
+    try {
+      if (myCloneRequired) {
+        new CloneCommand(settings, workingDir).execute();
+      }
+
+      return executor.execute(settings, workingDir);
+    } finally {
+      tf.cleanup();
+    }
+  }
+
+  public interface CommandExecutor<T> {
+    T execute(@NotNull Settings settings, File workingDir) throws VcsException;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CatCommandTest.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,74 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import jetbrains.buildServer.util.FileUtil;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.util.List;
+
+import static com.intellij.openapi.util.io.FileUtil.delete;
+import static java.util.Arrays.asList;
+
+/**
+ * @author dmitry.neverov
+ */
+@Test
+public class CatCommandTest extends BaseCommandTestCase {
+
+  //TW-13178
+  public void command_should_not_leave_garbage_in_temp_dir() throws IOException, VcsException {
+    cleanCatResultDirs();
+    setRepository("mercurial-tests/testData/rep1", true);
+    final String nonExisting = "/non/existing/path";
+    try {
+      runCat(asList(nonExisting));
+      fail("exception should be thrown for non-existing path");
+    } catch (UnknownFileException e) {
+      assertEquals(nonExisting, e.getPath());
+      checkTempDirDoesNotContainCatResults();
+    }
+  }
+
+  public void should_not_throw_exception_if_not_asked_to() throws IOException, VcsException {
+    cleanCatResultDirs();
+    setRepository("mercurial-tests/testData/rep1", true);
+    runCommand(new CommandExecutor<File>() {
+      public File execute(@NotNull final Settings settings, @NotNull final File workingDir) throws VcsException {
+        CatCommand cat = new CatCommand(settings, workingDir);
+        return cat.execute(asList("/non/existing/path"), false);
+      }
+    });
+  }
+
+  private void cleanCatResultDirs() {
+    for (File f : getCatResultDirs())
+      delete(f);
+  }
+
+  private void checkTempDirDoesNotContainCatResults() throws IOException {
+    File[] catresults = getCatResultDirs();
+    assertTrue("cat result dirs are not cleaned: " + asList(catresults), catresults.length == 0);
+  }
+
+  private File[] getCatResultDirs() {
+    String tempDirPath = FileUtil.getTempDirectory();
+    return new File(tempDirPath).listFiles(new FilenameFilter() {
+      public boolean accept(File dir, String name) {
+        return name.startsWith("mercurial") && name.endsWith("catresult");
+      }
+    });
+  }
+
+  private File runCat(@NotNull final List<String> paths) throws IOException, VcsException {
+    return runCommand(new CommandExecutor<File>() {
+      public File execute(@NotNull final Settings settings, @NotNull final File workingDir) throws VcsException {
+        CatCommand cat = new CatCommand(settings, workingDir);
+        return cat.execute(paths);
+      }
+    });
+  }
+}
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CloneCommandTest.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CloneCommandTest.java	Wed Feb 15 13:23:53 2012 +0400
@@ -2,9 +2,7 @@
 
 import jetbrains.buildServer.BaseTestCase;
 import jetbrains.buildServer.TempFiles;
-import jetbrains.buildServer.buildTriggers.vcs.mercurial.Constants;
-import jetbrains.buildServer.buildTriggers.vcs.mercurial.LocalRepositoryUtil;
-import jetbrains.buildServer.buildTriggers.vcs.mercurial.Util;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.*;
 import jetbrains.buildServer.vcs.VcsException;
 import jetbrains.buildServer.vcs.impl.VcsRootImpl;
 import org.testng.annotations.AfterMethod;
@@ -44,7 +42,8 @@
     root.addProperty(Constants.HG_COMMAND_PATH_PROP, Util.getHgPath());
 
     File workingDir = myTempFiles.createTempDir();
-    Settings settings = new Settings(root);
+    ServerPluginConfig config = new ServerPluginConfigBuilder().cachesDir(myTempFiles.createTempDir()).build();
+    Settings settings = new Settings(new ServerHgPathProvider(config), root);
     settings.setCustomWorkingDir(workingDir);
 
     CloneCommand clone = new CloneCommand(settings, workingDir);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommandResultTest.java	Wed Feb 15 13:23:53 2012 +0400
@@ -0,0 +1,182 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import com.intellij.openapi.diagnostic.Logger;
+import jetbrains.buildServer.ExecResult;
+import jetbrains.buildServer.StreamGobbler;
+import jetbrains.buildServer.vcs.VcsException;
+import org.apache.log4j.Level;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.io.ByteArrayInputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+
+import static org.testng.AssertJUnit.*;
+
+/**
+ * @author dmitry.neverov
+ */
+@Test
+public class CommandResultTest {
+
+  private RecordingLogger myLogger;
+
+  @BeforeMethod
+  public void setUp() {
+    myLogger = new RecordingLogger();
+  }
+
+  public void output_should_not_contain_private_data() {
+    String password = "pass";
+    CommandResult commandResult = commandResultFor(execResult().withStdout(password).withStderr(password), password);
+    assertFalse(commandResult.getStdout().contains(password));
+    myLogger.assertLogMessagesDontContain(password);
+  }
+
+  @Test(expectedExceptions = UnrelatedRepositoryException.class)
+  public void should_detect_unrelated_repository_error() throws VcsException {
+    String unrelatedRepositoryStderr = "abort: repository is unrelated\n";
+    CommandResult commandResult = commandResultFor(execResult().withStderr(unrelatedRepositoryStderr).withExitCode(255));
+    commandResult.checkCommandFailed();
+  }
+
+  @Test
+  public void should_detect_unknown_revision_error() throws VcsException {
+    String unknownRevision = "9c6a6b4aede0";
+    String unknownRevisionError = "abort: unknown revision '" + unknownRevision + "'\n";
+    CommandResult commandResult = commandResultFor(execResult().withStderr(unknownRevisionError).withExitCode(255));
+    try {
+      commandResult.checkCommandFailed();
+      fail("unknown exception should be thrown");
+    } catch (UnknownRevisionException e) {
+      assertEquals(unknownRevision, e.getRevision());
+    }
+  }
+
+  @Test(expectedExceptions = VcsException.class)
+  public void should_detect_failure_when_delegate_has_exception() throws VcsException {
+    CommandResult commandResult = commandResultFor(execResult().withException(new RuntimeException()));
+    commandResult.checkCommandFailed();
+  }
+
+  public void should_detect_failure_with_non_zero_exit_code() throws VcsException {
+    CommandResult commandResult = commandResultFor(execResult().withExitCode(1));
+    commandResult.checkCommandFailed();
+  }
+
+
+  ExecResultBuilder execResult() {
+    return new ExecResultBuilder();
+  }
+
+  CommandResult commandResultFor(ExecResultBuilder builder, String... privateData) {
+    return new CommandResult(myLogger, "", builder.build(), new HashSet<String>(Arrays.asList(privateData)));
+  }
+
+  private class ExecResultBuilder {
+    private String myStdout = "";
+    private String myStderr = "";
+    private Throwable myException = null;
+    private int myExitCode = 0;
+
+    ExecResultBuilder withStdout(String stdout) {
+      myStdout = stdout;
+      return this;
+    }
+    ExecResultBuilder withStderr(String stderr) {
+      myStderr = stderr;
+      return this;
+    }
+    ExecResultBuilder withException(Throwable exception) {
+      myException = exception;
+      return this;
+    }
+    ExecResultBuilder withExitCode(int exitCode) {
+      myExitCode = exitCode;
+      return this;
+    }
+
+    ExecResult build() {
+      ExecResult result = new ExecResult();
+      result.setOutputGobbler(createStringGobbler(myStdout));
+      result.setErrorGobbler(createStringGobbler(myStderr));
+      if (myException != null)
+        result.setException(myException);
+      if (myExitCode != 0)
+        result.setExitCode(myExitCode);
+      return result;
+    }
+
+    private StreamGobbler createStringGobbler(@NotNull final String str) {
+      StreamGobbler gobbler = new StreamGobbler(new ByteArrayInputStream(str.getBytes()));
+      gobbler.start();
+      try {
+        Thread.sleep(10);//wait for gobbler to read string
+      } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+      }
+      return gobbler;
+    }
+  }
+
+  private class RecordingLogger extends Logger {
+
+    private List<String> myMessages = new ArrayList<String>();
+
+    public void assertLogMessagesDontContain(@NotNull String... strs) {
+      for (String s : strs) {
+        for (String message : myMessages)
+          assertFalse("'" + s + "' was logged", message.contains(s));
+      }
+    }
+
+    @Override
+    public boolean isDebugEnabled() {
+      return true;
+    }
+
+    @Override
+    public void debug(@NonNls String s) {
+      myMessages.add(s);
+    }
+
+    @Override
+    public void debug(Throwable throwable) {
+      myMessages.add(throwable.getMessage());
+    }
+
+    @Override
+    public void debug(@NonNls String s, Throwable throwable) {
+      myMessages.add(s);
+    }
+
+    @Override
+    public void error(@NonNls String s, Throwable throwable, @NonNls String... strings) {
+      myMessages.add(s);
+    }
+
+    @Override
+    public void info(@NonNls String s) {
+      myMessages.add(s);
+    }
+
+    @Override
+    public void info(@NonNls String s, Throwable throwable) {
+      myMessages.add(s);
+    }
+
+    @Override
+    public void warn(@NonNls String s, Throwable throwable) {
+      myMessages.add(s);
+    }
+
+    @Override
+    public void setLevel(Level level) {
+    }
+  }
+}
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommandTest.java	Wed Feb 15 12:07:12 2012 +0400
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommandTest.java	Wed Feb 15 13:23:53 2012 +0400
@@ -1,140 +1,184 @@
-/*
- * Copyright 2000-2011 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
-
-import jetbrains.buildServer.TempFiles;
-import jetbrains.buildServer.buildTriggers.vcs.mercurial.MercurialVcsSupport;
-import jetbrains.buildServer.util.FileUtil;
-import jetbrains.buildServer.vcs.VcsException;
-import org.jetbrains.annotations.NotNull;
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.List;
-
-@Test
-public class LogCommandTest extends BaseCommandTestCase {
-
-  private TempFiles myTempFiles = new TempFiles();
-  private File myTemplateFile;
-
-
-  @BeforeMethod
-  @Override
-  protected void setUp() throws Exception {
-    super.setUp();
-    setRepository("mercurial-tests/testData/rep1", true);
-    myTemplateFile = myTempFiles.createTempFile();
-    FileUtil.copyResource(MercurialVcsSupport.class, "/buildServerResources/log.template", myTemplateFile);
-  }
-
-  @AfterMethod
-  public void tearDown() {
-    myTempFiles.cleanup();
-  }
-
-
-  public void testOneChangeSet() throws Exception {
-    final String toId = "9875b412a788";
-    List<ChangeSet> changes = runLog(null, toId);
-    assertEquals(1, changes.size());
-    final ChangeSet changeSet = changes.get(0);
-    assertEquals(0, changeSet.getRevNumber());
-    assertEquals(toId, changeSet.getId());
-    assertEquals("pavel@localhost", changeSet.getUser());
-    assertEquals("dir1 created", changeSet.getDescription());
-    assertTrue(changeSet.getParents().isEmpty());
-  }
-
-  public void testMoreThanOneChangeSet() throws Exception {
-    final String fromId = "9875b412a788";
-    final String toId = "7209b1f1d793";
-    List<ChangeSet> changes = runLog(fromId, toId);
-    assertEquals(3, changes.size());
-    ChangeSet changeSet1 = changes.get(0);
-    final ChangeSet changeSet2 = changes.get(1);
-    final ChangeSet changeSet3 = changes.get(2);
-    assertEquals("dir1 created", changeSet1.getDescription());
-    assertEquals("new file added", changeSet2.getDescription());
-    assertEquals("file4.txt added", changeSet3.getDescription());
-
-    changes = runLog(null, toId);
-    assertEquals(3, changes.size());
-    changeSet1 = changes.get(2);
-    assertEquals("file4.txt added", changeSet1.getDescription());
-  }
-
-  public void changeset_parents() throws VcsException, IOException {
-    setRepository("mercurial-tests/testData/rep2", true);
-    List<ChangeSet> changes = runLog("6eeb8974fe67", "6eeb8974fe67");
-    assertEquals(1, changes.size());
-    ChangeSet cs = changes.get(0);
-    assertNotNull(cs.getParents());
-    assertEquals(2, cs.getParents().size());
-    assertTrue(cs.getParents().contains(new ChangeSetRevision("1:a3d15477d297")));
-    assertTrue(cs.getParents().contains(new ChangeSetRevision("3:2538c02bafeb")));
-  }
-
-  public void parse_multiline_description() throws VcsException, IOException {
-    List<ChangeSet> changes = runLog("9babcf2d5705", "9c6a6b4aede0");
-    assertEquals(1, changes.size());
-    assertEquals("Multiline description\n" +
-            "description with new\n" +
-            "lines\n" +
-            "aaaa\n" +
-            "bbb", changes.get(0).getDescription());
-  }
-
-  public void log_result_should_contain_changed_files() throws Exception {
-    final String fromId = "7209b1f1d793";
-    final String toId = "b06a290a363b";
-    List<ChangeSet> csets = runLog(fromId, toId);
-    assertEquals(3, csets.size());
-
-    List<ModifiedFile> files = csets.get(0).getModifiedFiles();
-    assertEquals(1, files.size());
-    ModifiedFile file = files.get(0);
-    assertEquals(ModifiedFile.Status.ADDED, file.getStatus());
-    assertEquals("dir1/file4.txt", file.getPath());
-
-    files = csets.get(1).getModifiedFiles();
-    assertEquals(1, files.size());
-    file = files.get(0);
-    assertEquals(ModifiedFile.Status.REMOVED, file.getStatus());
-    assertEquals("dir1/file4.txt", file.getPath());
-
-