view mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java @ 980:1168c4c64d49

Merge branch Indore-2017.2.x
author Dmitry Neverov <dmitry.neverov@gmail.com>
date Wed, 24 Jan 2018 17:38:56 +0100
parents 7bf4d943d5bb 38adef4f1b8f
children f342d25311c1
line wrap: on
line source
/*
 * Copyright 2000-2018 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.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.buildTriggers.vcs.mercurial.command.exception.WrongSubrepoUrlException;
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.*;
import java.util.*;

import static com.intellij.openapi.util.text.StringUtil.isEmpty;
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 VcsOperationProgressProvider myProgressProvider;
  private final MirrorManager myMirrorManager;
  private final ServerPluginConfig myConfig;
  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 VcsOperationProgressProvider progressProvider,
                             @NotNull final EventDispatcher<ServerListener> dispatcher,
                             @NotNull final ResetCacheRegister resetCacheHandlerManager,
                             @NotNull final ServerPluginConfig config,
                             @NotNull final RepoFactory repoFactory,
                             @NotNull final MirrorManager mirrorManager,
                             @NotNull final HgVcsRootFactory hgVcsRootFactory,
                             @NotNull final HgTestConnectionSupport testConnection,
                             @NotNull final SubrepoCheckoutRulesProvider subrepoCheckoutRulesProvider) {
    myProgressProvider = progressProvider;
    myConfig = config;
    myMirrorManager = mirrorManager;
    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 = getHgRoot(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 HgVcsRoot getHgRoot(@NotNull final VcsRoot vcsRoot) throws VcsException {
    return myHgVcsRootFactory.createHgRoot(vcsRoot);
  }

  @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(@NotNull final VcsRoot vcsRoot) {
    return "mercurial: " + vcsRoot.getProperty(Constants.REPOSITORY_PROP);
  }

  @NotNull
  public String describeVcsRoot(@NotNull final HgVcsRoot vcsRoot) {
    return "mercurial: " + vcsRoot.getProperty(Constants.REPOSITORY_PROP);
  }

  @Override
  @NotNull
  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 {
    final HgRepo repo = createRepo(root);
    final List<FileStatus> modifiedFiles = repo.status().fromRevision(fromVer).toRevision(toVer).call();
    final List<String> notDeletedFiles = new ArrayList<String>();
    for (FileStatus f: modifiedFiles) {
      if (f.getStatus() != Status.REMOVED) {
        notDeletedFiles.add(f.getPath());
      }
    }

    File parentDir = null;
    try {
      if (root.useArchiveForPatch()) {
        parentDir = HgFileUtil.createTempDir();
        final File archFile = new File(parentDir, "arch.tar");
        buildIncrementalPatchWithArchive(builder, repo, toVer, checkoutRules, modifiedFiles, notDeletedFiles, archFile);
      } else {
        parentDir = repo.cat().files(notDeletedFiles).atRevision(toVer).call();
        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 {
            final File realFile = new File(parentDir, f.getPath());
            final InputStream is = new BufferedInputStream(new FileInputStream(realFile));
            try {
              builder.changeOrCreateBinaryFile(virtualFile, null, is, realFile.length());
            } finally {
              FileUtil.close(is);
            }
          }
        }
      }
      if (root.includeSubreposInPatch())
        buildSubrepoPatch(root, fromVer, toVer, builder, checkoutRules, repo);
    } catch (Exception e) {
      if (e instanceof VcsException)
        throw (VcsException) e;
      throw new VcsException("Error while building an incremental patch", e);
    } finally {
      if (parentDir != null)
        deleteDir(parentDir, Loggers.VCS);
    }
  }

  private void buildIncrementalPatchWithArchive(@NotNull final PatchBuilder builder,
                                                @NotNull final HgRepo repo,
                                                @NotNull final ChangeSet toVer,
                                                @NotNull final CheckoutRules checkoutRules,
                                                @NotNull final List<FileStatus> modifiedFiles,
                                                @NotNull final List<String> notDeletedFiles,
                                                @NotNull final File archiveFile) throws VcsException, IOException {
    ArchiveCommand archive = repo.archive().revision(toVer).type("tar").destination(archiveFile);
    int i = 0;
    while (i < notDeletedFiles.size()) {
      String mappedPath = checkoutRules.map(notDeletedFiles.get(i));
      if (mappedPath == null) {
        i++;
        continue;
      }
      if (archive.addIncludePathRule(/*"path:" + */notDeletedFiles.get(i))) {
        i++;
        continue;
      }
      //archive command is full, call it
      archive.call();
      buildPatchFromArchive(builder, archiveFile, checkoutRules, new ExcludeHgArchival());
      FileUtil.delete(archiveFile);
      archive = repo.archive().revision(toVer).type("tar").destination(archiveFile);
    }
    if (!notDeletedFiles.isEmpty()) {
      archive.call();
      buildPatchFromArchive(builder, archiveFile, checkoutRules, new ExcludeHgArchival());
      FileUtil.delete(archiveFile);
    }

    //delete removed files
    for (FileStatus f: modifiedFiles) {
      if (f.getStatus() != Status.REMOVED) continue; //other files processed below

      final String mappedPath = checkoutRules.map(f.getPath());
      if (mappedPath == null) continue; // skip

      builder.deleteFile(new File(mappedPath), true);
    }
  }


  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()) {
          deleteDirInPatch(builder, mainRootRules, subrepoPath);

        } else if (configChange.subrepoUrlChanged()) {
          assert currentSubrepo != null;
          deleteDirInPatch(builder, mainRootRules, subrepoPath);

          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 (WrongSubrepoUrlException e) {
        throw new VcsException("Error while resolving subrepo url", e);
      }
    }
  }

  private void deleteDirInPatch(@NotNull PatchBuilder builder, @NotNull CheckoutRules rules, @NotNull String unMappedPath) throws IOException {
    for (IncludeRule rule : rules.getRootIncludeRules()) {
      String mappedPath = rule.map(unMappedPath);
      if (mappedPath != null)
        builder.deleteDirectory(new File(mappedPath), true);
    }
  }

  // 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 {
    final File tempDir = HgFileUtil.createTempDir();
    try {
      final HgRepo repo = createRepo(root);
      if (root.includeSubreposInPatch()) {
        final 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");
          final File mirrorDir = getWorkingDir(root);
          final HgRepo cloneOfTheMirror = createRepo(root, tempDir);
          cloneOfTheMirror.init().call();
          cloneOfTheMirror.setDefaultPath(root.getRepository());
          cloneOfTheMirror.setTeamCityConfig(root.getCustomHgConfig());
          cloneOfTheMirror.pull().fromRepository(mirrorDir).call();
          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 ExcludeHgArchival());
          } 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()) {
          final File archive = new File(tempDir, "arch.tar");
          repo.archive().revision(toVer).type("tar").destination(archive).call();
          buildPatchFromArchive(builder, archive, checkoutRules, new ExcludeHgArchival());
        } 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.init().call();
        cloneOfSubrepoMirror.setDefaultPath(subrepoUrl);
        cloneOfSubrepoMirror.setTeamCityConfig(mainRoot.getCustomHgConfig());
        cloneOfSubrepoMirror.pull().fromRepository(subrepoMirrorDir).call();
        Map<String, SubRepo> subSubrepos = subrepo.getSubrepositories(subrepoConfig.revision());
        if (!subSubrepos.isEmpty())
          cloneSubrepos(subrepoRoot, subrepoDir, subSubrepos);
      } catch (WrongSubrepoUrlException error) {
        //ignore it, will try to clone from network during main repository update
      }
    }
  }

  private void buildPatchFromArchive(@NotNull final PatchBuilder builder,
                                     @NotNull final File archive,
                                     @NotNull final CheckoutRules checkoutRules,
                                     @NotNull final FileFilter filter) throws IOException {
    InputStream fis = null;
    ArchiveInputStream is = null;
    try {
      fis = new BufferedInputStream(new FileInputStream(archive));
      is = new TarArchiveInputStream(fis);

      ArchiveEntry entry;
      while ((entry = is.getNextEntry()) != null) {
        if (entry.isDirectory()) continue;

        String fileName = entry.getName();
        //TODO: does it work if I have arch/ in my repo?
        if (fileName.startsWith("arch/")) fileName = fileName.substring(5);

        if (!filter.accept(new File(fileName))) continue;

        final String mappedFile = checkoutRules.map(fileName);
        if (!isEmpty(mappedFile)) {
          builder.createBinaryFile(new File(mappedFile), null, is, entry.getSize());
        }
      }
    } finally {
      FileUtil.closeAll(is, fis);
    }
  }

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

  private void buildPatchFromDirectory(@NotNull final File curDir,
                                       @NotNull final PatchBuilder builder,
                                       @NotNull final File repRoot,
                                       @NotNull 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())
        repo.init().call();
      if (repo.containsRevision(cset))
        return;
      repo.setDefaultPath(root.getRepository());
      repo.setTeamCityConfig(root.getCustomHgConfig());
      try {
        repo.pull().fromRepository(root.getRepository())
                .withTimeout(myConfig.getPullTimeout())
                .withProfile(myConfig.runWithProfile(root))
                .call();
      } catch (UnrelatedRepositoryException e) {
        Loggers.VCS.warn("Repository at " + root.getRepository() + " is unrelated, clone it again");
        myMirrorManager.forgetDir(workingDir);
        syncRepository(root, cset);
      }
    } finally {
      unlockWorkDir(workingDir);
    }
  }

  public void syncRepository(@NotNull final VcsRoot root) throws VcsException {
    syncRepository(getHgRoot(root));
  }

  public void syncRepository(@NotNull final HgVcsRoot root) throws VcsException {
    syncRepository(root, new SyncSettings<Void>(VcsCallable.NO_OP));
  }

  public <T> T syncRepository(@NotNull HgVcsRoot root, @NotNull SyncSettings<T> settings) throws VcsException {
    return syncRepository(root, settings, null);
  }

  public <T> T syncRepository(@NotNull HgVcsRoot root, @NotNull SyncSettings<T> settings, @Nullable OperationContext context) throws VcsException {
    boolean customWorkingDir = root.getCustomWorkingDir() != null;
    File workingDir = getWorkingDir(root);
    int attemptsLeft = 3;
    VcsException lastError = null;
    while (attemptsLeft-- > 0) {
      try {
        return syncRepositoryOnce(root, settings, workingDir, context);
      } catch (UnrelatedRepositoryException e) {
        if (customWorkingDir)
          throw new VcsException(e.getMessage() + ". VCS root uses a custom clone dir, manual recovery is required.", e);
        Loggers.VCS.warn("Repository at " + workingDir.getAbsolutePath() + " is unrelated to " + root.getRepository() +
                ". Clone it again, attempts left " + attemptsLeft);
        myMirrorManager.forgetDir(workingDir);
        lastError = e;
      } catch (AbandonedTransactionFound e) {
        if (customWorkingDir)
          throw new VcsException(e.getMessage() + ". VCS root uses a custom clone dir, manual recovery is required.", e);
        Loggers.VCS.warn("Abandoned transaction found in repository " + root.getRepository() + " at "
                + workingDir.getAbsolutePath() + ". Clone it again, attempts left " + attemptsLeft);
        myMirrorManager.forgetDir(workingDir);
        lastError = e;
      }
    }
    throw lastError;
  }


  private <T> T syncRepositoryOnce(@NotNull HgVcsRoot root, @NotNull SyncSettings<T> settings, @NotNull File workingDir, @Nullable OperationContext context) throws VcsException {
    lockWorkDir(workingDir, settings.getProgressConsumer());
    HgRepo repo = context != null ? context.createRepo(root) : createRepo(root);
    try {
      if (!repo.isValidRepository())
        repo.init().call();
      repo.setDefaultPath(root.getRepository());
      repo.setTeamCityConfig(root.getCustomHgConfig());
      resetBookmarks(repo);
      repo.pull().fromRepository(root.getRepository())
              .withTimeout(myConfig.getPullTimeout())
              .withProfile(myConfig.runWithProfile(root))
              .call();
      return settings.getCmd().call();
    } 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(myProgressProvider, this, myConfig, myHgVcsRootFactory, myRepoFactory);
  }


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

  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 {
    final HgVcsRoot hgRoot = getHgRoot(root);
    buildPatch(hgRoot, fromVersion, toVersion, builder, checkoutRules);
  }

  public void buildPatch(@NotNull final HgVcsRoot hgRoot,
                         @Nullable final String fromVersion,
                         @NotNull final String toVersion,
                         @NotNull final PatchBuilder builder,
                         @NotNull final CheckoutRules checkoutRules) throws IOException, VcsException {
    syncRepository(hgRoot);

    final ChangeSet to = new ChangeSet(toVersion);

    if (fromVersion == null) {
      buildFullPatch(hgRoot, to, builder, checkoutRules);
      return;
    }

    final ChangeSet from = new ChangeSet(fromVersion);
    final HgRepo repo = createRepo(hgRoot);
    if (repo.containsRevision(from)) {
      buildIncrementalPatch(hgRoot, from, to, builder, checkoutRules);
      return;
    }

    Loggers.VCS.info("Cannot find revision " + fromVersion + " in repository " + hgRoot.getRepository() + ", will build a full patch");
    cleanCheckoutDir(builder, checkoutRules);
    buildFullPatch(hgRoot, 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) {
    lockWorkDir(workDir, null);
  }

  private void lockWorkDir(@NotNull File workDir, @Nullable ProgressParser.ProgressConsumer progressConsumer) {
    if (progressConsumer != null)
      progressConsumer.consume(-1f, "Acquire repository lock");
    myMirrorManager.lockDir(workDir);
    if (progressConsumer != null)
      progressConsumer.consume(-1f, "Repository lock acquired");
  }

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

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

  @NotNull
  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 = getHgRoot(root);
      hgRoot.setCustomWorkingDir(tmpDir);
      syncRepository(hgRoot);
      HgRepo repo = createRepo(hgRoot);
      String branchName = getCommitBranch(repo, version);
      HgVersion hgVersion = repo.version().call();
      if (hgVersion.isEqualsOrGreaterThan(ServerHgRepo.REVSET_HG_VERSION)) {
        repo.update().branch("branch('" + branchName + "') and head()").call();
      } else {
        repo.update().branch(branchName).call();
      }

      String fixedTagname = fixTagName(label);
      try {
        repo.tag().revision(version)
                .tagName(fixedTagname)
                .byUser(hgRoot.getUserForTag())
                .call();
      } catch (VcsException e) {
        String msg = e.getMessage();
        if (msg != null && msg.contains("not at a branch head") && hgVersion.isLessThan(ServerHgRepo.REVSET_HG_VERSION)) {
          Loggers.VCS.warn("Please upgrade mercurial to the version supporting revsets(" + ServerHgRepo.REVSET_HG_VERSION + "+), current version: " + hgVersion);
        }
        throw e;
      }
      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;
    }
  }

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

  @NotNull
  public ServerHgRepo createRepo(@NotNull HgVcsRoot root, @NotNull File customDir) throws VcsException {
    return myRepoFactory.createRepo(root, customDir);
  }

  @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));
    String customHgConfig = rootProperties.get(Constants.CUSTOM_HG_CONFIG_PROP);
    if (!isEmpty(customHgConfig))
      repositoryProperties.put(Constants.CUSTOM_HG_CONFIG_PROP, customHgConfig);
    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);
  }


  private static class ExcludeHgArchival implements FileFilter {
    public boolean accept(File f) {
      return !f.getName().equals(".hg_archival.txt");
    }
  }
}