view mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialCollectChangesPolicy.java @ 593:ad112e314be5

Report subrepo revision in each commit
author Dmitry Neverov <dmitry.neverov@jetbrains.com>
date Thu, 25 Apr 2013 20:54:42 +0400
parents e97a636cc9b7
children 9cbf9205208e
line wrap: on
line source
package jetbrains.buildServer.buildTriggers.vcs.mercurial;

import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.*;
import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.exception.UnknownRevisionException;
import jetbrains.buildServer.log.Loggers;
import jetbrains.buildServer.vcs.*;
import jetbrains.buildServer.vcs.impl.VcsRootImpl;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.net.URISyntaxException;
import java.util.*;

import static java.util.Arrays.asList;
import static java.util.Collections.EMPTY_LIST;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;

public class MercurialCollectChangesPolicy implements CollectChangesBetweenRoots, CollectChangesBetweenRepositories {

  private final MercurialVcsSupport myVcs;
  private final ServerPluginConfig myConfig;
  private final HgVcsRootFactory myHgVcsRootFactory;
  private final RepoFactory myRepoFactory;
  private final HgPathProvider myHgPathProvider;


  public MercurialCollectChangesPolicy(@NotNull MercurialVcsSupport vcs,
                                       @NotNull ServerPluginConfig config,
                                       @NotNull HgVcsRootFactory hgVcsRootFactory,
                                       @NotNull RepoFactory repoFactory,
                                       @NotNull HgPathProvider hgPathProvider) {
    myVcs = vcs;
    myConfig = config;
    myHgVcsRootFactory = hgVcsRootFactory;
    myRepoFactory = repoFactory;
    myHgPathProvider = hgPathProvider;
  }


  @NotNull
  public RepositoryStateData getCurrentState(@NotNull VcsRoot root) throws VcsException {
    HgVcsRoot hgRoot = myHgVcsRootFactory.createHgRoot(root);
    myVcs.syncRepository(hgRoot);
    Map<String, String> revisions = new HashMap<String, String>();
    revisions.putAll(getBookmarkRevisions(hgRoot));
    revisions.putAll(getBranchesRevisions(hgRoot));
    String defaultBranchName = hgRoot.getBranchName();
    if (revisions.get(defaultBranchName) == null) {
      throw new VcsException("Cannot find revision of the default branch '" +
              defaultBranchName + "' of vcs root " + myVcs.describeVcsRoot(root));
    }
    return RepositoryStateData.createVersionState(defaultBranchName, revisions);
  }


  @NotNull
  private Map<String, String> getBranchesRevisions(@NotNull HgVcsRoot root) throws VcsException {
    HgRepo repo = myVcs.createRepo(root);
    return repo.branches().call();
  }


  @NotNull
  private Map<String, String> getBookmarkRevisions(@NotNull HgVcsRoot root) throws VcsException {
    ServerHgRepo repo = myVcs.createRepo(root);
    if (!myConfig.bookmarksEnabled())
      return emptyMap();
    HgVersion version = repo.version().call();
    if (!version.isEqualsOrGreaterThan(BookmarksCommand.REQUIRED_HG_VERSION))
      return emptyMap();
    return repo.bookmarks().call();
  }


  @NotNull
  public List<ModificationData> collectChanges(@NotNull VcsRoot fromRoot,
                                               @NotNull RepositoryStateData fromState,
                                               @NotNull VcsRoot toRoot,
                                               @NotNull RepositoryStateData toState,
                                               @NotNull CheckoutRules rules) throws VcsException {
    return collectChanges(toRoot, fromState, toState, rules);
  }


  @NotNull
  public List<ModificationData> collectChanges(@NotNull VcsRoot root,
                                               @NotNull RepositoryStateData fromState,
                                               @NotNull RepositoryStateData toState,
                                               @NotNull CheckoutRules rules) throws VcsException {
    List<ModificationData> changes = new ArrayList<ModificationData>();
    HgVcsRoot hgRoot = myHgVcsRootFactory.createHgRoot(root);
    OperationContext ctx = new OperationContext(myVcs, myRepoFactory, myHgPathProvider, fromState, toState);
    for (Map.Entry<String, String> entry : toState.getBranchRevisions().entrySet()) {
      String branch = entry.getKey();
      String toRevision = entry.getValue();
      String fromRevision = fromState.getBranchRevisions().get(branch);
      if (fromRevision == null)
        fromRevision = fromState.getBranchRevisions().get(fromState.getDefaultBranchName());
      if (toRevision.equals(fromRevision) || fromRevision == null)
        continue;

      Collection<String> fromRevisions = ctx.getFromRevisionsForBranch(hgRoot, fromRevision, toRevision);
      List<ModificationData> branchChanges = collectChanges(ctx, root, fromRevisions, toRevision, rules);
      for (ModificationData change : branchChanges) {
        if (!ctx.isReportedModification(change)) {
          changes.add(change);
          ctx.markAsReported(change);
        }
      }
    }
    changes.addAll(getSubrepoChanges(ctx, root, changes));
    return changes;
  }


  @NotNull
  public List<ModificationData> collectChanges(@NotNull VcsRoot fromRoot,
                                               @NotNull String fromRootRevision,
                                               @NotNull VcsRoot toRoot,
                                               @Nullable String toRootRevision,
                                               @NotNull CheckoutRules checkoutRules) throws VcsException {
    HgVcsRoot hgRoot = myHgVcsRootFactory.createHgRoot(toRoot);
    myVcs.syncRepository(hgRoot);
    String toRevision = toRootRevision != null ? toRootRevision : myVcs.getCurrentVersion(toRoot);
    String mergeBase = getMergeBase(hgRoot, fromRootRevision, toRevision);
    if (mergeBase == null)
      return Collections.emptyList();
    return collectChanges(toRoot, mergeBase, toRootRevision, checkoutRules);
  }


  public List<ModificationData> collectChanges(@NotNull VcsRoot root,
                                               @NotNull String fromVersion,
                                               @Nullable String currentVersion,
                                               @NotNull CheckoutRules checkoutRules) throws VcsException {
    if (currentVersion == null)
      return emptyList();
    OperationContext ctx = new OperationContext(myVcs, myRepoFactory, myHgPathProvider, fromVersion, currentVersion);
    List<ModificationData> changes = collectChanges(ctx, root, asList(fromVersion), currentVersion, checkoutRules);
    changes.addAll(getSubrepoChanges(ctx, root, changes));
    return changes;
  }


  @Nullable
  private String getMergeBase(@NotNull HgVcsRoot root, @NotNull String revision1, @NotNull String revision2) throws VcsException {
    String result = myVcs.createRepo(root).mergeBase()
            .revision1(revision1)
            .revision2(revision2)
            .call();
    if (result == null)
      result = getMinusNthCommit(root, 10);
    return result;
  }


  @Nullable
  private String getMinusNthCommit(@NotNull HgVcsRoot root, int n) throws VcsException {
    LogCommand log = myVcs.createRepo(root).log()
            .inBranch(root.getBranchName())
            .toNamedRevision(root.getBranchName());
    if (n > 0)
      log.setLimit(n);
    List<ChangeSet> changeSets = log.call();
    if (changeSets.isEmpty())
      return null;
    return changeSets.get(0).getId();
  }


  private List<ModificationData> collectChanges(@NotNull OperationContext ctx,
                                                @NotNull VcsRoot root,
                                                @NotNull Collection<String> fromVersion,
                                                @Nullable String currentVersion,
                                                @NotNull CheckoutRules checkoutRules) throws VcsException {
    HgVcsRoot hgRoot = myHgVcsRootFactory.createHgRoot(root);
    ctx.syncRepository(hgRoot);
    List<ModificationData> result = new ArrayList<ModificationData>();
    for (ChangeSet cset : getChangesets(ctx, hgRoot, fromVersion, currentVersion)) {
      result.add(createModificationData(ctx, cset, root, checkoutRules));
    }
    return result;
  }


  @NotNull
  private List<ChangeSet> getChangesets(@NotNull OperationContext ctx,
                                        @NotNull final HgVcsRoot root,
                                        @NotNull final Collection<String> fromVersions,
                                        @Nullable final String toVersion) throws VcsException {
    if (toVersion == null)
      return emptyList();
    List<String> fromCommits = new ArrayList<String>();
    for (String fromVersion : fromVersions) {
      fromCommits.add(new ChangeSetRevision(fromVersion).getId());
    }
    String toCommit = new ChangeSetRevision(toVersion).getId();
    try {
      ServerHgRepo repo = myVcs.createRepo(ctx, root);
      List<ChangeSet> csets = repo.collectChanges(root)
              .fromRevision(fromCommits)
              .toRevision(toCommit)
              .includeFromRevision(ctx.includeFromRevisions())
              .call();
      if (!ctx.includeFromRevisions()) {
        Iterator<ChangeSet> iter = csets.iterator();
        while (iter.hasNext()) {
          ChangeSet cset = iter.next();
          if (fromVersions.contains(cset.getId()))
            iter.remove();
        }
      }
      return csets;
    } catch (UnknownRevisionException e) {
      Loggers.VCS.warn("Revision '" + e.getRevision() + "' is unknown, will return no changes");
      return emptyList();
    }
  }


  private ModificationData createModificationData(@NotNull OperationContext ctx,
                                                  @NotNull final ChangeSet cset,
                                                  @NotNull final VcsRoot root,
                                                  @NotNull final CheckoutRules checkoutRules) throws VcsException {
    List<ChangeSetRevision> parents = cset.getParents();
    if (parents.isEmpty())
      throw new IllegalStateException("Commit " + cset.getId() + " has no parents");
    String version = ctx.getStringFromPool(cset.getId());
    List<VcsChange> files = toVcsChanges(ctx, cset.getModifiedFiles(), ctx.getStringFromPool(parents.get(0).getId()), version, checkoutRules);
    final ModificationData result = new ModificationData(cset.getTimestamp(), files, cset.getDescription(),
            ctx.getStringFromPool(cset.getUser()), root, version, version);
    for (ChangeSetRevision parent : parents) {
      result.addParentRevision(ctx.getStringFromPool(parent.getId()));
    }
    setCanBeIgnored(result, cset);
    result.setAttributes(getAttributes(ctx, root, cset));
    return result;
  }


  private void setCanBeIgnored(@NotNull ModificationData md, @NotNull ChangeSet cset) {
    if (md.getParentRevisions().size() > 1) {
      //don't ignore merge commits
      md.setCanBeIgnored(false);
    } else if (cset.getModifiedFiles().isEmpty()) {
      //don't ignore empty commits
      md.setCanBeIgnored(false);
    }
  }


  private List<VcsChange> toVcsChanges(@NotNull OperationContext ctx,
                                       @NotNull List<FileStatus> modifiedFiles,
                                       @NotNull String prevVer,
                                       @NotNull String curVer,
                                       @NotNull CheckoutRules rules) {
    List<VcsChange> files = new ArrayList<VcsChange>(0);
    for (FileStatus mf: modifiedFiles) {
      String path = rules.map(mf.getPath());
      if (!shouldInclude(path))
        continue;
      String normalizedPath = PathUtil.normalizeSeparator(mf.getPath());
      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(), ctx.getStringFromPool(normalizedPath), ctx.getStringFromPool(path), prevVer, curVer));
    }
    if (files.isEmpty())
      return emptyList();
    return files;
  }


  private boolean shouldInclude(String path) {
    return path != null;
  }


  private VcsChangeInfo.Type getChangeType(final 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
  private List<ModificationData> getSubrepoChanges(@NotNull OperationContext ctx, @NotNull VcsRoot root, @NotNull List<ModificationData> changes) throws VcsException {
    if (changes.isEmpty())
      return emptyList();

    ctx.setIncludeFromRevisions(true);

    HgVcsRoot hgRoot = myHgVcsRootFactory.createHgRoot(root);
    if (!detectSubrepoChanges(hgRoot))
      return emptyList();

    List<SubrepoConfigChange> subrepoConfigChanges = getSubrepoConfigChanges(changes);
    List<ModificationData> subrepoChanges = new ArrayList<ModificationData>();

    for (SubrepoConfigChange configChange : subrepoConfigChanges) {
      VcsRootImpl subrepo = new VcsRootImpl(root.getId(), configChange.getSubrepoRootParams());
      if (ctx.isProcessedSubrepoChanges(subrepo, configChange.getPreviousSubrepoRevisions(), configChange.getCurrentSubrepoRevision()))
        continue;
      List<ModificationData> subChanges = collectChanges(ctx, subrepo, configChange.getPreviousSubrepoRevisions(), configChange.getCurrentSubrepoRevision(), CheckoutRules.DEFAULT);
      for (ModificationData m : subChanges) {
        if (!ctx.isReportedModification(m)) {
          subrepoChanges.add(m);
          ctx.markAsReported(m);
        }
      }
      ctx.markProcessedSubrepoChanges(subrepo, configChange.getPreviousSubrepoRevisions(), configChange.getCurrentSubrepoRevision());
    }

    List<ModificationData> subSubrepoChanges = getSubrepoChanges(ctx, root, subrepoChanges);
    subrepoChanges.addAll(subSubrepoChanges);
    return subrepoChanges;
  }


  private boolean detectSubrepoChanges(@NotNull HgVcsRoot root) {
    return myConfig.detectSubrepoChanges() && root.detectSubrepoChanges();
  }


  private List<SubrepoConfigChange> getSubrepoConfigChanges(@NotNull List<ModificationData> mainRootChanges) {
    List<SubrepoConfigChange> subrepoConfigChanges = new ArrayList<SubrepoConfigChange>();
    for (ModificationData m : mainRootChanges) {
      subrepoConfigChanges.addAll(getSubrepoConfigChanges(m));
    }
    return subrepoConfigChanges;
  }


  private List<SubrepoConfigChange> getSubrepoConfigChanges(@NotNull ModificationData m) {
    List<SubrepoConfigChange> configChanges = new ArrayList<SubrepoConfigChange>();
    for (SubrepoConfigChange configChange : SubrepoConfigChangesAttributes.readSubrepoConfigChanges(m.getAttributes())) {
      if (configChange.getSubrepoRootParams().isEmpty())
        continue;
      if (configChange.getPreviousSubrepoRevisions().isEmpty())
        continue;
      configChanges.add(configChange);
    }
    return configChanges;
  }


  @NotNull
  private Map<String, String> getAttributes(@NotNull OperationContext ctx, @NotNull VcsRoot mainRoot, @NotNull ChangeSet cset) throws VcsException {
    Map<String, String> attributes = new HashMap<String, String>();
    HgVcsRoot root = myHgVcsRootFactory.createHgRoot(mainRoot);
    if (detectSubrepoChanges(root)) {
      try {
        ServerHgRepo repo = myVcs.createRepo(ctx, root);
        SubrepoConfigChangesAttributes builder = new SubrepoConfigChangesAttributes();
        for (HgSubrepoConfigChange c : repo.getSubrepoConfigChanges(cset)) {
          fillSubrepoConfigChanges(ctx, builder, root, c);
        }
        attributes.putAll(builder.buildAttributes());

        SubrepoRevisionAttributesBuilder attrBuilder = new SubrepoRevisionAttributesBuilder();
        for (HgSubrepoConfigChange c : repo.getSubrepoConfigChanges(cset)) {
          if (!c.subrepoRemoved()) {
            attrBuilder.addSubrepo(new SubrepoConfig(root)
                    .setSubrepoPath(root.expandSubrepoPath(c.getPath()))
                    .setSubrepoRootParamDiff(Constants.REPOSITORY_PROP, ctx.getStringFromPool(c.getCurrent().resolveUrl(root.getRepository())))
                    .setSubrepoRevision(c.getCurrent().revision()));
          }
        }
        attributes.putAll(attrBuilder.buildAttributes());
      } catch (Exception e) {
        Loggers.VCS.warn("Error while reporting subrepo config changes", e);
        if (e instanceof VcsExtension)
          throw (VcsException) e;
        throw new VcsException(e);
      }
    }
    if (attributes.isEmpty())
      return emptyMap();
    return attributes;
  }


  private void fillSubrepoConfigChanges(@NotNull OperationContext ctx,
                                        @NotNull SubrepoConfigChangesAttributes builder,
                                        @NotNull HgVcsRoot mainRoot,
                                        @NotNull HgSubrepoConfigChange c) throws URISyntaxException, VcsException {
    List<String> prevRevisions = new ArrayList<String>();
    if (!(c.subrepoUrlChanged() || c.subrepoAdded() || c.subrepoRemoved())) {
      String subrepoUrl = c.getCurrent().resolveUrl(mainRoot.getRepository());
      String curRevision = c.getCurrent().revision();
      for (SubRepo prevSubrepo : c.getPrevious()) {
        prevRevisions.add(ctx.getStringFromPool(prevSubrepo.revision()));
      }
      builder.addSubrepoConfigChange(new SubrepoConfigChange(mainRoot)
              .setSubrepoPath(ctx.getStringFromPool(mainRoot.expandSubrepoPath(c.getPath())))
              .setSubrepoRootParamDiff(Constants.REPOSITORY_PROP, ctx.getStringFromPool(subrepoUrl))
              .setCurrentSubrepoRevision(ctx.getStringFromPool(curRevision))
              .setPreviousSubrepoRevisions(prevRevisions));
    }
  }
}