view mercurial-agent/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialIncludeRuleUpdater.java @ 599:109d7d3cdc8f

TW-28512 support subrepos nested inside the main repo
author Dmitry Neverov <dmitry.neverov@jetbrains.com>
date Wed, 22 May 2013 12:35:08 +0400
parents 1e5b80e2c023
children 96af0d63f80a
line wrap: on
line source
package jetbrains.buildServer.buildTriggers.vcs.mercurial;

import jetbrains.buildServer.agent.AgentRunningBuild;
import jetbrains.buildServer.agent.BuildProgressLogger;
import jetbrains.buildServer.agent.vcs.IncludeRuleUpdater;
import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.AuthSettings;
import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.HgVcsRoot;
import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.exception.UnrelatedRepositoryException;
import jetbrains.buildServer.vcs.IncludeRule;
import jetbrains.buildServer.vcs.VcsException;
import jetbrains.buildServer.vcs.VcsRoot;
import org.jetbrains.annotations.NotNull;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Map;

import static com.intellij.openapi.util.io.FileUtil.delete;

/**
 * @author dmitry.neverov
 */
public class MercurialIncludeRuleUpdater implements IncludeRuleUpdater {

  private final AgentPluginConfig myConfig;
  private final MirrorManager myMirrorManager;
  private final AgentRepoFactory myRepoFactory;
  private final HgVcsRoot myRoot;
  private final AuthSettings myAuthSettings;
  private final String myHgPath;
  private final String myToVersion;
  private final BuildProgressLogger myLogger;
  private final boolean myUseLocalMirrors;
  private int myPullTimeout;
  private final boolean myUseTraceback;

  public MercurialIncludeRuleUpdater(@NotNull AgentPluginConfig pluginConfig,
                                     @NotNull MirrorManager mirrorManager,
                                     @NotNull HgPathProvider hgPathProvider,
                                     @NotNull AgentRepoFactory repoFactory,
                                     @NotNull VcsRoot root,
                                     @NotNull String toVersion,
                                     @NotNull AgentRunningBuild build) {
    myConfig = pluginConfig;
    myMirrorManager = mirrorManager;
    myRepoFactory = repoFactory;
    myRoot = new HgVcsRoot(root);
    myAuthSettings = myRoot.getAuthSettings();
    myHgPath = hgPathProvider.getHgPath(myRoot);
    myToVersion = toVersion;
    myLogger = build.getBuildLogger();
    myUseLocalMirrors = myConfig.isUseLocalMirrors(build);
    myPullTimeout = myConfig.getPullTimeout(build);
    myUseTraceback = myConfig.runWithTraceback(build);
  }


  public void process(@NotNull IncludeRule rule, @NotNull File workingDir) throws VcsException {
    try {
      checkRuleIsValid(rule);
      if (myUseLocalMirrors)
        updateLocalMirror(myRoot.getRepository(), myToVersion);
      updateRepository(workingDir);
      updateWorkingDir(workingDir, myToVersion, myRoot.getRepository());
    } catch (Exception e) {
      throwVcsException(e);
    }
  }


  public void dispose() throws VcsException {
  }


  private void updateLocalMirror(@NotNull String repositoryUrl, @NotNull String revision) throws VcsException, IOException {
    File mirrorDir = myMirrorManager.getMirrorDir(repositoryUrl);
    HgRepo mirrorRepo = myRepoFactory.create(mirrorDir, myHgPath, myAuthSettings);
    if (!mirrorRepo.isValidRepository()) {
      delete(mirrorDir);
      myLogger.message("Clone repository " + myAuthSettings.getRepositoryUrlWithHiddenPassword(repositoryUrl) + " into local mirror " + mirrorRepo.path());
      mirrorRepo.doClone().fromRepository(repositoryUrl)
              .withTraceback(myUseTraceback)
              .setUpdateWorkingDir(false)
              .setUsePullProtocol(false)
              .useUncompressedTransfer(myRoot.isUncompressedTransfer())
              .call();
      myLogger.message("Clone successfully finished");
    } else {
      myLogger.message("Update local mirror of " + myAuthSettings.getRepositoryUrlWithHiddenPassword(repositoryUrl) + " at " + mirrorDir);
      if (mirrorRepo.containsRevision(revision)) {
        myLogger.message("Local mirror is already up-to-date");
      } else {
        myLogger.message("Start pulling changes from " + myAuthSettings.getRepositoryUrlWithHiddenPassword(repositoryUrl));
        mirrorRepo.pull().fromRepository(repositoryUrl)
                .withTraceback(myUseTraceback)
                .withTimeout(myPullTimeout)
                .call();
        myLogger.message("Local mirror changes successfully pulled");
      }
    }
  }


  private void updateRepository(@NotNull File workingDir) throws VcsException, IOException {
    String repositoryUrl = getDefaultPullUrl(myRoot, myUseLocalMirrors);
    HgRepo repo = myRepoFactory.create(workingDir, myHgPath, myAuthSettings);
    myLogger.message("Update repository " + workingDir.getAbsolutePath());
    if (repo.isEmpty()) {//can do clone only in empty dir
      myLogger.message("Start cloning from " + (myUseLocalMirrors ? "local mirror " : "") + myAuthSettings.getRepositoryUrlWithHiddenPassword(repositoryUrl));
      repo.doClone().fromRepository(repositoryUrl)
              .withTraceback(myUseTraceback)
              .setUsePullProtocol(false)
              .setUpdateWorkingDir(false)
              .useUncompressedTransfer(!myUseLocalMirrors && myRoot.isUncompressedTransfer())
              .call();
      repo.setDefaultPath(myRoot.getRepository());
      myLogger.message("Repository successfully cloned");
    } else {
      if (!repo.isValidRepository())
        repo.init().call();
      repo.setDefaultPath(myRoot.getRepository());
      if (repo.containsRevision(myToVersion)) {
        myLogger.message("Repository already contains revision " + myToVersion);
      } else {
        myLogger.message("Start pulling changes from " + (myUseLocalMirrors ? "local mirror " : "") + myAuthSettings.getRepositoryUrlWithHiddenPassword(repositoryUrl));
        try {
          repo.pull().fromRepository(repositoryUrl)
                  .withTraceback(myUseTraceback)
                  .withTimeout(myPullTimeout)
                  .call();
        } catch (UnrelatedRepositoryException e) {
          throw new UnrelatedRepositoryException(myAuthSettings.getRepositoryUrlWithHiddenPassword(repositoryUrl), workingDir);
        }
        myLogger.message("Changes successfully pulled");
      }
    }
  }


  private void updateWorkingDir(@NotNull File workingDir, @NotNull String toVersion, @NotNull String repositoryUrl) throws VcsException, IOException {
    HgRepo repo = myRepoFactory.create(workingDir, myHgPath, myAuthSettings);
    updateSubrepositories(repo, toVersion, repositoryUrl);
    doUpdateWorkingDir(repo, toVersion);
  }

  private void updateSubrepositories(@NotNull HgRepo repo, @NotNull String toVersion, @NotNull String parentRepositoryUrl) throws VcsException, IOException {
    if (!repo.hasSubreposAtRevision(toVersion))
      return;
    myLogger.message("Process subrepos of " + parentRepositoryUrl);
    String workingDirRevision = repo.getWorkingDirRevision();
    Map<String, SubRepo> workingDirSubrepos = repo.getSubrepositories(workingDirRevision);
    Map<String, SubRepo> subrepos = repo.getSubrepositories(toVersion);
    for (Map.Entry<String, SubRepo> entry : subrepos.entrySet()) {
      String path = entry.getKey();
      SubRepo subrepoConfig = entry.getValue();
      myLogger.message("Process subrepoConfig at path " + path + " (url: " + subrepoConfig.url() + ")");
      SubRepo workingDirSubrepo = workingDirSubrepos.get(path);
      if (workingDirSubrepo != null && subrepoConfig.hasDifferentUrlThan(workingDirSubrepo)) {
        myLogger.message("The url of subrepoConfig was changed between revisions " + workingDirRevision + " and " + toVersion + " , delete the subrepoConfig");
        delete(subrepoConfigDir(repo, subrepoConfig));
      }
      HgRepo subrepository = myRepoFactory.create(subrepoConfigDir(repo, subrepoConfig), myHgPath, myAuthSettings);
      String subrepoUrl;
      try {
        subrepoUrl = subrepoConfig.resolveUrl(parentRepositoryUrl);
        if (myUseLocalMirrors && subrepoConfig.vcsType() == SubRepo.VcsType.hg && !isRelativeUrl(subrepoUrl))
          syncSubrepo(subrepository, subrepoUrl, subrepoConfig.revision());
      } catch (URISyntaxException e) {
        myLogger.warning("Failed to resolve subrepo url '" + subrepoConfig.url() + "': " + e.getMessage());
        subrepoUrl = subrepoConfig.url();
      }
      updateSubrepositories(subrepository, subrepoConfig.revision(), subrepoUrl);
    }
  }

  private boolean isRelativeUrl(@NotNull String url) {
    return url.startsWith(".");
  }

  private void syncSubrepo(@NotNull HgRepo subrepository, @NotNull String url, @NotNull String revision) throws VcsException, IOException {
    if (!subrepository.isValidRepository() || !subrepository.containsRevision(revision)) {
      updateLocalMirror(url, revision);
      File mirrorDir = myMirrorManager.getMirrorDir(url);
      if (subrepository.isValidRepository()) {
        myLogger.message("Pull from local mirror");
        subrepository.pull().fromRepository(mirrorDir)
                .withTraceback(myUseTraceback)
                .withTimeout(myPullTimeout)
                .call();
        myLogger.message("done");
      } else {
        myLogger.message("Clone subrepo from local mirror");
        subrepository.doClone().fromRepository(mirrorDir)
                .withTraceback(myUseTraceback)
                .setUpdateWorkingDir(false)
                .setUsePullProtocol(false)
                .call();
        subrepository.setDefaultPath(url);
        myLogger.message("done");
      }
    }
  }


  private void doUpdateWorkingDir(@NotNull HgRepo repo, @NotNull String revision) throws VcsException {
    myLogger.message("Updating working dir " + repo.path() + " to revision " + revision);
    repo.update().withTraceback(myUseTraceback).toRevision(revision).call();
    myLogger.message("Working dir updated");
  }


  private String getDefaultPullUrl(HgVcsRoot root, boolean useLocalMirror) throws IOException {
    if (useLocalMirror) {
      File mirrorDir = myMirrorManager.getMirrorDir(root.getRepository());
      return mirrorDir.getCanonicalPath();
    } else {
      return root.getRepository();
    }
  }


  private void checkRuleIsValid(IncludeRule includeRule) throws VcsException {
    if (includeRule.getTo() != null && includeRule.getTo().length() > 0) {
      if (!".".equals(includeRule.getFrom()) && includeRule.getFrom().length() != 0)
        throw new VcsException("Invalid include rule: " + includeRule.toString() + ", Mercurial plugin supports mapping of the form: +:.=>dir only.");
    }
  }


  private void throwVcsException(Exception e) throws VcsException {
    if (e instanceof VcsException)
      throw (VcsException) e;
    else
      throw new VcsException(e);
  }

  private File subrepoConfigDir(@NotNull HgRepo parentRepo, @NotNull SubRepo subrepo) {
    return new File(parentRepo.getWorkingDir(), subrepo.path());
  }
}