blob: 7fe5e69631c4022ca6e4d56e17f7af92779f6324 [file] [log] [blame]
// Copyright (C) 2012 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.gerrit.server.submit.CommitMergeStatus.EMPTY_COMMIT;
import static com.google.gerrit.server.submit.CommitMergeStatus.SKIPPED_IDENTICAL_TREE;
import static java.util.Objects.requireNonNull;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BooleanProjectConfig;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetInfo;
import com.google.gerrit.extensions.restapi.MergeConflictException;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.MergeTip;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.update.ChangeContext;
import com.google.gerrit.server.update.RepoContext;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.revwalk.RevCommit;
public class CherryPick extends SubmitStrategy {
CherryPick(SubmitStrategy.Arguments args) {
super(args);
}
@Override
public ImmutableList<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
List<CodeReviewCommit> sorted = CodeReviewCommit.ORDER.sortedCopy(toMerge);
ImmutableList.Builder<SubmitStrategyOp> ops =
ImmutableList.builderWithExpectedSize(sorted.size());
boolean first = true;
while (!sorted.isEmpty()) {
CodeReviewCommit n = sorted.remove(0);
if (first && args.mergeTip.getInitialTip() == null) {
ops.add(new FastForwardOp(args, n));
} else if (n.getParentCount() == 0) {
ops.add(new CherryPickRootOp(n));
} else if (n.getParentCount() == 1) {
ops.add(new CherryPickOneOp(n));
} else {
ops.add(new CherryPickMultipleParentsOp(n));
}
first = false;
}
return ops.build();
}
private class CherryPickRootOp extends SubmitStrategyOp {
private CherryPickRootOp(CodeReviewCommit toMerge) {
super(CherryPick.this.args, toMerge);
}
@Override
public void updateRepoImpl(RepoContext ctx) {
// Refuse to merge a root commit into an existing branch, we cannot obtain
// a delta for the cherry-pick to apply.
toMerge.setStatusCode(CommitMergeStatus.CANNOT_CHERRY_PICK_ROOT);
}
}
private class CherryPickOneOp extends SubmitStrategyOp {
private PatchSet.Id psId;
private CodeReviewCommit newCommit;
private PatchSetInfo patchSetInfo;
private CherryPickOneOp(CodeReviewCommit toMerge) {
super(CherryPick.this.args, toMerge);
}
@Override
protected void updateRepoImpl(RepoContext ctx)
throws IntegrationConflictException, IOException, MethodNotAllowedException {
// If there is only one parent, a cherry-pick can be done by taking the
// delta relative to that one parent and redoing that on the current merge
// tip.
args.rw.parseBody(toMerge);
psId =
ChangeUtil.nextPatchSetIdFromChangeRefs(
ctx.getRepoView().getRefs(getId().toRefPrefix()).keySet(),
toMerge.change().currentPatchSetId());
RevCommit mergeTip = args.mergeTip.getCurrentTip();
args.rw.parseBody(mergeTip);
String cherryPickCmtMsg = args.mergeUtil.createCommitMessageOnSubmit(toMerge, mergeTip);
PersonIdent committer =
Optional.ofNullable(toMerge.getCommitterIdent())
.map(ident -> ctx.newCommitterIdent(ident.getEmailAddress(), args.caller))
.orElseGet(() -> ctx.newCommitterIdent(args.caller));
try {
newCommit =
args.mergeUtil.createCherryPickFromCommit(
ctx.getInserter(),
ctx.getRepoView().getConfig(),
args.mergeTip.getCurrentTip(),
toMerge,
committer,
cherryPickCmtMsg,
args.rw,
0,
false,
false);
} catch (MergeConflictException mce) {
// Keep going in the case of a single merge failure; the goal is to
// cherry-pick as many commits as possible.
toMerge.setStatusCode(CommitMergeStatus.PATH_CONFLICT);
return;
} catch (MergeIdenticalTreeException mie) {
if (args.project.is(BooleanProjectConfig.REJECT_EMPTY_COMMIT)) {
toMerge.setStatusCode(EMPTY_COMMIT);
return;
}
toMerge.setStatusCode(SKIPPED_IDENTICAL_TREE);
return;
}
// Initial copy doesn't have new patch set ID since change hasn't been
// updated yet.
newCommit = amendGitlink(newCommit);
newCommit.copyFrom(toMerge);
newCommit.setPatchsetId(psId);
newCommit.setStatusCode(CommitMergeStatus.CLEAN_PICK);
args.mergeTip.moveTipTo(newCommit, newCommit);
args.commitStatus.put(newCommit);
ctx.addRefUpdate(ObjectId.zeroId(), newCommit, psId.toRefName());
patchSetInfo = args.patchSetInfoFactory.get(ctx.getRevWalk(), newCommit, psId);
}
@Nullable
@Override
public PatchSet updateChangeImpl(ChangeContext ctx) throws NoSuchChangeException, IOException {
if (newCommit == null && toMerge.getStatusCode() == SKIPPED_IDENTICAL_TREE) {
return null;
}
requireNonNull(
newCommit,
() ->
String.format(
"no new commit produced by CherryPick of %s, expected to fail fast",
toMerge.change().getId()));
PatchSet prevPs = args.psUtil.current(ctx.getNotes());
PatchSet newPs =
args.psUtil.insert(
ctx.getRevWalk(),
ctx.getUpdate(psId),
psId,
newCommit,
prevPs != null ? prevPs.groups() : ImmutableList.of(),
null,
null);
ctx.getChange().setCurrentPatchSet(patchSetInfo);
// Don't copy approvals, as this is already taken care of by
// SubmitStrategyOp.
newCommit.setNotes(ctx.getNotes());
return newPs;
}
}
private class CherryPickMultipleParentsOp extends SubmitStrategyOp {
private CherryPickMultipleParentsOp(CodeReviewCommit toMerge) {
super(CherryPick.this.args, toMerge);
}
@Override
public void updateRepoImpl(RepoContext ctx) throws IntegrationConflictException, IOException {
if (args.mergeUtil.hasMissingDependencies(args.mergeSorter, toMerge)) {
// One or more dependencies were not met. The status was already marked
// on the commit so we have nothing further to perform at this time.
return;
}
// There are multiple parents, so this is a merge commit. We don't want
// to cherry-pick the merge as clients can't easily rebase their history
// with that merge present and replaced by an equivalent merge with a
// different first parent. So instead behave as though MERGE_IF_NECESSARY
// was configured.
MergeTip mergeTip = args.mergeTip;
if (args.rw.isMergedInto(mergeTip.getCurrentTip(), toMerge)
&& !args.subscriptionGraph.hasSubscription(args.destBranch)) {
mergeTip.moveTipTo(toMerge, toMerge);
} else {
PersonIdent myIdent = ctx.newPersonIdent(args.serverIdent);
CodeReviewCommit result =
args.mergeUtil.mergeOneCommit(
myIdent,
myIdent,
args.rw,
ctx.getInserter(),
ctx.getRepoView().getConfig(),
args.destBranch,
mergeTip.getCurrentTip(),
toMerge);
result = amendGitlink(result);
mergeTip.moveTipTo(result, toMerge);
args.mergeUtil.markCleanMerges(
args.rw, args.canMergeFlag, mergeTip.getCurrentTip(), args.alreadyAccepted);
}
}
}
static boolean dryRun(
SubmitDryRun.Arguments args, CodeReviewCommit mergeTip, CodeReviewCommit toMerge) {
return args.mergeUtil.canCherryPick(args.mergeSorter, args.repo, mergeTip, args.rw, toMerge);
}
}