view mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java @ 696:a07f685ce394

Get rid of depricated getCurrentVersion() method
author Dmitry Neverov <dmitry.neverov@jetbrains.com>
date Fri, 27 Dec 2013 19:10:48 +0100
parents 5164285ece2b
children d1469a7cc038
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 com.intellij.openapi.util.text.StringUtil;
import jetbrains.buildServer.Used;
import jetbrains.buildServer.buildTriggers.vcs.AbstractVcsPropertiesProcessor;
import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.*;
import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.exception.AbandonedTransactionFound;
import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.exception.UnrelatedRepositoryException;
import jetbrains.buildServer.log.Loggers;
import jetbrains.buildServer.serverSide.InvalidProperty;
import jetbrains.buildServer.serverSide.PropertiesProcessor;
import jetbrains.buildServer.serverSide.ServerListener;
import jetbrains.buildServer.serverSide.ServerListenerAdapter;
import jetbrains.buildServer.util.EventDispatcher;
import jetbrains.buildServer.util.FileUtil;
import jetbrains.buildServer.util.cache.ResetCacheRegister;
import jetbrains.buildServer.vcs.*;
import jetbrains.buildServer.vcs.patches.PatchBuilder;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
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.net.URISyntaxException;
import java.util.*;

import static jetbrains.buildServer.buildTriggers.vcs.mercurial.HgFileUtil.deleteDir;

/**
 * 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, BuildPatchByCheckoutRules {
  private final MirrorManager myMirrorManager;
  private final ServerPluginConfig myConfig;
  private final HgPathProvider myHgPathProvider;
  private final RepoFactory myRepoFactory;
  private final HgVcsRootFactory myHgVcsRootFactory;
  private final FileFilter myIgnoreDotHgFilter = new IgnoreDotHgFilter();
  private final FileFilter myAcceptAllFilter = new AcceptAllFilter();
  private final HgTestConnectionSupport myTestConnection;
  private final SubrepoCheckoutRulesProvider mySubrepoCheckoutRulesProvider;
  private final Collection<MercurialServerExtension> myExtensions = new ArrayList<MercurialServerExtension>();

  public MercurialVcsSupport(@NotNull final EventDispatcher<ServerListener> dispatcher,
                             @NotNull final ResetCacheRegister resetCacheHandlerManager,
                             @NotNull final ServerPluginConfig config,
                             @NotNull final HgPathProvider hgPathProvider,
                             @NotNull final RepoFactory repoFactory,
                             @NotNull final MirrorManager mirrorManager,
                             @NotNull final HgVcsRootFactory hgVcsRootFactory,
                             @NotNull final HgTestConnectionSupport testConnection,
                             @NotNull final SubrepoCheckoutRulesProvider subrepoCheckoutRulesProvider) {
    myConfig = config;
    myMirrorManager = mirrorManager;
    myHgPathProvider = hgPathProvider;
    myRepoFactory = repoFactory;
    myHgVcsRootFactory = hgVcsRootFactory;
    myTestConnection = testConnection;
    mySubrepoCheckoutRulesProvider = subrepoCheckoutRulesProvider;
    resetCacheHandlerManager.registerHandler(new MercurialResetCacheHandler(myMirrorManager));
    dispatcher.addListener(new ServerListenerAdapter() {
      @Override
      public void serverShutdown() {
        myRepoFactory.dispose();
      }
    });
    logUsedHg();
  }

  public void addExtensions(@NotNull Collection<MercurialServerExtension> extensions) {
    myExtensions.addAll(extensions);
  }

  public void addExtension(@NotNull MercurialServerExtension extension) {
    myExtensions.add(extension);
  }

  private void logUsedHg() {
    String hgPath = myConfig.getHgPath();
    if (hgPath != null)
      Loggers.VCS.info("Use server-wide hg path " + hgPath + ", path in the VCS root settings will be ignored");
    else
      Loggers.VCS.info("Server-wide hg path is not set, will use path from the VCS root settings");
  }

  @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.getFileName(), 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);
    HgVcsRoot root = myHgVcsRootFactory.createHgRoot(vcsRoot);
    syncRepository(root, cset);
    HgRepo repo = createRepo(root);
    File parentDir = repo.cat().files(filePath).atRevision(cset).call();
    File file = new File(parentDir, filePath);
    try {
      return FileUtil.loadFileBytes(file);
    } catch (IOException e) {
      throw new VcsException("Failed to load content of file " + filePath + " at revision " + version, e);
    } finally {
      deleteDir(parentDir, Loggers.VCS);
    }
  }

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

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

  @Nullable
  public Map<String, String> getDefaultVcsProperties() {
    Map<String, String> defaults = new HashMap<String, String>();
    defaults.put(Constants.BRANCH_NAME_PROP, "default");
    defaults.put(Constants.HG_COMMAND_PATH_PROP, "hg");
    defaults.put(Constants.UNCOMPRESSED_TRANSFER, "true");
    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(@NotNull final HgVcsRoot root,
                                     @NotNull final ChangeSet fromVer,
                                     @NotNull final ChangeSet toVer,
                                     @NotNull final PatchBuilder builder,
                                     @NotNull final CheckoutRules checkoutRules) throws VcsException, IOException {
    HgRepo repo = createRepo(root);
    List<FileStatus> modifiedFiles = repo.status().fromRevision(fromVer).toRevision(toVer).call();
    List<String> notDeletedFiles = new ArrayList<String>();
    for (FileStatus f: modifiedFiles) {
      if (f.getStatus() != Status.REMOVED) {
        notDeletedFiles.add(f.getPath());
      }
    }

    File parentDir = repo.cat().files(notDeletedFiles).atRevision(toVer).call();
    try {
      for (FileStatus f: modifiedFiles) {
        String mappedPath = checkoutRules.map(f.getPath());
        if (mappedPath == null) continue; // skip
        final File virtualFile = new File(mappedPath);
        if (f.getStatus() == 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();
          }
        }
      }
      if (root.includeSubreposInPatch())
        buildSubrepoPatch(root, fromVer, toVer, builder, checkoutRules, repo);
    } catch (Exception e) {
      Loggers.VCS.warn("Cannot build an incremental patch in repository " + root.getRepository() +
              " from revision " + fromVer.getId() + " to revision " + toVer.getId(), e);
      builder.deleteDirectory(new File(""), true);//clean patch
      buildFullPatch(root, toVer, builder, checkoutRules);
    } finally {
      deleteDir(parentDir, Loggers.VCS);
    }
  }


  private void buildSubrepoPatch(@NotNull HgVcsRoot mainRoot,
                                 @NotNull ChangeSet mainRootFromRevision,
                                 @NotNull ChangeSet mainRootToRevision,
                                 @NotNull PatchBuilder builder,
                                 @NotNull CheckoutRules mainRootRules,
                                 @NotNull HgRepo mainRepo) throws IOException, VcsException {
    List<HgSubrepoConfigChange> subrepoConfigChanges = mainRepo.getSubrepoConfigChanges(mainRootFromRevision.getId(), mainRootToRevision.getId());
    for (HgSubrepoConfigChange configChange : subrepoConfigChanges) {
      try {
        String subrepoPath = configChange.getPath();
        if (!mainRootRules.shouldInclude(subrepoPath))
          continue;
        SubRepo currentSubrepo = configChange.getCurrent();

        if (configChange.subrepoAdded()) {
          assert currentSubrepo != null;
          String subrepoUrl = currentSubrepo.resolveUrl(mainRoot.getRepository());
          HgVcsRoot subrepoRoot = mainRoot.withUrl(subrepoUrl);
          String subrepoFromRevision = null;
          String subrepoToRevision = currentSubrepo.revision();
          CheckoutRules subrepoRules = mySubrepoCheckoutRulesProvider.getSubrepoRules(mainRootRules, subrepoPath);
          buildPatch(subrepoRoot, subrepoFromRevision, subrepoToRevision, builder, subrepoRules);

        } else if (configChange.subrepoRemoved()) {
          builder.deleteDirectory(new File(subrepoPath), true);

        } else if (configChange.subrepoUrlChanged()) {
          assert currentSubrepo != null;
          builder.deleteDirectory(new File(subrepoPath), true);

          String subrepoUrl = currentSubrepo.resolveUrl(mainRoot.getRepository());
          HgVcsRoot subrepoRoot = mainRoot.withUrl(subrepoUrl);
          String subrepoFromRevision = null;
          String subrepoToRevision = currentSubrepo.revision();
          CheckoutRules subrepoRules = mySubrepoCheckoutRulesProvider.getSubrepoRules(mainRootRules, subrepoPath);
          buildPatch(subrepoRoot, subrepoFromRevision, subrepoToRevision, builder, subrepoRules);

        } else {
          assert currentSubrepo != null;
          String subrepoUrl = currentSubrepo.resolveUrl(mainRoot.getRepository());
          HgVcsRoot subrepoRoot = mainRoot.withUrl(subrepoUrl);
          String subrepoFromRevision = configChange.getPrevious().get(0).revision();
          String subrepoToRevision = currentSubrepo.revision();
          CheckoutRules subrepoRules = mySubrepoCheckoutRulesProvider.getSubrepoRules(mainRootRules, subrepoPath);
          buildPatch(subrepoRoot, subrepoFromRevision, subrepoToRevision, builder, subrepoRules);
        }
      } catch (URISyntaxException e) {
        throw new VcsException("Error while resolving subrepo url", e);
      }
    }
  }

  // builds patch by exporting files using specified version
  private void buildFullPatch(@NotNull final HgVcsRoot root,
                              @NotNull final ChangeSet toVer,
                              @NotNull final PatchBuilder builder,
                              @NotNull final CheckoutRules checkoutRules) throws IOException, VcsException {
    File tempDir = HgFileUtil.createTempDir();
    try {
      HgRepo repo = createRepo(root);
      if (root.includeSubreposInPatch()) {
        Map<String, SubRepo> subrepos = repo.getSubrepositories(toVer);
        if (!subrepos.isEmpty()) {
          Loggers.VCS.debug("Repository '" + root.getRepository() + "' has subrepos at revision " + toVer.getId() + ", use 'hg clone' to build clean patch");
          File mirrorDir = getWorkingDir(root);
          HgRepo cloneOfTheMirror = createRepo(root, tempDir);
          cloneOfTheMirror.doClone().fromRepository(mirrorDir)
                  .setUpdateWorkingDir(false)
                  .setUsePullProtocol(false)
                  .useUncompressedTransfer(false)
                  .call();
          cloneOfTheMirror.setDefaultPath(root.getRepository());
          cloneSubrepos(root, tempDir, subrepos);
          cloneOfTheMirror.update().toRevision(toVer).call();
          buildPatchFromDirectory(builder, tempDir, checkoutRules, myIgnoreDotHgFilter);
        } else {
          Loggers.VCS.debug("Repository '" + root.getRepository() + "' doesn't have subrepos at revision " + toVer.getId() + ", use 'hg archive' to build clean patch");
          if (root.useArchiveForPatch()) {
            File archive = new File(tempDir, "arch.tar");
            repo.archive().revision(toVer).type("tar").destination(archive).call();
            buildPatchFromArchive(builder, archive, checkoutRules, new FileFilter() {
              public boolean accept(File f) {
                return !f.getName().equals(".hg_archival.txt");
              }
            });
          } else {
            repo.archive().revision(toVer).destination(tempDir).call();
            buildPatchFromDirectory(builder, tempDir, checkoutRules, myAcceptAllFilter);
          }
        }
      } else {
        Loggers.VCS.debug("Subrepos disabled in VCS root, use 'hg archive' to build clean patch");
        if (root.useArchiveForPatch()) {
          File archive = new File(tempDir, "arch.tar");
          repo.archive().revision(toVer).type("tar").destination(archive).call();
          buildPatchFromArchive(builder, archive, checkoutRules, new FileFilter() {
            public boolean accept(File f) {
              return !f.getName().equals(".hg_archival.txt");
            }
          });
        } else {
          repo.archive().revision(toVer).destination(tempDir).call();
          buildPatchFromDirectory(builder, tempDir, checkoutRules, myAcceptAllFilter);
        }
      }
    } finally {
      deleteDir(tempDir, Loggers.VCS);
    }
  }

  private void cloneSubrepos(@NotNull HgVcsRoot mainRoot, @NotNull File mainRootDir, @NotNull Map<String, SubRepo> subrepos) throws VcsException, IOException {
    for (Map.Entry<String, SubRepo> e : subrepos.entrySet()) {
      String path = e.getKey();
      SubRepo subrepoConfig = e.getValue();
      if (subrepoConfig.vcsType() != SubRepo.VcsType.hg)
        continue;
      String parentRepositoryUrl = mainRoot.getRepository();
      try {
        String subrepoUrl = subrepoConfig.resolveUrl(parentRepositoryUrl);
        HgVcsRoot subrepoRoot = mainRoot.withUrl(subrepoUrl);
        HgRepo subrepo = createRepo(subrepoRoot);
        if (!subrepo.containsRevision(subrepoConfig.revision()))
          syncRepository(subrepoRoot);

        File subrepoMirrorDir = getWorkingDir(subrepoRoot);
        File subrepoDir = new File(mainRootDir, path);
        HgRepo cloneOfSubrepoMirror = createRepo(subrepoRoot, subrepoDir);
        cloneOfSubrepoMirror.doClone().fromRepository(subrepoMirrorDir)
                .setUpdateWorkingDir(false)
                .setUsePullProtocol(false)
                .useUncompressedTransfer(false)
                .call();
        cloneOfSubrepoMirror.setDefaultPath(subrepoConfig.url());

        Map<String, SubRepo> subSubrepos = subrepo.getSubrepositories(subrepoConfig.revision());
        if (!subSubrepos.isEmpty())
          cloneSubrepos(subrepoRoot, subrepoDir, subSubrepos);
      } catch (URISyntaxException error) {
        //ignore it, will try to clone from network during main repository update
      }
    }
  }

  private void buildPatchFromArchive(@NotNull PatchBuilder builder,
                                     @NotNull File archive,
                                     @NotNull CheckoutRules checkoutRules,
                                     @NotNull FileFilter filter) throws IOException {
    FileInputStream fis = new FileInputStream(archive);
    ArchiveInputStream is = null;
    try {
      is = new TarArchiveInputStream(fis);
      ArchiveEntry entry = null;
      while ((entry = is.getNextEntry()) != null) {
        String fileName = entry.getName();
        if (fileName.startsWith("arch/"))
          fileName = fileName.substring(5);
        if (!filter.accept(new File(fileName)))
          continue;
        String mappedFile = checkoutRules.map(fileName);
        if (!StringUtil.isEmpty(mappedFile))
          builder.createBinaryFile(new File(mappedFile), null, is, entry.getSize());
      }
    } finally {
      fis.close();
      if (is != null)
        is.close();
    }
  }

  private void buildPatchFromDirectory(final PatchBuilder builder, final File repRoot, final CheckoutRules checkoutRules, @NotNull final FileFilter filter) throws IOException {
    buildPatchFromDirectory(repRoot, builder, repRoot, checkoutRules, filter);
  }

  private void buildPatchFromDirectory(File curDir, final PatchBuilder builder, final File repRoot, final CheckoutRules checkoutRules, @NotNull final FileFilter filter) 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, checkoutRules, filter);
          } 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, checkoutRules, filter);
          }
        }
      }
    }
  }

  /* clone the repo if it doesn't exist, pull the repo if it doesn't contain specified changeSet */
  private void syncRepository(@NotNull final HgVcsRoot root, @NotNull final ChangeSet cset) throws VcsException {
    File workingDir = getWorkingDir(root);
    lockWorkDir(workingDir);
    HgRepo repo = createRepo(root);
    try {
      if (repo.isValidRepository()) {
        if (repo.containsRevision(cset))
          return;
        try {
          repo.pull().fromRepository(root.getRepository())
                  .withTimeout(myConfig.getPullTimeout())
                  .call();
        } catch (UnrelatedRepositoryException e) {
          Loggers.VCS.warn("Repository at " + root.getRepository() + " is unrelated, clone it again");
          myMirrorManager.forgetDir(workingDir);
          syncRepository(root, cset);
        }
      } else {
        repo.doClone().fromRepository(root.getRepository())
                .useUncompressedTransfer(root.isUncompressedTransfer())
                .setUpdateWorkingDir(false)
                .call();
        repo.setDefaultPath(root.getRepository());
      }
    } finally {
      unlockWorkDir(workingDir);
    }
  }

  public void syncRepository(@NotNull final VcsRoot root) throws VcsException {
    syncRepository(myHgVcsRootFactory.createHgRoot(root));
  }

  public void syncRepository(@NotNull final HgVcsRoot root) throws VcsException {
    File workingDir = getWorkingDir(root);
    lockWorkDir(workingDir);
    HgRepo repo = createRepo(root);
    try {
      if (repo.isValidRepository()) {
        try {
          resetBookmarks(repo);
          repo.pull().fromRepository(root.getRepository())
                  .withTimeout(myConfig.getPullTimeout())
                  .call();
        } catch (UnrelatedRepositoryException e) {
          Loggers.VCS.warn("Repository at " + root.getRepository() + " is unrelated, clone it again");
          myMirrorManager.forgetDir(workingDir);
          syncRepository(root);
        } catch (AbandonedTransactionFound e) {
          Loggers.VCS.warn("Abandoned transaction found in repository " + root.getRepository() + ", clone it again");
          myMirrorManager.forgetDir(workingDir);
          syncRepository(root);
        }
      } else {
        repo.doClone().fromRepository(root.getRepository())
                .setUpdateWorkingDir(false)
                .useUncompressedTransfer(root.isUncompressedTransfer())
                .call();
        repo.setDefaultPath(root.getRepository());
      }
    } finally {
      unlockWorkDir(workingDir);
    }
  }


  private void resetBookmarks(HgRepo repo) throws VcsException {
    if (!myConfig.bookmarksEnabled())
      return;
    HgVersion v = repo.version().call();
    if (v.isEqualsOrGreaterThan(BookmarksCommand.REQUIRED_HG_VERSION))
      repo.resetBookmarks();
  }

  @Override
  public LabelingSupport getLabelingSupport() {
    return this;
  }

  @NotNull
  public VcsFileContentProvider getContentProvider() {
    return this;
  }

  @NotNull
  public MercurialCollectChangesPolicy getCollectChangesPolicy() {
    return new MercurialCollectChangesPolicy(this, myConfig, myHgVcsRootFactory, myRepoFactory, myHgPathProvider);
  }


  @NotNull
  public BuildPatchPolicy getBuildPatchPolicy() {
    return this;
  }

  public void buildPatch(@NotNull VcsRoot root, @Nullable String fromVersion, @NotNull String toVersion, @NotNull PatchBuilder builder, @NotNull CheckoutRules checkoutRules) throws IOException, VcsException {
    HgVcsRoot hgRoot = myHgVcsRootFactory.createHgRoot(root);
    buildPatch(hgRoot, fromVersion, toVersion, builder, checkoutRules);
  }

  public void buildPatch(@NotNull HgVcsRoot hgRoot,
                         @Nullable String fromVersion,
                         @NotNull String toVersion,
                         @NotNull PatchBuilder builder,
                         @NotNull CheckoutRules checkoutRules) throws IOException, VcsException {
    syncRepository(hgRoot);
    ChangeSet to = new ChangeSet(toVersion);
    if (fromVersion == null) {
      buildFullPatch(hgRoot, to, builder, checkoutRules);
    } else {
      ChangeSet from = new ChangeSet(fromVersion);
      HgRepo repo = createRepo(hgRoot);
      if (!repo.containsRevision(from)) {
        Loggers.VCS.info("Cannot find revision " + fromVersion + " in repository " + hgRoot.getRepository() + ", will build a full patch");
        cleanCheckoutDir(builder, checkoutRules);
        buildFullPatch(hgRoot, to, builder, checkoutRules);
      } else {
        buildIncrementalPatch(hgRoot, from, to, builder, checkoutRules);
      }
    }
  }

  private void cleanCheckoutDir(@NotNull PatchBuilder builder, @NotNull CheckoutRules checkoutRules) throws IOException {
    builder.deleteDirectory(new File(checkoutRules.map("")), true);
  }

  private void lockWorkDir(@NotNull File workDir) {
    myMirrorManager.lockDir(workDir);
  }

  private void unlockWorkDir(@NotNull File workDir) {
    myMirrorManager.unlockDir(workDir);
  }

  @Override
  public boolean allowSourceCaching() {
    return myConfig.allowSourceCaching();
  }

  public String label(@NotNull String label, @NotNull String version, @NotNull VcsRoot root, @NotNull CheckoutRules checkoutRules) throws VcsException {
    File tmpDir = null;
    try {
      tmpDir = createLabelingTmpDir();
      HgVcsRoot hgRoot = myHgVcsRootFactory.createHgRoot(root);
      hgRoot.setCustomWorkingDir(tmpDir);
      syncRepository(hgRoot);
      HgRepo repo = createRepo(hgRoot);
      String branchName = getCommitBranch(repo, version);
      repo.update().branch(branchName).call();

      String fixedTagname = fixTagName(label);
      repo.tag().revision(version)
              .tagName(fixedTagname)
              .byUser(hgRoot.getUserForTag())
              .call();

      repo.push().toRepository(hgRoot.getRepository()).call();
      return fixedTagname;
    } finally {
      deleteDir(tmpDir, Loggers.VCS);
    }
  }

  @NotNull
  private String getCommitBranch(@NotNull HgRepo repo, @NotNull String cset) throws VcsException {
    return repo.id().inLocalRepository().revision(cset).showBranch().call();
  }

  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', '_');
  }

  public File getWorkingDir(HgVcsRoot root) {
    File customDir = root.getCustomWorkingDir();
    return customDir != null ? customDir : myMirrorManager.getMirrorDir(root.getRepository());
  }


  public boolean isAgentSideCheckoutAvailable() {
    return true;
  }


  private File createLabelingTmpDir() throws VcsException {
    try {
      return HgFileUtil.createTempDir();
    } catch (IOException e) {
      throw new VcsException("Unable to create temporary directory");
    }
  }


  /* for tests only */
  public MirrorManager getMirrorManager() {
    return myMirrorManager;
  }


  @Override
  public boolean isDAGBasedVcs() {
    return true;
  }

  private static class IgnoreDotHgFilter implements FileFilter {
    public boolean accept(final File file) {
      return !(file.isDirectory() && ".hg".equals(file.getName()));
    }
  }

  private static class AcceptAllFilter implements FileFilter {
    public boolean accept(File pathname) {
      return true;
    }
  }

  public ServerHgRepo createRepo(@NotNull HgVcsRoot root) throws VcsException {
    return myRepoFactory.create(getWorkingDir(root), myHgPathProvider.getHgPath(root), root.getAuthSettings());
  }

  public ServerHgRepo createRepo(@NotNull OperationContext ctx, @NotNull HgVcsRoot root) throws VcsException {
    return ctx.createRepo(getWorkingDir(root), myHgPathProvider.getHgPath(root), root.getAuthSettings());
  }


  public HgRepo createRepo(@NotNull HgVcsRoot root, @NotNull File customDir) throws VcsException {
    return myRepoFactory.create(customDir, myHgPathProvider.getHgPath(root), root.getAuthSettings());
  }

  @NotNull
  @Override
  public Map<String, String> getCheckoutProperties(@NotNull VcsRoot root) {
    Map<String, String> rootProperties = root.getProperties();
    Map<String, String> repositoryProperties = new HashMap<String, String>();
    repositoryProperties.put(Constants.REPOSITORY_PROP, rootProperties.get(Constants.REPOSITORY_PROP));
    repositoryProperties.put(Constants.INCLUDE_SUBREPOS_IN_PATCH, rootProperties.get(Constants.INCLUDE_SUBREPOS_IN_PATCH));
    return repositoryProperties;
  }


  @Override
  public ListFilesPolicy getListFilesPolicy() {
    return new ListFilesSupport(this, myConfig, myHgVcsRootFactory);
  }

  @NotNull
  public UrlSupport getUrlSupport() {
    return new MercurialUrlSupport(this);
  }

  @Override
  @Nullable
  protected <T extends VcsExtension> T getVcsCustomExtension(@NotNull final Class<T> extensionClass) {
    for (MercurialServerExtension e : myExtensions) {
      if (extensionClass.isInstance(e))
        return extensionClass.cast(e);
    }
    return super.getVcsCustomExtension(extensionClass);
  }
}