view mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java @ 285:aeaf4d594967 Eluru-6.0.x

Use customized xml output from the 'hg log' command Do that to parse commit messages correctly (TW-18036). Also 'hg log' can provide information on changed files, so we will not run a 'hg status' for every found cset, that should improve changes collecting performance. Use custom xml format mainly because of the difference in the author output. Default xml splits the author to the person and the email, while default verbose log uses unsplitted author. It is not clear how to make original author from the person and the email, because author|person is not empty even if there is no person in the ui.username config. Also default xml uses date format rfc3339date, which is harder to parse. root: /home/nd/sandbox/hg-plugin/original/ HG: branch: Eluru-6.0.x HG: committing mercurial-common/mercurial-common.iml mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/ChangeSet.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommandUtil.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommand.java mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommandTest.java mercurial.ipr mercurial.xml mercurial-server/resources/buildServerResources/log.template HG: Press C-c C-c when you are done editing.
author Dmitry Neverov <dmitry.neverov@jetbrains.com>
date Mon, 29 Aug 2011 17:31:31 +0400
parents 20817ebd1a05
children 41529b72c059
line wrap: on
line source
/*
 * 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.impl.VcsRootImpl;
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 {

  private final String LOG_TEMPLATE_NAME = "log.template";
  private ConcurrentMap<String, Lock> myWorkDirLocks= new ConcurrentHashMap<String, Lock>();
  private VcsManager myVcsManager;
  private File myDefaultWorkFolderParent;
  private File myLogTemplate;

  public MercurialVcsSupport(@NotNull final VcsManager vcsManager,
                             @NotNull ServerPaths paths,
                             @NotNull final SBuildServer server,
                             @NotNull EventDispatcher<BuildServerListener> dispatcher) throws Exception {
    myLogTemplate = createLogTemplate(paths.getPluginDataDirectory());
    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 File createLogTemplate(@NotNull final File templateFileDir) throws IOException {
    File template = new File(templateFileDir, LOG_TEMPLATE_NAME);
    if (!template.exists()) {
      FileUtil.copyResource(MercurialVcsSupport.class, "/buildServerResources/log.template", template);
    }
    return template;
  }

  private List<VcsChange> toVcsChanges(final List<ModifiedFile> modifiedFiles, String prevVer, String curVer, final IncludeRule includeRule) {
    List<VcsChange> files = new ArrayList<VcsChange>();
    for (ModifiedFile mf: modifiedFiles) {
      String normalizedPath = PathUtil.normalizeSeparator(mf.getPath());
      if (!normalizedPath.startsWith(includeRule.getFrom())) continue; // skip files which do not match include rule
      String relPath = StringUtil.removeLeadingSlash(normalizedPath.substring(includeRule.getFrom().length()));

      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, relPath, 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 {
    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 {
    ChangeSet cset = new ChangeSet(version);
    syncClonedRepository(vcsRoot, cset);
    Settings settings = createSettings(vcsRoot);
    CatCommand cc = new CatCommand(settings);
    cc.setRevId(cset.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"));
        }
        if (isEmpty(properties.get(Constants.SERVER_CLONE_PATH_PROP))) {
          properties.put(Constants.SERVER_CLONE_PATH_PROP, myDefaultWorkFolderParent.getAbsolutePath());
        }
        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.SERVER_CLONE_PATH_PROP, myDefaultWorkFolderParent.getAbsolutePath());
    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);
          }
        }
      }
    }
  }

  /**
   * clone the repo if it doesn't exist, pull the repo if it doesn't contain specified changeSet
   */
  private void syncClonedRepository(final VcsRoot root, final ChangeSet cset) throws VcsException {
    Settings settings = createSettings(root);
    File workDir = settings.getLocalRepositoryDir();
    lockWorkDir(workDir);
    try {
      if (settings.hasCopyOfRepository()) {
        if (!isChangeSetExist(settings, cset)) {
          PullCommand pull = new PullCommand(settings);
          pull.execute();
        }
      } else {
        CloneCommand cl = new CloneCommand(settings);
        cl.setDestDir(workDir.getAbsolutePath());
        cl.setUpdateWorkingDir(false);
        cl.execute();
      }
    } finally {
      unlockWorkDir(workDir);
    }
  }

  /**
   * Check if changeSet is present in local repository.
   * @param settings root settings
   * @param cset change set of interest
   * @return true if changeSet is present in local repository
   */
  private boolean isChangeSetExist(Settings settings, final ChangeSet cset) {
    try {
      File workDir = settings.getLocalRepositoryDir();
      IdentifyCommand identify = new IdentifyCommand(settings);
      identify.setWorkingDir(workDir);
      identify.setInLocalRepository(true);
      identify.setChangeSet(cset);
      identify.execute();
      return true;
    } catch (VcsException e) {
      return false;
    }
  }

  // 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 CollectChangesPolicy getCollectChangesPolicy() {
    return new CollectChangesByIncludeRules() {
      @NotNull
      public IncludeRuleChangeCollector getChangeCollector(@NotNull final VcsRoot root, @NotNull final String fromVersion, @Nullable final String currentVersion) throws VcsException {
        return new IncludeRuleChangeCollector() {
          @NotNull
          public List<ModificationData> collectChanges(@NotNull final IncludeRule includeRule) throws VcsException {
            syncClonedRepository(root);

            List<ModificationData> result = new ArrayList<ModificationData>();
            if (currentVersion == null)
              return result;

            Settings settings = createSettings(root);
            LogCommand lc = new LogCommand(settings, myLogTemplate);
            String fromId = new ChangeSetRevision(fromVersion).getId();
            lc.setFromRevId(fromId);
            lc.setToRevId(new ChangeSetRevision(currentVersion).getId());
            List<ChangeSet> changeSets = lc.execute();
            if (changeSets.isEmpty()) {
              return result;
            }

            ChangeSet prev = new ChangeSet(fromVersion);
            for (ChangeSet cur : changeSets) {
              if (cur.getId().equals(fromId))
                continue; // skip already reported changeset

              boolean merge = cur.getParents().size() > 1;
              List<ModifiedFile> modifiedFiles = cur.getModifiedFiles();
              List<VcsChange> files = toVcsChanges(modifiedFiles, prev.getFullVersion(), cur.getFullVersion(), includeRule);
              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;
          }

          public void dispose() throws VcsException {
          }
        };
      }
    };
  }

  @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 {
    File tmpDir = null;
    try {
      tmpDir = createLabelingTmpDir();
      ((VcsRootImpl) root).addProperty(Constants.SERVER_CLONE_PATH_PROP, tmpDir.getAbsolutePath());

      syncClonedRepository(root);
      Settings settings = createSettings(root);
      new UpdateCommand(settings).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.execute();
      return fixedTagname;
    } finally {
      if (tmpDir != null)
        FileUtil.delete(tmpDir);
    }
  }

  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;
  }


  private File createLabelingTmpDir() throws VcsException {
    try {
      return FileUtil.createTempDirectory("mercurial", "label");
    } catch (IOException e) {
      throw new VcsException("Unable to create temporary directory");
    }
  }
}