view mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/CollectChangesContext.java @ 915:6b5e83970a26 Hajipur-9.0.x

Fix test: test repository requires a more recent mercurial
author Dmitry Neverov <dmitry.neverov@jetbrains.com>
date Fri, 02 Jan 2015 12:36:52 +0100
parents 39ff04730ccc
children 31ac1d822fd7
line wrap: on
line source
/*
 * Copyright 2000-2014 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.Pair;
import gnu.trove.TLongObjectHashMap;
import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.HgVcsRoot;
import jetbrains.buildServer.util.Hash;
import jetbrains.buildServer.util.graph.BFSVisitorAdapter;
import jetbrains.buildServer.util.graph.DAG;
import jetbrains.buildServer.vcs.ModificationData;
import jetbrains.buildServer.vcs.RepositoryStateData;
import jetbrains.buildServer.vcs.VcsException;
import jetbrains.buildServer.vcs.VcsRoot;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.util.*;

import static java.util.Collections.singleton;
import static jetbrains.buildServer.util.CollectionsUtil.setOf;

public class CollectChangesContext extends OperationContext {

  private final Set<String> myUninterestingRevisions;
  private final Map<VcsRootKey, DAG<String>> myDags = new HashMap<VcsRootKey, DAG<String>>();
  private Set<File> mySyncedDirs = new HashSet<File>();
  private Map<String, Set<SubrepoChangesInterval>> myProcessedSubrepoChanges = new HashMap<String, Set<SubrepoChangesInterval>>();//subrepo url -> processed changes intervals
  private Map<VcsRootKey, Set<String>> myRevisionsPerRoot = new HashMap<VcsRootKey, Set<String>>();
  private Map<File, ServerHgRepo> myRepos = new HashMap<File, ServerHgRepo>();
  private boolean myIncludeFromRevisions = false;//by default don't include them, they should be included only for subrepos
  private TLongObjectHashMap<String> myStringPool = new TLongObjectHashMap<String>();

  public CollectChangesContext(@NotNull MercurialVcsSupport vcs,
                               @NotNull RepoFactory repoFactory,
                               @NotNull MercurialProgress progress,
                               @NotNull RepositoryStateData fromState) {
    this(vcs, repoFactory, progress, new HashSet<String>(fromState.getBranchRevisions().values()));
  }

  public CollectChangesContext(@NotNull MercurialVcsSupport vcs,
                               @NotNull RepoFactory repoFactory,
                               @NotNull MercurialProgress progress,
                               @NotNull String fromVersion) {
    this(vcs, repoFactory, progress, setOf(fromVersion));
  }

  public CollectChangesContext(@NotNull MercurialVcsSupport vcs,
                               @NotNull RepoFactory repoFactory,
                               @NotNull MercurialProgress progress,
                               @NotNull Collection<String> fromVersions) {
    super(vcs, repoFactory, progress);
    myUninterestingRevisions = new HashSet<String>(fromVersions);
  }

  public void syncRepository(@NotNull HgVcsRoot root) throws VcsException {
    File dir = myVcs.getWorkingDir(root);
    if (mySyncedDirs.contains(dir))
      return;
    super.syncRepository(root);
    mySyncedDirs.add(dir);
  }

  public void markProcessedSubrepoChanges(@NotNull VcsRoot subrepo, @NotNull List<String> previousSubrepoRevisions, @NotNull String currentSubrepoRevision) {
    String subrepoUrl = subrepo.getProperty(Constants.REPOSITORY_PROP);
    Set<SubrepoChangesInterval> processedSubrepoChanges = myProcessedSubrepoChanges.get(subrepoUrl);
    if (processedSubrepoChanges == null) {
      processedSubrepoChanges = new HashSet<SubrepoChangesInterval>();
      myProcessedSubrepoChanges.put(subrepoUrl, processedSubrepoChanges);
    }
    processedSubrepoChanges.add(new SubrepoChangesInterval(previousSubrepoRevisions, currentSubrepoRevision));
  }

  public boolean isProcessedSubrepoChanges(@NotNull VcsRoot subrepo, @NotNull List<String> previousSubrepoRevisions, @NotNull String currentSubrepoRevision) {
    String subrepoUrl = subrepo.getProperty(Constants.REPOSITORY_PROP);
    Set<SubrepoChangesInterval> processedSubrepoChanges = myProcessedSubrepoChanges.get(subrepoUrl);
    if (processedSubrepoChanges == null)
      return false;
    return processedSubrepoChanges.contains(new SubrepoChangesInterval(previousSubrepoRevisions, currentSubrepoRevision));
  }

  @NotNull
  public ServerHgRepo createRepo(@NotNull final HgVcsRoot root, @NotNull final File workingDir) throws VcsException {
    ServerHgRepo repo = myRepos.get(workingDir);
    if (repo != null) {
      return repo;
    }

    repo = super.createRepo(root, workingDir);
    repo.setOperationContext(this);
    myRepos.put(workingDir, repo);
    return repo;
  }

  @NotNull
  @Override
  public ServerHgRepo createRepo(@NotNull HgVcsRoot root) throws VcsException {
    File workingDir = myVcs.getWorkingDir(root);
    return createRepo(root, workingDir);
  }

  public boolean isReportedModification(@NotNull ModificationData m) {
    Set<String> revisions = getReportedRootRevisions(m);
    return revisions.contains(m.getVersion());
  }

  public void markAsReported(@NotNull ModificationData m) {
    Set<String> revisions = getReportedRootRevisions(m);
    revisions.add(m.getVersion());
  }

  @NotNull
  private Set<String> getReportedRootRevisions(@NotNull ModificationData m) {
    VcsRootKey key = VcsRootKey.create(m.getVcsRoot());
    Set<String> revisions = myRevisionsPerRoot.get(key);
    if (revisions == null) {
      revisions = new HashSet<String>();
      myRevisionsPerRoot.put(key, revisions);
    }
    return revisions;
  }


  public boolean includeFromRevisions() {
    return myIncludeFromRevisions;
  }


  public void setIncludeFromRevisions(boolean doInclude) {
    myIncludeFromRevisions = doInclude;
  }


  public String getStringFromPool(@Nullable String s) {
    if (s == null)
      return null;
    final long hash = Hash.calc(s);
    String reused = myStringPool.get(hash);
    if (reused != null) {
      if (!reused.equals(s))  // hash collision
        return s;
      return reused;
    }
    //noinspection RedundantStringConstructorCall
    reused = new String(s);
    myStringPool.put(hash, reused);
    return reused;
  }


  /**
   * Collecting changes is per branch, but we should take revisions of other branches
   * into account otherwise plugin can report redundant changes. Consider the following graph:
   *
   * default
   * | topic
   * |  |
   * V  V
   *103
   * | 102
   * | |
   *101|
   * |\|
   * | 100
   * | |
   *99 |
   *.....
   * | |
   * |/
   * 1
   *
   * Let's say plugin collects changes between states {default:99, topic:100} and {default: 103, topic:102}.
   * If we run hg log -r 'ancestors(103)-ancestors(99)' it will return changes [1..103]. But changes
   * [1..100] were probably already reported and TeamCity will not persist them.
   *
   * This method detects such a cases and returns (99, 100) for 103.
   */
  @NotNull
  public Collection<String> getFromRevisionsForBranch(@NotNull HgVcsRoot root,
                                                      @NotNull String fromRevision,
                                                      @NotNull String toRevision,
                                                      @NotNull RepositoryStateData toState) throws VcsException {
    syncRepository(root);
    ServerHgRepo repo = createRepo(root, myVcs.getWorkingDir(root));

    if (!repo.supportRevsets())
      return singleton(fromRevision);

    Set<String> fromRevisions = new HashSet<String>();
    if (toState.getBranchRevisions().size() > 1) {
      VcsRootKey rootKey = VcsRootKey.create(root);
      DAG<String> dag = myDags.get(rootKey);
      if (dag == null) {
        dag = repo.loadDag();
        myDags.put(rootKey, dag);
      }
      if (dag.containsNode(toRevision)) {
        FindIntervalVisitor visitor = new FindIntervalVisitor(dag, myUninterestingRevisions);
        dag.breadthFirstSearch(toRevision, visitor);
        fromRevisions.addAll(visitor.getEndpoints());
      } else {
        fromRevisions.add(fromRevision);
      }
    } else {
      fromRevisions.add(fromRevision);
    }

    if (fromRevisions.isEmpty())
      fromRevisions.add(toRevision);

    return fromRevisions;
  }

  private final static class SubrepoChangesInterval extends Pair<List<String>, String> {
    private SubrepoChangesInterval(@NotNull List<String> prevRevisions, @NotNull String currentRevision) {
      super(prevRevisions, currentRevision);
    }
  }

  private final static class VcsRootKey extends Pair<String, String> {
    private VcsRootKey(@NotNull String repositoryUrl, @Nullable String subrepoPath) {
      super(repositoryUrl, subrepoPath);
    }

    private static VcsRootKey create(@NotNull HgVcsRoot root) {
      return new VcsRootKey(root.getProperty(Constants.REPOSITORY_PROP), root.getProperty("teamcity.internal.subrepo.path"));
    }

    private static VcsRootKey create(@NotNull VcsRoot root) {
      return new VcsRootKey(root.getProperty(Constants.REPOSITORY_PROP), root.getProperty("teamcity.internal.subrepo.path"));
    }
  }



  private final static class FindIntervalVisitor extends BFSVisitorAdapter<String> {

    private final DAG<String> myDag;
    private final Collection<String> myUninterestingRevisions;
    private final Set<String> myVisitedUninterestingRevisions = new HashSet<String>();
    private final Set<String> myEndpoints = new HashSet<String>();

    private FindIntervalVisitor(@NotNull DAG<String> dag, @NotNull Collection<String> uninteresting) {
      myDag = dag;
      myUninterestingRevisions = uninteresting;
    }

    @Override
    public boolean discover(@NotNull String revision) {
      markUninteresting();
      if (!isInteresting(revision)) {
        addEndpoint(revision);
        return false;
      }
      return true;
    }

    private void markUninteresting() {
      if (!myVisitedUninterestingRevisions.isEmpty())
        return;
      for (String rev : myUninterestingRevisions) {
        if (myDag.containsNode(rev))
          myDag.breadthFirstSearch(rev, new MarkUninterestingRevisions());
      }
    }

    private boolean isInteresting(@NotNull String revision) {
      return !myVisitedUninterestingRevisions.contains(revision);
    }

    private void addEndpoint(@NotNull String revision) {
      myEndpoints.add(revision);
    }

    @NotNull
    public Set<String> getEndpoints() {
      return myEndpoints;
    }

    private class MarkUninterestingRevisions extends BFSVisitorAdapter<String> {
      @Override
      public boolean discover(@NotNull String node) {
        if (myVisitedUninterestingRevisions.contains(node))
          return false;
        myVisitedUninterestingRevisions.add(node);
        return true;
      }
    }
  }
}