blob: ecac2b35364493fea974a7bf6047af0e61b64b66 [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.git.strategy;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.restapi.MergeConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.PatchSetInfo;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.git.BatchUpdate;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
import com.google.gerrit.server.git.BatchUpdate.RepoContext;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.CommitMergeStatus;
import com.google.gerrit.server.git.GroupCollector;
import com.google.gerrit.server.git.IntegrationException;
import com.google.gerrit.server.git.MergeIdenticalTreeException;
import com.google.gerrit.server.git.MergeTip;
import com.google.gerrit.server.git.UpdateException;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.ReceiveCommand;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class CherryPick extends SubmitStrategy {
private final PatchSetInfoFactory patchSetInfoFactory;
private final Map<Change.Id, CodeReviewCommit> newCommits;
CherryPick(SubmitStrategy.Arguments args,
PatchSetInfoFactory patchSetInfoFactory) {
super(args);
this.patchSetInfoFactory = patchSetInfoFactory;
this.newCommits = new HashMap<>();
}
@Override
protected MergeTip _run(CodeReviewCommit branchTip,
Collection<CodeReviewCommit> toMerge) throws IntegrationException {
MergeTip mergeTip = new MergeTip(branchTip, toMerge);
List<CodeReviewCommit> sorted = CodeReviewCommit.ORDER.sortedCopy(toMerge);
boolean first = true;
try (BatchUpdate u = args.newBatchUpdate(TimeUtil.nowTs())) {
while (!sorted.isEmpty()) {
CodeReviewCommit n = sorted.remove(0);
Change.Id cid = n.change().getId();
if (first && branchTip == null) {
u.addOp(cid, new CherryPickUnbornRootOp(mergeTip, n));
} else if (n.getParentCount() == 0) {
u.addOp(cid, new CherryPickRootOp(n));
} else if (n.getParentCount() == 1) {
u.addOp(cid, new CherryPickOneOp(mergeTip, n));
} else {
u.addOp(cid, new CherryPickMultipleParentsOp(mergeTip, n));
}
first = false;
}
u.execute();
} catch (UpdateException | RestApiException e) {
throw new IntegrationException(
"Cannot cherry-pick onto " + args.destBranch);
}
// TODO(dborowitz): When BatchUpdate is hoisted out of CherryPick,
// SubmitStrategy should probably no longer return MergeTip, instead just
// mutating a single shared MergeTip passed in from the caller.
return mergeTip;
}
private static class CherryPickUnbornRootOp extends BatchUpdate.Op {
private final MergeTip mergeTip;
private final CodeReviewCommit toMerge;
private CherryPickUnbornRootOp(MergeTip mergeTip,
CodeReviewCommit toMerge) {
this.mergeTip = mergeTip;
this.toMerge = toMerge;
}
@Override
public void updateRepo(RepoContext ctx) {
// The branch is unborn. Take fast-forward resolution to create the
// branch.
mergeTip.moveTipTo(toMerge, toMerge);
toMerge.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
}
}
private static class CherryPickRootOp extends BatchUpdate.Op {
private final CodeReviewCommit toMerge;
private CherryPickRootOp(CodeReviewCommit toMerge) {
this.toMerge = toMerge;
}
@Override
public void updateRepo(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 BatchUpdate.Op {
private final MergeTip mergeTip;
private final CodeReviewCommit toMerge;
private PatchSet.Id psId;
private CodeReviewCommit newCommit;
private PatchSetInfo patchSetInfo;
private CherryPickOneOp(MergeTip mergeTip, CodeReviewCommit n) {
this.mergeTip = mergeTip;
this.toMerge = n;
}
@Override
public void updateRepo(RepoContext ctx) throws IOException {
// 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.nextPatchSetId(
args.repo, toMerge.change().currentPatchSetId());
String cherryPickCmtMsg =
args.mergeUtil.createCherryPickCommitMessage(toMerge);
PersonIdent committer = args.caller.newCommitterIdent(
ctx.getWhen(), args.serverIdent.get().getTimeZone());
try {
newCommit = args.mergeUtil.createCherryPickFromCommit(
args.repo, args.inserter, mergeTip.getCurrentTip(), toMerge,
committer, cherryPickCmtMsg, args.rw);
mergeTip.moveTipTo(newCommit, newCommit);
ctx.addRefUpdate(
new ReceiveCommand(ObjectId.zeroId(), newCommit, psId.toRefName()));
patchSetInfo =
patchSetInfoFactory.get(ctx.getRevWalk(), newCommit, psId);
} 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);
} catch (MergeIdenticalTreeException mie) {
toMerge.setStatusCode(CommitMergeStatus.ALREADY_MERGED);
}
}
@Override
public void updateChange(ChangeContext ctx) throws OrmException,
NoSuchChangeException {
if (newCommit == null) {
// Merge conflict; don't update change.
return;
}
PatchSet ps = new PatchSet(psId);
ps.setCreatedOn(ctx.getWhen());
ps.setUploader(args.caller.getAccountId());
ps.setRevision(new RevId(newCommit.getId().getName()));
Change c = toMerge.change();
ps.setGroups(GroupCollector.getCurrentGroups(args.db, c));
args.db.patchSets().insert(Collections.singleton(ps));
c.setCurrentPatchSet(patchSetInfo);
args.db.changes().update(Collections.singletonList(c));
List<PatchSetApproval> approvals = Lists.newArrayList();
for (PatchSetApproval a : args.approvalsUtil.byPatchSet(
args.db, toMerge.getControl(), toMerge.getPatchsetId())) {
approvals.add(new PatchSetApproval(ps.getId(), a));
}
args.db.patchSetApprovals().insert(approvals);
newCommit.copyFrom(toMerge);
newCommit.setStatusCode(CommitMergeStatus.CLEAN_PICK);
newCommit.setControl(
args.changeControlFactory.controlFor(toMerge.change(), args.caller));
newCommits.put(c.getId(), newCommit);
setRefLogIdent();
}
}
private class CherryPickMultipleParentsOp extends BatchUpdate.Op {
private final MergeTip mergeTip;
private final CodeReviewCommit toMerge;
private CherryPickMultipleParentsOp(MergeTip mergeTip,
CodeReviewCommit toMerge) {
this.mergeTip = mergeTip;
this.toMerge = toMerge;
}
@Override
public void updateRepo(RepoContext ctx)
throws IntegrationException, 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.
if (args.rw.isMergedInto(mergeTip.getCurrentTip(), toMerge)) {
mergeTip.moveTipTo(toMerge, toMerge);
} else {
PersonIdent myIdent =
new PersonIdent(args.serverIdent.get(), ctx.getWhen());
CodeReviewCommit result = args.mergeUtil.mergeOneCommit(myIdent,
myIdent, args.repo, args.rw, args.inserter,
args.canMergeFlag, args.destBranch, mergeTip.getCurrentTip(),
toMerge);
mergeTip.moveTipTo(result, toMerge);
}
RevCommit initialTip = mergeTip.getInitialTip();
args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
mergeTip.getCurrentTip(), initialTip == null
? ImmutableSet.<RevCommit> of() : ImmutableSet.of(initialTip));
setRefLogIdent();
}
}
@Override
public Map<Change.Id, CodeReviewCommit> getNewCommits() {
return newCommits;
}
@Override
public boolean dryRun(CodeReviewCommit mergeTip, CodeReviewCommit toMerge)
throws IntegrationException {
return args.mergeUtil.canCherryPick(args.mergeSorter, args.repo,
mergeTip, args.rw, toMerge);
}
}