blob: 6d15ed3c2b9ade34d8b9d540bc0def9efb5a2bf6 [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.Lists;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetAncestor;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.CommitMergeStatus;
import com.google.gerrit.server.git.MergeException;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.util.TimeUtil;
import com.google.gwtorm.server.OrmException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.revwalk.RevCommit;
import java.io.IOException;
import java.util.ArrayList;
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 GitReferenceUpdated gitRefUpdated;
private final Map<Change.Id, CodeReviewCommit> newCommits;
CherryPick(final SubmitStrategy.Arguments args,
final PatchSetInfoFactory patchSetInfoFactory,
final GitReferenceUpdated gitRefUpdated) {
super(args);
this.patchSetInfoFactory = patchSetInfoFactory;
this.gitRefUpdated = gitRefUpdated;
this.newCommits = new HashMap<Change.Id, CodeReviewCommit>();
}
@Override
protected CodeReviewCommit _run(CodeReviewCommit mergeTip,
final List<CodeReviewCommit> toMerge) throws MergeException {
while (!toMerge.isEmpty()) {
final CodeReviewCommit n = toMerge.remove(0);
try {
if (mergeTip == null) {
// The branch is unborn. Take a fast-forward resolution to
// create the branch.
//
mergeTip = n;
n.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
} else if (n.getParentCount() == 0) {
// Refuse to merge a root commit into an existing branch,
// we cannot obtain a delta for the cherry-pick to apply.
//
n.setStatusCode(CommitMergeStatus.CANNOT_CHERRY_PICK_ROOT);
} else if (n.getParentCount() == 1) {
// 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.
//
mergeTip = writeCherryPickCommit(mergeTip, n);
if (mergeTip != null) {
newCommits.put(mergeTip.getPatchsetId().getParentKey(), mergeTip);
} else {
n.setStatusCode(CommitMergeStatus.PATH_CONFLICT);
}
} else {
// 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.mergeUtil.hasMissingDependencies(args.mergeSorter, n)) {
if (args.rw.isMergedInto(mergeTip, n)) {
mergeTip = n;
} else {
mergeTip =
args.mergeUtil.mergeOneCommit(args.myIdent, args.repo,
args.rw, args.inserter, args.canMergeFlag,
args.destBranch, mergeTip, n);
}
final PatchSetApproval submitApproval =
args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
mergeTip, args.alreadyAccepted);
setRefLogIdent(submitApproval);
} else {
// 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.
//
}
}
} catch (NoSuchChangeException | IOException | OrmException e) {
throw new MergeException("Cannot merge " + n.name(), e);
}
}
return mergeTip;
}
private CodeReviewCommit writeCherryPickCommit(CodeReviewCommit mergeTip,
CodeReviewCommit n) throws IOException, OrmException,
NoSuchChangeException {
args.rw.parseBody(n);
final PatchSetApproval submitAudit = args.mergeUtil.getSubmitter(n);
IdentifiedUser cherryPickUser;
PersonIdent cherryPickCommitterIdent;
if (submitAudit != null) {
cherryPickUser =
args.identifiedUserFactory.create(submitAudit.getAccountId());
cherryPickCommitterIdent = cherryPickUser.newCommitterIdent(
submitAudit.getGranted(), args.myIdent.getTimeZone());
} else {
cherryPickUser = args.identifiedUserFactory.create(n.change().getOwner());
cherryPickCommitterIdent = args.myIdent;
}
final String cherryPickCmtMsg = args.mergeUtil.createCherryPickCommitMessage(n);
final CodeReviewCommit newCommit =
(CodeReviewCommit) args.mergeUtil.createCherryPickFromCommit(args.repo,
args.inserter, mergeTip, n, cherryPickCommitterIdent,
cherryPickCmtMsg, args.rw);
if (newCommit == null) {
return null;
}
PatchSet.Id id =
ChangeUtil.nextPatchSetId(args.repo, n.change().currentPatchSetId());
final PatchSet ps = new PatchSet(id);
ps.setCreatedOn(TimeUtil.nowTs());
ps.setUploader(submitAudit.getAccountId());
ps.setRevision(new RevId(newCommit.getId().getName()));
final RefUpdate ru;
args.db.changes().beginTransaction(n.change().getId());
try {
insertAncestors(args.db, ps.getId(), newCommit);
args.db.patchSets().insert(Collections.singleton(ps));
n.change()
.setCurrentPatchSet(patchSetInfoFactory.get(newCommit, ps.getId()));
args.db.changes().update(Collections.singletonList(n.change()));
final List<PatchSetApproval> approvals = Lists.newArrayList();
for (PatchSetApproval a
: args.approvalsUtil.byPatchSet(args.db, n.getControl(), n.getPatchsetId())) {
approvals.add(new PatchSetApproval(ps.getId(), a));
}
args.db.patchSetApprovals().insert(approvals);
ru = args.repo.updateRef(ps.getRefName());
ru.setExpectedOldObjectId(ObjectId.zeroId());
ru.setNewObjectId(newCommit);
ru.disableRefLog();
if (ru.update(args.rw) != RefUpdate.Result.NEW) {
throw new IOException(String.format(
"Failed to create ref %s in %s: %s", ps.getRefName(), n.change()
.getDest().getParentKey().get(), ru.getResult()));
}
args.db.commit();
} finally {
args.db.rollback();
}
gitRefUpdated.fire(n.change().getProject(), ru);
newCommit.copyFrom(n);
newCommit.setStatusCode(CommitMergeStatus.CLEAN_PICK);
newCommit.setControl(args.changeControlFactory.controlFor(n.change(), cherryPickUser));
newCommits.put(newCommit.getPatchsetId().getParentKey(), newCommit);
setRefLogIdent(submitAudit);
return newCommit;
}
private static void insertAncestors(ReviewDb db, PatchSet.Id id, RevCommit src)
throws OrmException {
final int cnt = src.getParentCount();
List<PatchSetAncestor> toInsert = new ArrayList<PatchSetAncestor>(cnt);
for (int p = 0; p < cnt; p++) {
PatchSetAncestor a;
a = new PatchSetAncestor(new PatchSetAncestor.Id(id, p + 1));
a.setAncestorRevision(new RevId(src.getParent(p).getId().name()));
toInsert.add(a);
}
db.patchSetAncestors().insert(toInsert);
}
@Override
public Map<Change.Id, CodeReviewCommit> getNewCommits() {
return newCommits;
}
@Override
public boolean dryRun(final CodeReviewCommit mergeTip,
final CodeReviewCommit toMerge) throws MergeException {
return args.mergeUtil.canCherryPick(args.mergeSorter, args.repo,
mergeTip, args.rw, toMerge);
}
}