| // Copyright (C) 2016 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.gerrit.server.submit; |
| |
| import static com.google.common.base.Preconditions.checkState; |
| import static java.util.Objects.requireNonNull; |
| |
| import com.google.common.base.CharMatcher; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.entities.Change; |
| import com.google.gerrit.extensions.api.changes.SubmitInput; |
| import com.google.gerrit.extensions.restapi.ResourceConflictException; |
| import com.google.gerrit.server.change.TestSubmitInput; |
| import com.google.gerrit.server.git.CodeReviewCommit; |
| import com.google.gerrit.server.submit.MergeOp.CommitStatus; |
| import com.google.gerrit.server.update.BatchUpdateListener; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.Set; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| |
| public class SubmitStrategyListener implements BatchUpdateListener { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| private final Collection<SubmitStrategy> strategies; |
| private final CommitStatus commitStatus; |
| private final boolean failAfterRefUpdates; |
| |
| public SubmitStrategyListener( |
| SubmitInput input, Collection<SubmitStrategy> strategies, CommitStatus commitStatus) { |
| this.strategies = strategies; |
| this.commitStatus = commitStatus; |
| if (input instanceof TestSubmitInput) { |
| failAfterRefUpdates = ((TestSubmitInput) input).failAfterRefUpdates; |
| } else { |
| failAfterRefUpdates = false; |
| } |
| } |
| |
| @Override |
| public void afterUpdateRepos() throws ResourceConflictException { |
| markCleanMerges(); |
| List<Change.Id> alreadyMerged = checkCommitStatus(); |
| findUnmergedChanges(alreadyMerged); |
| } |
| |
| @Override |
| public void afterUpdateRefs() throws ResourceConflictException { |
| if (failAfterRefUpdates) { |
| throw new ResourceConflictException("Failing after ref updates"); |
| } |
| } |
| |
| private void findUnmergedChanges(List<Change.Id> alreadyMerged) throws ResourceConflictException { |
| for (SubmitStrategy strategy : strategies) { |
| if (strategy instanceof CherryPick) { |
| // Can't do this sanity check for CherryPick since: |
| // * CherryPick might have picked a subset of changes |
| // * CherryPick might have status SKIPPED_IDENTICAL_TREE |
| continue; |
| } |
| SubmitStrategy.Arguments args = strategy.args; |
| Set<Change.Id> unmerged = |
| args.mergeUtil.findUnmergedChanges( |
| args.commitStatus.getChangeIds(args.destBranch), |
| args.rw, |
| args.canMergeFlag, |
| args.mergeTip.getInitialTip(), |
| args.mergeTip.getCurrentTip(), |
| alreadyMerged); |
| checkState(unmerged.isEmpty(), "changes not reachable from new branch tip: %s", unmerged); |
| } |
| commitStatus.maybeFailVerbose(); |
| } |
| |
| private void markCleanMerges() { |
| for (SubmitStrategy strategy : strategies) { |
| SubmitStrategy.Arguments args = strategy.args; |
| RevCommit initialTip = args.mergeTip.getInitialTip(); |
| args.mergeUtil.markCleanMerges( |
| args.rw, |
| args.canMergeFlag, |
| args.mergeTip.getCurrentTip(), |
| initialTip == null ? ImmutableSet.of() : ImmutableSet.of(initialTip)); |
| } |
| } |
| |
| private List<Change.Id> checkCommitStatus() throws ResourceConflictException { |
| List<Change.Id> alreadyMerged = new ArrayList<>(commitStatus.getChangeIds().size()); |
| for (Change.Id id : commitStatus.getChangeIds()) { |
| CodeReviewCommit commit = commitStatus.get(id); |
| CommitMergeStatus s = commit != null ? commit.getStatusCode() : null; |
| requireNonNull( |
| s, String.format("change %d: change not processed by merge strategy", id.get())); |
| |
| if (commit.getStatusMessage().isPresent()) { |
| logger.atFine().log( |
| "change %d: Status for commit %s is %s. %s", |
| id.get(), commit.name(), s, commit.getStatusMessage().get()); |
| } else { |
| logger.atFine().log("change %d: Status for commit %s is %s.", id.get(), commit.name(), s); |
| } |
| switch (s) { |
| case CLEAN_MERGE: |
| case CLEAN_REBASE: |
| case CLEAN_PICK: |
| case SKIPPED_IDENTICAL_TREE: |
| break; // Merge strategy accepted this change. |
| |
| case ALREADY_MERGED: |
| // Already an ancestor of tip. |
| alreadyMerged.add(commit.getPatchsetId().changeId()); |
| break; |
| |
| case PATH_CONFLICT: |
| case REBASE_MERGE_CONFLICT: |
| case MANUAL_RECURSIVE_MERGE: |
| case CANNOT_CHERRY_PICK_ROOT: |
| case CANNOT_REBASE_ROOT: |
| case NOT_FAST_FORWARD: |
| case EMPTY_COMMIT: |
| case MISSING_DEPENDENCY: |
| case FAST_FORWARD_INDEPENDENT_CHANGES: |
| // TODO(dborowitz): Reformat these messages to be more appropriate for |
| // short problem descriptions. |
| String message = s.getDescription(); |
| if (commit.getStatusMessage().isPresent()) { |
| message += " " + commit.getStatusMessage().get(); |
| } |
| commitStatus.problem(id, CharMatcher.is('\n').collapseFrom(message, ' ')); |
| break; |
| |
| default: |
| commitStatus.problem(id, "unspecified merge failure: " + s); |
| break; |
| } |
| } |
| commitStatus.maybeFailVerbose(); |
| return alreadyMerged; |
| } |
| |
| @Override |
| public void afterUpdateChanges() throws ResourceConflictException { |
| commitStatus.maybeFail("Error updating status"); |
| } |
| } |