blob: f8bcfc189b6d29cf818855a65e1d34eddac7de54 [file] [log] [blame]
// 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 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 {
try {
markCleanMerges();
List<Change.Id> alreadyMerged = checkCommitStatus();
findUnmergedChanges(alreadyMerged);
} catch (IntegrationException e) {
throw new ResourceConflictException(e.getMessage(), e);
}
}
@Override
public void afterUpdateRefs() throws ResourceConflictException {
if (failAfterRefUpdates) {
throw new ResourceConflictException("Failing after ref updates");
}
}
private void findUnmergedChanges(List<Change.Id> alreadyMerged)
throws ResourceConflictException, IntegrationException {
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);
for (Change.Id id : unmerged) {
commitStatus.problem(id, "internal error: change not reachable from new branch tip");
}
}
commitStatus.maybeFailVerbose();
}
private void markCleanMerges() throws IntegrationException {
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;
if (s == null) {
logger.atSevere().log("change %d: change not processed by merge strategy", id.get());
commitStatus.problem(id, "internal error: change not processed by merge strategy");
continue;
}
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:
// 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");
}
}