LCOV - code coverage report
Current view: top level - server/submit - MergeOp.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 345 458 75.3 %
Date: 2022-11-19 15:00:39 Functions: 39 48 81.2 %

          Line data    Source code
       1             : // Copyright (C) 2008 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.checkArgument;
      19             : import static java.util.Comparator.comparing;
      20             : import static java.util.Objects.requireNonNull;
      21             : import static java.util.stream.Collectors.toSet;
      22             : 
      23             : import com.github.rholder.retry.Attempt;
      24             : import com.github.rholder.retry.RetryListener;
      25             : import com.google.auto.value.AutoValue;
      26             : import com.google.common.base.Joiner;
      27             : import com.google.common.collect.ImmutableList;
      28             : import com.google.common.collect.ImmutableMap;
      29             : import com.google.common.collect.ImmutableSet;
      30             : import com.google.common.collect.ImmutableSetMultimap;
      31             : import com.google.common.collect.ListMultimap;
      32             : import com.google.common.collect.MultimapBuilder;
      33             : import com.google.common.collect.SetMultimap;
      34             : import com.google.common.flogger.FluentLogger;
      35             : import com.google.gerrit.common.Nullable;
      36             : import com.google.gerrit.entities.BranchNameKey;
      37             : import com.google.gerrit.entities.Change;
      38             : import com.google.gerrit.entities.Change.Status;
      39             : import com.google.gerrit.entities.PatchSet;
      40             : import com.google.gerrit.entities.Project;
      41             : import com.google.gerrit.entities.SubmissionId;
      42             : import com.google.gerrit.entities.SubmitRecord;
      43             : import com.google.gerrit.entities.SubmitRequirement;
      44             : import com.google.gerrit.entities.SubmitRequirementResult;
      45             : import com.google.gerrit.entities.SubmitTypeRecord;
      46             : import com.google.gerrit.exceptions.MergeUpdateException;
      47             : import com.google.gerrit.exceptions.StorageException;
      48             : import com.google.gerrit.extensions.api.changes.NotifyHandling;
      49             : import com.google.gerrit.extensions.api.changes.SubmitInput;
      50             : import com.google.gerrit.extensions.client.SubmitType;
      51             : import com.google.gerrit.extensions.restapi.AuthException;
      52             : import com.google.gerrit.extensions.restapi.ResourceConflictException;
      53             : import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
      54             : import com.google.gerrit.extensions.restapi.RestApiException;
      55             : import com.google.gerrit.git.LockFailureException;
      56             : import com.google.gerrit.metrics.Counter0;
      57             : import com.google.gerrit.metrics.Description;
      58             : import com.google.gerrit.metrics.MetricMaker;
      59             : import com.google.gerrit.server.ChangeMessagesUtil;
      60             : import com.google.gerrit.server.ChangeUtil;
      61             : import com.google.gerrit.server.IdentifiedUser;
      62             : import com.google.gerrit.server.InternalUser;
      63             : import com.google.gerrit.server.change.NotifyResolver;
      64             : import com.google.gerrit.server.git.CodeReviewCommit;
      65             : import com.google.gerrit.server.git.MergeTip;
      66             : import com.google.gerrit.server.git.validators.MergeValidationException;
      67             : import com.google.gerrit.server.git.validators.MergeValidators;
      68             : import com.google.gerrit.server.logging.RequestId;
      69             : import com.google.gerrit.server.logging.TraceContext;
      70             : import com.google.gerrit.server.notedb.ChangeNotes;
      71             : import com.google.gerrit.server.notedb.StoreSubmitRequirementsOp;
      72             : import com.google.gerrit.server.permissions.PermissionBackendException;
      73             : import com.google.gerrit.server.project.NoSuchProjectException;
      74             : import com.google.gerrit.server.project.SubmitRuleOptions;
      75             : import com.google.gerrit.server.query.change.ChangeData;
      76             : import com.google.gerrit.server.query.change.InternalChangeQuery;
      77             : import com.google.gerrit.server.submit.MergeOpRepoManager.OpenBranch;
      78             : import com.google.gerrit.server.submit.MergeOpRepoManager.OpenRepo;
      79             : import com.google.gerrit.server.update.BatchUpdate;
      80             : import com.google.gerrit.server.update.BatchUpdateOp;
      81             : import com.google.gerrit.server.update.ChangeContext;
      82             : import com.google.gerrit.server.update.RetryHelper;
      83             : import com.google.gerrit.server.update.SubmissionExecutor;
      84             : import com.google.gerrit.server.update.SubmissionListener;
      85             : import com.google.gerrit.server.update.SuperprojectUpdateOnSubmission;
      86             : import com.google.gerrit.server.update.UpdateException;
      87             : import com.google.gerrit.server.util.time.TimeUtil;
      88             : import com.google.inject.Inject;
      89             : import com.google.inject.Provider;
      90             : import com.google.inject.Singleton;
      91             : import java.io.IOException;
      92             : import java.time.Instant;
      93             : import java.util.ArrayList;
      94             : import java.util.Collection;
      95             : import java.util.HashMap;
      96             : import java.util.HashSet;
      97             : import java.util.LinkedHashSet;
      98             : import java.util.List;
      99             : import java.util.Map;
     100             : import java.util.Optional;
     101             : import java.util.Set;
     102             : import java.util.function.Function;
     103             : import java.util.stream.Collectors;
     104             : import org.eclipse.jgit.errors.ConfigInvalidException;
     105             : import org.eclipse.jgit.errors.IncorrectObjectTypeException;
     106             : import org.eclipse.jgit.lib.Constants;
     107             : import org.eclipse.jgit.lib.ObjectId;
     108             : import org.eclipse.jgit.lib.Ref;
     109             : import org.eclipse.jgit.revwalk.RevCommit;
     110             : 
     111             : /**
     112             :  * Merges changes in submission order into a single branch.
     113             :  *
     114             :  * <p>Branches are reduced to the minimum number of heads needed to merge everything. This allows
     115             :  * commits to be entered into the queue in any order (such as ancestors before descendants) and only
     116             :  * the most recent commit on any line of development will be merged. All unmerged commits along a
     117             :  * line of development must be in the submission queue in order to merge the tip of that line.
     118             :  *
     119             :  * <p>Conflicts are handled by discarding the entire line of development and marking it as
     120             :  * conflicting, even if an earlier commit along that same line can be merged cleanly.
     121             :  */
     122             : public class MergeOp implements AutoCloseable {
     123          76 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
     124             : 
     125          76 :   private static final SubmitRuleOptions SUBMIT_RULE_OPTIONS = SubmitRuleOptions.builder().build();
     126          76 :   private static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_ALLOW_CLOSED =
     127          76 :       SUBMIT_RULE_OPTIONS.toBuilder().recomputeOnClosedChanges(true).build();
     128             : 
     129             :   public static class CommitStatus {
     130             :     private final ImmutableMap<Change.Id, ChangeData> changes;
     131             :     private final ImmutableSetMultimap<BranchNameKey, Change.Id> byBranch;
     132             :     private final Map<Change.Id, CodeReviewCommit> commits;
     133             :     private final ListMultimap<Change.Id, String> problems;
     134             :     private final boolean allowClosed;
     135             : 
     136          53 :     private CommitStatus(ChangeSet cs, boolean allowClosed) {
     137          53 :       checkArgument(
     138          53 :           !cs.furtherHiddenChanges(), "CommitStatus must not be called with hidden changes");
     139          53 :       changes = cs.changesById();
     140          53 :       ImmutableSetMultimap.Builder<BranchNameKey, Change.Id> bb = ImmutableSetMultimap.builder();
     141          53 :       for (ChangeData cd : cs.changes()) {
     142          53 :         bb.put(cd.change().getDest(), cd.getId());
     143          53 :       }
     144          53 :       byBranch = bb.build();
     145          53 :       commits = new HashMap<>();
     146          53 :       problems = MultimapBuilder.treeKeys(comparing(Change.Id::get)).arrayListValues(1).build();
     147          53 :       this.allowClosed = allowClosed;
     148          53 :     }
     149             : 
     150             :     public ImmutableSet<Change.Id> getChangeIds() {
     151          53 :       return changes.keySet();
     152             :     }
     153             : 
     154             :     public ImmutableSet<Change.Id> getChangeIds(BranchNameKey branch) {
     155          53 :       return byBranch.get(branch);
     156             :     }
     157             : 
     158             :     public CodeReviewCommit get(Change.Id changeId) {
     159          53 :       return commits.get(changeId);
     160             :     }
     161             : 
     162             :     public void put(CodeReviewCommit c) {
     163          53 :       commits.put(c.change().getId(), c);
     164          53 :     }
     165             : 
     166             :     public void problem(Change.Id id, String problem) {
     167          16 :       problems.put(id, problem);
     168          16 :     }
     169             : 
     170             :     public void logProblem(Change.Id id, Throwable t) {
     171           0 :       String msg = "Error reading change";
     172           0 :       logger.atSevere().withCause(t).log("%s %s", msg, id);
     173           0 :       problems.put(id, msg);
     174           0 :     }
     175             : 
     176             :     public void logProblem(Change.Id id, String msg) {
     177           0 :       logger.atSevere().log("%s %s", msg, id);
     178           0 :       problems.put(id, msg);
     179           0 :     }
     180             : 
     181             :     public boolean isOk() {
     182          53 :       return problems.isEmpty();
     183             :     }
     184             : 
     185             :     public List<SubmitRecord> getSubmitRecords(Change.Id id) {
     186             :       // Use the cached submit records from the original ChangeData in the input
     187             :       // ChangeSet, which were checked earlier in the integrate process. Even in
     188             :       // the case of a race where the submit records may have changed, it makes
     189             :       // more sense to store the original results of the submit rule evaluator
     190             :       // than to fail at this point.
     191             :       //
     192             :       // However, do NOT expose that ChangeData directly, as it is way out of
     193             :       // date by this point.
     194          53 :       ChangeData cd = requireNonNull(changes.get(id), () -> String.format("ChangeData for %s", id));
     195          53 :       return requireNonNull(
     196          53 :           cd.submitRecords(submitRuleOptions(allowClosed)),
     197             :           "getSubmitRecord only valid after submit rules are evalutated");
     198             :     }
     199             : 
     200             :     public void maybeFailVerbose() throws ResourceConflictException {
     201          53 :       if (isOk()) {
     202          53 :         return;
     203             :       }
     204          16 :       String msg =
     205             :           "Failed to submit "
     206          16 :               + changes.size()
     207             :               + " change"
     208          16 :               + (changes.size() > 1 ? "s" : "")
     209             :               + " due to the following problems:\n";
     210          16 :       List<String> ps = new ArrayList<>(problems.keySet().size());
     211          16 :       for (Change.Id id : problems.keySet()) {
     212          16 :         ps.add("Change " + id + ": " + Joiner.on("; ").join(problems.get(id)));
     213          16 :       }
     214          16 :       throw new ResourceConflictException(msg + Joiner.on('\n').join(ps));
     215             :     }
     216             : 
     217             :     public void maybeFail(String msgPrefix) throws ResourceConflictException {
     218          53 :       if (isOk()) {
     219          53 :         return;
     220             :       }
     221           0 :       StringBuilder msg = new StringBuilder(msgPrefix).append(" of change");
     222           0 :       Set<Change.Id> ids = problems.keySet();
     223           0 :       if (ids.size() == 1) {
     224           0 :         msg.append(" ").append(ids.iterator().next());
     225             :       } else {
     226           0 :         msg.append("s ").append(Joiner.on(", ").join(ids));
     227             :       }
     228           0 :       throw new ResourceConflictException(msg.toString());
     229             :     }
     230             :   }
     231             : 
     232             :   private final ChangeMessagesUtil cmUtil;
     233             :   private final BatchUpdate.Factory batchUpdateFactory;
     234             :   private final InternalUser.Factory internalUserFactory;
     235             :   private final MergeSuperSet mergeSuperSet;
     236             :   private final MergeValidators.Factory mergeValidatorsFactory;
     237             :   private final Provider<InternalChangeQuery> queryProvider;
     238             :   private final SubmitStrategyFactory submitStrategyFactory;
     239             :   private final SubscriptionGraph.Factory subscriptionGraphFactory;
     240             :   private final SubmoduleCommits.Factory submoduleCommitsFactory;
     241             :   private final ImmutableList<SubmissionListener> superprojectUpdateSubmissionListeners;
     242             :   private final Provider<MergeOpRepoManager> ormProvider;
     243             :   private final NotifyResolver notifyResolver;
     244             :   private final RetryHelper retryHelper;
     245             :   private final ChangeData.Factory changeDataFactory;
     246             :   private final StoreSubmitRequirementsOp.Factory storeSubmitRequirementsOpFactory;
     247             : 
     248             :   // Changes that were updated by this MergeOp.
     249             :   private final Map<Change.Id, Change> updatedChanges;
     250             : 
     251             :   private Instant ts;
     252             :   private SubmissionId submissionId;
     253             :   private IdentifiedUser caller;
     254             : 
     255             :   private MergeOpRepoManager orm;
     256             :   private CommitStatus commitStatus;
     257             :   private SubmitInput submitInput;
     258             :   private NotifyResolver.Result notify;
     259             :   private Set<Project.NameKey> projects;
     260             :   private boolean dryrun;
     261             :   private TopicMetrics topicMetrics;
     262             : 
     263             :   @Inject
     264             :   MergeOp(
     265             :       ChangeMessagesUtil cmUtil,
     266             :       BatchUpdate.Factory batchUpdateFactory,
     267             :       InternalUser.Factory internalUserFactory,
     268             :       MergeSuperSet mergeSuperSet,
     269             :       MergeValidators.Factory mergeValidatorsFactory,
     270             :       Provider<InternalChangeQuery> queryProvider,
     271             :       SubmitStrategyFactory submitStrategyFactory,
     272             :       SubmoduleCommits.Factory submoduleCommitsFactory,
     273             :       SubscriptionGraph.Factory subscriptionGraphFactory,
     274             :       @SuperprojectUpdateOnSubmission
     275             :           ImmutableList<SubmissionListener> superprojectUpdateSubmissionListeners,
     276             :       Provider<MergeOpRepoManager> ormProvider,
     277             :       NotifyResolver notifyResolver,
     278             :       TopicMetrics topicMetrics,
     279             :       RetryHelper retryHelper,
     280             :       ChangeData.Factory changeDataFactory,
     281          53 :       StoreSubmitRequirementsOp.Factory storeSubmitRequirementsOpFactory) {
     282          53 :     this.cmUtil = cmUtil;
     283          53 :     this.batchUpdateFactory = batchUpdateFactory;
     284          53 :     this.internalUserFactory = internalUserFactory;
     285          53 :     this.mergeSuperSet = mergeSuperSet;
     286          53 :     this.mergeValidatorsFactory = mergeValidatorsFactory;
     287          53 :     this.queryProvider = queryProvider;
     288          53 :     this.submitStrategyFactory = submitStrategyFactory;
     289          53 :     this.submoduleCommitsFactory = submoduleCommitsFactory;
     290          53 :     this.subscriptionGraphFactory = subscriptionGraphFactory;
     291          53 :     this.superprojectUpdateSubmissionListeners = superprojectUpdateSubmissionListeners;
     292          53 :     this.ormProvider = ormProvider;
     293          53 :     this.notifyResolver = notifyResolver;
     294          53 :     this.retryHelper = retryHelper;
     295          53 :     this.topicMetrics = topicMetrics;
     296          53 :     this.changeDataFactory = changeDataFactory;
     297          53 :     this.updatedChanges = new HashMap<>();
     298          53 :     this.storeSubmitRequirementsOpFactory = storeSubmitRequirementsOpFactory;
     299          53 :   }
     300             : 
     301             :   @Override
     302             :   public void close() {
     303          53 :     if (orm != null) {
     304          53 :       orm.close();
     305             :     }
     306          53 :   }
     307             : 
     308             :   public static void checkSubmitRequirements(ChangeData cd) throws ResourceConflictException {
     309          69 :     PatchSet patchSet = cd.currentPatchSet();
     310          69 :     if (patchSet == null) {
     311           0 :       throw new ResourceConflictException("missing current patch set for change " + cd.getId());
     312             :     }
     313          69 :     Map<SubmitRequirement, SubmitRequirementResult> srResults =
     314          69 :         cd.submitRequirementsIncludingLegacy();
     315          69 :     if (srResults.values().stream().allMatch(SubmitRequirementResult::fulfilled)) {
     316          46 :       return;
     317          52 :     } else if (srResults.isEmpty()) {
     318           0 :       throw new IllegalStateException(
     319           0 :           String.format(
     320             :               "Submit requirement results for change '%s' and patchset '%s' "
     321             :                   + "are empty in project '%s'",
     322           0 :               cd.getId(), patchSet.id(), cd.change().getProject().get()));
     323             :     }
     324             : 
     325          52 :     for (SubmitRequirementResult srResult : srResults.values()) {
     326          52 :       switch (srResult.status()) {
     327             :         case SATISFIED:
     328             :         case NOT_APPLICABLE:
     329             :         case OVERRIDDEN:
     330             :         case FORCED:
     331           5 :           break;
     332             : 
     333             :         case ERROR:
     334           1 :           throw new ResourceConflictException(
     335           1 :               String.format(
     336             :                   "submit requirement '%s' has an error: %s",
     337           1 :                   srResult.submitRequirement().name(), srResult.errorMessage().orElse("")));
     338             : 
     339             :         case UNSATISFIED:
     340          52 :           throw new ResourceConflictException(
     341          52 :               String.format(
     342          52 :                   "submit requirement '%s' is unsatisfied.", srResult.submitRequirement().name()));
     343             : 
     344             :         default:
     345           0 :           throw new IllegalStateException(
     346           0 :               String.format(
     347             :                   "Unexpected submit requirement status %s for %s in %s",
     348           0 :                   srResult.status().name(), patchSet.id().getId(), cd.change().getProject().get()));
     349             :       }
     350           5 :     }
     351           0 :     throw new IllegalStateException();
     352             :   }
     353             : 
     354             :   private static SubmitRuleOptions submitRuleOptions(boolean allowClosed) {
     355          53 :     return allowClosed ? SUBMIT_RULE_OPTIONS_ALLOW_CLOSED : SUBMIT_RULE_OPTIONS;
     356             :   }
     357             : 
     358             :   private static List<SubmitRecord> getSubmitRecords(ChangeData cd) {
     359           8 :     return cd.submitRecords(submitRuleOptions(/* allowClosed= */ false));
     360             :   }
     361             : 
     362             :   private void checkSubmitRulesAndState(ChangeSet cs, boolean allowMerged)
     363             :       throws ResourceConflictException {
     364          46 :     checkArgument(
     365          46 :         !cs.furtherHiddenChanges(), "checkSubmitRulesAndState called for topic with hidden change");
     366          46 :     for (ChangeData cd : cs.changes()) {
     367             :       try {
     368          46 :         if (!cd.change().isNew()) {
     369           0 :           if (!(cd.change().isMerged() && allowMerged)) {
     370           0 :             commitStatus.problem(
     371           0 :                 cd.getId(), "Change " + cd.getId() + " is " + ChangeUtil.status(cd.change()));
     372             :           }
     373          46 :         } else if (cd.change().isWorkInProgress()) {
     374           6 :           commitStatus.problem(cd.getId(), "Change " + cd.getId() + " is work in progress");
     375             :         } else {
     376          46 :           checkSubmitRequirements(cd);
     377             :         }
     378           5 :       } catch (ResourceConflictException e) {
     379           5 :         commitStatus.problem(cd.getId(), e.getMessage());
     380           0 :       } catch (StorageException e) {
     381           0 :         String msg = "Error checking submit rules for change";
     382           0 :         logger.atWarning().withCause(e).log("%s %s", msg, cd.getId());
     383           0 :         commitStatus.problem(cd.getId(), msg);
     384          46 :       }
     385          46 :     }
     386          46 :     commitStatus.maybeFailVerbose();
     387          46 :   }
     388             : 
     389             :   private void bypassSubmitRulesAndRequirements(ChangeSet cs) {
     390           8 :     checkArgument(
     391           8 :         !cs.furtherHiddenChanges(), "cannot bypass submit rules for topic with hidden change");
     392           8 :     for (ChangeData cd : cs.changes()) {
     393           8 :       Change change = cd.change();
     394           8 :       if (change == null) {
     395           0 :         throw new StorageException("Change not found");
     396             :       }
     397           8 :       if (change.isClosed()) {
     398             :         // No need to check submit rules if the change is closed.
     399           0 :         continue;
     400             :       }
     401           8 :       List<SubmitRecord> records = new ArrayList<>(getSubmitRecords(cd));
     402           8 :       SubmitRecord forced = new SubmitRecord();
     403           8 :       forced.status = SubmitRecord.Status.FORCED;
     404           8 :       records.add(forced);
     405           8 :       cd.setSubmitRecords(submitRuleOptions(/* allowClosed= */ false), records);
     406             : 
     407             :       // Also bypass submit requirements. Mark them as forced.
     408           8 :       Map<SubmitRequirement, SubmitRequirementResult> forcedSRs =
     409           8 :           cd.submitRequirementsIncludingLegacy().entrySet().stream()
     410           8 :               .collect(
     411           8 :                   Collectors.toMap(
     412             :                       Map.Entry::getKey,
     413           8 :                       entry -> entry.getValue().toBuilder().forced(Optional.of(true)).build()));
     414           8 :       cd.setSubmitRequirements(forcedSRs);
     415           8 :     }
     416           8 :   }
     417             : 
     418             :   /**
     419             :    * Merges the given change.
     420             :    *
     421             :    * <p>Depending on the server configuration, more changes may be affected, e.g. by submission of a
     422             :    * topic or via superproject subscriptions. All affected changes are integrated using the projects
     423             :    * integration strategy.
     424             :    *
     425             :    * @param change the change to be merged.
     426             :    * @param caller the identity of the caller
     427             :    * @param checkSubmitRules whether the prolog submit rules should be evaluated
     428             :    * @param submitInput parameters regarding the merge
     429             :    * @throws RestApiException if an error occurred.
     430             :    * @throws PermissionBackendException if permissions can't be checked
     431             :    * @throws IOException an error occurred reading from NoteDb.
     432             :    * @return the merged change
     433             :    */
     434             :   public Change merge(
     435             :       Change change,
     436             :       IdentifiedUser caller,
     437             :       boolean checkSubmitRules,
     438             :       SubmitInput submitInput,
     439             :       boolean dryrun)
     440             :       throws RestApiException, UpdateException, IOException, ConfigInvalidException,
     441             :           PermissionBackendException {
     442          53 :     this.submitInput = submitInput;
     443          53 :     this.notify =
     444          53 :         notifyResolver.resolve(
     445          53 :             firstNonNull(submitInput.notify, NotifyHandling.ALL), submitInput.notifyDetails);
     446          53 :     this.dryrun = dryrun;
     447          53 :     this.caller = caller;
     448          53 :     this.ts = TimeUtil.now();
     449          53 :     this.submissionId = new SubmissionId(change);
     450             : 
     451             :     try (TraceContext traceContext =
     452          53 :         TraceContext.open()
     453          53 :             .addTag(RequestId.Type.SUBMISSION_ID, new RequestId(submissionId.toString()))) {
     454          53 :       openRepoManager();
     455             : 
     456          53 :       logger.atFine().log("Beginning integration of %s", change);
     457             :       try {
     458          53 :         ChangeSet indexBackedChangeSet =
     459             :             mergeSuperSet
     460          53 :                 .setMergeOpRepoManager(orm)
     461          53 :                 .completeChangeSet(change, caller, /* includingTopicClosure= */ false);
     462          53 :         if (!indexBackedChangeSet.ids().contains(change.getId())) {
     463             :           // indexBackedChangeSet contains only open changes, if the change is missing in this set
     464             :           // it might be that the change was concurrently submitted in the meantime.
     465           0 :           change = changeDataFactory.create(change).reloadChange();
     466           0 :           if (!change.isNew()) {
     467           0 :             throw new ResourceConflictException("change is " + ChangeUtil.status(change));
     468             :           }
     469           0 :           throw new IllegalStateException(
     470           0 :               String.format("change %s missing from %s", change.getId(), indexBackedChangeSet));
     471             :         }
     472             : 
     473          53 :         if (indexBackedChangeSet.furtherHiddenChanges()) {
     474           7 :           throw new AuthException(
     475           7 :               "A change to be submitted with " + change.getId() + " is not visible");
     476             :         }
     477          53 :         logger.atFine().log("Calculated to merge %s", indexBackedChangeSet);
     478             : 
     479             :         // Reload ChangeSet so that we don't rely on (potentially) stale index data for merging
     480          53 :         ChangeSet noteDbChangeSet = reloadChanges(indexBackedChangeSet);
     481             : 
     482             :         // At this point, any change that isn't new can be filtered out since they were only here
     483             :         // in the first place due to stale index.
     484          53 :         List<ChangeData> filteredChanges = new ArrayList<>();
     485          53 :         for (ChangeData changeData : noteDbChangeSet.changes()) {
     486          53 :           if (!changeData.change().getStatus().equals(Status.NEW)) {
     487           0 :             logger.atFine().log(
     488             :                 "Change %s has status %s due to stale index, so it is skipped during submit",
     489           0 :                 changeData.getId(), changeData.change().getStatus().name());
     490           0 :             continue;
     491             :           }
     492          53 :           filteredChanges.add(changeData);
     493          53 :         }
     494             : 
     495             :         // There are no hidden changes (or else we would have thrown AuthException above).
     496          53 :         ChangeSet filteredNoteDbChangeSet =
     497          53 :             new ChangeSet(filteredChanges, /* hiddenChanges= */ ImmutableList.of());
     498             : 
     499             :         // Count cross-project submissions outside of the retry loop. The chance of a single project
     500             :         // failing increases with the number of projects, so the failure count would be inflated if
     501             :         // this metric were incremented inside of integrateIntoHistory.
     502          53 :         int projects = filteredNoteDbChangeSet.projects().size();
     503          53 :         if (projects > 1) {
     504          11 :           topicMetrics.topicSubmissions.increment();
     505             :         }
     506             : 
     507          53 :         SubmissionExecutor submissionExecutor =
     508             :             new SubmissionExecutor(dryrun, superprojectUpdateSubmissionListeners);
     509          53 :         RetryTracker retryTracker = new RetryTracker();
     510          53 :         retryHelper
     511          53 :             .changeUpdate(
     512             :                 "integrateIntoHistory",
     513             :                 updateFactory -> {
     514          53 :                   long attempt = retryTracker.lastAttemptNumber + 1;
     515          53 :                   boolean isRetry = attempt > 1;
     516          53 :                   if (isRetry) {
     517           9 :                     logger.atFine().log("Retrying, attempt #%d; skipping merged changes", attempt);
     518           9 :                     this.ts = TimeUtil.now();
     519           9 :                     openRepoManager();
     520             :                   }
     521          53 :                   this.commitStatus = new CommitStatus(filteredNoteDbChangeSet, isRetry);
     522          53 :                   if (checkSubmitRules) {
     523          46 :                     logger.atFine().log("Checking submit rules and state");
     524          46 :                     checkSubmitRulesAndState(filteredNoteDbChangeSet, isRetry);
     525             :                   } else {
     526           8 :                     logger.atFine().log("Bypassing submit rules");
     527           8 :                     bypassSubmitRulesAndRequirements(filteredNoteDbChangeSet);
     528             :                   }
     529          53 :                   integrateIntoHistory(
     530             :                       filteredNoteDbChangeSet, submissionExecutor, checkSubmitRules);
     531          53 :                   return null;
     532             :                 })
     533          53 :             .listener(retryTracker)
     534             :             // Up to the entire submit operation is retried, including possibly many projects.
     535             :             // Multiply the timeout by the number of projects we're actually attempting to
     536             :             // submit. Times 2 to retry more persistently, to increase success rate.
     537          53 :             .defaultTimeoutMultiplier(filteredNoteDbChangeSet.projects().size() * 2)
     538             :             // By default, we only retry lock failures. Here it's better to also retry unexpected
     539             :             // runtime exceptions.
     540          53 :             .retryOn(t -> t instanceof RuntimeException)
     541          53 :             .call();
     542          53 :         submissionExecutor.afterExecutions(orm);
     543             : 
     544          53 :         if (projects > 1) {
     545          11 :           topicMetrics.topicSubmissionsCompleted.increment();
     546             :         }
     547             : 
     548             :         // It's expected that callers invoke this method only for open changes and that the provided
     549             :         // change either gets updated to merged or that this method fails with an exception. For
     550             :         // safety, fall-back to return the provided change if there was no update for this change
     551             :         // (e.g. caller provided a change that was already merged).
     552          53 :         return updatedChanges.containsKey(change.getId())
     553          53 :             ? updatedChanges.get(change.getId())
     554          53 :             : change;
     555           0 :       } catch (IOException e) {
     556             :         // Anything before the merge attempt is an error
     557           0 :         throw new StorageException(e);
     558             :       }
     559             :     }
     560             :   }
     561             : 
     562             :   private void openRepoManager() {
     563          53 :     if (orm != null) {
     564           9 :       orm.close();
     565             :     }
     566          53 :     orm = ormProvider.get();
     567          53 :     orm.setContext(ts, caller, notify);
     568          53 :   }
     569             : 
     570             :   private ChangeSet reloadChanges(ChangeSet changeSet) {
     571          53 :     List<ChangeData> visible = new ArrayList<>(changeSet.changes().size());
     572          53 :     List<ChangeData> nonVisible = new ArrayList<>(changeSet.nonVisibleChanges().size());
     573          53 :     changeSet.changes().forEach(c -> visible.add(changeDataFactory.create(c.project(), c.getId())));
     574          53 :     changeSet
     575          53 :         .nonVisibleChanges()
     576          53 :         .forEach(c -> nonVisible.add(changeDataFactory.create(c.project(), c.getId())));
     577          53 :     return new ChangeSet(visible, nonVisible);
     578             :   }
     579             : 
     580          53 :   private class RetryTracker implements RetryListener {
     581             :     long lastAttemptNumber;
     582             : 
     583             :     @Override
     584             :     public <V> void onRetry(Attempt<V> attempt) {
     585          53 :       lastAttemptNumber = attempt.getAttemptNumber();
     586          53 :     }
     587             :   }
     588             : 
     589             :   @Singleton
     590             :   private static class TopicMetrics {
     591             :     final Counter0 topicSubmissions;
     592             :     final Counter0 topicSubmissionsCompleted;
     593             : 
     594             :     @Inject
     595          53 :     TopicMetrics(MetricMaker metrics) {
     596          53 :       topicSubmissions =
     597          53 :           metrics.newCounter(
     598             :               "topic/cross_project_submit",
     599          53 :               new Description("Attempts at cross project topic submission").setRate());
     600          53 :       topicSubmissionsCompleted =
     601          53 :           metrics.newCounter(
     602             :               "topic/cross_project_submit_completed",
     603             :               new Description("Cross project topic submissions that concluded successfully")
     604          53 :                   .setRate());
     605          53 :     }
     606             :   }
     607             : 
     608             :   private void integrateIntoHistory(
     609             :       ChangeSet cs, SubmissionExecutor submissionExecutor, boolean checkSubmitRules)
     610             :       throws RestApiException, UpdateException {
     611          53 :     checkArgument(!cs.furtherHiddenChanges(), "cannot integrate hidden changes into history");
     612          53 :     logger.atFine().log("Beginning merge attempt on %s", cs);
     613          53 :     Map<BranchNameKey, BranchBatch> toSubmit = new HashMap<>();
     614             : 
     615             :     ListMultimap<BranchNameKey, ChangeData> cbb;
     616             :     try {
     617          53 :       cbb = cs.changesByBranch();
     618           0 :     } catch (StorageException e) {
     619           0 :       throw new StorageException("Error reading changes to submit", e);
     620          53 :     }
     621          53 :     Set<BranchNameKey> branches = cbb.keySet();
     622             : 
     623          53 :     for (BranchNameKey branch : branches) {
     624          53 :       OpenRepo or = openRepo(branch.project());
     625          53 :       if (or != null) {
     626          53 :         toSubmit.put(branch, validateChangeList(or, cbb.get(branch)));
     627             :       }
     628          53 :     }
     629             : 
     630             :     // Done checks that don't involve running submit strategies.
     631          53 :     commitStatus.maybeFailVerbose();
     632             : 
     633             :     try {
     634          53 :       SubscriptionGraph subscriptionGraph = subscriptionGraphFactory.compute(branches, orm);
     635          53 :       SubmoduleCommits submoduleCommits = submoduleCommitsFactory.create(orm);
     636          53 :       UpdateOrderCalculator updateOrderCalculator = new UpdateOrderCalculator(subscriptionGraph);
     637          53 :       List<SubmitStrategy> strategies =
     638          53 :           getSubmitStrategies(
     639             :               toSubmit, updateOrderCalculator, submoduleCommits, subscriptionGraph, dryrun);
     640          53 :       this.projects = updateOrderCalculator.getProjectsInOrder();
     641          53 :       List<BatchUpdate> batchUpdates =
     642          53 :           orm.batchUpdates(
     643          53 :               projects, /* refLogMessage= */ checkSubmitRules ? "merged" : "forced-merge");
     644             :       // Group batch updates by project
     645          53 :       Map<Project.NameKey, BatchUpdate> batchUpdatesByProject =
     646          53 :           batchUpdates.stream().collect(Collectors.toMap(b -> b.getProject(), Function.identity()));
     647          53 :       for (Map.Entry<Change.Id, ChangeData> entry : cs.changesById().entrySet()) {
     648          53 :         Project.NameKey project = entry.getValue().project();
     649          53 :         Change.Id changeId = entry.getKey();
     650          53 :         ChangeData cd = entry.getValue();
     651          53 :         batchUpdatesByProject
     652          53 :             .get(project)
     653          53 :             .addOp(
     654             :                 changeId,
     655          53 :                 storeSubmitRequirementsOpFactory.create(
     656          53 :                     cd.submitRequirementsIncludingLegacy().values(), cd));
     657          53 :       }
     658             :       try {
     659          53 :         submissionExecutor.setAdditionalBatchUpdateListeners(
     660          53 :             ImmutableList.of(new SubmitStrategyListener(submitInput, strategies, commitStatus)));
     661          53 :         submissionExecutor.execute(batchUpdates);
     662             :       } finally {
     663             :         // If the BatchUpdate fails it can be that merging some of the changes was actually
     664             :         // successful. This is why we must to collect the updated changes also when an
     665             :         // exception was thrown.
     666          53 :         strategies.forEach(s -> updatedChanges.putAll(s.getUpdatedChanges()));
     667             : 
     668             :         // Do not leave executed BatchUpdates in the OpenRepos
     669          53 :         if (!dryrun) {
     670          53 :           orm.resetUpdates(ImmutableSet.copyOf(this.projects));
     671             :         }
     672             :       }
     673           0 :     } catch (NoSuchProjectException e) {
     674           0 :       throw new ResourceNotFoundException(e.getMessage());
     675           0 :     } catch (IOException e) {
     676           0 :       throw new StorageException(e);
     677           1 :     } catch (SubmoduleConflictException e) {
     678           1 :       throw new IntegrationConflictException(e.getMessage(), e);
     679           8 :     } catch (UpdateException e) {
     680           8 :       if (e.getCause() instanceof LockFailureException) {
     681             :         // Lock failures are a special case: RetryHelper depends on this specific causal chain in
     682             :         // order to trigger a retry. The downside of throwing here is we will not get the nicer
     683             :         // error message constructed below, in the case where this is the final attempt and the
     684             :         // operation is not retried further. This is not a huge downside, and is hopefully so rare
     685             :         // as to be unnoticeable, assuming RetryHelper is retrying sufficiently.
     686           8 :         throw e;
     687             :       }
     688             : 
     689             :       // BatchUpdate may have inadvertently wrapped an IntegrationConflictException
     690             :       // thrown by some legacy SubmitStrategyOp code that intended the error
     691             :       // message to be user-visible. Copy the message from the wrapped
     692             :       // exception.
     693             :       //
     694             :       // If you happen across one of these, the correct fix is to convert the
     695             :       // inner IntegrationConflictException to a ResourceConflictException.
     696           1 :       if (e.getCause() instanceof IntegrationConflictException) {
     697           0 :         throw (IntegrationConflictException) e.getCause();
     698             :       }
     699           1 :       throw new MergeUpdateException(genericMergeError(cs), e);
     700          53 :     }
     701          53 :   }
     702             : 
     703             :   public Set<Project.NameKey> getAllProjects() {
     704           0 :     return projects;
     705             :   }
     706             : 
     707             :   public MergeOpRepoManager getMergeOpRepoManager() {
     708           0 :     return orm;
     709             :   }
     710             : 
     711             :   private List<SubmitStrategy> getSubmitStrategies(
     712             :       Map<BranchNameKey, BranchBatch> toSubmit,
     713             :       UpdateOrderCalculator updateOrderCalculator,
     714             :       SubmoduleCommits submoduleCommits,
     715             :       SubscriptionGraph subscriptionGraph,
     716             :       boolean dryrun)
     717             :       throws IntegrationConflictException, NoSuchProjectException, IOException {
     718          53 :     List<SubmitStrategy> strategies = new ArrayList<>();
     719          53 :     Set<BranchNameKey> allBranches = updateOrderCalculator.getBranchesInOrder();
     720          53 :     Set<CodeReviewCommit> allCommits =
     721          53 :         toSubmit.values().stream().map(BranchBatch::commits).flatMap(Set::stream).collect(toSet());
     722             : 
     723          53 :     for (BranchNameKey branch : allBranches) {
     724          53 :       OpenRepo or = orm.getRepo(branch.project());
     725          53 :       if (toSubmit.containsKey(branch)) {
     726          53 :         BranchBatch submitting = toSubmit.get(branch);
     727          53 :         logger.atFine().log("adding ops for branch %s, batch = %s", branch, submitting);
     728          53 :         OpenBranch ob = or.getBranch(branch);
     729          53 :         requireNonNull(
     730          53 :             submitting.submitType(),
     731          53 :             String.format("null submit type for %s; expected to previously fail fast", submitting));
     732          53 :         Set<CodeReviewCommit> commitsToSubmit = submitting.commits();
     733          53 :         ob.mergeTip = new MergeTip(ob.oldTip, commitsToSubmit);
     734          53 :         SubmitStrategy strategy =
     735          53 :             submitStrategyFactory.create(
     736          53 :                 submitting.submitType(),
     737             :                 or.rw,
     738             :                 or.canMergeFlag,
     739          53 :                 getAlreadyAccepted(or, ob.oldTip),
     740             :                 allCommits,
     741             :                 branch,
     742             :                 caller,
     743             :                 ob.mergeTip,
     744             :                 commitStatus,
     745             :                 submissionId,
     746             :                 submitInput,
     747             :                 submoduleCommits,
     748             :                 subscriptionGraph,
     749             :                 dryrun);
     750          53 :         strategies.add(strategy);
     751          53 :         strategy.addOps(or.getUpdate(), commitsToSubmit);
     752             :       }
     753          53 :     }
     754             : 
     755          53 :     return strategies;
     756             :   }
     757             : 
     758             :   private Set<RevCommit> getAlreadyAccepted(OpenRepo or, CodeReviewCommit branchTip) {
     759          53 :     Set<RevCommit> alreadyAccepted = new HashSet<>();
     760             : 
     761          53 :     if (branchTip != null) {
     762          53 :       alreadyAccepted.add(branchTip);
     763             :     }
     764             : 
     765             :     try {
     766          53 :       for (Ref r : or.repo.getRefDatabase().getRefsByPrefix(Constants.R_HEADS)) {
     767             :         try {
     768          52 :           CodeReviewCommit aac = or.rw.parseCommit(r.getObjectId());
     769          52 :           if (!commitStatus.commits.values().contains(aac)) {
     770          52 :             alreadyAccepted.add(aac);
     771             :           }
     772           1 :         } catch (IncorrectObjectTypeException iote) {
     773             :           // Not a commit? Skip over it.
     774          52 :         }
     775          52 :       }
     776           0 :     } catch (IOException e) {
     777           0 :       throw new StorageException("Failed to determine already accepted commits.", e);
     778          53 :     }
     779             : 
     780          53 :     logger.atFine().log("Found %d existing heads: %s", alreadyAccepted.size(), alreadyAccepted);
     781          53 :     return alreadyAccepted;
     782             :   }
     783             : 
     784             :   @AutoValue
     785          53 :   abstract static class BranchBatch {
     786             :     @Nullable
     787             :     abstract SubmitType submitType();
     788             : 
     789             :     abstract ImmutableSet<CodeReviewCommit> commits();
     790             :   }
     791             : 
     792             :   private BranchBatch validateChangeList(OpenRepo or, Collection<ChangeData> submitted) {
     793          53 :     logger.atFine().log("Validating %d changes", submitted.size());
     794          53 :     Set<CodeReviewCommit> toSubmit = new LinkedHashSet<>(submitted.size());
     795          53 :     SetMultimap<ObjectId, PatchSet.Id> revisions = getRevisions(or, submitted);
     796             : 
     797          53 :     SubmitType submitType = null;
     798          53 :     ChangeData choseSubmitTypeFrom = null;
     799          53 :     for (ChangeData cd : submitted) {
     800          53 :       Change.Id changeId = cd.getId();
     801             :       ChangeNotes notes;
     802             :       Change chg;
     803             :       SubmitType st;
     804             :       try {
     805          53 :         notes = cd.notes();
     806          53 :         chg = cd.change();
     807          53 :         st = getSubmitType(cd);
     808           0 :       } catch (StorageException e) {
     809           0 :         commitStatus.logProblem(changeId, e);
     810           0 :         continue;
     811          53 :       }
     812             : 
     813          53 :       if (st == null) {
     814           0 :         commitStatus.logProblem(changeId, "No submit type for change");
     815           0 :         continue;
     816             :       }
     817          53 :       if (submitType == null) {
     818          53 :         submitType = st;
     819          53 :         choseSubmitTypeFrom = cd;
     820          17 :       } else if (st != submitType) {
     821           1 :         commitStatus.problem(
     822             :             changeId,
     823           1 :             String.format(
     824             :                 "Change has submit type %s, but previously chose submit type %s "
     825             :                     + "from change %s in the same batch",
     826           1 :                 st, submitType, choseSubmitTypeFrom.getId()));
     827           1 :         continue;
     828             :       }
     829          53 :       if (chg.currentPatchSetId() == null) {
     830           0 :         String msg = "Missing current patch set on change";
     831           0 :         logger.atSevere().log("%s %s", msg, changeId);
     832           0 :         commitStatus.problem(changeId, msg);
     833           0 :         continue;
     834             :       }
     835             : 
     836             :       PatchSet ps;
     837          53 :       BranchNameKey destBranch = chg.getDest();
     838             :       try {
     839          53 :         ps = cd.currentPatchSet();
     840           0 :       } catch (StorageException e) {
     841           0 :         commitStatus.logProblem(changeId, e);
     842           0 :         continue;
     843          53 :       }
     844          53 :       if (ps == null) {
     845           0 :         commitStatus.logProblem(changeId, "Missing patch set on change");
     846           0 :         continue;
     847             :       }
     848             : 
     849          53 :       ObjectId id = ps.commitId();
     850          53 :       if (!revisions.containsEntry(id, ps.id())) {
     851           0 :         if (revisions.containsValue(ps.id())) {
     852             :           // TODO This is actually an error, the patch set ref exists but points to a revision that
     853             :           // is different from the revision that we have stored for the patch set in the change
     854             :           // meta data.
     855           0 :           commitStatus.logProblem(
     856             :               changeId,
     857             :               "Revision "
     858           0 :                   + id.name()
     859             :                   + " of patch set "
     860           0 :                   + ps.number()
     861             :                   + " does not match the revision of the patch set ref "
     862           0 :                   + ps.id().toRefName());
     863           0 :           continue;
     864             :         }
     865             : 
     866             :         // The patch set ref is not found but we want to merge the change. We can't safely do that
     867             :         // if the patch set ref is missing. In a cluster setups with multiple primary nodes this can
     868             :         // indicate a replication lag (e.g. the change meta data was already replicated, but the
     869             :         // replication of the patch set ref is still pending).
     870           0 :         commitStatus.logProblem(
     871             :             changeId,
     872             :             "Patch set ref "
     873           0 :                 + ps.id().toRefName()
     874             :                 + " not found. Expected patch set ref of "
     875           0 :                 + ps.number()
     876             :                 + " to point to revision "
     877           0 :                 + id.name());
     878           0 :         continue;
     879             :       }
     880             : 
     881             :       CodeReviewCommit commit;
     882             :       try {
     883          53 :         commit = or.rw.parseCommit(id);
     884           0 :       } catch (IOException e) {
     885           0 :         commitStatus.logProblem(changeId, e);
     886           0 :         continue;
     887          53 :       }
     888             : 
     889          53 :       commit.setNotes(notes);
     890          53 :       commit.setPatchsetId(ps.id());
     891          53 :       commitStatus.put(commit);
     892             : 
     893          53 :       MergeValidators mergeValidators = mergeValidatorsFactory.create();
     894             :       try {
     895          53 :         mergeValidators.validatePreMerge(
     896          53 :             or.repo, or.rw, commit, or.project, destBranch, ps.id(), caller);
     897           4 :       } catch (MergeValidationException mve) {
     898           4 :         commitStatus.problem(changeId, mve.getMessage());
     899           4 :         continue;
     900          53 :       }
     901          53 :       commit.add(or.canMergeFlag);
     902          53 :       toSubmit.add(commit);
     903          53 :     }
     904          53 :     logger.atFine().log("Submitting on this run: %s", toSubmit);
     905          53 :     return new AutoValue_MergeOp_BranchBatch(submitType, ImmutableSet.copyOf(toSubmit));
     906             :   }
     907             : 
     908             :   private SetMultimap<ObjectId, PatchSet.Id> getRevisions(OpenRepo or, Collection<ChangeData> cds) {
     909             :     try {
     910          53 :       List<String> refNames = new ArrayList<>(cds.size());
     911          53 :       for (ChangeData cd : cds) {
     912          53 :         Change c = cd.change();
     913          53 :         if (c != null) {
     914          53 :           refNames.add(c.currentPatchSetId().toRefName());
     915             :         }
     916          53 :       }
     917          53 :       SetMultimap<ObjectId, PatchSet.Id> revisions =
     918          53 :           MultimapBuilder.hashKeys(cds.size()).hashSetValues(1).build();
     919             :       for (Map.Entry<String, Ref> e :
     920          53 :           or.repo
     921          53 :               .getRefDatabase()
     922          53 :               .exactRef(refNames.toArray(new String[refNames.size()]))
     923          53 :               .entrySet()) {
     924          53 :         revisions.put(e.getValue().getObjectId(), PatchSet.Id.fromRef(e.getKey()));
     925          53 :       }
     926          53 :       return revisions;
     927           0 :     } catch (IOException | StorageException e) {
     928           0 :       throw new StorageException("Failed to validate changes", e);
     929             :     }
     930             :   }
     931             : 
     932             :   @Nullable
     933             :   private SubmitType getSubmitType(ChangeData cd) {
     934          53 :     SubmitTypeRecord str = cd.submitTypeRecord();
     935          53 :     return str.isOk() ? str.type : null;
     936             :   }
     937             : 
     938             :   @Nullable
     939             :   private OpenRepo openRepo(Project.NameKey project) {
     940             :     try {
     941          53 :       return orm.getRepo(project);
     942           0 :     } catch (NoSuchProjectException e) {
     943           0 :       logger.atWarning().log("Project %s no longer exists, abandoning open changes.", project);
     944           0 :       abandonAllOpenChangeForDeletedProject(project);
     945           0 :     } catch (IOException e) {
     946           0 :       throw new StorageException("Error opening project " + project, e);
     947           0 :     }
     948           0 :     return null;
     949             :   }
     950             : 
     951             :   private void abandonAllOpenChangeForDeletedProject(Project.NameKey destProject) {
     952             :     try {
     953           0 :       for (ChangeData cd : queryProvider.get().byProjectOpen(destProject)) {
     954           0 :         try (BatchUpdate bu =
     955           0 :             batchUpdateFactory.create(destProject, internalUserFactory.create(), ts)) {
     956           0 :           bu.addOp(
     957           0 :               cd.getId(),
     958           0 :               new BatchUpdateOp() {
     959             :                 @Override
     960             :                 public boolean updateChange(ChangeContext ctx) {
     961           0 :                   Change change = ctx.getChange();
     962           0 :                   if (!change.isNew()) {
     963           0 :                     return false;
     964             :                   }
     965             : 
     966           0 :                   change.setStatus(Change.Status.ABANDONED);
     967             : 
     968           0 :                   cmUtil.setChangeMessage(
     969             :                       ctx, "Project was deleted.", ChangeMessagesUtil.TAG_MERGED);
     970             : 
     971           0 :                   return true;
     972             :                 }
     973             :               });
     974             :           try {
     975           0 :             bu.execute();
     976           0 :           } catch (UpdateException | RestApiException e) {
     977           0 :             logger.atWarning().withCause(e).log(
     978             :                 "Cannot abandon changes for deleted project %s", destProject);
     979           0 :           }
     980             :         }
     981           0 :       }
     982           0 :     } catch (StorageException e) {
     983           0 :       logger.atWarning().withCause(e).log(
     984             :           "Cannot abandon changes for deleted project %s", destProject);
     985           0 :     }
     986           0 :   }
     987             : 
     988             :   private String genericMergeError(ChangeSet cs) {
     989           1 :     int c = cs.size();
     990           1 :     if (c == 1) {
     991           1 :       return "Error submitting change";
     992             :     }
     993           0 :     int p = cs.projects().size();
     994           0 :     if (p == 1) {
     995             :       // Fused updates: it's correct to say that none of the n changes were submitted.
     996           0 :       return "Error submitting " + c + " changes";
     997             :     }
     998             :     // Multiple projects involved, but we don't know at this point what failed. At least give the
     999             :     // user a heads up that some changes may be unsubmitted, even if the change screen they land on
    1000             :     // after the error message says that this particular change was submitted.
    1001           0 :     return "Error submitting some of the "
    1002             :         + c
    1003             :         + " changes to one or more of the "
    1004             :         + p
    1005             :         + " projects involved; some projects may have submitted successfully, but others may have"
    1006             :         + " failed";
    1007             :   }
    1008             : }

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