Add POST /changes/{id}/revisions/{sha1}/cherrypick
Allow cherry picking of a patch set to a destination branch using the REST Api. Change-Id: If3a78d17e4be6d3bb06eef70d3b0e67867883d45
This commit is contained in:
@@ -1680,6 +1680,57 @@ Deletes the reviewed flag of the calling user from a patch of a revision.
|
||||
HTTP/1.1 204 No Content
|
||||
----
|
||||
|
||||
[[cherry-pick]]
|
||||
Cherry Pick Revision
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
[verse]
|
||||
'POST /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/cherrypick'
|
||||
|
||||
Cherry picks a revision to a destination branch.
|
||||
|
||||
The commit message and destination branch must be provided in the request body inside a
|
||||
link:#cherrypick-input[CherryPickInput] entity.
|
||||
|
||||
.Request
|
||||
----
|
||||
POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/cherrypick HTTP/1.0
|
||||
Content-Type: application/json;charset=UTF-8
|
||||
|
||||
{
|
||||
"message" : "Implementing Feature X";
|
||||
"destination" : "release-branch";
|
||||
}
|
||||
----
|
||||
|
||||
As response a link:#change-info[ChangeInfo] entity is returned that
|
||||
describes the resulting cherry picked change.
|
||||
|
||||
.Response
|
||||
----
|
||||
HTTP/1.1 200 OK
|
||||
Content-Disposition: attachment
|
||||
Content-Type: application/json;charset=UTF-8
|
||||
|
||||
)]}'
|
||||
{
|
||||
"kind": "gerritcodereview#change",
|
||||
"id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9941",
|
||||
"project": "myProject",
|
||||
"branch": "release-branch",
|
||||
"change_id": "I8473b95934b5732ac55d26311a706c9c2bde9941",
|
||||
"subject": "Implementing Feature X",
|
||||
"status": "NEW",
|
||||
"created": "2013-02-01 09:59:32.126000000",
|
||||
"updated": "2013-02-21 11:16:36.775000000",
|
||||
"reviewed": true,
|
||||
"mergeable": true,
|
||||
"_sortkey": "0023412400000f7d",
|
||||
"_number": 3965,
|
||||
"owner": {
|
||||
"name": "John Doe"
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
[[ids]]
|
||||
IDs
|
||||
@@ -1733,7 +1784,6 @@ This can be:
|
||||
change ("674ac754"), at least 4 digits are required
|
||||
* a legacy numeric patch number ("1" for first patch set of the change)
|
||||
|
||||
|
||||
[[json-entities]]
|
||||
JSON Entities
|
||||
-------------
|
||||
@@ -1883,6 +1933,18 @@ The link:rest-api.html#timestamp[timestamp] this message was posted.
|
||||
Which patchset (if any) generated this message.
|
||||
|==================================
|
||||
|
||||
[[cherrypick-input]]
|
||||
CherryPickInput
|
||||
~~~~~~~~~~~~~~~
|
||||
The `CherryPickInput` entity contains information for cherry-picking a change to a new branch.
|
||||
|
||||
[options="header",width="50%",cols="1,6"]
|
||||
|===========================
|
||||
|Field Name |Description
|
||||
|`message` |Commit message for the cherry-picked change
|
||||
|`destination` |Destination Branch
|
||||
|===========================
|
||||
|
||||
[[comment-info]]
|
||||
CommentInfo
|
||||
~~~~~~~~~~~
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
// 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.change;
|
||||
|
||||
import com.google.gerrit.extensions.restapi.AuthException;
|
||||
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||
import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
||||
import com.google.gerrit.extensions.restapi.RestModifyView;
|
||||
import com.google.gerrit.reviewdb.client.Change;
|
||||
import com.google.gerrit.reviewdb.client.PatchSet;
|
||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||
import com.google.gerrit.server.change.CherryPick.Input;
|
||||
import com.google.gerrit.server.change.CherryPickChange;
|
||||
import com.google.gerrit.server.git.MergeException;
|
||||
import com.google.gerrit.server.project.ChangeControl;
|
||||
import com.google.gerrit.server.project.InvalidChangeOperationException;
|
||||
import com.google.gerrit.server.project.RefControl;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
|
||||
class CherryPick implements RestModifyView<RevisionResource, Input> {
|
||||
private final Provider<ReviewDb> dbProvider;
|
||||
private final CherryPickChange cherryPickChange;
|
||||
private final ChangeJson json;
|
||||
|
||||
static class Input {
|
||||
String message;
|
||||
String destination;
|
||||
}
|
||||
|
||||
@Inject
|
||||
CherryPick(Provider<ReviewDb> dbProvider, CherryPickChange cherryPickChange,
|
||||
ChangeJson json) {
|
||||
this.dbProvider = dbProvider;
|
||||
this.cherryPickChange = cherryPickChange;
|
||||
this.json = json;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object apply(RevisionResource revision, Input input)
|
||||
throws AuthException, BadRequestException, ResourceConflictException,
|
||||
Exception {
|
||||
final ChangeControl control = revision.getControl();
|
||||
|
||||
if (input.message == null || input.message.trim().isEmpty()) {
|
||||
throw new BadRequestException("message must be non-empty");
|
||||
} else if (input.destination == null || input.destination.trim().isEmpty()) {
|
||||
throw new BadRequestException("destination must be non-empty");
|
||||
}
|
||||
|
||||
ReviewDb db = dbProvider.get();
|
||||
if (!control.isVisible(db)) {
|
||||
throw new AuthException("Cherry pick not permitted");
|
||||
}
|
||||
|
||||
String refName = input.destination;
|
||||
if (!refName.startsWith("refs/")) {
|
||||
refName = "refs/heads/" + input.destination;
|
||||
}
|
||||
|
||||
RefControl refControl = control.getProjectControl().controlForRef(refName);
|
||||
if (!refControl.canUpload()) {
|
||||
throw new AuthException("Not allowed to cherry pick "
|
||||
+ revision.getChange().getId().toString() + " to "
|
||||
+ input.destination);
|
||||
}
|
||||
|
||||
final PatchSet.Id patchSetId = revision.getPatchSet().getId();
|
||||
try {
|
||||
Change.Id cherryPickedChangeId =
|
||||
cherryPickChange.cherryPick(patchSetId, input.message,
|
||||
input.destination, refControl);
|
||||
return json.format(cherryPickedChangeId);
|
||||
} catch (InvalidChangeOperationException e) {
|
||||
throw new BadRequestException(e.getMessage());
|
||||
} catch (MergeException e) {
|
||||
throw new ResourceConflictException(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
// 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.change;
|
||||
|
||||
import com.google.gerrit.common.data.LabelTypes;
|
||||
import com.google.gerrit.common.errors.EmailException;
|
||||
import com.google.gerrit.reviewdb.client.Account;
|
||||
import com.google.gerrit.reviewdb.client.Branch;
|
||||
import com.google.gerrit.reviewdb.client.Change;
|
||||
import com.google.gerrit.reviewdb.client.ChangeMessage;
|
||||
import com.google.gerrit.reviewdb.client.PatchSet;
|
||||
import com.google.gerrit.reviewdb.client.PatchSetInfo;
|
||||
import com.google.gerrit.reviewdb.client.Project;
|
||||
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.GerritPersonIdent;
|
||||
import com.google.gerrit.server.IdentifiedUser;
|
||||
import com.google.gerrit.server.events.CommitReceivedEvent;
|
||||
import com.google.gerrit.server.git.GitRepositoryManager;
|
||||
import com.google.gerrit.server.git.MergeException;
|
||||
import com.google.gerrit.server.git.MergeUtil;
|
||||
import com.google.gerrit.server.git.validators.CommitValidationException;
|
||||
import com.google.gerrit.server.git.validators.CommitValidators;
|
||||
import com.google.gerrit.server.patch.PatchSetInfoFactory;
|
||||
import com.google.gerrit.server.project.InvalidChangeOperationException;
|
||||
import com.google.gerrit.server.project.NoSuchChangeException;
|
||||
import com.google.gerrit.server.project.ProjectState;
|
||||
import com.google.gerrit.server.project.RefControl;
|
||||
import com.google.gerrit.server.ssh.NoSshInfo;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
import com.google.inject.Inject;
|
||||
|
||||
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
|
||||
import org.eclipse.jgit.errors.MissingObjectException;
|
||||
import org.eclipse.jgit.errors.RepositoryNotFoundException;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.ObjectInserter;
|
||||
import org.eclipse.jgit.lib.PersonIdent;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.eclipse.jgit.lib.RefUpdate;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.revwalk.FooterKey;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import org.eclipse.jgit.revwalk.RevWalk;
|
||||
import org.eclipse.jgit.transport.ReceiveCommand;
|
||||
import org.eclipse.jgit.util.ChangeIdUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class CherryPickChange {
|
||||
|
||||
private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
|
||||
|
||||
private final PatchSetInfoFactory patchSetInfoFactory;
|
||||
private final ReviewDb db;
|
||||
private final GitRepositoryManager gitManager;
|
||||
private final PersonIdent myIdent;
|
||||
private final IdentifiedUser currentUser;
|
||||
private final CommitValidators.Factory commitValidatorsFactory;
|
||||
private final ChangeInserter changeInserter;
|
||||
final MergeUtil.Factory mergeUtilFactory;
|
||||
|
||||
@Inject
|
||||
CherryPickChange(final PatchSetInfoFactory patchSetInfoFactory,
|
||||
final ReviewDb db, @GerritPersonIdent final PersonIdent myIdent,
|
||||
final GitRepositoryManager gitManager, final IdentifiedUser currentUser,
|
||||
final CommitValidators.Factory commitValidatorsFactory,
|
||||
final ChangeInserter changeInserter,
|
||||
final MergeUtil.Factory mergeUtilFactory) {
|
||||
this.patchSetInfoFactory = patchSetInfoFactory;
|
||||
this.db = db;
|
||||
this.gitManager = gitManager;
|
||||
this.myIdent = myIdent;
|
||||
this.currentUser = currentUser;
|
||||
this.commitValidatorsFactory = commitValidatorsFactory;
|
||||
this.changeInserter = changeInserter;
|
||||
this.mergeUtilFactory = mergeUtilFactory;
|
||||
}
|
||||
|
||||
public Change.Id cherryPick(final PatchSet.Id patchSetId,
|
||||
final String message, final String destinationBranch,
|
||||
final RefControl refControl) throws NoSuchChangeException,
|
||||
EmailException, OrmException, MissingObjectException,
|
||||
IncorrectObjectTypeException, IOException,
|
||||
InvalidChangeOperationException, MergeException {
|
||||
|
||||
final Change.Id changeId = patchSetId.getParentKey();
|
||||
final PatchSet patch = db.patchSets().get(patchSetId);
|
||||
if (patch == null) {
|
||||
throw new NoSuchChangeException(changeId);
|
||||
}
|
||||
if (destinationBranch == null || destinationBranch.length() == 0) {
|
||||
throw new InvalidChangeOperationException(
|
||||
"Cherry Pick: Destination branch cannot be null or empty");
|
||||
}
|
||||
|
||||
Project.NameKey project = db.changes().get(changeId).getProject();
|
||||
final Repository git;
|
||||
try {
|
||||
git = gitManager.openRepository(project);
|
||||
} catch (RepositoryNotFoundException e) {
|
||||
throw new NoSuchChangeException(changeId, e);
|
||||
}
|
||||
|
||||
try {
|
||||
CommitValidators commitValidators =
|
||||
commitValidatorsFactory.create(refControl, new NoSshInfo(), git);
|
||||
|
||||
RevWalk revWalk = new RevWalk(git);
|
||||
try {
|
||||
Ref destRef = git.getRef(destinationBranch);
|
||||
if (destRef == null) {
|
||||
throw new InvalidChangeOperationException("Branch "
|
||||
+ destinationBranch + " does not exist.");
|
||||
}
|
||||
|
||||
final RevCommit mergeTip = revWalk.parseCommit(destRef.getObjectId());
|
||||
|
||||
RevCommit commitToCherryPick =
|
||||
revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get()));
|
||||
|
||||
PersonIdent committerIdent =
|
||||
currentUser.newCommitterIdent(myIdent.getWhen(),
|
||||
myIdent.getTimeZone());
|
||||
|
||||
RevCommit cherryPickCommit;
|
||||
ObjectInserter oi = git.newObjectInserter();
|
||||
try {
|
||||
ProjectState projectState = refControl.getProjectControl().getProjectState();
|
||||
cherryPickCommit =
|
||||
mergeUtilFactory.create(projectState).createCherryPickFromCommit(git, oi, mergeTip,
|
||||
commitToCherryPick, committerIdent, message, revWalk);
|
||||
} finally {
|
||||
oi.release();
|
||||
}
|
||||
|
||||
if (cherryPickCommit == null) {
|
||||
throw new MergeException(
|
||||
"Could not create a merge commit during the cherry pick");
|
||||
}
|
||||
|
||||
Change.Key changeKey;
|
||||
final List<String> idList = cherryPickCommit.getFooterLines(CHANGE_ID);
|
||||
if (!idList.isEmpty()) {
|
||||
final String idStr = idList.get(idList.size() - 1).trim();
|
||||
changeKey = new Change.Key(idStr);
|
||||
} else {
|
||||
final ObjectId computedChangeId =
|
||||
ChangeIdUtil
|
||||
.computeChangeId(cherryPickCommit.getTree(), mergeTip,
|
||||
cherryPickCommit.getAuthorIdent(), myIdent, message);
|
||||
|
||||
changeKey = new Change.Key("I" + computedChangeId.name());
|
||||
}
|
||||
|
||||
List<Change> destChanges =
|
||||
db.changes()
|
||||
.byBranchKey(
|
||||
new Branch.NameKey(db.changes().get(changeId).getProject(),
|
||||
destRef.getName()), changeKey).toList();
|
||||
|
||||
Change change;
|
||||
if (destChanges.size() > 1) {
|
||||
throw new InvalidChangeOperationException("Several changes with key "
|
||||
+ changeKey + " resides on the same branch. "
|
||||
+ "Cannot create a new patch set.");
|
||||
} else if (destChanges.size() == 1) {
|
||||
// The change key exists on the destination branch.
|
||||
throw new InvalidChangeOperationException(
|
||||
"Change with same change-id: " + changeKey
|
||||
+ " already resides on the same branch. "
|
||||
+ "Cannot create a new change.");
|
||||
} else {
|
||||
// Change key not found on destination branch. We can create a new
|
||||
// change.
|
||||
change =
|
||||
new Change(changeKey, new Change.Id(db.nextChangeId()),
|
||||
currentUser.getAccountId(), new Branch.NameKey(project,
|
||||
destRef.getName()));
|
||||
}
|
||||
|
||||
PatchSet.Id id =
|
||||
new PatchSet.Id(change.getId(), Change.INITIAL_PATCH_SET_ID);
|
||||
PatchSet newPatchSet = new PatchSet(id);
|
||||
newPatchSet.setCreatedOn(new Timestamp(System.currentTimeMillis()));
|
||||
newPatchSet.setUploader(change.getOwner());
|
||||
newPatchSet.setRevision(new RevId(cherryPickCommit.name()));
|
||||
|
||||
PatchSetInfo newPatchSetInfo =
|
||||
patchSetInfoFactory.get(cherryPickCommit, newPatchSet.getId());
|
||||
change.setCurrentPatchSet(newPatchSetInfo);
|
||||
|
||||
CommitReceivedEvent commitReceivedEvent =
|
||||
new CommitReceivedEvent(new ReceiveCommand(ObjectId.zeroId(),
|
||||
cherryPickCommit.getId(), newPatchSet.getRefName()), refControl
|
||||
.getProjectControl().getProject(), refControl.getRefName(),
|
||||
cherryPickCommit, currentUser);
|
||||
|
||||
try {
|
||||
commitValidators.validateForGerritCommits(commitReceivedEvent);
|
||||
} catch (CommitValidationException e) {
|
||||
throw new InvalidChangeOperationException(e.getMessage());
|
||||
}
|
||||
|
||||
ChangeUtil.updated(change);
|
||||
|
||||
final RefUpdate ru = git.updateRef(newPatchSet.getRefName());
|
||||
ru.setExpectedOldObjectId(ObjectId.zeroId());
|
||||
ru.setNewObjectId(cherryPickCommit);
|
||||
ru.disableRefLog();
|
||||
if (ru.update(revWalk) != RefUpdate.Result.NEW) {
|
||||
throw new IOException(String.format(
|
||||
"Failed to create ref %s in %s: %s", newPatchSet.getRefName(),
|
||||
change.getDest().getParentKey().get(), ru.getResult()));
|
||||
}
|
||||
|
||||
final ChangeMessage cmsg =
|
||||
new ChangeMessage(new ChangeMessage.Key(changeId,
|
||||
ChangeUtil.messageUUID(db)), currentUser.getAccountId(),
|
||||
patchSetId);
|
||||
final StringBuilder msgBuf =
|
||||
new StringBuilder("Patch Set " + patchSetId.get()
|
||||
+ ": Cherry Picked");
|
||||
msgBuf.append("\n\n");
|
||||
msgBuf.append("This patchset was cherry picked to change: "
|
||||
+ change.getKey().get());
|
||||
cmsg.setMessage(msgBuf.toString());
|
||||
|
||||
LabelTypes labelTypes = refControl.getProjectControl().getLabelTypes();
|
||||
|
||||
changeInserter.insertChange(db, change, cmsg, newPatchSet,
|
||||
cherryPickCommit, labelTypes, newPatchSetInfo,
|
||||
Collections.<Account.Id> emptySet());
|
||||
|
||||
return change.getId();
|
||||
} finally {
|
||||
revWalk.release();
|
||||
}
|
||||
} finally {
|
||||
git.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,8 @@ public class Module extends RestApiModule {
|
||||
post(REVISION_KIND, "test.submit_rule").to(TestSubmitRule.class);
|
||||
post(REVISION_KIND, "test.submit_type").to(TestSubmitType.class);
|
||||
|
||||
post(REVISION_KIND, "cherrypick").to(CherryPick.class);
|
||||
|
||||
child(REVISION_KIND, "drafts").to(Drafts.class);
|
||||
put(REVISION_KIND, "drafts").to(CreateDraft.class);
|
||||
get(DRAFT_KIND).to(GetDraft.class);
|
||||
|
||||
@@ -148,8 +148,9 @@ public class CherryPick extends SubmitStrategy {
|
||||
final String cherryPickCmtMsg = args.mergeUtil.createCherryPickCommitMessage(n);
|
||||
|
||||
final CodeReviewCommit newCommit =
|
||||
args.mergeUtil.createCherryPickFromCommit(args.repo, args.inserter, mergeTip, n,
|
||||
cherryPickCommitterIdent, cherryPickCmtMsg, args.rw);
|
||||
(CodeReviewCommit) args.mergeUtil.createCherryPickFromCommit(args.repo,
|
||||
args.inserter, mergeTip, n, cherryPickCommitterIdent,
|
||||
cherryPickCmtMsg, args.rw);
|
||||
|
||||
if (newCommit == null) {
|
||||
return null;
|
||||
|
||||
@@ -180,8 +180,8 @@ public class MergeUtil {
|
||||
return submitter;
|
||||
}
|
||||
|
||||
public CodeReviewCommit createCherryPickFromCommit(Repository repo,
|
||||
ObjectInserter inserter, CodeReviewCommit mergeTip, CodeReviewCommit originalCommit,
|
||||
public RevCommit createCherryPickFromCommit(Repository repo,
|
||||
ObjectInserter inserter, RevCommit mergeTip, RevCommit originalCommit,
|
||||
PersonIdent cherryPickCommitterIdent, String commitMsg, RevWalk rw)
|
||||
throws MissingObjectException, IncorrectObjectTypeException, IOException {
|
||||
|
||||
@@ -199,10 +199,8 @@ public class MergeUtil {
|
||||
mergeCommit.setMessage(commitMsg);
|
||||
|
||||
final ObjectId id = commit(inserter, mergeCommit);
|
||||
final CodeReviewCommit newCommit =
|
||||
(CodeReviewCommit) rw.parseCommit(id);
|
||||
|
||||
return newCommit;
|
||||
return rw.parseCommit(id);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user