changeset 173:c3157374a356 remote-run/dmitry.neverov/uncompressed

merge TW-15195
author Dmitry Neverov <dmitry.neverov@jetbrains.com>
date Wed, 09 Feb 2011 14:01:26 +0300
parents cfd745c297f1 (current diff) ded3c1ad49be (diff)
children d79286880dee
files mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java
diffstat 8 files changed, 1148 insertions(+), 1112 deletions(-) [+]
line wrap: on
line diff
--- a/build.xml	Fri Feb 04 13:53:02 2011 +0300
+++ b/build.xml	Wed Feb 09 14:01:26 2011 +0300
@@ -41,6 +41,10 @@
 
   <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">
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/Constants.java	Fri Feb 04 13:53:02 2011 +0300
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/Constants.java	Wed Feb 09 14:01:26 2011 +0300
@@ -26,4 +26,5 @@
   String SERVER_CLONE_PATH_PROP = "serverClonePath";
   String USERNAME = "username";
   String PASSWORD = VcsRoot.SECURE_PROPERTY_PREFIX + "password";
+  String UNCOMPRESSED_TRANSFER = "uncompressedTransfer";
 }
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CloneCommand.java	Fri Feb 04 13:53:02 2011 +0300
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CloneCommand.java	Wed Feb 09 14:01:26 2011 +0300
@@ -68,6 +68,9 @@
     if (!myUpdateWorkingDir) {
       cli.addParameter("-U");
     }
+    if (getSettings().isUncompressedTransfer()) {
+      cli.addParameter("--uncompressed");
+    }
     cli.addParameter(myRepository);
     cli.addParameter(dir.getName());
 
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/Settings.java	Fri Feb 04 13:53:02 2011 +0300
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/Settings.java	Wed Feb 09 14:01:26 2011 +0300
@@ -42,6 +42,7 @@
   private String myUsername;
   private String myPassword;
   private String myBranchName;
+  private boolean myUncompressedTransfer = false;
   private static final String DEFAULT_BRANCH_NAME = "default";
 
   public Settings(@NotNull File workFolderParentDir, @NotNull VcsRoot vcsRoot) {
@@ -52,6 +53,7 @@
 
     myUsername = vcsRoot.getProperty(Constants.USERNAME);
     myPassword = vcsRoot.getProperty(Constants.PASSWORD);
+    myUncompressedTransfer = "true".equals(vcsRoot.getProperty(Constants.UNCOMPRESSED_TRANSFER));
   }
 
   public Settings() {
@@ -78,6 +80,10 @@
     return getBranchName().equals(DEFAULT_BRANCH_NAME);
   }
 
+  public boolean isUncompressedTransfer() {
+    return myUncompressedTransfer;
+  }
+
   /**
    * Returns path to hg command
    * @return path to hg command
--- a/mercurial-server/resources/buildServerResources/mercurialSettings.jsp	Fri Feb 04 13:53:02 2011 +0300
+++ b/mercurial-server/resources/buildServerResources/mercurialSettings.jsp	Wed Feb 09 14:01:26 2011 +0300
@@ -31,6 +31,12 @@
       <div class="smallNote" style="margin: 0;">Provide path to a parent directory on TeamCity server where a cloned repository should be created (applicable to "Automatically on server" checkout mode only). Leave blank to use default path.</div>
     </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>
+    </td>
+  </tr>
   </l:settingsGroup>
   <l:settingsGroup title="Authorization settings">
   <tr>
--- a/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java	Fri Feb 04 13:53:02 2011 +0300
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java	Wed Feb 09 14:01:26 2011 +0300
@@ -1,681 +1,682 @@
-/*
- * 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.BuildAgent;
-import jetbrains.buildServer.Used;
-import jetbrains.buildServer.buildTriggers.vcs.AbstractVcsPropertiesProcessor;
-import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.*;
-import jetbrains.buildServer.log.Loggers;
-import jetbrains.buildServer.serverSide.*;
-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.vcs.*;
-import jetbrains.buildServer.vcs.patches.PatchBuilder;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-import java.io.File;
-import java.io.FileFilter;
-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;
-
-/**
- * Mercurial VCS plugin for TeamCity works as follows:
- * <ul>
- * <li>clones repository to internal storage
- * <li>before any operation with working copy of repository pulls changes from the original repository
- * <li>executes corresponding hg command
- * </ul>
- *
- * <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 ConcurrentMap<String, Lock> myWorkDirLocks= new ConcurrentHashMap<String, Lock>();
-  private VcsManager myVcsManager;
-  private File myDefaultWorkFolderParent;
-
-  public MercurialVcsSupport(@NotNull final VcsManager vcsManager,
-                             @NotNull ServerPaths paths,
-                             @NotNull final SBuildServer server,
-                             @NotNull EventDispatcher<BuildServerListener> dispatcher) {
-    myVcsManager = vcsManager;
-    myDefaultWorkFolderParent = new File(paths.getCachesDir(), "mercurial");
-    dispatcher.addListener(new BuildServerAdapter() {
-      @Override
-      public void cleanupFinished() {
-        super.cleanupFinished();
-        server.getExecutor().submit(new Runnable() {
-          public void run() {
-            removeOldWorkFolders();
-          }
-        });
-      }
-
-      @Override
-      public void sourcesVersionReleased(@NotNull final BuildAgent agent) {
-        super.sourcesVersionReleased(agent);
-        server.getExecutor().submit(new Runnable() {
-          public void run() {
-            Set<File> clonedRepos = getAllClonedRepos();
-            if (clonedRepos == null) return;
-            for (File f: clonedRepos) {
-              lockWorkDir(f);
-              try {
-                FileUtil.delete(f);
-              } finally {
-                unlockWorkDir(f);
-              }
-            }
-          }
-        });
-      }
-    });
-  }
-
-  private Collection<ModifiedFile> computeModifiedFilesForMergeCommit(final Settings settings, final ChangeSet cur) throws VcsException {
-    ChangedFilesCommand cfc = new ChangedFilesCommand(settings);
-    cfc.setRevId(cur.getId());
-    return cfc.execute();
-  }
-
-  private List<VcsChange> toVcsChanges(final List<ModifiedFile> modifiedFiles, String prevVer, String curVer, CheckoutRules rules) {
-    List<VcsChange> files = new ArrayList<VcsChange>();
-    for (ModifiedFile mf: modifiedFiles) {
-      String normalizedPath = PathUtil.normalizeSeparator(mf.getPath());
-      VcsChangeInfo.Type changeType = getChangeType(mf.getStatus());
-      if (changeType == null) {
-        Loggers.VCS.warn("Unable to convert status: " + mf.getStatus() + " to VCS change type");
-        changeType = VcsChangeInfo.Type.NOT_CHANGED;
-      }
-      files.add(new VcsChange(changeType, mf.getStatus().getName(), normalizedPath, rules.map(mf.getPath()), prevVer, curVer));
-    }
-    return files;
-  }
-
-  private VcsChangeInfo.Type getChangeType(final ModifiedFile.Status status) {
-    switch (status) {
-      case ADDED:return VcsChangeInfo.Type.ADDED;
-      case MODIFIED:return VcsChangeInfo.Type.CHANGED;
-      case REMOVED:return VcsChangeInfo.Type.REMOVED;
-    }
-    return null;
-  }
-
-  @NotNull
-  public byte[] getContent(@NotNull final VcsModification vcsModification,
-                           @NotNull final VcsChangeInfo change,
-                           @NotNull final VcsChangeInfo.ContentType contentType,
-                           @NotNull final VcsRoot vcsRoot) throws VcsException {
-    syncClonedRepository(vcsRoot);
-    String version = contentType == VcsChangeInfo.ContentType.AFTER_CHANGE ? change.getAfterChangeRevisionNumber() : change.getBeforeChangeRevisionNumber();
-    return getContent(change.getRelativeFileName(), vcsRoot, version);
-  }
-
-  @NotNull
-  public byte[] getContent(@NotNull final String filePath, @NotNull final VcsRoot vcsRoot, @NotNull final String version) throws VcsException {
-    syncClonedRepository(vcsRoot);
-    Settings settings = createSettings(vcsRoot);
-    CatCommand cc = new CatCommand(settings);
-    cc.setRevId(new ChangeSet(version).getId());
-    File parentDir = cc.execute(Collections.singletonList(filePath));
-    try {
-      File file = new File(parentDir, filePath);
-      if (file.isFile()) {
-        try {
-          return FileUtil.loadFileBytes(file);
-        } catch (IOException e) {
-          throw new VcsException("Failed to load content of file: " + file.getAbsolutePath(), e);
-        }
-      } else {
-        Loggers.VCS.warn("Unable to obtain content of the file: " + filePath);
-      }
-    } finally {
-      deleteTmpDir(parentDir);
-    }
-    return new byte[0];
-  }
-
-  @NotNull
-  public String getName() {
-    return Constants.VCS_NAME;
-  }
-
-  @NotNull
-  @Used("jsp")
-  public String getDisplayName() {
-    return "Mercurial";
-  }
-
-  @Nullable
-  public PropertiesProcessor getVcsPropertiesProcessor() {
-    return new AbstractVcsPropertiesProcessor() {
-      public Collection<InvalidProperty> process(final Map<String, String> properties) {
-        List<InvalidProperty> result = new ArrayList<InvalidProperty>();
-        if (isEmpty(properties.get(Constants.HG_COMMAND_PATH_PROP))) {
-          result.add(new InvalidProperty(Constants.HG_COMMAND_PATH_PROP, "Path to 'hg' command must be specified"));
-        } 
-        if (isEmpty(properties.get(Constants.REPOSITORY_PROP))) {
-          result.add(new InvalidProperty(Constants.REPOSITORY_PROP, "Repository must be specified"));
-        }
-        return result;
-      }
-    };
-  }
-
-  @NotNull
-  public String getVcsSettingsJspFilePath() {
-    return "mercurialSettings.jsp";
-  }
-
-  @NotNull
-  public String getCurrentVersion(@NotNull final VcsRoot root) throws VcsException {
-    // we will return full version of the most recent change as current version
-    syncClonedRepository(root);
-    Settings settings = createSettings(root);
-    BranchesCommand branches = new BranchesCommand(settings);
-    Map<String, ChangeSet> result = branches.execute();
-    if (!result.containsKey(settings.getBranchName())) {
-      throw new VcsException("Unable to find current version for the branch: " + settings.getBranchName());
-    }
-
-    return result.get(settings.getBranchName()).getFullVersion();
-  }
-
-  public boolean sourcesUpdatePossibleIfChangesNotFound(@NotNull final VcsRoot root) {
-    return false;
-  }
-
-  @NotNull
-  public String describeVcsRoot(final VcsRoot vcsRoot) {
-    return "mercurial: " + vcsRoot.getProperty(Constants.REPOSITORY_PROP);
-  }
-
-  @Override
-  public TestConnectionSupport getTestConnectionSupport() {
-    return new TestConnectionSupport() {
-      public String testConnection(@NotNull final VcsRoot vcsRoot) throws VcsException {
-        Settings settings = createSettings(vcsRoot);
-        IdentifyCommand id = new IdentifyCommand(settings);
-        StringBuilder res = new StringBuilder();
-        res.append(quoteIfNeeded(settings.getHgCommandPath()));
-        res.append(" identify ");
-        final String obfuscatedUrl = CommandUtil.removePrivateData(settings.getRepositoryUrl(), Collections.singleton(settings.getPassword()));
-        res.append(quoteIfNeeded(obfuscatedUrl));
-        res.append('\n').append(id.execute());
-        return res.toString();
-      }
-    };
-  }
-
-  private String quoteIfNeeded(@NotNull String str) {
-    if (str.indexOf(' ') != -1) {
-      return "\"" + str + "\"";
-    }
-
-    return str;
-  }
-
-  @Nullable
-  public Map<String, String> getDefaultVcsProperties() {
-    Map<String, String> defaults = new HashMap<String, String>();
-    defaults.put(Constants.HG_COMMAND_PATH_PROP, "hg");
-    return defaults;
-  }
-
-  public String getVersionDisplayName(@NotNull final String version, @NotNull final VcsRoot root) throws VcsException {
-    return new ChangeSet(version).getId();
-  }
-
-  @NotNull
-  public Comparator<String> getVersionComparator() {
-    // comparator is called when TeamCity needs to sort modifications in the order of their appearance,
-    // currently we sort changes by revision number, not sure however that this is a good idea,
-    // probably it would be better to sort them by timestamp (and to add timestamp into the version).
-    return new Comparator<String>() {
-      public int compare(final String o1, final String o2) {
-        try {
-          return new ChangeSet(o1).getRevNumber() - new ChangeSet(o2).getRevNumber();
-        } catch (Exception e) {
-          return 1;
-        }
-      }
-    };
-  }
-
-  // builds patch from version to version
-  private void buildIncrementalPatch(final Settings settings, @NotNull final ChangeSet fromVer, @NotNull final ChangeSet toVer, final PatchBuilder builder, final CheckoutRules checkoutRules)
-    throws VcsException, IOException {
-    StatusCommand st = new StatusCommand(settings);
-    st.setFromRevId(fromVer.getId());
-    st.setToRevId(toVer.getId());
-    List<ModifiedFile> modifiedFiles = st.execute();
-    List<String> notDeletedFiles = new ArrayList<String>();
-    for (ModifiedFile f: modifiedFiles) {
-      if (f.getStatus() != ModifiedFile.Status.REMOVED) {
-        notDeletedFiles.add(f.getPath());
-      }
-    }
-
-    if (notDeletedFiles.isEmpty()) return;
-
-    CatCommand cc = new CatCommand(settings);
-    cc.setRevId(toVer.getId());
-    File parentDir = cc.execute(notDeletedFiles);
-
-    try {
-      for (ModifiedFile f: modifiedFiles) {
-        String mappedPath = checkoutRules.map(f.getPath());
-        if (mappedPath == null) continue; // skip
-        final File virtualFile = new File(mappedPath);
-        if (f.getStatus() == ModifiedFile.Status.REMOVED) {
-          builder.deleteFile(virtualFile, true);
-        } else {
-          File realFile = new File(parentDir, f.getPath());
-          FileInputStream is = new FileInputStream(realFile);
-          try {
-            builder.changeOrCreateBinaryFile(virtualFile, null, is, realFile.length());
-          } finally {
-            is.close();
-          }
-        }
-      }
-    } finally {
-      deleteTmpDir(parentDir);
-    }
-  }
-
-  private void deleteTmpDir(File parentDir) {
-    boolean dirDeleted = FileUtil.delete(parentDir);
-    if (!dirDeleted) {
-      Loggers.VCS.warn("Can not delete directory \"" + parentDir.getAbsolutePath() + "\"");
-    }
-  }
-
-  // builds patch by exporting files using specified version
-  private void buildFullPatch(final Settings settings, @NotNull final ChangeSet toVer, final PatchBuilder builder, final CheckoutRules checkoutRules)
-    throws IOException, VcsException {
-    CloneCommand cl = new CloneCommand(settings);
-    // clone from the local repository
-    cl.setRepository(settings.getLocalRepositoryDir().getAbsolutePath());
-    cl.setToId(toVer.getId());
-    cl.setUpdateWorkingDir(false);
-    File tempDir = FileUtil.createTempDirectory("mercurial", toVer.getId());
-    try {
-      final File repRoot = new File(tempDir, "rep");
-      cl.setDestDir(repRoot.getAbsolutePath());
-      cl.execute();
-
-      UpdateCommand up = new UpdateCommand(settings);
-      up.setWorkDirectory(repRoot.getAbsolutePath());
-      up.setToId(toVer.getId());
-      up.execute();
-
-      buildPatchFromDirectory(builder, repRoot, new FileFilter() {
-        public boolean accept(final File file) {
-          return !(file.isDirectory() && ".hg".equals(file.getName()));
-        }
-      }, checkoutRules);
-    } finally {
-      FileUtil.delete(tempDir);
-    }
-  }
-
-  private void buildPatchFromDirectory(final PatchBuilder builder, final File repRoot, final FileFilter filter, final CheckoutRules checkoutRules) throws IOException {
-    buildPatchFromDirectory(repRoot, builder, repRoot, filter, checkoutRules);
-  }
-
-  private void buildPatchFromDirectory(File curDir, final PatchBuilder builder, final File repRoot, final FileFilter filter, final CheckoutRules checkoutRules) throws IOException {
-    File[] files = curDir.listFiles(filter);
-    if (files != null) {
-      for (File realFile: files) {
-        String relPath = realFile.getAbsolutePath().substring(repRoot.getAbsolutePath().length());
-        String mappedPath = checkoutRules.map(relPath);
-        if (mappedPath != null && mappedPath.length() > 0) {
-          final File virtualFile = new File(mappedPath);
-          if (realFile.isDirectory()) {
-            builder.createDirectory(virtualFile);
-            buildPatchFromDirectory(realFile, builder, repRoot, filter, checkoutRules);
-          } else {
-            final FileInputStream is = new FileInputStream(realFile);
-            try {
-              builder.createBinaryFile(virtualFile, null, is, realFile.length());
-            } finally {
-              is.close();
-            }
-          }
-        } else {
-          if (realFile.isDirectory()) {
-            buildPatchFromDirectory(realFile, builder, repRoot, filter, checkoutRules);
-          }
-        }
-      }
-    }
-  }
-
-  // updates current working copy of repository by pulling changes from the repository specified in VCS root
-  private void syncClonedRepository(final VcsRoot root) throws VcsException {
-    Settings settings = createSettings(root);
-    File workDir = settings.getLocalRepositoryDir();
-    lockWorkDir(workDir);
-    try {
-      if (settings.hasCopyOfRepository()) {
-        // update
-        PullCommand pull = new PullCommand(settings);
-        pull.execute();
-      } else {
-        // clone
-        CloneCommand cl = new CloneCommand(settings);
-        cl.setDestDir(workDir.getAbsolutePath());
-        cl.setUpdateWorkingDir(false);
-        cl.execute();
-      }
-    } finally {
-      unlockWorkDir(workDir);
-    }
-  }
-
-  @Override
-  public LabelingSupport getLabelingSupport() {
-    return this;
-  }
-
-  @NotNull
-  public VcsFileContentProvider getContentProvider() {
-    return this;
-  }
-
-  @NotNull
-  public String getRemoteRunOnBranchPattern() {
-    return "remote-run/{teamcity.username}/.+";
-  }
-
-  @NotNull
-  public Map<String, String> getBranchesRevisions(@NotNull VcsRoot root) throws VcsException {
-    syncClonedRepository(root);
-    Settings settings = createSettings(root);
-    BranchesCommand branches = new BranchesCommand(settings);
-    Map<String, String> result = new HashMap<String, String>();
-    for (Map.Entry<String, ChangeSet> entry : branches.execute().entrySet()) {
-      result.put(entry.getKey(), entry.getValue().getId());
-    }
-    return result;
-  }
-
-  @NotNull
-  public Map<String, String> getBranchRootOptions(@NotNull VcsRoot root, @NotNull String branchName) {
-    final Map<String, String> options = new HashMap<String, String>(root.getProperties());
-    options.put(Constants.BRANCH_NAME_PROP, branchName);
-    return options;
-  }
-
-  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
-    syncClonedRepository(fromRoot);
-    String branchPoint = getBranchPoint(fromRoot, fromRootRevision, toRootRevision);
-    return ((CollectChangesByCheckoutRules) getCollectChangesPolicy()).collectChanges(toRoot, branchPoint, toRootRevision, checkoutRules);
-  }
-
-  private String getBranchPoint(@NotNull VcsRoot root, String branchOneRev, String branchTwoRev) throws VcsException {
-    Settings settings = createSettings(root);
-    LogCommand lc = new LogCommand(settings);
-    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();
-    }
-  }
-
-  @NotNull
-  public CollectChangesPolicy getCollectChangesPolicy() {
-    return new CollectChangesByCheckoutRules() {
-      public List<ModificationData> collectChanges(@NotNull VcsRoot root, @NotNull String fromVersion, @Nullable String currentVersion, @NotNull CheckoutRules checkoutRules) throws VcsException {
-        syncClonedRepository(root);
-
-        // first obtain changes between specified versions
-        List<ModificationData> result = new ArrayList<ModificationData>();
-        if (currentVersion == null) return result;
-
-        Settings settings = createSettings(root);
-        LogCommand lc = new LogCommand(settings);
-        String fromId = new ChangeSetRevision(fromVersion).getId();
-        lc.setFromRevId(fromId);
-        lc.setToRevId(new ChangeSetRevision(currentVersion).getId());
-        List<ChangeSet> changeSets = lc.execute();
-        if (changeSets.isEmpty()) {
-          return result;
-        }
-
-        // invoke status command for each changeset and determine what files were modified in these changesets
-        StatusCommand st = new StatusCommand(settings);
-        ChangeSet prev = new ChangeSet(fromVersion);
-        for (ChangeSet cur : changeSets) {
-          if (cur.getId().equals(fromId)) continue; // skip already reported changeset
-
-          String prevId = prev.getId();
-          List<ChangeSetRevision> curParents = cur.getParents();
-          boolean merge = curParents != null && curParents.size() > 1;
-          if (curParents != null && !merge) {
-            prevId = curParents.get(0).getId();
-          }
-
-          List<ModifiedFile> modifiedFiles = new ArrayList<ModifiedFile>();
-          if (merge) {
-            modifiedFiles.addAll(computeModifiedFilesForMergeCommit(settings, cur));
-          } else {
-            st.setFromRevId(prevId);
-            st.setToRevId(cur.getId());
-            modifiedFiles = st.execute();
-          }
-
-          // changeset full version will be set into VcsChange structure and
-          // stored in database (note that getContent method will be invoked with this version)
-          List<VcsChange> files = toVcsChanges(modifiedFiles, prev.getFullVersion(), cur.getFullVersion(), checkoutRules);
-          if (files.isEmpty() && !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;
-      }
-    };
-  }
-
-  @NotNull
-  public BuildPatchPolicy getBuildPatchPolicy() {
-    return new BuildPatchByCheckoutRules() {
-      public void buildPatch(@NotNull final VcsRoot root,
-                             @Nullable final String fromVersion,
-                             @NotNull final String toVersion,
-                             @NotNull final PatchBuilder builder,
-                             @NotNull final CheckoutRules checkoutRules) throws IOException, VcsException {
-        syncClonedRepository(root);
-        Settings settings = createSettings(root);
-        if (fromVersion == null) {
-          buildFullPatch(settings, new ChangeSet(toVersion), builder, checkoutRules);
-        } else {
-          buildIncrementalPatch(settings, new ChangeSet(fromVersion), new ChangeSet(toVersion), builder, checkoutRules);
-        }
-      }
-    };
-  }
-
-  private void lockWorkDir(@NotNull File workDir) {
-    getWorkDirLock(workDir).lock();
-  }
-
-  private void unlockWorkDir(@NotNull File workDir) {
-    getWorkDirLock(workDir).unlock();
-  }
-
-  @Override
-  public boolean allowSourceCaching() {
-    // since a copy of repository for each VCS root is already stored on disk
-    // we do not need separate cache for our patches
-    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 = getAllClonedRepos();
-    if (workDirs == null) return;
-
-    for (VcsRoot vcsRoot: getMercurialVcsRoots()) {
-      try {
-        Settings s = createSettings(vcsRoot);
-        workDirs.remove(PathUtil.getCanonicalFile(s.getLocalRepositoryDir()));
-      } catch (VcsException e) {
-        Loggers.VCS.error(e);
-      }
-    }
-
-    for (File f: workDirs) {
-      lockWorkDir(f);
-      try {
-        FileUtil.delete(f);
-      } finally {
-        unlockWorkDir(f);
-      }
-    }
-  }
-
-  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;
-  }
-
-  @Nullable
-  private Set<File> getAllClonedRepos() {
-    File workFoldersParent = myDefaultWorkFolderParent;
-    if (!workFoldersParent.isDirectory()) return null;
-
-    Set<File> workDirs = new HashSet<File>();
-    File[] files = workFoldersParent.listFiles(new FileFilter() {
-      public boolean accept(final File file) {
-        return file.isDirectory() && file.getName().startsWith(Settings.DEFAULT_WORK_DIR_PREFIX);
-      }
-    });
-    if (files != null) {
-      for (File f: files) {
-        workDirs.add(PathUtil.getCanonicalFile(f));
-      }
-    }
-    return workDirs;
-  }
-
-  public String label(@NotNull String label, @NotNull String version, @NotNull VcsRoot root, @NotNull CheckoutRules checkoutRules) throws VcsException {
-    syncClonedRepository(root);
-
-    Settings settings = createSettings(root);
-
-    // I do not know why but hg tag does not work correctly if
-    // update command was not invoked for the current repo
-    // in such case if there were no tags before Mercurial attempts to
-    // create new head when tag is pushed to the parent repository
-    UpdateCommand uc = new UpdateCommand(settings);
-    uc.execute();
-
-    String fixedTagname = fixTagName(label);
-    TagCommand tc = new TagCommand(settings);
-    tc.setRevId(new ChangeSet(version).getId());
-    tc.setTag(fixedTagname);
-    tc.execute();
-
-    PushCommand pc = new PushCommand(settings);
-//    pc.setForce(true);
-    pc.execute();
-    return fixedTagname;
-  }
-
-  private String fixTagName(final String label) {
-    // according to Mercurial documentation http://hgbook.red-bean.com/hgbookch8.html#x12-1570008
-    // tag name must not contain:
-    // Colon (ASCII 58, �:�)
-    // Carriage return (ASCII 13, �\r�)
-    // Newline (ASCII 10, �\n�)
-    // all these characters will be replaced with _ (underscore)
-    return label.replace(':', '_').replace('\r', '_').replace('\n', '_');
-  }
-
-  private Settings createSettings(final VcsRoot root) throws VcsException {
-    Settings settings = new Settings(myDefaultWorkFolderParent, root);
-    String customClonePath = root.getProperty(Constants.SERVER_CLONE_PATH_PROP);
-    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[] splitted = repPath.split("[/\\\\]");
-      if (splitted.length > 0) {
-        repPath = splitted[splitted.length-1];
-      }
-
-      File customWorkingDir = new File(parentDir, repPath);
-      settings.setWorkingDir(customWorkingDir);
-    } else {
-      createClonedRepositoryParentDir(myDefaultWorkFolderParent);
-    }
-    return settings;
-  }
-
-  private void createClonedRepositoryParentDir(final File parentDir) throws VcsException {
-    if (!parentDir.exists() && !parentDir.mkdirs()) {
-      throw new VcsException("Failed to create parent directory for cloned repository: " + parentDir.getAbsolutePath());
-    }
-  }
-
-  public boolean isAgentSideCheckoutAvailable() {
-    return true;
-  }
-}
+/*
+ * 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.BuildAgent;
+import jetbrains.buildServer.Used;
+import jetbrains.buildServer.buildTriggers.vcs.AbstractVcsPropertiesProcessor;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.*;
+import jetbrains.buildServer.log.Loggers;
+import jetbrains.buildServer.serverSide.*;
+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.vcs.*;
+import jetbrains.buildServer.vcs.patches.PatchBuilder;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.FileFilter;
+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;
+
+/**
+ * Mercurial VCS plugin for TeamCity works as follows:
+ * <ul>
+ * <li>clones repository to internal storage
+ * <li>before any operation with working copy of repository pulls changes from the original repository
+ * <li>executes corresponding hg command
+ * </ul>
+ *
+ * <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 ConcurrentMap<String, Lock> myWorkDirLocks= new ConcurrentHashMap<String, Lock>();
+  private VcsManager myVcsManager;
+  private File myDefaultWorkFolderParent;
+
+  public MercurialVcsSupport(@NotNull final VcsManager vcsManager,
+                             @NotNull ServerPaths paths,
+                             @NotNull final SBuildServer server,
+                             @NotNull EventDispatcher<BuildServerListener> dispatcher) {
+    myVcsManager = vcsManager;
+    myDefaultWorkFolderParent = new File(paths.getCachesDir(), "mercurial");
+    dispatcher.addListener(new BuildServerAdapter() {
+      @Override
+      public void cleanupFinished() {
+        super.cleanupFinished();
+        server.getExecutor().submit(new Runnable() {
+          public void run() {
+            removeOldWorkFolders();
+          }
+        });
+      }
+
+      @Override
+      public void sourcesVersionReleased(@NotNull final BuildAgent agent) {
+        super.sourcesVersionReleased(agent);
+        server.getExecutor().submit(new Runnable() {
+          public void run() {
+            Set<File> clonedRepos = getAllClonedRepos();
+            if (clonedRepos == null) return;
+            for (File f: clonedRepos) {
+              lockWorkDir(f);
+              try {
+                FileUtil.delete(f);
+              } finally {
+                unlockWorkDir(f);
+              }
+            }
+          }
+        });
+      }
+    });
+  }
+
+  private Collection<ModifiedFile> computeModifiedFilesForMergeCommit(final Settings settings, final ChangeSet cur) throws VcsException {
+    ChangedFilesCommand cfc = new ChangedFilesCommand(settings);
+    cfc.setRevId(cur.getId());
+    return cfc.execute();
+  }
+
+  private List<VcsChange> toVcsChanges(final List<ModifiedFile> modifiedFiles, String prevVer, String curVer, CheckoutRules rules) {
+    List<VcsChange> files = new ArrayList<VcsChange>();
+    for (ModifiedFile mf: modifiedFiles) {
+      String normalizedPath = PathUtil.normalizeSeparator(mf.getPath());
+      VcsChangeInfo.Type changeType = getChangeType(mf.getStatus());
+      if (changeType == null) {
+        Loggers.VCS.warn("Unable to convert status: " + mf.getStatus() + " to VCS change type");
+        changeType = VcsChangeInfo.Type.NOT_CHANGED;
+      }
+      files.add(new VcsChange(changeType, mf.getStatus().getName(), normalizedPath, rules.map(mf.getPath()), prevVer, curVer));
+    }
+    return files;
+  }
+
+  private VcsChangeInfo.Type getChangeType(final ModifiedFile.Status status) {
+    switch (status) {
+      case ADDED:return VcsChangeInfo.Type.ADDED;
+      case MODIFIED:return VcsChangeInfo.Type.CHANGED;
+      case REMOVED:return VcsChangeInfo.Type.REMOVED;
+    }
+    return null;
+  }
+
+  @NotNull
+  public byte[] getContent(@NotNull final VcsModification vcsModification,
+                           @NotNull final VcsChangeInfo change,
+                           @NotNull final VcsChangeInfo.ContentType contentType,
+                           @NotNull final VcsRoot vcsRoot) throws VcsException {
+    syncClonedRepository(vcsRoot);
+    String version = contentType == VcsChangeInfo.ContentType.AFTER_CHANGE ? change.getAfterChangeRevisionNumber() : change.getBeforeChangeRevisionNumber();
+    return getContent(change.getRelativeFileName(), vcsRoot, version);
+  }
+
+  @NotNull
+  public byte[] getContent(@NotNull final String filePath, @NotNull final VcsRoot vcsRoot, @NotNull final String version) throws VcsException {
+    syncClonedRepository(vcsRoot);
+    Settings settings = createSettings(vcsRoot);
+    CatCommand cc = new CatCommand(settings);
+    cc.setRevId(new ChangeSet(version).getId());
+    File parentDir = cc.execute(Collections.singletonList(filePath));
+    try {
+      File file = new File(parentDir, filePath);
+      if (file.isFile()) {
+        try {
+          return FileUtil.loadFileBytes(file);
+        } catch (IOException e) {
+          throw new VcsException("Failed to load content of file: " + file.getAbsolutePath(), e);
+        }
+      } else {
+        Loggers.VCS.warn("Unable to obtain content of the file: " + filePath);
+      }
+    } finally {
+      deleteTmpDir(parentDir);
+    }
+    return new byte[0];
+  }
+
+  @NotNull
+  public String getName() {
+    return Constants.VCS_NAME;
+  }
+
+  @NotNull
+  @Used("jsp")
+  public String getDisplayName() {
+    return "Mercurial";
+  }
+
+  @Nullable
+  public PropertiesProcessor getVcsPropertiesProcessor() {
+    return new AbstractVcsPropertiesProcessor() {
+      public Collection<InvalidProperty> process(final Map<String, String> properties) {
+        List<InvalidProperty> result = new ArrayList<InvalidProperty>();
+        if (isEmpty(properties.get(Constants.HG_COMMAND_PATH_PROP))) {
+          result.add(new InvalidProperty(Constants.HG_COMMAND_PATH_PROP, "Path to 'hg' command must be specified"));
+        } 
+        if (isEmpty(properties.get(Constants.REPOSITORY_PROP))) {
+          result.add(new InvalidProperty(Constants.REPOSITORY_PROP, "Repository must be specified"));
+        }
+        return result;
+      }
+    };
+  }
+
+  @NotNull
+  public String getVcsSettingsJspFilePath() {
+    return "mercurialSettings.jsp";
+  }
+
+  @NotNull
+  public String getCurrentVersion(@NotNull final VcsRoot root) throws VcsException {
+    // we will return full version of the most recent change as current version
+    syncClonedRepository(root);
+    Settings settings = createSettings(root);
+    BranchesCommand branches = new BranchesCommand(settings);
+    Map<String, ChangeSet> result = branches.execute();
+    if (!result.containsKey(settings.getBranchName())) {
+      throw new VcsException("Unable to find current version for the branch: " + settings.getBranchName());
+    }
+
+    return result.get(settings.getBranchName()).getFullVersion();
+  }
+
+  public boolean sourcesUpdatePossibleIfChangesNotFound(@NotNull final VcsRoot root) {
+    return false;
+  }
+
+  @NotNull
+  public String describeVcsRoot(final VcsRoot vcsRoot) {
+    return "mercurial: " + vcsRoot.getProperty(Constants.REPOSITORY_PROP);
+  }
+
+  @Override
+  public TestConnectionSupport getTestConnectionSupport() {
+    return new TestConnectionSupport() {
+      public String testConnection(@NotNull final VcsRoot vcsRoot) throws VcsException {
+        Settings settings = createSettings(vcsRoot);
+        IdentifyCommand id = new IdentifyCommand(settings);
+        StringBuilder res = new StringBuilder();
+        res.append(quoteIfNeeded(settings.getHgCommandPath()));
+        res.append(" identify ");
+        final String obfuscatedUrl = CommandUtil.removePrivateData(settings.getRepositoryUrl(), Collections.singleton(settings.getPassword()));
+        res.append(quoteIfNeeded(obfuscatedUrl));
+        res.append('\n').append(id.execute());
+        return res.toString();
+      }
+    };
+  }
+
+  private String quoteIfNeeded(@NotNull String str) {
+    if (str.indexOf(' ') != -1) {
+      return "\"" + str + "\"";
+    }
+
+    return str;
+  }
+
+  @Nullable
+  public Map<String, String> getDefaultVcsProperties() {
+    Map<String, String> defaults = new HashMap<String, String>();
+    defaults.put(Constants.HG_COMMAND_PATH_PROP, "hg");
+    defaults.put(Constants.UNCOMPRESSED_TRANSFER, "false");
+    return defaults;
+  }
+
+  public String getVersionDisplayName(@NotNull final String version, @NotNull final VcsRoot root) throws VcsException {
+    return new ChangeSet(version).getId();
+  }
+
+  @NotNull
+  public Comparator<String> getVersionComparator() {
+    // comparator is called when TeamCity needs to sort modifications in the order of their appearance,
+    // currently we sort changes by revision number, not sure however that this is a good idea,
+    // probably it would be better to sort them by timestamp (and to add timestamp into the version).
+    return new Comparator<String>() {
+      public int compare(final String o1, final String o2) {
+        try {
+          return new ChangeSet(o1).getRevNumber() - new ChangeSet(o2).getRevNumber();
+        } catch (Exception e) {
+          return 1;
+        }
+      }
+    };
+  }
+
+  // builds patch from version to version
+  private void buildIncrementalPatch(final Settings settings, @NotNull final ChangeSet fromVer, @NotNull final ChangeSet toVer, final PatchBuilder builder, final CheckoutRules checkoutRules)
+    throws VcsException, IOException {
+    StatusCommand st = new StatusCommand(settings);
+    st.setFromRevId(fromVer.getId());
+    st.setToRevId(toVer.getId());
+    List<ModifiedFile> modifiedFiles = st.execute();
+    List<String> notDeletedFiles = new ArrayList<String>();
+    for (ModifiedFile f: modifiedFiles) {
+      if (f.getStatus() != ModifiedFile.Status.REMOVED) {
+        notDeletedFiles.add(f.getPath());
+      }
+    }
+
+    if (notDeletedFiles.isEmpty()) return;
+
+    CatCommand cc = new CatCommand(settings);
+    cc.setRevId(toVer.getId());
+    File parentDir = cc.execute(notDeletedFiles);
+
+    try {
+      for (ModifiedFile f: modifiedFiles) {
+        String mappedPath = checkoutRules.map(f.getPath());
+        if (mappedPath == null) continue; // skip
+        final File virtualFile = new File(mappedPath);
+        if (f.getStatus() == ModifiedFile.Status.REMOVED) {
+          builder.deleteFile(virtualFile, true);
+        } else {
+          File realFile = new File(parentDir, f.getPath());
+          FileInputStream is = new FileInputStream(realFile);
+          try {
+            builder.changeOrCreateBinaryFile(virtualFile, null, is, realFile.length());
+          } finally {
+            is.close();
+          }
+        }
+      }
+    } finally {
+      deleteTmpDir(parentDir);
+    }
+  }
+
+  private void deleteTmpDir(File parentDir) {
+    boolean dirDeleted = FileUtil.delete(parentDir);
+    if (!dirDeleted) {
+      Loggers.VCS.warn("Can not delete directory \"" + parentDir.getAbsolutePath() + "\"");
+    }
+  }
+
+  // builds patch by exporting files using specified version
+  private void buildFullPatch(final Settings settings, @NotNull final ChangeSet toVer, final PatchBuilder builder, final CheckoutRules checkoutRules)
+    throws IOException, VcsException {
+    CloneCommand cl = new CloneCommand(settings);
+    // clone from the local repository
+    cl.setRepository(settings.getLocalRepositoryDir().getAbsolutePath());
+    cl.setToId(toVer.getId());
+    cl.setUpdateWorkingDir(false);
+    File tempDir = FileUtil.createTempDirectory("mercurial", toVer.getId());
+    try {
+      final File repRoot = new File(tempDir, "rep");
+      cl.setDestDir(repRoot.getAbsolutePath());
+      cl.execute();
+
+      UpdateCommand up = new UpdateCommand(settings);
+      up.setWorkDirectory(repRoot.getAbsolutePath());
+      up.setToId(toVer.getId());
+      up.execute();
+
+      buildPatchFromDirectory(builder, repRoot, new FileFilter() {
+        public boolean accept(final File file) {
+          return !(file.isDirectory() && ".hg".equals(file.getName()));
+        }
+      }, checkoutRules);
+    } finally {
+      FileUtil.delete(tempDir);
+    }
+  }
+
+  private void buildPatchFromDirectory(final PatchBuilder builder, final File repRoot, final FileFilter filter, final CheckoutRules checkoutRules) throws IOException {
+    buildPatchFromDirectory(repRoot, builder, repRoot, filter, checkoutRules);
+  }
+
+  private void buildPatchFromDirectory(File curDir, final PatchBuilder builder, final File repRoot, final FileFilter filter, final CheckoutRules checkoutRules) throws IOException {
+    File[] files = curDir.listFiles(filter);
+    if (files != null) {
+      for (File realFile: files) {
+        String relPath = realFile.getAbsolutePath().substring(repRoot.getAbsolutePath().length());
+        String mappedPath = checkoutRules.map(relPath);
+        if (mappedPath != null && mappedPath.length() > 0) {
+          final File virtualFile = new File(mappedPath);
+          if (realFile.isDirectory()) {
+            builder.createDirectory(virtualFile);
+            buildPatchFromDirectory(realFile, builder, repRoot, filter, checkoutRules);
+          } else {
+            final FileInputStream is = new FileInputStream(realFile);
+            try {
+              builder.createBinaryFile(virtualFile, null, is, realFile.length());
+            } finally {
+              is.close();
+            }
+          }
+        } else {
+          if (realFile.isDirectory()) {
+            buildPatchFromDirectory(realFile, builder, repRoot, filter, checkoutRules);
+          }
+        }
+      }
+    }
+  }
+
+  // updates current working copy of repository by pulling changes from the repository specified in VCS root
+  private void syncClonedRepository(final VcsRoot root) throws VcsException {
+    Settings settings = createSettings(root);
+    File workDir = settings.getLocalRepositoryDir();
+    lockWorkDir(workDir);
+    try {
+      if (settings.hasCopyOfRepository()) {
+        // update
+        PullCommand pull = new PullCommand(settings);
+        pull.execute();
+      } else {
+        // clone
+        CloneCommand cl = new CloneCommand(settings);
+        cl.setDestDir(workDir.getAbsolutePath());
+        cl.setUpdateWorkingDir(false);
+        cl.execute();
+      }
+    } finally {
+      unlockWorkDir(workDir);
+    }
+  }
+
+  @Override
+  public LabelingSupport getLabelingSupport() {
+    return this;
+  }
+
+  @NotNull
+  public VcsFileContentProvider getContentProvider() {
+    return this;
+  }
+
+  @NotNull
+  public String getRemoteRunOnBranchPattern() {
+    return "remote-run/{teamcity.username}/.+";
+  }
+
+  @NotNull
+  public Map<String, String> getBranchesRevisions(@NotNull VcsRoot root) throws VcsException {
+    syncClonedRepository(root);
+    Settings settings = createSettings(root);
+    BranchesCommand branches = new BranchesCommand(settings);
+    Map<String, String> result = new HashMap<String, String>();
+    for (Map.Entry<String, ChangeSet> entry : branches.execute().entrySet()) {
+      result.put(entry.getKey(), entry.getValue().getId());
+    }
+    return result;
+  }
+
+  @NotNull
+  public Map<String, String> getBranchRootOptions(@NotNull VcsRoot root, @NotNull String branchName) {
+    final Map<String, String> options = new HashMap<String, String>(root.getProperties());
+    options.put(Constants.BRANCH_NAME_PROP, branchName);
+    return options;
+  }
+
+  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
+    syncClonedRepository(fromRoot);
+    String branchPoint = getBranchPoint(fromRoot, fromRootRevision, toRootRevision);
+    return ((CollectChangesByCheckoutRules) getCollectChangesPolicy()).collectChanges(toRoot, branchPoint, toRootRevision, checkoutRules);
+  }
+
+  private String getBranchPoint(@NotNull VcsRoot root, String branchOneRev, String branchTwoRev) throws VcsException {
+    Settings settings = createSettings(root);
+    LogCommand lc = new LogCommand(settings);
+    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();
+    }
+  }
+
+  @NotNull
+  public CollectChangesPolicy getCollectChangesPolicy() {
+    return new CollectChangesByCheckoutRules() {
+      public List<ModificationData> collectChanges(@NotNull VcsRoot root, @NotNull String fromVersion, @Nullable String currentVersion, @NotNull CheckoutRules checkoutRules) throws VcsException {
+        syncClonedRepository(root);
+
+        // first obtain changes between specified versions
+        List<ModificationData> result = new ArrayList<ModificationData>();
+        if (currentVersion == null) return result;
+
+        Settings settings = createSettings(root);
+        LogCommand lc = new LogCommand(settings);
+        String fromId = new ChangeSetRevision(fromVersion).getId();
+        lc.setFromRevId(fromId);
+        lc.setToRevId(new ChangeSetRevision(currentVersion).getId());
+        List<ChangeSet> changeSets = lc.execute();
+        if (changeSets.isEmpty()) {
+          return result;
+        }
+
+        // invoke status command for each changeset and determine what files were modified in these changesets
+        StatusCommand st = new StatusCommand(settings);
+        ChangeSet prev = new ChangeSet(fromVersion);
+        for (ChangeSet cur : changeSets) {
+          if (cur.getId().equals(fromId)) continue; // skip already reported changeset
+
+          String prevId = prev.getId();
+          List<ChangeSetRevision> curParents = cur.getParents();
+          boolean merge = curParents != null && curParents.size() > 1;
+          if (curParents != null && !merge) {
+            prevId = curParents.get(0).getId();
+          }
+
+          List<ModifiedFile> modifiedFiles = new ArrayList<ModifiedFile>();
+          if (merge) {
+            modifiedFiles.addAll(computeModifiedFilesForMergeCommit(settings, cur));
+          } else {
+            st.setFromRevId(prevId);
+            st.setToRevId(cur.getId());
+            modifiedFiles = st.execute();
+          }
+
+          // changeset full version will be set into VcsChange structure and
+          // stored in database (note that getContent method will be invoked with this version)
+          List<VcsChange> files = toVcsChanges(modifiedFiles, prev.getFullVersion(), cur.getFullVersion(), checkoutRules);
+          if (files.isEmpty() && !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;
+      }
+    };
+  }
+
+  @NotNull
+  public BuildPatchPolicy getBuildPatchPolicy() {
+    return new BuildPatchByCheckoutRules() {
+      public void buildPatch(@NotNull final VcsRoot root,
+                             @Nullable final String fromVersion,
+                             @NotNull final String toVersion,
+                             @NotNull final PatchBuilder builder,
+                             @NotNull final CheckoutRules checkoutRules) throws IOException, VcsException {
+        syncClonedRepository(root);
+        Settings settings = createSettings(root);
+        if (fromVersion == null) {
+          buildFullPatch(settings, new ChangeSet(toVersion), builder, checkoutRules);
+        } else {
+          buildIncrementalPatch(settings, new ChangeSet(fromVersion), new ChangeSet(toVersion), builder, checkoutRules);
+        }
+      }
+    };
+  }
+
+  private void lockWorkDir(@NotNull File workDir) {
+    getWorkDirLock(workDir).lock();
+  }
+
+  private void unlockWorkDir(@NotNull File workDir) {
+    getWorkDirLock(workDir).unlock();
+  }
+
+  @Override
+  public boolean allowSourceCaching() {
+    // since a copy of repository for each VCS root is already stored on disk
+    // we do not need separate cache for our patches
+    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 = getAllClonedRepos();
+    if (workDirs == null) return;
+
+    for (VcsRoot vcsRoot: getMercurialVcsRoots()) {
+      try {
+        Settings s = createSettings(vcsRoot);
+        workDirs.remove(PathUtil.getCanonicalFile(s.getLocalRepositoryDir()));
+      } catch (VcsException e) {
+        Loggers.VCS.error(e);
+      }
+    }
+
+    for (File f: workDirs) {
+      lockWorkDir(f);
+      try {
+        FileUtil.delete(f);
+      } finally {
+        unlockWorkDir(f);
+      }
+    }
+  }
+
+  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;
+  }
+
+  @Nullable
+  private Set<File> getAllClonedRepos() {
+    File workFoldersParent = myDefaultWorkFolderParent;
+    if (!workFoldersParent.isDirectory()) return null;
+
+    Set<File> workDirs = new HashSet<File>();
+    File[] files = workFoldersParent.listFiles(new FileFilter() {
+      public boolean accept(final File file) {
+        return file.isDirectory() && file.getName().startsWith(Settings.DEFAULT_WORK_DIR_PREFIX);
+      }
+    });
+    if (files != null) {
+      for (File f: files) {
+        workDirs.add(PathUtil.getCanonicalFile(f));
+      }
+    }
+    return workDirs;
+  }
+
+  public String label(@NotNull String label, @NotNull String version, @NotNull VcsRoot root, @NotNull CheckoutRules checkoutRules) throws VcsException {
+    syncClonedRepository(root);
+
+    Settings settings = createSettings(root);
+
+    // I do not know why but hg tag does not work correctly if
+    // update command was not invoked for the current repo
+    // in such case if there were no tags before Mercurial attempts to
+    // create new head when tag is pushed to the parent repository
+    UpdateCommand uc = new UpdateCommand(settings);
+    uc.execute();
+
+    String fixedTagname = fixTagName(label);
+    TagCommand tc = new TagCommand(settings);
+    tc.setRevId(new ChangeSet(version).getId());
+    tc.setTag(fixedTagname);
+    tc.execute();
+
+    PushCommand pc = new PushCommand(settings);
+//    pc.setForce(true);
+    pc.execute();
+    return fixedTagname;
+  }
+
+  private String fixTagName(final String label) {
+    // according to Mercurial documentation http://hgbook.red-bean.com/hgbookch8.html#x12-1570008
+    // tag name must not contain:
+    // Colon (ASCII 58, �:�)
+    // Carriage return (ASCII 13, �\r�)
+    // Newline (ASCII 10, �\n�)
+    // all these characters will be replaced with _ (underscore)
+    return label.replace(':', '_').replace('\r', '_').replace('\n', '_');
+  }
+
+  private Settings createSettings(final VcsRoot root) throws VcsException {
+    Settings settings = new Settings(myDefaultWorkFolderParent, root);
+    String customClonePath = root.getProperty(Constants.SERVER_CLONE_PATH_PROP);
+    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[] splitted = repPath.split("[/\\\\]");
+      if (splitted.length > 0) {
+        repPath = splitted[splitted.length-1];
+      }
+
+      File customWorkingDir = new File(parentDir, repPath);
+      settings.setWorkingDir(customWorkingDir);
+    } else {
+      createClonedRepositoryParentDir(myDefaultWorkFolderParent);
+    }
+    return settings;
+  }
+
+  private void createClonedRepositoryParentDir(final File parentDir) throws VcsException {
+    if (!parentDir.exists() && !parentDir.mkdirs()) {
+      throw new VcsException("Failed to create parent directory for cloned repository: " + parentDir.getAbsolutePath());
+    }
+  }
+
+  public boolean isAgentSideCheckoutAvailable() {
+    return true;
+  }
+}
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java	Fri Feb 04 13:53:02 2011 +0300
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java	Wed Feb 09 14:01:26 2011 +0300
@@ -1,431 +1,439 @@
-/*
- * 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 com.intellij.execution.configurations.GeneralCommandLine;
-import jetbrains.buildServer.ExecResult;
-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.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FilenameFilter;
-import java.io.IOException;
-import java.util.*;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-
-@Test
-public class MercurialVcsSupportTest extends BaseMercurialTestCase {
-  private MercurialVcsSupport myVcs;
-  private ServerPaths myServerPaths;
-
-  @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);
-  }
-
-  protected String getTestDataPath() {
-    return "mercurial-tests/testData";
-  }
-
-  public void test_get_current_version() throws Exception {
-    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
-
-    assertEquals(myVcs.getCurrentVersion(vcsRoot), "10:9c6a6b4aede0");
-    assertEquals("9c6a6b4aede0", myVcs.getVersionDisplayName("10:9c6a6b4aede0", vcsRoot));
-
-    assertEquals(myVcs.getCurrentVersion(createVcsRoot(simpleRepo(), "test_branch")), "8:04c3ae4c6312");
-
-    assertEquals(myVcs.getCurrentVersion(createVcsRoot(simpleRepo(), "name with space")), "9:9babcf2d5705");
-  }
-
-  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);
-  }
-
-  public void test_collect_changes_between_two_same_roots() throws Exception {
-    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
-    VcsRootImpl sameVcsRoot = createVcsRoot(simpleRepo());
-    List<ModificationData> changes = myVcs.collectChanges(vcsRoot, "0:9875b412a788", sameVcsRoot, "3:9522278aa38d", new CheckoutRules(""));
-    do_check_for_collect_changes(changes);
-  }
-
-  public void test_collect_changes() throws Exception {
-    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
-    List<ModificationData> changes = collectChanges(vcsRoot, "0:9875b412a788", "3:9522278aa38d", new CheckoutRules(""));
-    do_check_for_collect_changes(changes);
-  }
-
-  private void do_check_for_collect_changes(List<ModificationData> changes) throws Exception {
-    assertEquals(3, changes.size());
-
-    ModificationData md1 = changes.get(0);
-    ModificationData md2 = changes.get(1);
-    ModificationData md3 = changes.get(2);
-    assertEquals(md1.getVersion(), "1:1d446e82d356");
-    assertEquals(md1.getDescription(), "new file added");
-    List<VcsChange> files1 = md1.getChanges();
-    assertEquals(1, files1.size());
-    assertEquals(VcsChangeInfo.Type.ADDED, files1.get(0).getType());
-    assertEquals(normalizePath(files1.get(0).getRelativeFileName()), "dir1/file3.txt");
-
-    assertEquals(md2.getVersion(), "2:7209b1f1d793");
-    assertEquals(md2.getDescription(), "file4.txt added");
-    List<VcsChange> files2 = md2.getChanges();
-    assertEquals(1, files2.size());
-    assertEquals(files2.get(0).getType(), VcsChangeInfo.Type.ADDED);
-    assertEquals(normalizePath(files2.get(0).getRelativeFileName()), "dir1/file4.txt");
-
-    assertEquals(md3.getVersion(), "3:9522278aa38d");
-    assertEquals(md3.getDescription(), "file removed");
-    List<VcsChange> files3 = md3.getChanges();
-    assertEquals(1, files3.size());
-    assertEquals(files3.get(0).getType(), VcsChangeInfo.Type.REMOVED);
-    assertEquals(normalizePath(files3.get(0).getRelativeFileName()), "dir1/file4.txt");
-  }
-
-  private ByteArrayOutputStream buildPatch(VcsRoot vcsRoot, String from, String to, CheckoutRules rules) throws IOException, VcsException {
-    final ByteArrayOutputStream output = new ByteArrayOutputStream();
-    final PatchBuilderImpl builder = new PatchBuilderImpl(output);
-
-    ((BuildPatchByCheckoutRules)myVcs.getBuildPatchPolicy()).buildPatch(vcsRoot, from, to, builder, rules);
-    builder.close();
-    return output;
-  }
-  
-  @Test
-  public void test_build_patch() throws IOException, VcsException {
-    setName("cleanPatch1");
-    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
-
-    ByteArrayOutputStream output = buildPatch(vcsRoot, null, "4:b06a290a363b", new CheckoutRules(""));
-    checkPatchResult(output.toByteArray());
-
-    File clonedReposParentDir = new File(myServerPaths.getCachesDir(), "mercurial");
-    assertTrue(clonedReposParentDir.isDirectory());
-    assertTrue(1 == clonedReposParentDir.list(new FilenameFilter() {
-      public boolean accept(final File dir, final String name) {
-        return name.startsWith("hg_");
-      }
-    }).length);
-  }
-
-  public void test_build_incremental_patch() throws IOException, VcsException {
-    setName("patch1");
-    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
-
-    ByteArrayOutputStream output = buildPatch(vcsRoot, "3:9522278aa38d", "4:b06a290a363b", new CheckoutRules(""));
-
-    checkPatchResult(output.toByteArray());
-  }
-
-  public void test_build_incremental_patch_checkout_rules() throws IOException, VcsException {
-    setName("patch1_checkout_rules");
-    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
-
-    ByteArrayOutputStream output = buildPatch(vcsRoot, "3:9522278aa38d", "4:b06a290a363b", new CheckoutRules("+:dir1=>path"));
-
-    checkPatchResult(output.toByteArray());
-  }
-
-  public void test_build_clean_patch_checkout_rules() throws IOException, VcsException {
-    setName("cleanPatch1_checkout_rules");
-    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
-
-    ByteArrayOutputStream output = buildPatch(vcsRoot, null, "4:b06a290a363b", new CheckoutRules("+:dir1/subdir=>."));
-
-    checkPatchResult(output.toByteArray());
-  }
-
-  public void test_build_incremental_patch_file_with_space() throws IOException, VcsException {
-    setName("patch2");
-    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
-
-    ByteArrayOutputStream output = buildPatch(vcsRoot, "3:9522278aa38d", "6:b9deb9a1c6f4", new CheckoutRules(""));
-
-    checkPatchResult(output.toByteArray());
-  }
-
-  public void test_get_content() throws IOException, VcsException {
-    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
-
-    byte[] content = myVcs.getContent("dir1/subdir/file2.txt", vcsRoot, "4:b06a290a363b");
-    assertEquals(new String(content), "bbb");
-    content = myVcs.getContent("dir1/subdir/file2.txt", vcsRoot, "5:1d2cc6f3bc29");
-    assertEquals(new String(content), "modified\r\nbbb");
-  }
-
-  public void test_get_content_in_branch() throws IOException, VcsException {
-    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch");
-
-    byte[] content = myVcs.getContent("file_in_branch.txt", vcsRoot, "8:04c3ae4c6312");
-    assertEquals(new String(content), "file from the test_branch\r\nfile modified");
-  }
-
-  public void test_test_connection() throws IOException, VcsException {
-    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
-
-    System.out.println(myVcs.getTestConnectionSupport().testConnection(vcsRoot));
-
-    vcsRoot.addProperty(Constants.REPOSITORY_PROP, "/some/non/existent/path");
-    try {
-      myVcs.getTestConnectionSupport().testConnection(vcsRoot);
-      fail("Exception expected");
-    } catch (VcsException e) {
-    }
-  }
-
-  public void test_tag() throws IOException, VcsException {
-    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
-    cleanRepositoryAfterTest(simpleRepo());
-
-    String actualTag = myVcs.label("new:tag", "1:1d446e82d356", vcsRoot, new CheckoutRules(""));
-    assertEquals(actualTag, "new_tag");
-
-    // check the tag is pushed to the parent repository
-    GeneralCommandLine cli = new GeneralCommandLine();
-    cli.setExePath(vcsRoot.getProperty(Constants.HG_COMMAND_PATH_PROP));
-    cli.setWorkDirectory(vcsRoot.getProperty(Constants.REPOSITORY_PROP));
-    cli.addParameter("tags");
-    ExecResult res = CommandUtil.runCommand(cli);
-    assertTrue(res.getStdout().contains("new_tag"));
-    assertTrue(res.getStdout().contains("1:1d446e82d356"));
-  }
-
-  public void test_tag_in_branch() throws IOException, VcsException {
-    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch");
-    cleanRepositoryAfterTest(simpleRepo());
-
-    String actualTag = myVcs.label("branch_tag", "7:376dcf05cd2a", vcsRoot, new CheckoutRules(""));
-    assertEquals(actualTag, "branch_tag");
-
-    // check the tag is pushed to the parent repository
-    GeneralCommandLine cli = new GeneralCommandLine();
-    cli.setExePath(vcsRoot.getProperty(Constants.HG_COMMAND_PATH_PROP));
-    cli.setWorkDirectory(vcsRoot.getProperty(Constants.REPOSITORY_PROP));
-    cli.addParameter("tags");
-    ExecResult res = CommandUtil.runCommand(cli);
-    assertTrue(res.getStdout().contains("branch_tag"));
-    assertTrue(res.getStdout().contains("7:376dcf05cd2a"));
-  }
-
-  public void test_collect_changes_in_branch() throws Exception {
-    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch");
-
-    // fromVersion(6:b9deb9a1c6f4) is not in the branch (it is in the default branch)
-    List<ModificationData> changes = collectChanges(vcsRoot, "6:b9deb9a1c6f4", "7:376dcf05cd2a", CheckoutRules.DEFAULT);
-    assertEquals(1, changes.size());
-
-    ModificationData md1 = changes.get(0);
-    assertEquals(md1.getVersion(), "7:376dcf05cd2a");
-    assertEquals(md1.getDescription(), "new file added in the test_branch");
-    List<VcsChange> files1 = md1.getChanges();
-    assertEquals(1, files1.size());
-    assertEquals(VcsChangeInfo.Type.ADDED, files1.get(0).getType());
-    assertEquals(normalizePath(files1.get(0).getRelativeFileName()), "file_in_branch.txt");
-
-    changes = collectChanges(vcsRoot, "7:376dcf05cd2a", "8:04c3ae4c6312", CheckoutRules.DEFAULT);
-    assertEquals(1, changes.size());
-
-    md1 = changes.get(0);
-    assertEquals(md1.getVersion(), "8:04c3ae4c6312");
-    assertEquals(md1.getDescription(), "file modified");
-  }
-
-  public void test_full_patch_from_branch() throws IOException, VcsException {
-    setName("patch3");
-    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch");
-
-    ByteArrayOutputStream output = buildPatch(vcsRoot, null, "7:376dcf05cd2a", new CheckoutRules(""));
-
-    checkPatchResult(output.toByteArray());
-  }
-
-  public void test_full_patch_from_branch_with_checkout_rules() throws IOException, VcsException {
-    setName("patch3_checkout_rules1");
-    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch");
-
-    ByteArrayOutputStream output = buildPatch(vcsRoot, null, "7:376dcf05cd2a", new CheckoutRules("+:.=>path"));
-
-    checkPatchResult(output.toByteArray());
-  }
-
-  public void test_full_patch_from_branch_with_checkout_rules_mapped_and_skipped() throws IOException, VcsException {
-    setName("patch3_checkout_rules2");
-    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch");
-
-    ByteArrayOutputStream output = buildPatch(vcsRoot, null, "7:376dcf05cd2a", new CheckoutRules("+:dir1=>path/dir1\n+:dir with space"));
-
-    checkPatchResult(output.toByteArray());
-  }
-
-  public void test_incremental_patch_from_branch() throws IOException, VcsException {
-    setName("patch4");
-    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch");
-
-    ByteArrayOutputStream output = buildPatch(vcsRoot, "7:376dcf05cd2a", "8:04c3ae4c6312", new CheckoutRules(""));
-
-    checkPatchResult(output.toByteArray());
-  }
-
-  @Test(enabled = false)
-  public void support_anchor_branch_notation() throws IOException {
-    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
-    String repPath = vcsRoot.getProperty(Constants.REPOSITORY_PROP);
-    vcsRoot.addProperty(Constants.REPOSITORY_PROP, repPath + "#test_branch");
-    Settings settings = new Settings(new File(myServerPaths.getCachesDir()), vcsRoot);
-    assertEquals("test_branch", settings.getBranchName());
-
-    vcsRoot.addProperty(Constants.REPOSITORY_PROP, repPath + "#");
-    settings = new Settings(new File(myServerPaths.getCachesDir()), vcsRoot);
-    assertEquals("default", settings.getBranchName());
-
-    vcsRoot.addProperty(Constants.REPOSITORY_PROP, repPath);
-    settings = new Settings(new File(myServerPaths.getCachesDir()), vcsRoot);
-    assertEquals("default", settings.getBranchName());
-  }
-
-  public void build_patch_using_custom_clone_path() throws IOException, VcsException {
-    setName("cleanPatch1");
-    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
-    File cloneDir = myTempFiles.createTempDir();
-    vcsRoot.addProperty(Constants.SERVER_CLONE_PATH_PROP, cloneDir.getAbsolutePath());
-
-    ByteArrayOutputStream output = buildPatch(vcsRoot, null, "4:b06a290a363b", new CheckoutRules(""));
-
-    checkPatchResult(output.toByteArray());
-
-    assertTrue(new File(cloneDir, new File(vcsRoot.getProperty(Constants.REPOSITORY_PROP)).getName()).isDirectory());
-  }
-
-  private String mergeCommittsRepo() {
-    return new File("mercurial-tests/testData/rep2").getAbsolutePath();
-  }
-
-  public void test_collect_changes_between_two_different_roots() throws Exception {
-    VcsRootImpl defaultRoot = createVcsRoot(mergeCommittsRepo());
-    VcsRootImpl branchRoot = createVcsRoot(mergeCommittsRepo(), "test");
-    List<ModificationData> changes = myVcs.collectChanges(defaultRoot, "11:48177654181c", branchRoot, "10:fc524efc2bc4", CheckoutRules.DEFAULT);
-    assertEquals(changes.size(), 2);
-
-    assertEquals("9:8c44244d6645", changes.get(0).getVersion());
-    assertEquals("10:fc524efc2bc4", changes.get(1).getVersion());
-  }
-
-  public void test_collect_changes_merge() throws Exception {
-    VcsRootImpl vcsRoot = createVcsRoot(mergeCommittsRepo());
-
-    List<ModificationData> changes = collectChanges(vcsRoot, "1:a3d15477d297", "4:6eeb8974fe67", CheckoutRules.DEFAULT);
-    assertEquals(changes.size(), 3);
-
-    assertEquals("2:db8a04d262f3", changes.get(0).getVersion());
-    assertEquals("3:2538c02bafeb", changes.get(1).getVersion());
-    assertEquals("4:6eeb8974fe67", changes.get(2).getVersion());
-
-    assertFiles(Arrays.asList("A dir1/file1.txt"), changes.get(0));
-    assertFiles(Arrays.asList("A dir2/file2.txt"), changes.get(1));
-    assertFiles(Arrays.asList("A dir1/file1.txt", "A dir2/file2.txt"), changes.get(2));
-  }
-
-  public void test_collect_changes_merge_conflict() throws Exception {
-    VcsRootImpl vcsRoot = createVcsRoot(mergeCommittsRepo());
-
-    List<ModificationData> changes = collectChanges(vcsRoot, "6:6066b677d026", "8:b6e2d176fe8e", CheckoutRules.DEFAULT);
-    assertEquals(changes.size(), 2);
-
-    assertFiles(Arrays.asList("A dir4/file41.txt"), changes.get(0));
-    assertFiles(Arrays.asList("M dir4/file41.txt", "A dir4/file42.txt", "A dir4/file43.txt", "R dir3/file3.txt"), changes.get(1));
-  }
-
-  public void test_collect_changes_merge_conflict_named_branch() throws Exception {
-    VcsRootImpl vcsRoot = createVcsRoot(mergeCommittsRepo());
-
-    List<ModificationData> changes = collectChanges(vcsRoot, "8:b6e2d176fe8e", "12:1e620196c4b6", CheckoutRules.DEFAULT);
-    assertEquals(changes.size(), 2);
-
-    assertFiles(Arrays.asList("A dir6/file6.txt"), changes.get(0));
-    assertFiles(Arrays.asList("M dir6/file6.txt", "A dir5/file5.txt"), changes.get(1));
-  }
-
-  //TW-10172
-  public void should_not_fill_server_clone_path() {
-    assertFalse(myVcs.getDefaultVcsProperties().containsKey(Constants.SERVER_CLONE_PATH_PROP));
-
-    Map<String, String> rootProperties = new HashMap<String, String>() {{
-      put(Constants.HG_COMMAND_PATH_PROP, "hg");
-      put(Constants.REPOSITORY_PROP, "http://somewhere.com/path");
-    }};
-
-    assertFalse(rootProperties.containsKey(Constants.SERVER_CLONE_PATH_PROP));
-    myVcs.getVcsPropertiesProcessor().process(rootProperties);
-    assertFalse(rootProperties.containsKey(Constants.SERVER_CLONE_PATH_PROP));
-  }
-
-  private void assertFiles(final List<String> expectedFiles, final ModificationData modificationData) {
-    List<String> actualFiles = new ArrayList<String>();
-    for (VcsChange vc: modificationData.getChanges()) {
-      actualFiles.add(toFileStatus(vc.getType()) + " " + vc.getRelativeFileName());
-    }
-    Assert.assertEquals("Actual files: " + actualFiles.toString(), expectedFiles, actualFiles);
-  }
-
-  private String toFileStatus(VcsChange.Type type) {
-    switch (type) {
-      case ADDED:
-        return "A";
-      case REMOVED:
-        return "R";
-      case CHANGED:
-        return "M";
-    }
-    return "?";
-  }
-
-  private Object normalizePath(final String path) {
-    return path.replace(File.separatorChar, '/');
-  }
-
-
-  public void test_collect_changes_using_checkout_rules() {
-    assertTrue(myVcs.getCollectChangesPolicy() instanceof CollectChangesByCheckoutRules);
-  }
-}
+/*
+ * 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 com.intellij.execution.configurations.GeneralCommandLine;
+import jetbrains.buildServer.ExecResult;
+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.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.util.*;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+
+@Test
+public class MercurialVcsSupportTest extends BaseMercurialTestCase {
+  private MercurialVcsSupport myVcs;
+  private ServerPaths myServerPaths;
+
+  @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);
+  }
+
+  protected String getTestDataPath() {
+    return "mercurial-tests/testData";
+  }
+
+  public void test_get_current_version() throws Exception {
+    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
+
+    assertEquals(myVcs.getCurrentVersion(vcsRoot), "10:9c6a6b4aede0");
+    assertEquals("9c6a6b4aede0", myVcs.getVersionDisplayName("10:9c6a6b4aede0", vcsRoot));
+
+    assertEquals(myVcs.getCurrentVersion(createVcsRoot(simpleRepo(), "test_branch")), "8:04c3ae4c6312");
+
+    assertEquals(myVcs.getCurrentVersion(createVcsRoot(simpleRepo(), "name with space")), "9:9babcf2d5705");
+  }
+
+  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);
+  }
+
+  public void test_collect_changes_between_two_same_roots() throws Exception {
+    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
+    VcsRootImpl sameVcsRoot = createVcsRoot(simpleRepo());
+    List<ModificationData> changes = myVcs.collectChanges(vcsRoot, "0:9875b412a788", sameVcsRoot, "3:9522278aa38d", new CheckoutRules(""));
+    do_check_for_collect_changes(changes);
+  }
+
+  public void test_collect_changes() throws Exception {
+    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
+    List<ModificationData> changes = collectChanges(vcsRoot, "0:9875b412a788", "3:9522278aa38d", new CheckoutRules(""));
+    do_check_for_collect_changes(changes);
+  }
+
+  private void do_check_for_collect_changes(List<ModificationData> changes) throws Exception {
+    assertEquals(3, changes.size());
+
+    ModificationData md1 = changes.get(0);
+    ModificationData md2 = changes.get(1);
+    ModificationData md3 = changes.get(2);
+    assertEquals(md1.getVersion(), "1:1d446e82d356");
+    assertEquals(md1.getDescription(), "new file added");
+    List<VcsChange> files1 = md1.getChanges();
+    assertEquals(1, files1.size());
+    assertEquals(VcsChangeInfo.Type.ADDED, files1.get(0).getType());
+    assertEquals(normalizePath(files1.get(0).getRelativeFileName()), "dir1/file3.txt");
+
+    assertEquals(md2.getVersion(), "2:7209b1f1d793");
+    assertEquals(md2.getDescription(), "file4.txt added");
+    List<VcsChange> files2 = md2.getChanges();
+    assertEquals(1, files2.size());
+    assertEquals(files2.get(0).getType(), VcsChangeInfo.Type.ADDED);
+    assertEquals(normalizePath(files2.get(0).getRelativeFileName()), "dir1/file4.txt");
+
+    assertEquals(md3.getVersion(), "3:9522278aa38d");
+    assertEquals(md3.getDescription(), "file removed");
+    List<VcsChange> files3 = md3.getChanges();
+    assertEquals(1, files3.size());
+    assertEquals(files3.get(0).getType(), VcsChangeInfo.Type.REMOVED);
+    assertEquals(normalizePath(files3.get(0).getRelativeFileName()), "dir1/file4.txt");
+  }
+
+  private ByteArrayOutputStream buildPatch(VcsRoot vcsRoot, String from, String to, CheckoutRules rules) throws IOException, VcsException {
+    final ByteArrayOutputStream output = new ByteArrayOutputStream();
+    final PatchBuilderImpl builder = new PatchBuilderImpl(output);
+
+    ((BuildPatchByCheckoutRules)myVcs.getBuildPatchPolicy()).buildPatch(vcsRoot, from, to, builder, rules);
+    builder.close();
+    return output;
+  }
+  
+  @Test
+  public void test_build_patch() throws IOException, VcsException {
+    setName("cleanPatch1");
+    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
+
+    ByteArrayOutputStream output = buildPatch(vcsRoot, null, "4:b06a290a363b", new CheckoutRules(""));
+    checkPatchResult(output.toByteArray());
+
+    File clonedReposParentDir = new File(myServerPaths.getCachesDir(), "mercurial");
+    assertTrue(clonedReposParentDir.isDirectory());
+    assertTrue(1 == clonedReposParentDir.list(new FilenameFilter() {
+      public boolean accept(final File dir, final String name) {
+        return name.startsWith("hg_");
+      }
+    }).length);
+  }
+
+  public void test_build_incremental_patch() throws IOException, VcsException {
+    setName("patch1");
+    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
+
+    ByteArrayOutputStream output = buildPatch(vcsRoot, "3:9522278aa38d", "4:b06a290a363b", new CheckoutRules(""));
+
+    checkPatchResult(output.toByteArray());
+  }
+
+  public void test_build_incremental_patch_checkout_rules() throws IOException, VcsException {
+    setName("patch1_checkout_rules");
+    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
+
+    ByteArrayOutputStream output = buildPatch(vcsRoot, "3:9522278aa38d", "4:b06a290a363b", new CheckoutRules("+:dir1=>path"));
+
+    checkPatchResult(output.toByteArray());
+  }
+
+  public void test_build_clean_patch_checkout_rules() throws IOException, VcsException {
+    setName("cleanPatch1_checkout_rules");
+    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
+
+    ByteArrayOutputStream output = buildPatch(vcsRoot, null, "4:b06a290a363b", new CheckoutRules("+:dir1/subdir=>."));
+
+    checkPatchResult(output.toByteArray());
+  }
+
+  public void test_build_incremental_patch_file_with_space() throws IOException, VcsException {
+    setName("patch2");
+    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
+
+    ByteArrayOutputStream output = buildPatch(vcsRoot, "3:9522278aa38d", "6:b9deb9a1c6f4", new CheckoutRules(""));
+
+    checkPatchResult(output.toByteArray());
+  }
+
+  public void test_get_content() throws IOException, VcsException {
+    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
+
+    byte[] content = myVcs.getContent("dir1/subdir/file2.txt", vcsRoot, "4:b06a290a363b");
+    assertEquals(new String(content), "bbb");
+    content = myVcs.getContent("dir1/subdir/file2.txt", vcsRoot, "5:1d2cc6f3bc29");
+    assertEquals(new String(content), "modified\r\nbbb");
+  }
+
+  public void test_get_content_in_branch() throws IOException, VcsException {
+    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch");
+
+    byte[] content = myVcs.getContent("file_in_branch.txt", vcsRoot, "8:04c3ae4c6312");
+    assertEquals(new String(content), "file from the test_branch\r\nfile modified");
+  }
+
+  public void test_test_connection() throws IOException, VcsException {
+    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
+
+    System.out.println(myVcs.getTestConnectionSupport().testConnection(vcsRoot));
+
+    vcsRoot.addProperty(Constants.REPOSITORY_PROP, "/some/non/existent/path");
+    try {
+      myVcs.getTestConnectionSupport().testConnection(vcsRoot);
+      fail("Exception expected");
+    } catch (VcsException e) {
+    }
+  }
+
+  public void test_tag() throws IOException, VcsException {
+    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
+    cleanRepositoryAfterTest(simpleRepo());
+
+    String actualTag = myVcs.label("new:tag", "1:1d446e82d356", vcsRoot, new CheckoutRules(""));
+    assertEquals(actualTag, "new_tag");
+
+    // check the tag is pushed to the parent repository
+    GeneralCommandLine cli = new GeneralCommandLine();
+    cli.setExePath(vcsRoot.getProperty(Constants.HG_COMMAND_PATH_PROP));
+    cli.setWorkDirectory(vcsRoot.getProperty(Constants.REPOSITORY_PROP));
+    cli.addParameter("tags");
+    ExecResult res = CommandUtil.runCommand(cli);
+    assertTrue(res.getStdout().contains("new_tag"));
+    assertTrue(res.getStdout().contains("1:1d446e82d356"));
+  }
+
+  public void test_tag_in_branch() throws IOException, VcsException {
+    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch");
+    cleanRepositoryAfterTest(simpleRepo());
+
+    String actualTag = myVcs.label("branch_tag", "7:376dcf05cd2a", vcsRoot, new CheckoutRules(""));
+    assertEquals(actualTag, "branch_tag");
+
+    // check the tag is pushed to the parent repository
+    GeneralCommandLine cli = new GeneralCommandLine();
+    cli.setExePath(vcsRoot.getProperty(Constants.HG_COMMAND_PATH_PROP));
+    cli.setWorkDirectory(vcsRoot.getProperty(Constants.REPOSITORY_PROP));
+    cli.addParameter("tags");
+    ExecResult res = CommandUtil.runCommand(cli);
+    assertTrue(res.getStdout().contains("branch_tag"));
+    assertTrue(res.getStdout().contains("7:376dcf05cd2a"));
+  }
+
+  public void test_collect_changes_in_branch() throws Exception {
+    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch");
+
+    // fromVersion(6:b9deb9a1c6f4) is not in the branch (it is in the default branch)
+    List<ModificationData> changes = collectChanges(vcsRoot, "6:b9deb9a1c6f4", "7:376dcf05cd2a", CheckoutRules.DEFAULT);
+    assertEquals(1, changes.size());
+
+    ModificationData md1 = changes.get(0);
+    assertEquals(md1.getVersion(), "7:376dcf05cd2a");
+    assertEquals(md1.getDescription(), "new file added in the test_branch");
+    List<VcsChange> files1 = md1.getChanges();
+    assertEquals(1, files1.size());
+    assertEquals(VcsChangeInfo.Type.ADDED, files1.get(0).getType());
+    assertEquals(normalizePath(files1.get(0).getRelativeFileName()), "file_in_branch.txt");
+
+    changes = collectChanges(vcsRoot, "7:376dcf05cd2a", "8:04c3ae4c6312", CheckoutRules.DEFAULT);
+    assertEquals(1, changes.size());
+
+    md1 = changes.get(0);
+    assertEquals(md1.getVersion(), "8:04c3ae4c6312");
+    assertEquals(md1.getDescription(), "file modified");
+  }
+
+  public void test_full_patch_from_branch() throws IOException, VcsException {
+    setName("patch3");
+    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch");
+
+    ByteArrayOutputStream output = buildPatch(vcsRoot, null, "7:376dcf05cd2a", new CheckoutRules(""));
+
+    checkPatchResult(output.toByteArray());
+  }
+
+  public void test_full_patch_from_branch_with_checkout_rules() throws IOException, VcsException {
+    setName("patch3_checkout_rules1");
+    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch");
+
+    ByteArrayOutputStream output = buildPatch(vcsRoot, null, "7:376dcf05cd2a", new CheckoutRules("+:.=>path"));
+
+    checkPatchResult(output.toByteArray());
+  }
+
+  public void test_full_patch_from_branch_with_checkout_rules_mapped_and_skipped() throws IOException, VcsException {
+    setName("patch3_checkout_rules2");
+    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch");
+
+    ByteArrayOutputStream output = buildPatch(vcsRoot, null, "7:376dcf05cd2a", new CheckoutRules("+:dir1=>path/dir1\n+:dir with space"));
+
+    checkPatchResult(output.toByteArray());
+  }
+
+  public void test_incremental_patch_from_branch() throws IOException, VcsException {
+    setName("patch4");
+    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch");
+
+    ByteArrayOutputStream output = buildPatch(vcsRoot, "7:376dcf05cd2a", "8:04c3ae4c6312", new CheckoutRules(""));
+
+    checkPatchResult(output.toByteArray());
+  }
+
+  @Test(enabled = false)
+  public void support_anchor_branch_notation() throws IOException {
+    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
+    String repPath = vcsRoot.getProperty(Constants.REPOSITORY_PROP);
+    vcsRoot.addProperty(Constants.REPOSITORY_PROP, repPath + "#test_branch");
+    Settings settings = new Settings(new File(myServerPaths.getCachesDir()), vcsRoot);
+    assertEquals("test_branch", settings.getBranchName());
+
+    vcsRoot.addProperty(Constants.REPOSITORY_PROP, repPath + "#");
+    settings = new Settings(new File(myServerPaths.getCachesDir()), vcsRoot);
+    assertEquals("default", settings.getBranchName());
+
+    vcsRoot.addProperty(Constants.REPOSITORY_PROP, repPath);
+    settings = new Settings(new File(myServerPaths.getCachesDir()), vcsRoot);
+    assertEquals("default", settings.getBranchName());
+  }
+
+  public void build_patch_using_custom_clone_path() throws IOException, VcsException {
+    setName("cleanPatch1");
+    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
+    File cloneDir = myTempFiles.createTempDir();
+    vcsRoot.addProperty(Constants.SERVER_CLONE_PATH_PROP, cloneDir.getAbsolutePath());
+
+    ByteArrayOutputStream output = buildPatch(vcsRoot, null, "4:b06a290a363b", new CheckoutRules(""));
+
+    checkPatchResult(output.toByteArray());
+
+    assertTrue(new File(cloneDir, new File(vcsRoot.getProperty(Constants.REPOSITORY_PROP)).getName()).isDirectory());
+  }
+
+  private String mergeCommittsRepo() {
+    return new File("mercurial-tests/testData/rep2").getAbsolutePath();
+  }
+
+  public void test_collect_changes_between_two_different_roots() throws Exception {
+    VcsRootImpl defaultRoot = createVcsRoot(mergeCommittsRepo());
+    VcsRootImpl branchRoot = createVcsRoot(mergeCommittsRepo(), "test");
+    List<ModificationData> changes = myVcs.collectChanges(defaultRoot, "11:48177654181c", branchRoot, "10:fc524efc2bc4", CheckoutRules.DEFAULT);
+    assertEquals(changes.size(), 2);
+
+    assertEquals("9:8c44244d6645", changes.get(0).getVersion());
+    assertEquals("10:fc524efc2bc4", changes.get(1).getVersion());
+  }
+
+  public void test_collect_changes_merge() throws Exception {
+    VcsRootImpl vcsRoot = createVcsRoot(mergeCommittsRepo());
+
+    List<ModificationData> changes = collectChanges(vcsRoot, "1:a3d15477d297", "4:6eeb8974fe67", CheckoutRules.DEFAULT);
+    assertEquals(changes.size(), 3);
+
+    assertEquals("2:db8a04d262f3", changes.get(0).getVersion());
+    assertEquals("3:2538c02bafeb", changes.get(1).getVersion());
+    assertEquals("4:6eeb8974fe67", changes.get(2).getVersion());
+
+    assertFiles(Arrays.asList("A dir1/file1.txt"), changes.get(0));
+    assertFiles(Arrays.asList("A dir2/file2.txt"), changes.get(1));
+    assertFiles(Arrays.asList("A dir1/file1.txt", "A dir2/file2.txt"), changes.get(2));
+  }
+
+  public void test_collect_changes_merge_conflict() throws Exception {
+    VcsRootImpl vcsRoot = createVcsRoot(mergeCommittsRepo());
+
+    List<ModificationData> changes = collectChanges(vcsRoot, "6:6066b677d026", "8:b6e2d176fe8e", CheckoutRules.DEFAULT);
+    assertEquals(changes.size(), 2);
+
+    assertFiles(Arrays.asList("A dir4/file41.txt"), changes.get(0));
+    assertFiles(Arrays.asList("M dir4/file41.txt", "A dir4/file42.txt", "A dir4/file43.txt", "R dir3/file3.txt"), changes.get(1));
+  }
+
+  public void test_collect_changes_merge_conflict_named_branch() throws Exception {
+    VcsRootImpl vcsRoot = createVcsRoot(mergeCommittsRepo());
+
+    List<ModificationData> changes = collectChanges(vcsRoot, "8:b6e2d176fe8e", "12:1e620196c4b6", CheckoutRules.DEFAULT);
+    assertEquals(changes.size(), 2);
+
+    assertFiles(Arrays.asList("A dir6/file6.txt"), changes.get(0));
+    assertFiles(Arrays.asList("M dir6/file6.txt", "A dir5/file5.txt"), changes.get(1));
+  }
+
+  //TW-10172
+  public void should_not_fill_server_clone_path() {
+    assertFalse(myVcs.getDefaultVcsProperties().containsKey(Constants.SERVER_CLONE_PATH_PROP));
+
+    Map<String, String> rootProperties = new HashMap<String, String>() {{
+      put(Constants.HG_COMMAND_PATH_PROP, "hg");
+      put(Constants.REPOSITORY_PROP, "http://somewhere.com/path");
+    }};
+
+    assertFalse(rootProperties.containsKey(Constants.SERVER_CLONE_PATH_PROP));
+    myVcs.getVcsPropertiesProcessor().process(rootProperties);
+    assertFalse(rootProperties.containsKey(Constants.SERVER_CLONE_PATH_PROP));
+  }
+
+  public void use_compressed_transfer_by_default() {
+    VcsRootImpl root = new VcsRootImpl(1, Constants.VCS_NAME);
+    root.addAllProperties(myVcs.getDefaultVcsProperties());
+    root.addProperty(Constants.REPOSITORY_PROP, "http://host.com/path");
+    Settings settings = new Settings(new File("."), root);
+    assertFalse(settings.isUncompressedTransfer());
+  }
+
+  private void assertFiles(final List<String> expectedFiles, final ModificationData modificationData) {
+    List<String> actualFiles = new ArrayList<String>();
+    for (VcsChange vc: modificationData.getChanges()) {
+      actualFiles.add(toFileStatus(vc.getType()) + " " + vc.getRelativeFileName());
+    }
+    Assert.assertEquals("Actual files: " + actualFiles.toString(), expectedFiles, actualFiles);
+  }
+
+  private String toFileStatus(VcsChange.Type type) {
+    switch (type) {
+      case ADDED:
+        return "A";
+      case REMOVED:
+        return "R";
+      case CHANGED:
+        return "M";
+    }
+    return "?";
+  }
+
+  private Object normalizePath(final String path) {
+    return path.replace(File.separatorChar, '/');
+  }
+
+
+  public void test_collect_changes_using_checkout_rules() {
+    assertTrue(myVcs.getCollectChangesPolicy() instanceof CollectChangesByCheckoutRules);
+  }
+}
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/SettingsTest.java	Fri Feb 04 13:53:02 2011 +0300
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/SettingsTest.java	Wed Feb 09 14:01:26 2011 +0300
@@ -83,6 +83,13 @@
     assertEquals("file:///path/to/repo", settings.getRepositoryUrl());
   }
 
+  public void uncompressed_transfer() {
+    VcsRootImpl root = createVcsRoot("http://host.com/path");
+    root.addProperty(Constants.UNCOMPRESSED_TRANSFER, "true");
+    Settings settings = new Settings(new File("."), root);
+    assertTrue(settings.isUncompressedTransfer());
+  }
+
   private VcsRootImpl createVcsRoot(String url) {
     return createVcsRoot(url, "user", "pwd");
   }