All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.submit.SubmoduleCommits Maven / Gradle / Ivy

There is a newer version: 3.11.0-rc3
Show newest version
// Copyright (C) 2020 The Android Open Source Project
//
// 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 com.google.gerrit.server.submit;

import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;

import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.SubmoduleSubscription;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.VerboseSuperprojectUpdate;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.submit.MergeOpRepoManager.OpenRepo;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheEditor;
import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;

/** Create commit or amend existing one updating gitlinks. */
class SubmoduleCommits {

  private static final FluentLogger logger = FluentLogger.forEnclosingClass();

  private final PersonIdent myIdent;
  private final VerboseSuperprojectUpdate verboseSuperProject;
  private final MergeOpRepoManager orm;
  private final long maxCombinedCommitMessageSize;
  private final long maxCommitMessages;
  private final BranchTips branchTips = new BranchTips();

  @Singleton
  public static class Factory {
    private final Provider serverIdent;
    private final Config cfg;

    @Inject
    Factory(@GerritPersonIdent Provider serverIdent, @GerritServerConfig Config cfg) {
      this.serverIdent = serverIdent;
      this.cfg = cfg;
    }

    public SubmoduleCommits create(MergeOpRepoManager orm) {
      return new SubmoduleCommits(orm, serverIdent.get(), cfg);
    }
  }

  SubmoduleCommits(MergeOpRepoManager orm, PersonIdent myIdent, Config cfg) {
    this.orm = orm;
    this.myIdent = myIdent;
    this.verboseSuperProject =
        cfg.getEnum("submodule", null, "verboseSuperprojectUpdate", VerboseSuperprojectUpdate.TRUE);
    this.maxCombinedCommitMessageSize =
        cfg.getLong("submodule", "maxCombinedCommitMessageSize", 256 << 10);
    this.maxCommitMessages = cfg.getLong("submodule", "maxCommitMessages", 1000);
  }

  /**
   * Use the commit as tip of the branch
   *
   * 

This keeps track of the tip of the branch as the submission progresses. */ void addBranchTip(BranchNameKey branch, CodeReviewCommit tip) { branchTips.put(branch, tip); } /** * Create a separate gitlink commit * * @param subscriber superproject (and branch) * @param subscriptions subprojects the superproject is subscribed to * @return a new commit on top of subscriber with gitlinks update to the tips of the subprojects; * empty if nothing has changed. Subproject tips are read from the cached branched tips * (defaulting to the mergeOpRepoManager). */ Optional composeGitlinksCommit( BranchNameKey subscriber, Collection subscriptions) throws IOException, SubmoduleConflictException { OpenRepo or; try { or = orm.getRepo(subscriber.project()); } catch (NoSuchProjectException | IOException e) { throw new StorageException("Cannot access superproject", e); } CodeReviewCommit currentCommit = branchTips .getTip(subscriber, or) .orElseThrow( () -> new SubmoduleConflictException( "The branch was probably deleted from the subscriber repository")); StringBuilder msgbuf = new StringBuilder(); PersonIdent author = null; DirCache dc = readTree(or.getCodeReviewRevWalk(), currentCommit); DirCacheEditor ed = dc.editor(); int count = 0; for (SubmoduleSubscription s : sortByPath(subscriptions)) { if (count > 0) { msgbuf.append("\n\n"); } RevCommit newCommit = updateSubmodule(dc, ed, msgbuf, s); count++; if (newCommit != null) { PersonIdent newCommitAuthor = newCommit.getAuthorIdent(); if (author == null) { author = new PersonIdent(newCommitAuthor, myIdent.getWhen()); } else if (!author.getName().equals(newCommitAuthor.getName()) || !author.getEmailAddress().equals(newCommitAuthor.getEmailAddress())) { author = myIdent; } } } ed.finish(); ObjectId newTreeId = dc.writeTree(or.ins); // Gitlinks are already in the branch, return null if (newTreeId.equals(currentCommit.getTree())) { return Optional.empty(); } CommitBuilder commit = new CommitBuilder(); commit.setTreeId(newTreeId); commit.setParentId(currentCommit); StringBuilder commitMsg = new StringBuilder("Update git submodules\n\n"); if (verboseSuperProject != VerboseSuperprojectUpdate.FALSE) { commitMsg.append(msgbuf); } commit.setMessage(commitMsg.toString()); commit.setAuthor(author); commit.setCommitter(myIdent); ObjectId id = or.ins.insert(commit); return Optional.of(or.getCodeReviewRevWalk().parseCommit(id)); } /** Amend an existing commit with gitlink updates */ CodeReviewCommit amendGitlinksCommit( BranchNameKey subscriber, CodeReviewCommit currentCommit, Collection subscriptions) throws IOException, SubmoduleConflictException { OpenRepo or; try { or = orm.getRepo(subscriber.project()); } catch (NoSuchProjectException | IOException e) { throw new StorageException("Cannot access superproject", e); } StringBuilder msgbuf = new StringBuilder(); DirCache dc = readTree(or.rw, currentCommit); DirCacheEditor ed = dc.editor(); for (SubmoduleSubscription s : sortByPath(subscriptions)) { updateSubmodule(dc, ed, msgbuf, s); } ed.finish(); ObjectId newTreeId = dc.writeTree(or.ins); // Gitlinks are already updated, just return the commit if (newTreeId.equals(currentCommit.getTree())) { return currentCommit; } or.rw.parseBody(currentCommit); CommitBuilder commit = new CommitBuilder(); commit.setTreeId(newTreeId); commit.setParentIds(currentCommit.getParents()); if (verboseSuperProject != VerboseSuperprojectUpdate.FALSE) { // TODO(czhen): handle cherrypick footer commit.setMessage(currentCommit.getFullMessage() + "\n\n* submodules:\n" + msgbuf.toString()); } else { commit.setMessage(currentCommit.getFullMessage()); } commit.setAuthor(currentCommit.getAuthorIdent()); commit.setCommitter(myIdent); ObjectId id = or.ins.insert(commit); CodeReviewCommit newCommit = or.getCodeReviewRevWalk().parseCommit(id); newCommit.copyFrom(currentCommit); return newCommit; } @Nullable private RevCommit updateSubmodule( DirCache dc, DirCacheEditor ed, StringBuilder msgbuf, SubmoduleSubscription s) throws SubmoduleConflictException, IOException { logger.atFine().log("Updating gitlink for %s", s); OpenRepo subOr; try { subOr = orm.getRepo(s.getSubmodule().project()); } catch (NoSuchProjectException | IOException e) { throw new StorageException("Cannot access submodule", e); } DirCacheEntry dce = dc.getEntry(s.getPath()); RevCommit oldCommit = null; if (dce != null) { if (!dce.getFileMode().equals(FileMode.GITLINK)) { String errMsg = "Requested to update gitlink " + s.getPath() + " in " + s.getSubmodule().project().get() + " but entry " + "doesn't have gitlink file mode."; throw new SubmoduleConflictException(errMsg); } // Parse the current gitlink entry commit in the subproject repo. This is used to add a // shortlog for this submodule to the commit message in the superproject. // // Even if we don't strictly speaking need that commit message, parsing the commit is a sanity // check that the old gitlink is a commit that actually exists. If not, then there is an // inconsistency between the superproject and subproject state, and we don't want to risk // making things worse by updating the gitlink to something else. try { oldCommit = subOr.getCodeReviewRevWalk().parseCommit(dce.getObjectId()); } catch (IOException e) { // Broken gitlink; sanity check failed. Warn and continue so the submit operation can // proceed, it will just skip this gitlink update. logger.atSevere().withCause(e).log("Failed to read commit %s", dce.getObjectId().name()); return null; } } Optional maybeNewCommit = branchTips.getTip(s.getSubmodule(), subOr); if (!maybeNewCommit.isPresent()) { // For whatever reason, this submodule was not updated as part of this submit batch, but the // superproject is still subscribed to this branch. Re-read the ref to see if anything has // changed since the last time the gitlink was updated, and roll that update into the same // commit as all other submodule updates. ed.add(new DeletePath(s.getPath())); return null; } CodeReviewCommit newCommit = maybeNewCommit.get(); if (Objects.equals(newCommit, oldCommit)) { // gitlink have already been updated for this submodule return null; } ed.add( new PathEdit(s.getPath()) { @Override public void apply(DirCacheEntry ent) { ent.setFileMode(FileMode.GITLINK); ent.setObjectId(newCommit.getId()); } }); if (verboseSuperProject != VerboseSuperprojectUpdate.FALSE) { createSubmoduleCommitMsg(msgbuf, s, subOr, newCommit, oldCommit); } subOr.getCodeReviewRevWalk().parseBody(newCommit); return newCommit; } private void createSubmoduleCommitMsg( StringBuilder msgbuf, SubmoduleSubscription s, OpenRepo subOr, RevCommit newCommit, RevCommit oldCommit) { msgbuf.append("* Update "); msgbuf.append(s.getPath()); msgbuf.append(" from branch '"); msgbuf.append(s.getSubmodule().shortName()); msgbuf.append("'"); msgbuf.append("\n to "); msgbuf.append(newCommit.getName()); // newly created submodule gitlink, do not append whole history if (oldCommit == null) { return; } try { subOr.rw.resetRetain(subOr.canMergeFlag); subOr.rw.markStart(newCommit); subOr.rw.markUninteresting(oldCommit); int numMessages = 0; for (Iterator iter = subOr.rw.iterator(); iter.hasNext(); ) { RevCommit c = iter.next(); subOr.rw.parseBody(c); String message = verboseSuperProject == VerboseSuperprojectUpdate.SUBJECT_ONLY ? c.getShortMessage() : StringUtils.replace(c.getFullMessage(), "\n", "\n "); String bullet = "\n - "; String ellipsis = "\n\n[...]"; int newSize = msgbuf.length() + bullet.length() + message.length(); if (++numMessages > maxCommitMessages || newSize > maxCombinedCommitMessageSize || (iter.hasNext() && (newSize + ellipsis.length()) > maxCombinedCommitMessageSize)) { msgbuf.append(ellipsis); break; } msgbuf.append(bullet); msgbuf.append(message); } } catch (IOException e) { throw new StorageException( "Could not perform a revwalk to create superproject commit message", e); } } private static DirCache readTree(RevWalk rw, ObjectId base) throws IOException { final DirCache dc = DirCache.newInCore(); final DirCacheBuilder b = dc.builder(); b.addTree( new byte[0], // no prefix path DirCacheEntry.STAGE_0, // standard stage rw.getObjectReader(), rw.parseTree(base)); b.finish(); return dc; } private static List sortByPath( Collection subscriptions) { return subscriptions.stream() .sorted(comparing(SubmoduleSubscription::getPath)) .collect(toList()); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy