LCOV - code coverage report
Current view: top level - server/submit - SubmitStrategyOp.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 219 258 84.9 %
Date: 2022-11-19 15:00:39 Functions: 24 27 88.9 %

          Line data    Source code
       1             : // Copyright (C) 2016 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 com.google.common.base.MoreObjects.firstNonNull;
      18             : import static com.google.common.base.Preconditions.checkState;
      19             : import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
      20             : import static com.google.gerrit.server.project.ProjectCache.illegalState;
      21             : import static java.util.Comparator.comparing;
      22             : import static java.util.Objects.requireNonNull;
      23             : 
      24             : import com.google.common.flogger.FluentLogger;
      25             : import com.google.gerrit.common.Nullable;
      26             : import com.google.gerrit.entities.BranchNameKey;
      27             : import com.google.gerrit.entities.Change;
      28             : import com.google.gerrit.entities.LabelId;
      29             : import com.google.gerrit.entities.PatchSet;
      30             : import com.google.gerrit.entities.PatchSetApproval;
      31             : import com.google.gerrit.entities.Project;
      32             : import com.google.gerrit.entities.RefNames;
      33             : import com.google.gerrit.entities.SubmitRecord;
      34             : import com.google.gerrit.exceptions.StorageException;
      35             : import com.google.gerrit.extensions.restapi.AuthException;
      36             : import com.google.gerrit.server.ChangeMessagesUtil;
      37             : import com.google.gerrit.server.IdentifiedUser;
      38             : import com.google.gerrit.server.approval.ApprovalsUtil;
      39             : import com.google.gerrit.server.change.LabelNormalizer;
      40             : import com.google.gerrit.server.git.CodeReviewCommit;
      41             : import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
      42             : import com.google.gerrit.server.git.GroupCollector;
      43             : import com.google.gerrit.server.git.MergeUtil;
      44             : import com.google.gerrit.server.notedb.ChangeUpdate;
      45             : import com.google.gerrit.server.permissions.PermissionBackendException;
      46             : import com.google.gerrit.server.project.InvalidChangeOperationException;
      47             : import com.google.gerrit.server.project.ProjectConfig;
      48             : import com.google.gerrit.server.project.ProjectState;
      49             : import com.google.gerrit.server.update.BatchUpdateOp;
      50             : import com.google.gerrit.server.update.ChangeContext;
      51             : import com.google.gerrit.server.update.PostUpdateContext;
      52             : import com.google.gerrit.server.update.RepoContext;
      53             : import java.io.IOException;
      54             : import java.util.ArrayList;
      55             : import java.util.HashMap;
      56             : import java.util.List;
      57             : import java.util.Map;
      58             : import java.util.Objects;
      59             : import java.util.Optional;
      60             : import org.eclipse.jgit.errors.IncorrectObjectTypeException;
      61             : import org.eclipse.jgit.errors.MissingObjectException;
      62             : import org.eclipse.jgit.lib.ObjectId;
      63             : import org.eclipse.jgit.lib.Repository;
      64             : import org.eclipse.jgit.transport.ReceiveCommand;
      65             : 
      66             : abstract class SubmitStrategyOp implements BatchUpdateOp {
      67          53 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      68             : 
      69             :   protected final SubmitStrategy.Arguments args;
      70             :   protected final CodeReviewCommit toMerge;
      71             : 
      72             :   private ReceiveCommand command;
      73             :   private PatchSetApproval submitter;
      74             :   private ObjectId mergeResultRev;
      75             :   private PatchSet mergedPatchSet;
      76             :   private Change updatedChange;
      77             :   private CodeReviewCommit alreadyMergedCommit;
      78             :   private boolean changeAlreadyMerged;
      79             :   private String stickyApprovalDiff;
      80             : 
      81          53 :   protected SubmitStrategyOp(SubmitStrategy.Arguments args, CodeReviewCommit toMerge) {
      82          53 :     this.args = args;
      83          53 :     this.toMerge = toMerge;
      84          53 :   }
      85             : 
      86             :   final Change.Id getId() {
      87          53 :     return toMerge.change().getId();
      88             :   }
      89             : 
      90             :   final CodeReviewCommit getCommit() {
      91          53 :     return toMerge;
      92             :   }
      93             : 
      94             :   protected final BranchNameKey getDest() {
      95          53 :     return toMerge.change().getDest();
      96             :   }
      97             : 
      98             :   protected final Project.NameKey getProject() {
      99          53 :     return getDest().project();
     100             :   }
     101             : 
     102             :   @Override
     103             :   public final void updateRepo(RepoContext ctx) throws Exception {
     104          53 :     logger.atFine().log(
     105          53 :         "%s#updateRepo for change %s", getClass().getSimpleName(), toMerge.change().getId());
     106          53 :     checkState(
     107          53 :         ctx.getRevWalk() == args.rw,
     108             :         "SubmitStrategyOp requires callers to call BatchUpdate#setRepository with exactly the same"
     109             :             + " CodeReviewRevWalk instance from the SubmitStrategy.Arguments: %s != %s",
     110          53 :         ctx.getRevWalk(),
     111             :         args.rw);
     112             :     // Run the submit strategy implementation and record the merge tip state so
     113             :     // we can create the ref update.
     114          53 :     CodeReviewCommit tipBefore = args.mergeTip.getCurrentTip();
     115          53 :     alreadyMergedCommit = getAlreadyMergedCommit(ctx);
     116          53 :     if (alreadyMergedCommit == null) {
     117          53 :       updateRepoImpl(ctx);
     118             :     } else {
     119           7 :       logger.atFine().log("Already merged as %s", alreadyMergedCommit.name());
     120             :     }
     121          53 :     CodeReviewCommit tipAfter = args.mergeTip.getCurrentTip();
     122             : 
     123          53 :     if (Objects.equals(tipBefore, tipAfter)) {
     124          19 :       logger.atFine().log("Did not move tip");
     125          19 :       return;
     126          53 :     } else if (tipAfter == null) {
     127           0 :       logger.atFine().log("No merge tip, no update to perform");
     128           0 :       return;
     129             :     }
     130          53 :     logger.atFine().log("Moved tip from %s to %s", tipBefore, tipAfter);
     131             : 
     132          53 :     checkProjectConfig(ctx, tipAfter);
     133             : 
     134             :     // Needed by postUpdate, at which point mergeTip will have advanced further,
     135             :     // so it's easier to just snapshot the command.
     136          53 :     command =
     137             :         new ReceiveCommand(
     138          53 :             firstNonNull(tipBefore, ObjectId.zeroId()), tipAfter, getDest().branch());
     139          53 :     ctx.addRefUpdate(command);
     140          53 :     args.submoduleCommits.addBranchTip(getDest(), tipAfter);
     141          53 :   }
     142             : 
     143             :   private void checkProjectConfig(RepoContext ctx, CodeReviewCommit commit) {
     144          53 :     String refName = getDest().branch();
     145          53 :     if (RefNames.REFS_CONFIG.equals(refName)) {
     146           7 :       logger.atFine().log("Loading new configuration from %s", RefNames.REFS_CONFIG);
     147             :       try {
     148           7 :         ProjectConfig cfg = args.projectConfigFactory.create(getProject());
     149           7 :         cfg.load(ctx.getRevWalk(), commit);
     150           0 :       } catch (Exception e) {
     151           0 :         throw new StorageException(
     152             :             "Submit would store invalid"
     153             :                 + " project configuration "
     154           0 :                 + commit.name()
     155             :                 + " for "
     156           0 :                 + getProject(),
     157             :             e);
     158           7 :       }
     159             :     }
     160          53 :   }
     161             : 
     162             :   @Nullable
     163             :   private CodeReviewCommit getAlreadyMergedCommit(RepoContext ctx) throws IOException {
     164          53 :     CodeReviewCommit tip = args.mergeTip.getInitialTip();
     165          53 :     if (tip == null) {
     166          16 :       return null;
     167             :     }
     168          53 :     CodeReviewRevWalk rw = (CodeReviewRevWalk) ctx.getRevWalk();
     169          53 :     Change.Id id = getId();
     170          53 :     String refPrefix = id.toRefPrefix();
     171             : 
     172          53 :     Map<String, ObjectId> refs = ctx.getRepoView().getRefs(refPrefix);
     173          53 :     List<CodeReviewCommit> commits = new ArrayList<>(refs.size());
     174          53 :     for (Map.Entry<String, ObjectId> e : refs.entrySet()) {
     175          53 :       PatchSet.Id psId = PatchSet.Id.fromRef(refPrefix + e.getKey());
     176          53 :       if (psId == null) {
     177          53 :         continue;
     178             :       }
     179             :       try {
     180          53 :         CodeReviewCommit c = rw.parseCommit(e.getValue());
     181          53 :         c.setPatchsetId(psId);
     182          53 :         commits.add(c);
     183           0 :       } catch (MissingObjectException | IncorrectObjectTypeException ex) {
     184           0 :         continue; // Bogus ref, can't be merged into tip so we don't care.
     185          53 :       }
     186          53 :     }
     187          53 :     commits.sort(comparing((CodeReviewCommit c) -> c.getPatchsetId().get()).reversed());
     188          53 :     CodeReviewCommit result = MergeUtil.findAnyMergedInto(rw, commits, tip);
     189          53 :     if (result == null) {
     190          53 :       return null;
     191             :     }
     192             : 
     193             :     // Some patch set of this change is actually merged into the target
     194             :     // branch, most likely because a previous run of MergeOp failed after
     195             :     // updateRepo, during updateChange.
     196             :     //
     197             :     // Do the best we can to clean this up: mark the change as merged and set
     198             :     // the current patch set. Don't touch the dest branch at all. This can
     199             :     // lead to some odd situations like another change in the set merging in
     200             :     // a different patch set of this change, but that's unavoidable at this
     201             :     // point.  At least the change will end up in the right state.
     202             :     //
     203             :     // TODO(dborowitz): Consider deleting later junk patch set refs. They
     204             :     // presumably don't have PatchSets pointing to them.
     205           7 :     rw.parseBody(result);
     206           7 :     result.add(args.canMergeFlag);
     207           7 :     PatchSet.Id psId = result.getPatchsetId();
     208           7 :     result.copyFrom(toMerge);
     209           7 :     result.setPatchsetId(psId); // Got overwriten by copyFrom.
     210           7 :     result.setStatusCode(CommitMergeStatus.ALREADY_MERGED);
     211           7 :     args.commitStatus.put(result);
     212           7 :     return result;
     213             :   }
     214             : 
     215             :   @Override
     216             :   public final boolean updateChange(ChangeContext ctx) throws Exception {
     217          53 :     logger.atFine().log(
     218          53 :         "%s#updateChange for change %s", getClass().getSimpleName(), toMerge.change().getId());
     219          53 :     toMerge.setNotes(ctx.getNotes()); // Update change and notes from ctx.
     220             : 
     221          53 :     if (ctx.getChange().isMerged()) {
     222             :       // Either another thread won a race, or we are retrying a whole topic submission after one
     223             :       // repo failed with lock failure.
     224           7 :       if (alreadyMergedCommit == null) {
     225           0 :         logger.atFine().log(
     226             :             "Change is already merged according to its status, but we were unable to find it"
     227             :                 + " merged into the current tip (%s)",
     228           0 :             args.mergeTip.getCurrentTip().name());
     229             :       } else {
     230           7 :         logger.atFine().log("Change is already merged");
     231             :       }
     232           7 :       changeAlreadyMerged = true;
     233           7 :       return false;
     234             :     }
     235             : 
     236          53 :     if (alreadyMergedCommit != null) {
     237           6 :       alreadyMergedCommit.setNotes(ctx.getNotes());
     238           6 :       mergedPatchSet = getOrCreateAlreadyMergedPatchSet(ctx);
     239             :     } else {
     240          53 :       PatchSet newPatchSet = updateChangeImpl(ctx);
     241          53 :       PatchSet.Id oldPsId = requireNonNull(toMerge.getPatchsetId());
     242          53 :       PatchSet.Id newPsId = requireNonNull(ctx.getChange().currentPatchSetId());
     243          53 :       if (newPatchSet == null) {
     244          53 :         checkState(
     245          53 :             oldPsId.equals(newPsId),
     246             :             "patch set advanced from %s to %s but updateChangeImpl did not"
     247             :                 + " return new patch set instance",
     248             :             oldPsId,
     249             :             newPsId);
     250             :         // Ok to use stale notes to get the old patch set, which didn't change
     251             :         // during the submit strategy.
     252          53 :         mergedPatchSet =
     253          53 :             requireNonNull(
     254          53 :                 args.psUtil.get(ctx.getNotes(), oldPsId),
     255           0 :                 () -> String.format("missing old patch set %s", oldPsId));
     256             :       } else {
     257           9 :         PatchSet.Id n = newPatchSet.id();
     258           9 :         checkState(
     259           9 :             !n.equals(oldPsId) && n.equals(newPsId),
     260             :             "current patch was %s and is now %s, but updateChangeImpl returned"
     261             :                 + " new patch set instance at %s",
     262             :             oldPsId,
     263             :             newPsId,
     264             :             n);
     265           9 :         mergedPatchSet = newPatchSet;
     266             :       }
     267             :     }
     268             : 
     269          53 :     Change c = ctx.getChange();
     270          53 :     Change.Id id = c.getId();
     271          53 :     CodeReviewCommit commit = args.commitStatus.get(id);
     272          53 :     requireNonNull(commit, () -> String.format("missing commit for change %s", id));
     273          53 :     CommitMergeStatus s = commit.getStatusCode();
     274          53 :     requireNonNull(
     275             :         s,
     276           0 :         () -> String.format("status not set for change %s; expected to previously fail fast", id));
     277          53 :     logger.atFine().log("Status of change %s (%s) on %s: %s", id, commit.name(), c.getDest(), s);
     278          53 :     setApproval(ctx, args.caller);
     279             : 
     280          53 :     mergeResultRev =
     281          53 :         alreadyMergedCommit == null
     282          53 :             ? args.mergeTip.getMergeResults().get(commit)
     283             :             // Our fixup code is not smart enough to find a merge commit
     284             :             // corresponding to the merge result. This results in a different
     285             :             // ChangeMergedEvent in the fixup case, but we'll just live with that.
     286          53 :             : alreadyMergedCommit;
     287             :     try {
     288          53 :       setMerged(ctx, commit, message(ctx, commit, s));
     289           0 :     } catch (StorageException err) {
     290           0 :       String msg = "Error updating change status for " + id;
     291           0 :       logger.atSevere().withCause(err).log("%s", msg);
     292           0 :       args.commitStatus.logProblem(id, msg);
     293             :       // It's possible this happened before updating anything in the db, but
     294             :       // it's hard to know for sure, so just return true below to be safe.
     295          53 :     }
     296          53 :     updatedChange = c;
     297          53 :     return true;
     298             :   }
     299             : 
     300             :   /**
     301             :    * Returns the updated change after this op has been executed.
     302             :    *
     303             :    * @return the updated change after this op has been executed, {@link Optional#empty()} if the op
     304             :    *     was not executed yet, or if the execution has failed
     305             :    */
     306             :   public Optional<Change> getUpdatedChange() {
     307          53 :     return Optional.ofNullable(updatedChange);
     308             :   }
     309             : 
     310             :   private PatchSet getOrCreateAlreadyMergedPatchSet(ChangeContext ctx) throws IOException {
     311           6 :     PatchSet.Id psId = alreadyMergedCommit.getPatchsetId();
     312           6 :     logger.atFine().log("Fixing up already-merged patch set %s", psId);
     313           6 :     PatchSet prevPs = args.psUtil.current(ctx.getNotes());
     314           6 :     ctx.getRevWalk().parseBody(alreadyMergedCommit);
     315           6 :     ctx.getChange()
     316           6 :         .setCurrentPatchSet(
     317           6 :             psId, alreadyMergedCommit.getShortMessage(), ctx.getChange().getOriginalSubject());
     318           6 :     PatchSet existing = args.psUtil.get(ctx.getNotes(), psId);
     319           6 :     if (existing != null) {
     320           6 :       logger.atFine().log("Patch set row exists, only updating change");
     321           6 :       return existing;
     322             :     }
     323             :     // No patch set for the already merged commit, although we know it came form
     324             :     // a patch set ref. Fix up the database. Note that this uses the current
     325             :     // user as the uploader, which is as good a guess as any.
     326             :     List<String> groups =
     327           0 :         prevPs != null ? prevPs.groups() : GroupCollector.getDefaultGroups(alreadyMergedCommit);
     328           0 :     return args.psUtil.insert(
     329           0 :         ctx.getRevWalk(), ctx.getUpdate(psId), psId, alreadyMergedCommit, groups, null, null);
     330             :   }
     331             : 
     332             :   private void setApproval(ChangeContext ctx, IdentifiedUser user) {
     333          53 :     Change.Id id = ctx.getChange().getId();
     334          53 :     List<SubmitRecord> records = args.commitStatus.getSubmitRecords(id);
     335          53 :     PatchSet.Id oldPsId = toMerge.getPatchsetId();
     336          53 :     PatchSet.Id newPsId = ctx.getChange().currentPatchSetId();
     337             : 
     338          53 :     logger.atFine().log("Add approval for %s", id);
     339          53 :     ChangeUpdate origPsUpdate = ctx.getUpdate(oldPsId);
     340          53 :     origPsUpdate.putReviewer(user.getAccountId(), REVIEWER);
     341          53 :     LabelNormalizer.Result normalized = approve(ctx, origPsUpdate);
     342             : 
     343          53 :     ChangeUpdate newPsUpdate = ctx.getUpdate(newPsId);
     344          53 :     newPsUpdate.merge(args.submissionId, records);
     345             :     // If the submit strategy created a new revision (rebase, cherry-pick), copy
     346             :     // approvals as well.
     347          53 :     if (!newPsId.equals(oldPsId)) {
     348           9 :       saveApprovals(normalized, newPsUpdate, true);
     349           9 :       submitter = submitter.copyWithPatchSet(newPsId);
     350             :     }
     351          53 :   }
     352             : 
     353             :   private LabelNormalizer.Result approve(ChangeContext ctx, ChangeUpdate update) {
     354          53 :     PatchSet.Id psId = update.getPatchSetId();
     355          53 :     Map<PatchSetApproval.Key, PatchSetApproval> byKey = new HashMap<>();
     356          53 :     for (PatchSetApproval psa : args.approvalsUtil.byPatchSet(ctx.getNotes(), psId)) {
     357          46 :       byKey.put(psa.key(), psa);
     358          46 :     }
     359             : 
     360          53 :     submitter =
     361          53 :         ApprovalsUtil.newApproval(psId, ctx.getUser(), LabelId.legacySubmit(), 1, ctx.getWhen())
     362          53 :             .build();
     363          53 :     byKey.put(submitter.key(), submitter);
     364             : 
     365             :     // Flatten out existing approvals for this patch set based upon the current
     366             :     // permissions. Once the change is closed the approvals are not updated at
     367             :     // presentation view time, except for zero votes used to indicate a reviewer
     368             :     // was added. So we need to make sure votes are accurate now. This way if
     369             :     // permissions get modified in the future, historical records stay accurate.
     370          53 :     LabelNormalizer.Result normalized =
     371          53 :         args.labelNormalizer.normalize(ctx.getNotes(), byKey.values());
     372          53 :     update.putApproval(submitter.label(), submitter.value());
     373          53 :     saveApprovals(normalized, update, false);
     374          53 :     return normalized;
     375             :   }
     376             : 
     377             :   private void saveApprovals(
     378             :       LabelNormalizer.Result normalized, ChangeUpdate update, boolean includeUnchanged) {
     379          53 :     for (PatchSetApproval psa : normalized.updated()) {
     380           0 :       update.putApprovalFor(psa.accountId(), psa.label(), psa.value());
     381           0 :     }
     382          53 :     for (PatchSetApproval psa : normalized.deleted()) {
     383           0 :       update.removeApprovalFor(psa.accountId(), psa.label());
     384           0 :     }
     385             : 
     386             :     // TODO(dborowitz): Don't use a label in NoteDb; just check when status
     387             :     // change happened.
     388          53 :     for (PatchSetApproval psa : normalized.unchanged()) {
     389          53 :       if (includeUnchanged || psa.isLegacySubmit()) {
     390          53 :         logger.atFine().log("Adding submit label %s", psa);
     391          53 :         update.putApprovalFor(psa.accountId(), psa.label(), psa.value());
     392             :       }
     393          53 :     }
     394          53 :   }
     395             : 
     396             :   private String message(ChangeContext ctx, CodeReviewCommit commit, CommitMergeStatus s)
     397             :       throws AuthException, IOException, PermissionBackendException,
     398             :           InvalidChangeOperationException {
     399          53 :     requireNonNull(s, "CommitMergeStatus may not be null");
     400          53 :     String txt = s.getDescription();
     401          53 :     if (s == CommitMergeStatus.CLEAN_MERGE) {
     402          53 :       return message(ctx, txt);
     403          12 :     } else if (s == CommitMergeStatus.CLEAN_REBASE || s == CommitMergeStatus.CLEAN_PICK) {
     404           9 :       return message(ctx, txt + " as " + commit.name());
     405           7 :     } else if (s == CommitMergeStatus.SKIPPED_IDENTICAL_TREE) {
     406           2 :       return message(ctx, txt);
     407           6 :     } else if (s == CommitMergeStatus.ALREADY_MERGED) {
     408             :       // Best effort to mimic the message that would have happened had this
     409             :       // succeeded the first time around.
     410           6 :       switch (args.submitType) {
     411             :         case FAST_FORWARD_ONLY:
     412             :         case MERGE_ALWAYS:
     413             :         case MERGE_IF_NECESSARY:
     414           3 :           return message(ctx, commit, CommitMergeStatus.CLEAN_MERGE);
     415             :         case CHERRY_PICK:
     416           1 :           return message(ctx, commit, CommitMergeStatus.CLEAN_PICK);
     417             :         case REBASE_IF_NECESSARY:
     418             :         case REBASE_ALWAYS:
     419           2 :           return message(ctx, commit, CommitMergeStatus.CLEAN_REBASE);
     420             :         case INHERIT:
     421             :         default:
     422           0 :           throw new IllegalStateException(
     423             :               "unexpected submit type "
     424           0 :                   + args.submitType.toString()
     425             :                   + " for change "
     426           0 :                   + commit.change().getId());
     427             :       }
     428             :     } else {
     429           0 :       throw new IllegalStateException(
     430             :           "unexpected status "
     431             :               + s
     432             :               + " for change "
     433           0 :               + commit.change().getId()
     434             :               + "; expected to previously fail fast");
     435             :     }
     436             :   }
     437             : 
     438             :   private String message(ChangeContext ctx, String body)
     439             :       throws AuthException, IOException, PermissionBackendException,
     440             :           InvalidChangeOperationException {
     441          53 :     stickyApprovalDiff = args.submitWithStickyApprovalDiff.apply(ctx.getNotes(), ctx.getUser());
     442          53 :     return body + stickyApprovalDiff;
     443             :   }
     444             : 
     445             :   private void setMerged(ChangeContext ctx, CodeReviewCommit commit, String msg) {
     446          53 :     Change c = ctx.getChange();
     447          53 :     logger.atFine().log("Setting change %s merged", c.getId());
     448          53 :     c.setStatus(Change.Status.MERGED);
     449          53 :     c.setSubmissionId(args.submissionId.toString());
     450             :     // TODO(dborowitz): We need to be able to change the author of the message,
     451             :     // which is not the user from the update context. addMergedMessage was able
     452             :     // to do this in the past.
     453          53 :     if (msg != null) {
     454          53 :       args.cmUtil.setChangeMessage(
     455          53 :           ctx.getUpdate(commit.getPatchsetId()), msg, ChangeMessagesUtil.TAG_MERGED);
     456             :     }
     457          53 :   }
     458             : 
     459             :   @Override
     460             :   public final void postUpdate(PostUpdateContext ctx) throws Exception {
     461          53 :     if (changeAlreadyMerged) {
     462             :       // TODO(dborowitz): This is suboptimal behavior in the presence of retries: postUpdate steps
     463             :       // will never get run for changes that submitted successfully on any but the final attempt.
     464             :       // This is primarily a temporary workaround for the fact that the submitter field is not
     465             :       // populated in the changeAlreadyMerged case.
     466             :       //
     467             :       // If we naively execute postUpdate even if the change is already merged when updateChange
     468             :       // being, then we are subject to a race where postUpdate steps are run twice if two submit
     469             :       // processes run at the same time.
     470           7 :       logger.atFine().log(
     471           7 :           "Skipping post-update steps for change %s; submitter is %s", getId(), submitter);
     472           7 :       return;
     473             :     }
     474          53 :     logger.atFine().log(
     475          53 :         "Begin post-update steps for change %s; submitter is %s", getId(), submitter);
     476          53 :     postUpdateImpl(ctx);
     477             : 
     478          53 :     if (command != null) {
     479          53 :       args.tagCache.updateFastForward(
     480          53 :           getProject(), command.getRefName(), command.getOldId(), command.getNewId());
     481             :       // TODO(dborowitz): Move to BatchUpdate? Would also allow us to run once
     482             :       // per project even if multiple changes to refs/meta/config are submitted.
     483          53 :       if (RefNames.REFS_CONFIG.equals(getDest().branch())) {
     484           7 :         args.projectCache.evictAndReindex(getProject());
     485           7 :         ProjectState p =
     486           7 :             args.projectCache.get(getProject()).orElseThrow(illegalState(getProject()));
     487           7 :         try (Repository git = args.repoManager.openRepository(getProject())) {
     488           7 :           git.setGitwebDescription(p.getProject().getDescription());
     489           0 :         } catch (IOException e) {
     490           0 :           logger.atSevere().withCause(e).log("cannot update description of %s", p.getName());
     491           7 :         }
     492             :       }
     493             :     }
     494             : 
     495          53 :     logger.atFine().log(
     496          53 :         "Begin sending emails for submitting change %s; submitter is %s", getId(), submitter);
     497             : 
     498             :     // Assume the change must have been merged at this point, otherwise we would
     499             :     // have failed fast in one of the other steps.
     500             :     try {
     501          53 :       args.mergedSenderFactory
     502          53 :           .create(
     503          53 :               ctx.getProject(),
     504          53 :               toMerge.change(),
     505             :               args.caller,
     506          53 :               ctx.getNotify(getId()),
     507          53 :               ctx.getRepoView(),
     508             :               stickyApprovalDiff)
     509          53 :           .sendAsync();
     510           0 :     } catch (Exception e) {
     511           0 :       logger.atSevere().withCause(e).log("Cannot email merged notification for %s", getId());
     512          53 :     }
     513          53 :     if (mergeResultRev != null && !args.dryrun) {
     514          53 :       args.changeMerged.fire(
     515          53 :           ctx.getChangeData(updatedChange),
     516             :           mergedPatchSet,
     517          53 :           args.accountCache.get(submitter.accountId()).orElse(null),
     518          53 :           args.mergeTip.getCurrentTip().name(),
     519          53 :           ctx.getWhen());
     520             :     }
     521          53 :   }
     522             : 
     523             :   /**
     524             :    * See {@link #updateRepo(RepoContext)}
     525             :    *
     526             :    * @param ctx context for the repository update
     527             :    */
     528          15 :   protected void updateRepoImpl(RepoContext ctx) throws Exception {}
     529             : 
     530             :   /**
     531             :    * Returns a new patch set if one was created by the submit strategy, or null if not
     532             :    *
     533             :    * <p>See {@link #updateChange(ChangeContext)}
     534             :    *
     535             :    * @param ctx context for the change update
     536             :    */
     537             :   protected PatchSet updateChangeImpl(ChangeContext ctx) throws Exception {
     538          53 :     return null;
     539             :   }
     540             : 
     541             :   /**
     542             :    * See {@link #postUpdate(PostUpdateContext)}
     543             :    *
     544             :    * @param ctx context for the post update
     545             :    */
     546          53 :   protected void postUpdateImpl(PostUpdateContext ctx) throws Exception {}
     547             : 
     548             :   /** Amend the commit with gitlink update */
     549             :   protected CodeReviewCommit amendGitlink(CodeReviewCommit commit)
     550             :       throws IntegrationConflictException {
     551          22 :     if (!args.subscriptionGraph.hasSubscription(args.destBranch)) {
     552          22 :       return commit;
     553             :     }
     554             : 
     555             :     // Modify the commit with gitlink update
     556             :     try {
     557           1 :       return args.submoduleCommits.amendGitlinksCommit(
     558           1 :           args.destBranch, commit, args.subscriptionGraph.getSubscriptions(args.destBranch));
     559           0 :     } catch (IOException e) {
     560           0 :       throw new StorageException(
     561           0 :           String.format("cannot update gitlink for the commit at branch %s", args.destBranch), e);
     562           0 :     } catch (SubmoduleConflictException e) {
     563           0 :       throw new IntegrationConflictException(
     564           0 :           String.format(
     565             :               "cannot update gitlink for the commit at branch %s: %s",
     566           0 :               args.destBranch, e.getMessage()),
     567             :           e);
     568             :     }
     569             :   }
     570             : }

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