LCOV - code coverage report
Current view: top level - server/submit - SubmoduleCommits.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 147 163 90.2 %
Date: 2022-11-19 15:00:39 Functions: 13 14 92.9 %

          Line data    Source code
       1             : // Copyright (C) 2020 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.submit;
      16             : 
      17             : import static java.util.Comparator.comparing;
      18             : import static java.util.stream.Collectors.toList;
      19             : 
      20             : import com.google.common.flogger.FluentLogger;
      21             : import com.google.gerrit.common.Nullable;
      22             : import com.google.gerrit.entities.BranchNameKey;
      23             : import com.google.gerrit.entities.SubmoduleSubscription;
      24             : import com.google.gerrit.exceptions.StorageException;
      25             : import com.google.gerrit.server.GerritPersonIdent;
      26             : import com.google.gerrit.server.config.GerritServerConfig;
      27             : import com.google.gerrit.server.config.VerboseSuperprojectUpdate;
      28             : import com.google.gerrit.server.git.CodeReviewCommit;
      29             : import com.google.gerrit.server.project.NoSuchProjectException;
      30             : import com.google.gerrit.server.submit.MergeOpRepoManager.OpenRepo;
      31             : import com.google.inject.Inject;
      32             : import com.google.inject.Provider;
      33             : import com.google.inject.Singleton;
      34             : import java.io.IOException;
      35             : import java.util.Collection;
      36             : import java.util.Iterator;
      37             : import java.util.List;
      38             : import java.util.Objects;
      39             : import java.util.Optional;
      40             : import org.apache.commons.lang3.StringUtils;
      41             : import org.eclipse.jgit.dircache.DirCache;
      42             : import org.eclipse.jgit.dircache.DirCacheBuilder;
      43             : import org.eclipse.jgit.dircache.DirCacheEditor;
      44             : import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
      45             : import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
      46             : import org.eclipse.jgit.dircache.DirCacheEntry;
      47             : import org.eclipse.jgit.lib.CommitBuilder;
      48             : import org.eclipse.jgit.lib.Config;
      49             : import org.eclipse.jgit.lib.FileMode;
      50             : import org.eclipse.jgit.lib.ObjectId;
      51             : import org.eclipse.jgit.lib.PersonIdent;
      52             : import org.eclipse.jgit.revwalk.RevCommit;
      53             : import org.eclipse.jgit.revwalk.RevWalk;
      54             : 
      55             : /** Create commit or amend existing one updating gitlinks. */
      56             : class SubmoduleCommits {
      57             : 
      58         153 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      59             : 
      60             :   private final PersonIdent myIdent;
      61             :   private final VerboseSuperprojectUpdate verboseSuperProject;
      62             :   private final MergeOpRepoManager orm;
      63             :   private final long maxCombinedCommitMessageSize;
      64             :   private final long maxCommitMessages;
      65          69 :   private final BranchTips branchTips = new BranchTips();
      66             : 
      67             :   @Singleton
      68             :   public static class Factory {
      69             :     private final Provider<PersonIdent> serverIdent;
      70             :     private final Config cfg;
      71             : 
      72             :     @Inject
      73         142 :     Factory(@GerritPersonIdent Provider<PersonIdent> serverIdent, @GerritServerConfig Config cfg) {
      74         142 :       this.serverIdent = serverIdent;
      75         142 :       this.cfg = cfg;
      76         142 :     }
      77             : 
      78             :     public SubmoduleCommits create(MergeOpRepoManager orm) {
      79          68 :       return new SubmoduleCommits(orm, serverIdent.get(), cfg);
      80             :     }
      81             :   }
      82             : 
      83          69 :   SubmoduleCommits(MergeOpRepoManager orm, PersonIdent myIdent, Config cfg) {
      84          69 :     this.orm = orm;
      85          69 :     this.myIdent = myIdent;
      86          69 :     this.verboseSuperProject =
      87          69 :         cfg.getEnum("submodule", null, "verboseSuperprojectUpdate", VerboseSuperprojectUpdate.TRUE);
      88          69 :     this.maxCombinedCommitMessageSize =
      89          69 :         cfg.getLong("submodule", "maxCombinedCommitMessageSize", 256 << 10);
      90          69 :     this.maxCommitMessages = cfg.getLong("submodule", "maxCommitMessages", 1000);
      91          69 :   }
      92             : 
      93             :   /**
      94             :    * Use the commit as tip of the branch
      95             :    *
      96             :    * <p>This keeps track of the tip of the branch as the submission progresses.
      97             :    */
      98             :   void addBranchTip(BranchNameKey branch, CodeReviewCommit tip) {
      99          53 :     branchTips.put(branch, tip);
     100          53 :   }
     101             : 
     102             :   /**
     103             :    * Create a separate gitlink commit
     104             :    *
     105             :    * @param subscriber superproject (and branch)
     106             :    * @param subscriptions subprojects the superproject is subscribed to
     107             :    * @return a new commit on top of subscriber with gitlinks update to the tips of the subprojects;
     108             :    *     empty if nothing has changed. Subproject tips are read from the cached branched tips
     109             :    *     (defaulting to the mergeOpRepoManager).
     110             :    */
     111             :   Optional<CodeReviewCommit> composeGitlinksCommit(
     112             :       BranchNameKey subscriber, Collection<SubmoduleSubscription> subscriptions)
     113             :       throws IOException, SubmoduleConflictException {
     114             :     OpenRepo or;
     115             :     try {
     116           3 :       or = orm.getRepo(subscriber.project());
     117           0 :     } catch (NoSuchProjectException | IOException e) {
     118           0 :       throw new StorageException("Cannot access superproject", e);
     119           3 :     }
     120             : 
     121           3 :     CodeReviewCommit currentCommit =
     122             :         branchTips
     123           3 :             .getTip(subscriber, or)
     124           3 :             .orElseThrow(
     125             :                 () ->
     126           0 :                     new SubmoduleConflictException(
     127             :                         "The branch was probably deleted from the subscriber repository"));
     128             : 
     129           3 :     StringBuilder msgbuf = new StringBuilder();
     130           3 :     PersonIdent author = null;
     131           3 :     DirCache dc = readTree(or.getCodeReviewRevWalk(), currentCommit);
     132           3 :     DirCacheEditor ed = dc.editor();
     133           3 :     int count = 0;
     134             : 
     135           3 :     for (SubmoduleSubscription s : sortByPath(subscriptions)) {
     136           3 :       if (count > 0) {
     137           2 :         msgbuf.append("\n\n");
     138             :       }
     139           3 :       RevCommit newCommit = updateSubmodule(dc, ed, msgbuf, s);
     140           3 :       count++;
     141           3 :       if (newCommit != null) {
     142           3 :         PersonIdent newCommitAuthor = newCommit.getAuthorIdent();
     143           3 :         if (author == null) {
     144           3 :           author = new PersonIdent(newCommitAuthor, myIdent.getWhen());
     145           2 :         } else if (!author.getName().equals(newCommitAuthor.getName())
     146           2 :             || !author.getEmailAddress().equals(newCommitAuthor.getEmailAddress())) {
     147           1 :           author = myIdent;
     148             :         }
     149             :       }
     150           3 :     }
     151           3 :     ed.finish();
     152           3 :     ObjectId newTreeId = dc.writeTree(or.ins);
     153             : 
     154             :     // Gitlinks are already in the branch, return null
     155           3 :     if (newTreeId.equals(currentCommit.getTree())) {
     156           2 :       return Optional.empty();
     157             :     }
     158           3 :     CommitBuilder commit = new CommitBuilder();
     159           3 :     commit.setTreeId(newTreeId);
     160           3 :     commit.setParentId(currentCommit);
     161           3 :     StringBuilder commitMsg = new StringBuilder("Update git submodules\n\n");
     162           3 :     if (verboseSuperProject != VerboseSuperprojectUpdate.FALSE) {
     163           3 :       commitMsg.append(msgbuf);
     164             :     }
     165           3 :     commit.setMessage(commitMsg.toString());
     166           3 :     commit.setAuthor(author);
     167           3 :     commit.setCommitter(myIdent);
     168           3 :     ObjectId id = or.ins.insert(commit);
     169           3 :     return Optional.of(or.getCodeReviewRevWalk().parseCommit(id));
     170             :   }
     171             : 
     172             :   /** Amend an existing commit with gitlink updates */
     173             :   CodeReviewCommit amendGitlinksCommit(
     174             :       BranchNameKey subscriber,
     175             :       CodeReviewCommit currentCommit,
     176             :       Collection<SubmoduleSubscription> subscriptions)
     177             :       throws IOException, SubmoduleConflictException {
     178             :     OpenRepo or;
     179             :     try {
     180           2 :       or = orm.getRepo(subscriber.project());
     181           0 :     } catch (NoSuchProjectException | IOException e) {
     182           0 :       throw new StorageException("Cannot access superproject", e);
     183           2 :     }
     184             : 
     185           2 :     StringBuilder msgbuf = new StringBuilder();
     186           2 :     DirCache dc = readTree(or.rw, currentCommit);
     187           2 :     DirCacheEditor ed = dc.editor();
     188           2 :     for (SubmoduleSubscription s : sortByPath(subscriptions)) {
     189           2 :       updateSubmodule(dc, ed, msgbuf, s);
     190           2 :     }
     191           2 :     ed.finish();
     192           2 :     ObjectId newTreeId = dc.writeTree(or.ins);
     193             : 
     194             :     // Gitlinks are already updated, just return the commit
     195           2 :     if (newTreeId.equals(currentCommit.getTree())) {
     196           1 :       return currentCommit;
     197             :     }
     198           2 :     or.rw.parseBody(currentCommit);
     199           2 :     CommitBuilder commit = new CommitBuilder();
     200           2 :     commit.setTreeId(newTreeId);
     201           2 :     commit.setParentIds(currentCommit.getParents());
     202           2 :     if (verboseSuperProject != VerboseSuperprojectUpdate.FALSE) {
     203             :       // TODO(czhen): handle cherrypick footer
     204           2 :       commit.setMessage(currentCommit.getFullMessage() + "\n\n* submodules:\n" + msgbuf.toString());
     205             :     } else {
     206           0 :       commit.setMessage(currentCommit.getFullMessage());
     207             :     }
     208           2 :     commit.setAuthor(currentCommit.getAuthorIdent());
     209           2 :     commit.setCommitter(myIdent);
     210           2 :     ObjectId id = or.ins.insert(commit);
     211           2 :     CodeReviewCommit newCommit = or.getCodeReviewRevWalk().parseCommit(id);
     212           2 :     newCommit.copyFrom(currentCommit);
     213           2 :     return newCommit;
     214             :   }
     215             : 
     216             :   @Nullable
     217             :   private RevCommit updateSubmodule(
     218             :       DirCache dc, DirCacheEditor ed, StringBuilder msgbuf, SubmoduleSubscription s)
     219             :       throws SubmoduleConflictException, IOException {
     220           3 :     logger.atFine().log("Updating gitlink for %s", s);
     221             :     OpenRepo subOr;
     222             :     try {
     223           3 :       subOr = orm.getRepo(s.getSubmodule().project());
     224           0 :     } catch (NoSuchProjectException | IOException e) {
     225           0 :       throw new StorageException("Cannot access submodule", e);
     226           3 :     }
     227             : 
     228           3 :     DirCacheEntry dce = dc.getEntry(s.getPath());
     229           3 :     RevCommit oldCommit = null;
     230           3 :     if (dce != null) {
     231           3 :       if (!dce.getFileMode().equals(FileMode.GITLINK)) {
     232           0 :         String errMsg =
     233             :             "Requested to update gitlink "
     234           0 :                 + s.getPath()
     235             :                 + " in "
     236           0 :                 + s.getSubmodule().project().get()
     237             :                 + " but entry "
     238             :                 + "doesn't have gitlink file mode.";
     239           0 :         throw new SubmoduleConflictException(errMsg);
     240             :       }
     241             :       // Parse the current gitlink entry commit in the subproject repo. This is used to add a
     242             :       // shortlog for this submodule to the commit message in the superproject.
     243             :       //
     244             :       // Even if we don't strictly speaking need that commit message, parsing the commit is a sanity
     245             :       // check that the old gitlink is a commit that actually exists. If not, then there is an
     246             :       // inconsistency between the superproject and subproject state, and we don't want to risk
     247             :       // making things worse by updating the gitlink to something else.
     248             :       try {
     249           3 :         oldCommit = subOr.getCodeReviewRevWalk().parseCommit(dce.getObjectId());
     250           2 :       } catch (IOException e) {
     251             :         // Broken gitlink; sanity check failed. Warn and continue so the submit operation can
     252             :         // proceed, it will just skip this gitlink update.
     253           2 :         logger.atSevere().withCause(e).log("Failed to read commit %s", dce.getObjectId().name());
     254           2 :         return null;
     255           3 :       }
     256             :     }
     257             : 
     258           3 :     Optional<CodeReviewCommit> maybeNewCommit = branchTips.getTip(s.getSubmodule(), subOr);
     259           3 :     if (!maybeNewCommit.isPresent()) {
     260             :       // For whatever reason, this submodule was not updated as part of this submit batch, but the
     261             :       // superproject is still subscribed to this branch. Re-read the ref to see if anything has
     262             :       // changed since the last time the gitlink was updated, and roll that update into the same
     263             :       // commit as all other submodule updates.
     264           0 :       ed.add(new DeletePath(s.getPath()));
     265           0 :       return null;
     266             :     }
     267             : 
     268           3 :     CodeReviewCommit newCommit = maybeNewCommit.get();
     269           3 :     if (Objects.equals(newCommit, oldCommit)) {
     270             :       // gitlink have already been updated for this submodule
     271           1 :       return null;
     272             :     }
     273           3 :     ed.add(
     274           3 :         new PathEdit(s.getPath()) {
     275             :           @Override
     276             :           public void apply(DirCacheEntry ent) {
     277           3 :             ent.setFileMode(FileMode.GITLINK);
     278           3 :             ent.setObjectId(newCommit.getId());
     279           3 :           }
     280             :         });
     281             : 
     282           3 :     if (verboseSuperProject != VerboseSuperprojectUpdate.FALSE) {
     283           3 :       createSubmoduleCommitMsg(msgbuf, s, subOr, newCommit, oldCommit);
     284             :     }
     285           3 :     subOr.getCodeReviewRevWalk().parseBody(newCommit);
     286           3 :     return newCommit;
     287             :   }
     288             : 
     289             :   private void createSubmoduleCommitMsg(
     290             :       StringBuilder msgbuf,
     291             :       SubmoduleSubscription s,
     292             :       OpenRepo subOr,
     293             :       RevCommit newCommit,
     294             :       RevCommit oldCommit) {
     295           3 :     msgbuf.append("* Update ");
     296           3 :     msgbuf.append(s.getPath());
     297           3 :     msgbuf.append(" from branch '");
     298           3 :     msgbuf.append(s.getSubmodule().shortName());
     299           3 :     msgbuf.append("'");
     300           3 :     msgbuf.append("\n  to ");
     301           3 :     msgbuf.append(newCommit.getName());
     302             : 
     303             :     // newly created submodule gitlink, do not append whole history
     304           3 :     if (oldCommit == null) {
     305           2 :       return;
     306             :     }
     307             : 
     308             :     try {
     309           3 :       subOr.rw.resetRetain(subOr.canMergeFlag);
     310           3 :       subOr.rw.markStart(newCommit);
     311           3 :       subOr.rw.markUninteresting(oldCommit);
     312           3 :       int numMessages = 0;
     313           3 :       for (Iterator<RevCommit> iter = subOr.rw.iterator(); iter.hasNext(); ) {
     314           3 :         RevCommit c = iter.next();
     315           3 :         subOr.rw.parseBody(c);
     316             : 
     317             :         String message =
     318           3 :             verboseSuperProject == VerboseSuperprojectUpdate.SUBJECT_ONLY
     319           1 :                 ? c.getShortMessage()
     320           3 :                 : StringUtils.replace(c.getFullMessage(), "\n", "\n    ");
     321             : 
     322           3 :         String bullet = "\n  - ";
     323           3 :         String ellipsis = "\n\n[...]";
     324           3 :         int newSize = msgbuf.length() + bullet.length() + message.length();
     325           3 :         if (++numMessages > maxCommitMessages
     326             :             || newSize > maxCombinedCommitMessageSize
     327           3 :             || (iter.hasNext() && (newSize + ellipsis.length()) > maxCombinedCommitMessageSize)) {
     328           1 :           msgbuf.append(ellipsis);
     329           1 :           break;
     330             :         }
     331           3 :         msgbuf.append(bullet);
     332           3 :         msgbuf.append(message);
     333           3 :       }
     334           0 :     } catch (IOException e) {
     335           0 :       throw new StorageException(
     336             :           "Could not perform a revwalk to create superproject commit message", e);
     337           3 :     }
     338           3 :   }
     339             : 
     340             :   private static DirCache readTree(RevWalk rw, ObjectId base) throws IOException {
     341           3 :     final DirCache dc = DirCache.newInCore();
     342           3 :     final DirCacheBuilder b = dc.builder();
     343           3 :     b.addTree(
     344             :         new byte[0], // no prefix path
     345             :         DirCacheEntry.STAGE_0, // standard stage
     346           3 :         rw.getObjectReader(),
     347           3 :         rw.parseTree(base));
     348           3 :     b.finish();
     349           3 :     return dc;
     350             :   }
     351             : 
     352             :   private static List<SubmoduleSubscription> sortByPath(
     353             :       Collection<SubmoduleSubscription> subscriptions) {
     354           3 :     return subscriptions.stream()
     355           3 :         .sorted(comparing(SubmoduleSubscription::getPath))
     356           3 :         .collect(toList());
     357             :   }
     358             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750