view mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/HgRepo.java @ 1024:c0c4bf1db865

TW-66329 fix using archive command for creating full patch: previous implementation using clone produces very large overhead for big project repositories in case when settings are stored in the project repo since in case of freezing build settings, core system asks plugin to build full patch and cloning repos into temp dir even from local mirror can take tens of minutes
author Maxim Zaytsev <Maxim.Zaytsev@jetbrains.com>
date Mon, 15 Jun 2020 13:07:12 +0300
parents 1168c4c64d49
children 10dc26b32c35
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.buildTriggers.vcs.mercurial.command.*;
import jetbrains.buildServer.log.Loggers;
import jetbrains.buildServer.util.FileUtil;
import jetbrains.buildServer.vcs.VcsException;
import org.jetbrains.annotations.NotNull;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static jetbrains.buildServer.buildTriggers.vcs.mercurial.HgFileUtil.deleteDir;
import static jetbrains.buildServer.util.FileUtil.isEmptyDir;

/**
* @author dmitry.neverov
*/
public class HgRepo {

  protected final CommandSettingsFactory myCommandSettingsFactory;
  protected final File myWorkingDir;
  protected final String myHgPath;
  protected final AuthSettings myAuthSettings;
  protected final Map<String, Map<String, SubRepo>> mySubreposCache = new HashMap<String, Map<String, SubRepo>>();

  public HgRepo(@NotNull CommandSettingsFactory commandSettingsFactory,
                @NotNull File workingDir,
                @NotNull String hgPath,
                @NotNull AuthSettings authSettings) {
    myCommandSettingsFactory = commandSettingsFactory;
    myWorkingDir = workingDir;
    myHgPath = hgPath;
    myAuthSettings = authSettings;
  }

  public PullCommand pull() {
    return new PullCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir, myAuthSettings);
  }

  public PushCommand push() {
    return new PushCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir, myAuthSettings);
  }

  public CloneCommand doClone() {
    return new CloneCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir, myAuthSettings);
  }

  public IdentifyCommand id() {
    return new IdentifyCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir, myAuthSettings);
  }

  public Init init() {
    return new Init(myCommandSettingsFactory.create(), myHgPath, myWorkingDir, myAuthSettings);
  }

  public LogCommand log() {
    return new LogCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir, myAuthSettings);
  }

  @NotNull
  public CommitsAndMountPointsCommand logSubstates() throws VcsException {
    return new CommitsAndMountPointsCommand(this, myCommandSettingsFactory.create(), myHgPath, myWorkingDir, myAuthSettings);
  }

  public UpdateCommand update() {
    return new UpdateCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir, myAuthSettings);
  }

  public BranchCommand branch() {
    return new BranchCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir);
  }

  public BranchesCommand branches() {
    return new BranchesCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir, myAuthSettings);
  }

  public BookmarksCommand bookmarks() {
    return new BookmarksCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir, myAuthSettings);
  }

  public UpdateBookmarkCommand updateBookmark() {
    return new UpdateBookmarkCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir, myAuthSettings);
  }

  public TagsCommand tags() {
    return new TagsCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir, myAuthSettings);
  }

  public Map<String, String> getBranchRevisions(boolean includeBookmarks, boolean includeTags) throws VcsException {
    Map<String, String> revisions = new HashMap<String, String>();
    if (includeTags)
      revisions.putAll(tags().call());
    if (includeBookmarks && version().call().isEqualsOrGreaterThan(BookmarksCommand.REQUIRED_HG_VERSION))
      revisions.putAll(bookmarks().call());
    revisions.putAll(branches().call());
    return revisions;
  }

  public StatusCommand status() {
    return new StatusCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir, myAuthSettings);
  }

  public TagCommand tag() {
    return new TagCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir, myAuthSettings);
  }

  public CatCommand cat() {
    return new CatCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir, myAuthSettings);
  }

  public ArchiveCommand archive() {
    return new ArchiveCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir, myAuthSettings);
  }

  public VersionCommand version() {
    return new VersionCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir);
  }

  public ParentsCommand parents() {
    return new ParentsCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir);
  }

  public MergeCommand merge() {
    return new MergeCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir);
  }

  public ResolveCommand resolve() {
    return new ResolveCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir);
  }

  public CommitCommand commit() {
    return new CommitCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir);
  }

  public AddRemoveCommand addRemove() {
    return new AddRemoveCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir);
  }

  public RecoverCommand recover() {
    return new RecoverCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir);
  }

  public String path() {
    return myWorkingDir.getAbsolutePath();
  }

  public File getWorkingDir() {
    return myWorkingDir;
  }

  public boolean isEmpty() {
    return isEmptyDir(myWorkingDir);
  }

  public boolean isBookmark(@NotNull String branch) throws VcsException {
    if (branches().call().keySet().contains(branch))
      return false;
    return bookmarks().call().keySet().contains(branch);
  }

  public void resetBookmarks() {
    File dotHg = new File(getWorkingDir(), ".hg");
    FileUtil.delete(new File(dotHg, "bookmarks"));
    FileUtil.delete(new File(dotHg, "bookmarks.current"));
  }

  public PurgeCommand purge() {
    return new PurgeCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir);
  }

  @NotNull
  public CommandResult runCommand(@NotNull String command, @NotNull String... args) throws VcsException {
    return new FreeStyleCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir, command, args).call();
  }

  public String getHgPath() {
    return myHgPath;
  }

  @NotNull
  public List<String> listFiles(@NotNull String revision) throws VcsException {
    List<FileStatus> fileStatuses = status()
            .fromRevision(revision)
            .toRevision(revision)
            .hideStatus()
            .showAllFiles()
            .call();
    List<String> files = new ArrayList<String>(fileStatuses.size());
    for (FileStatus fileStatus : fileStatuses)
      files.add(fileStatus.getPath());
    return files;
  }

  @NotNull
  public String getWorkingDirRevision() throws VcsException {
    List<String> workingDirParents = parents().call();
    if (workingDirParents.isEmpty())
      return LogCommand.ZERO_PARENT_SHORT_ID;//'hg id' shows zeroid when a working dir has no parents
    //if a working dir is in an uncommitted merge state, choose the first parent
    return workingDirParents.get(0);
  }

  public boolean containsRevision(@NotNull String revision) {
    return containsRevision(new ChangeSet(revision));
  }

  public boolean containsRevision(@NotNull ChangeSet cset) {
    try {
      id().revision(cset).inLocalRepository().call();
      return true;
    } catch (VcsException e) {
      return false;
    }
  }

  public boolean isValidRepository() {
    // need better way to check that repository copy is ok
    return myWorkingDir.isDirectory() && new File(myWorkingDir, ".hg").isDirectory();
  }

  public void setDefaultPath(@NotNull String defaultPath) throws VcsException {
    if (defaultPath.contains("\n") || defaultPath.contains("\r"))
      throw new VcsException("Newline in repository url '" + defaultPath + "'");
    try {
      File hgrc = new File(new File(myWorkingDir, ".hg"), "hgrc");
      String content = "%include " + Constants.TEAMCITY_HG_CONFIG_FILE_NAME + "\n\n[paths]\ndefault = " + defaultPath;
      FileUtil.writeFileAndReportErrors(hgrc, content);
    } catch (IOException e) {
      throw new VcsException(e);
    }
  }

  public void setTeamCityConfig(@NotNull String configContent) throws VcsException {
    try {
      File teamcityConfig = new File(new File(myWorkingDir, ".hg"), Constants.TEAMCITY_HG_CONFIG_FILE_NAME);
      FileUtil.writeFileAndReportErrors(teamcityConfig, configContent);
    } catch (IOException e) {
      throw new VcsException(e);
    }
  }

  public boolean hasSubreposAtRevision(@NotNull String revision) {
    return !getSubrepositories(new ChangeSet(revision)).isEmpty();
  }

  public boolean hasSubreposAtRevision(@NotNull ChangeSet cset) {
    return !getSubrepositories(cset).isEmpty();
  }

  public Map<String, SubRepo> getSubrepositories(@NotNull String revision) {
    return getSubrepositories(new ChangeSet(revision));
  }

  public List<HgSubrepoConfigChange> getSubrepoConfigChanges(@NotNull String revision) throws VcsException {
    if (containsSubrepoConfigChange(revision)) {
      List<String> parents = parents().ofRevision(revision).call();
      return getSubrepoConfigChanges(revision, parents);
    }
    return emptyList();
  }

  public List<HgSubrepoConfigChange> getSubrepoConfigChanges(@NotNull ChangeSet cset) {
    if (containsSubrepoConfigChange(cset)) {
      List<String> parents = new ArrayList<String>();
      for (ChangeSetRevision p : cset.getParents()) {
        parents.add(p.getId());
      }
      return getSubrepoConfigChanges(cset.getId(), parents);
    }
    return emptyList();
  }

  public List<HgSubrepoConfigChange> getSubrepoConfigChanges(@NotNull String fromRevision, @NotNull String toRevision) {
    return getSubrepoConfigChanges(toRevision, asList(fromRevision));
  }

  public List<HgSubrepoConfigChange> getSubrepoConfigChanges(@NotNull String revision, @NotNull List<String> parentRevisions) {
    Map<String, SubRepo> curSubrepos = getSubrepositories(revision);
    List<Map<String, SubRepo>> prevSubrepos = new ArrayList<Map<String, SubRepo>>();
    for (String parentRevision : parentRevisions) {
      prevSubrepos.add(getSubrepositories(parentRevision));
    }
    return getSubrepoConfigChanges(revision, prevSubrepos, curSubrepos);

  }

  private List<HgSubrepoConfigChange> getSubrepoConfigChanges(@NotNull String mainRepoRevision,
                                                              @NotNull List<Map<String, SubRepo>> prevSubrepos,
                                                              @NotNull Map<String, SubRepo> curSubrepos) {
    List<HgSubrepoConfigChange> configChanges = new ArrayList<HgSubrepoConfigChange>();
    for (Map.Entry<String, SubRepo> e : curSubrepos.entrySet()) {
      String path = e.getKey();
      SubRepo curSubrepo = e.getValue();
      List<SubRepo> prevs = new ArrayList<SubRepo>();
      for (Map<String, SubRepo> prev : prevSubrepos) {
        SubRepo prevSubrepo = prev.remove(path);
        if (prevSubrepo == null) //no subrepo at this path in previous revision
          continue;
        if (prevSubrepo.equals(curSubrepo)) //subrepo configuration doesn't change since previous revision
          continue;
        prevs.add(prevSubrepo);
      }
      configChanges.add(new HgSubrepoConfigChange(mainRepoRevision, e.getKey(), prevs, curSubrepo));
    }
    for (Map<String, SubRepo> prev : prevSubrepos) {
      for (Map.Entry<String, SubRepo> e : prev.entrySet()) {
        configChanges.add(new HgSubrepoConfigChange(mainRepoRevision, e.getKey(), e.getValue(), null));
      }
    }
    return configChanges;
  }

  private boolean containsSubrepoConfigChange(@NotNull ChangeSet cset) {
    for (FileStatus f : cset.getModifiedFiles()) {
      if (containsSubrepoConfigChange(f))
        return true;
    }
    return false;
  }

  private boolean containsSubrepoConfigChange(@NotNull String revision) throws VcsException {
    List<FileStatus> changedFiles = status()
            .fromRevision(revision)
            .toRevision(revision)
            .showAllFiles()
            .call();
    for (FileStatus f : changedFiles) {
      if (containsSubrepoConfigChange(f))
        return true;
    }
    return false;
  }

  private boolean containsSubrepoConfigChange(@NotNull FileStatus f) {
    return f.getPath().equals(".hgsubstate");
  }

  //path->subrepo
  @NotNull
  public Map<String, SubRepo> getSubrepositories(@NotNull final ChangeSet cset) {
    final String revId = cset.getId();

    Map<String, SubRepo> subrepos = mySubreposCache.get(revId);
    if (subrepos != null) {
      return new HashMap<String, SubRepo>(subrepos);
    }

    File catDir = null;
    final CatCommand cc = cat().setRevId(revId).files(asList(".hgsub", ".hgsubstate")).checkForFailure(false);
    try {
      catDir = cc.call();
      subrepos = HgSubs.readSubrepositories(new File(catDir, ".hgsub"), new File(catDir, ".hgsubstate"));
      mySubreposCache.put(revId, subrepos);
      return new HashMap<String, SubRepo>(subrepos);
    } catch (VcsException e) {
      return emptyMap();
    } finally {
      deleteDir(catDir, Loggers.VCS);
    }
  }

  @Override
  public String toString() {
    return myWorkingDir.getAbsolutePath();
  }

  @NotNull
  public static String shortId(@NotNull final String s) {
    if (s.length() > 12)
      return s.substring(0, 12);
    return s;
  }
}