view mercurial-agent/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialIncludeRuleUpdater.java @ 304:e9cdb499350d remote-run/subrepos

Do clean checkout if subrepository URL changed Hg cannot do update correctly in this situation. The only thing that helps is a clean checkout.
author Dmitry Neverov <dmitry.neverov@jetbrains.com>
date Fri, 09 Sep 2011 11:47:59 +0400
parents
children 659287a241c2
line wrap: on
line source
package jetbrains.buildServer.buildTriggers.vcs.mercurial;

import com.intellij.openapi.util.Pair;
import jetbrains.buildServer.agent.AgentRunningBuild;
import jetbrains.buildServer.agent.BuildProgressLogger;
import jetbrains.buildServer.agent.vcs.IncludeRuleUpdater;
import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.*;
import jetbrains.buildServer.util.FileUtil;
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.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import static jetbrains.buildServer.buildTriggers.vcs.mercurial.command.CommandUtil.removePrivateData;

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

  private final MirrorManager myMirrorManager;
  private final HgPathProvider myHgPathProvider;
  private final VcsRoot myRoot;
  private final Settings mySettings;
  private final String myToVersion;
  private final BuildProgressLogger myLogger;
  private final boolean myUseLocalMirrors;


  public MercurialIncludeRuleUpdater(@NotNull final MirrorManager mirrorManager,
                                     @NotNull final HgPathProvider hgPathProvider,
                                     @NotNull final VcsRoot root,
                                     @NotNull final String toVersion,
                                     @NotNull final AgentRunningBuild build) {
    myMirrorManager = mirrorManager;
    myHgPathProvider = hgPathProvider;
    myRoot = root;
    mySettings = new Settings(myHgPathProvider, myRoot);
    myToVersion = toVersion;
    myLogger = build.getBuildLogger();
    myUseLocalMirrors = isUseLocalMirrors(build);
  }


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


  public void dispose() throws VcsException {
  }


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


  private boolean isUseLocalMirrors(AgentRunningBuild build) {
    String value = build.getSharedConfigParameters().get("teamcity.hg.use.local.mirrors");
    return "true".equals(value);
  }


  private void initRepository(Settings settings, File workingDir, boolean useLocalMirrors) throws VcsException {
    try {
      String defaultPullUrl = getDefaultPullUrl(settings, useLocalMirrors);
      myLogger.message("Init repository at " + workingDir.getAbsolutePath() + ", remote repository is " +
              removePrivateData(defaultPullUrl, Collections.singleton(settings.getPassword())));
      new Init(settings, workingDir, defaultPullUrl).execute();
    } catch (IOException e) {
      throw new VcsException("Error while initializing repository at " + workingDir.getAbsolutePath(), e);
    }
  }


  private void updateRepository(File workingDir) throws VcsException, IOException {
    if (!Settings.isValidRepository(workingDir)) {
      initRepository(mySettings, workingDir, myUseLocalMirrors);
    } else {
      ensureUseRightRepository(workingDir);
    }
    String defaultPullUrl = getDefaultPullUrl(mySettings, myUseLocalMirrors);
    myLogger.message("Start pulling changes from " + removePrivateData(defaultPullUrl, Collections.singleton(mySettings.getPassword())));
    new PullCommand(mySettings, workingDir).execute();
    myLogger.message("Changes successfully pulled");
  }


  private void ensureUseRightRepository(File workingDir) throws VcsException {
    boolean clonedFromWrongRepository = myUseLocalMirrors && !isClonedFromLocalMirror(workingDir)
                                     || !myUseLocalMirrors && isClonedFromLocalMirror(workingDir);

    if (clonedFromWrongRepository) {
      String rightRepository = myUseLocalMirrors ? "local mirror" : "remote repository";
      String wrongRepository = myUseLocalMirrors ? "remote repository" : "local mirror";
      myLogger.message("Repository in working directory is cloned from " + wrongRepository + ", clone it from " + rightRepository);
      FileUtil.delete(workingDir);
      initRepository(mySettings, workingDir, myUseLocalMirrors);
    }
  }


  private void updateLocalMirror() throws VcsException, IOException {
    Settings settings = new Settings(myHgPathProvider, myRoot);
    File mirrorDir = myMirrorManager.getMirrorDir(settings.getRepositoryUrl());
    myLogger.message("Update local mirror at " + mirrorDir);
    if (!Settings.isValidRepository(mirrorDir)) {
      initRepository(settings, mirrorDir, false);
    }
    final String defaultPullUrl = getDefaultPullUrl(settings, true);
    myLogger.message("Start pulling changes from " + removePrivateData(defaultPullUrl, Collections.singleton(settings.getPassword())));
    new PullCommand(settings, mirrorDir).execute();
    myLogger.message("Local mirror changes successfully pulled");
  }


  private void updateWorkingDir(@NotNull final File workingDir) throws VcsException, IOException {
    String workingDirRevision = getWorkingDirRevision(mySettings, workingDir);
    if (isInitialClone(workingDirRevision)) {
      doUpdateWorkingDir(workingDir);
    } else {
      Map<String, Pair<String, String>> currentSubrepos = getSubrepositories(workingDir, workingDirRevision);
      if (currentSubrepos.isEmpty()) {
        doUpdateWorkingDir(workingDir);
      } else {
        Map<String, Pair<String, String>> toVersionSubrepos = getSubrepositories(workingDir, myToVersion);
        Map<String, Pair<String, String>> subrepositoriesWithChangedUrls = getSubrepositoriesWithChangedUrls(currentSubrepos, toVersionSubrepos);
        if (subrepositoriesWithChangedUrls.isEmpty()) {
          doUpdateWorkingDir(workingDir);
        } else {
          myLogger.warning("URLs of subrepositories were changed, do clean checkout");
          FileUtil.delete(workingDir);
          updateRepository(workingDir);
          doUpdateWorkingDir(workingDir);
        }
      }
    }
  }


  /*returns map: relative path -> (repository url, repository revision)*/
  private Map<String, Pair<String, String>> getSubrepositories(@NotNull final File workingDir, @NotNull final String revision) throws VcsException, IOException {
    CatCommand cc = new CatCommand(mySettings, workingDir);
    cc.setRevId(revision);
    try {
      File parentDir = cc.execute(Arrays.asList(".hgsub", ".hgsubstate"), false);
      File hgsub = new File(parentDir, ".hgsub");
      File hgsubstate = new File(parentDir, ".hgsubstate");
      return readSubrepositories(hgsub, hgsubstate);
    } catch (VcsException e) {
      return Collections.emptyMap();
    }
  }


  private Map<String, Pair<String, String>> readSubrepositories(@NotNull final File hgsub, @NotNull final File hgsubstate) throws IOException {
    if (hgsub.exists() && hgsubstate.exists()) {
      Map<String, String> path2repo = readHgsub(hgsub);
      Map<String, String> path2revision = readHgsubstate(hgsubstate);
      Map<String, Pair<String, String>> result = new HashMap<String, Pair<String, String>>();
      for (Map.Entry<String, String> entry : path2repo.entrySet()) {
        String path = entry.getKey();
        String repo = entry.getValue();
        String revision = path2revision.get(path);
        if (revision != null) {
          result.put(path, Pair.create(repo, revision));
        } else {
          myLogger.warning("Cannot find revision for subrepository at path " + path + " skip it");
        }
      }
      return result;
    } else {
      return Collections.emptyMap();
    }
  }


  /*returns map: relative path -> repository url */
  private Map<String, String> readHgsub(@NotNull final File hgsub) throws IOException {
    Map<String, String> result = new HashMap<String, String>();
    for (String line : FileUtil.readFile(hgsub)) {
      String[] parts = line.split(" = ");
      if (parts.length == 2) {
        result.put(parts[0], parts[1]);
      } else {
        myLogger.warning("Cannot parse the line '" + line + "' from .hgsub, skip it");
      }
    }
    return result;
  }


  /*returns map: relative path -> revision */
  private Map<String, String> readHgsubstate(@NotNull final File hgsubstate) throws IOException {
    Map<String, String> result = new HashMap<String, String>();
    for (String line : FileUtil.readFile(hgsubstate)) {
      String[] parts = line.split(" ");
      if (parts.length == 2) {
        result.put(parts[1], parts[0]);
      } else {
        myLogger.warning("Cannot parse the line '" + line + "' from .hgsubstate, skip it");
      }
    }
    return result;
  }


  /*returns map repository path -> (old url, new url)*/
  private Map<String, Pair<String, String>> getSubrepositoriesWithChangedUrls(@NotNull final Map<String, Pair<String, String>> subrepos1, @NotNull final Map<String, Pair<String, String>> subrepos2) {
    Map<String, Pair<String, String>> result = new HashMap<String, Pair<String, String>>();
    for (Map.Entry<String, Pair<String, String>> entry : subrepos1.entrySet()) {
      String path = entry.getKey();
      String url1 = entry.getValue().first;
      Pair<String, String> urlRevision = subrepos2.get(path);
      if (urlRevision != null && !url1.equals(urlRevision.first))
        result.put(path, Pair.create(url1, urlRevision.first));
    }
    return result;
  }


  private boolean isInitialClone(@NotNull final String workingDirRevision) {
    return "000000000000".equals(workingDirRevision);
  }


  private String getWorkingDirRevision(@NotNull final Settings settings, @NotNull final File workingDir) throws VcsException {
    IdentifyCommand id = new IdentifyCommand(settings, workingDir);
    id.setInLocalRepository(true);
    return id.execute();
  }


  private void doUpdateWorkingDir(@NotNull final File workingDir) throws VcsException {
    myLogger.message("Updating folder " + workingDir.getAbsolutePath() + " to revision " + myToVersion);
    UpdateCommand uc = new UpdateCommand(mySettings, workingDir);
    ChangeSet cs = new ChangeSet(myToVersion);
    uc.setToId(cs.getId());
    uc.execute();
    myLogger.message("Folder successfully updated");
  }


  private String getDefaultPullUrl(Settings settings, boolean useLocalMirror) throws IOException {
    if (useLocalMirror) {
      File mirrorDir = myMirrorManager.getMirrorDir(settings.getRepositoryUrl());
      return mirrorDir.getCanonicalPath();
    } else {
      return settings.getRepositoryUrl();
    }
  }


  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.");
      }
    }
  }


  public boolean isClonedFromLocalMirror(@NotNull final File workingDir) {
    try {
      File mirrorDir = myMirrorManager.getMirrorDir(mySettings.getRepositoryUrl());
      File hgrc = new File(workingDir, ".hg" + File.separator + "hgrc");
      String config = FileUtil.readText(hgrc);
      return config.contains("default = " + mirrorDir.getCanonicalPath());
    } catch (Exception e) {
      return false;
    }
  }
}