Support cherry picking a commit without specific change

The current cherry pick REST endpoint only allows cherry picking a
revision.  This change allows cherry picking a commit which may not be
associated with a change.

Change-Id: Ic6b178dfe267e0e5cc9333470ba792de3c8111d7
This commit is contained in:
Changcheng Xiao
2017-03-22 17:16:57 +01:00
committed by Han-Wen Nienhuys
parent 13c1df76b8
commit 9d2ec04c1b
10 changed files with 429 additions and 43 deletions

View File

@@ -2060,6 +2060,60 @@ The content is returned as base64 encoded string.
Ly8gQ29weXJpZ2h0IChDKSAyMDEwIFRoZSBBbmRyb2lkIE9wZW4gU291cmNlIFByb2plY...
----
[[cherry-pick-commit]]
=== Cherry Pick Commit
--
'POST /projects/link:#project-name[\{project-name\}]/commits/link:#commit-id[\{commit-id\}]/cherrypick'
--
Cherry-picks a commit of a project to a destination branch.
The destination branch must be provided in the request body inside a
link:rest-api-changes.html#cherrypick-input[CherryPickInput] entity.
If the commit message is not set, the commit message of the source
commit will be used.
.Request
----
POST /projects/work%2Fmy-project/commits/a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96/cherrypick HTTP/1.0
Content-Type: application/json; charset=UTF-8
{
"message" : "Implementing Feature X",
"destination" : "release-branch"
}
----
As response a link:rest-api-changes.html#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
)]}'
{
"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",
"mergeable": true,
"insertions": 12,
"deletions": 11,
"_number": 3965,
"owner": {
"name": "John Doe"
}
}
----
[[dashboard-endpoints]]
== Dashboard Endpoints

View File

@@ -23,6 +23,7 @@ import static org.eclipse.jgit.lib.Constants.SIGNED_OFF_BY_TAG;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.GitUtil;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.PushOneCommit.Result;
import com.google.gerrit.acceptance.RestResponse;
@@ -30,11 +31,15 @@ import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.CherryPickInput;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.common.MergeInput;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -45,10 +50,13 @@ import com.google.gerrit.server.git.ChangeAlreadyMergedException;
import com.google.gerrit.testutil.ConfigSuite;
import com.google.gerrit.testutil.FakeEmailSender.Message;
import com.google.gerrit.testutil.TestTimeUtil;
import java.util.Iterator;
import java.util.List;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
@@ -278,6 +286,79 @@ public class CreateChangeIT extends AbstractDaemonTest {
assertCreateSucceeds(in);
}
@Test
public void cherryPickCommitWithoutChangeId() throws Exception {
// This test is a little superfluous, since the current cherry-pick code ignores
// the commit message of the to-be-cherry-picked change, using the one in
// CherryPickInput instead.
CherryPickInput input = new CherryPickInput();
input.destination = "foo";
input.message = "it goes to foo branch";
gApi.projects().name(project.get()).branch(input.destination).create(new BranchInput());
RevCommit revCommit = createNewCommitWithoutChangeId();
ChangeInfo changeInfo =
gApi.projects().name(project.get()).commit(revCommit.getName()).cherryPick(input).get();
assertThat(changeInfo.messages).hasSize(1);
Iterator<ChangeMessageInfo> messageIterator = changeInfo.messages.iterator();
String expectedMessage =
String.format("Patch Set 1: Cherry Picked from commit %s.", revCommit.getName());
assertThat(messageIterator.next().message).isEqualTo(expectedMessage);
RevisionInfo revInfo = changeInfo.revisions.get(changeInfo.currentRevision);
assertThat(revInfo).isNotNull();
CommitInfo commitInfo = revInfo.commit;
assertThat(commitInfo.message)
.isEqualTo(input.message + "\n\nChange-Id: " + changeInfo.changeId + "\n");
}
@Test
public void cherryPickCommitWithChangeId() throws Exception {
CherryPickInput input = new CherryPickInput();
input.destination = "foo";
RevCommit revCommit = createChange().getCommit();
List<String> footers = revCommit.getFooterLines("Change-Id");
assertThat(footers).hasSize(1);
String changeId = footers.get(0);
input.message = "it goes to foo branch\n\nChange-Id: " + changeId;
gApi.projects().name(project.get()).branch(input.destination).create(new BranchInput());
ChangeInfo changeInfo =
gApi.projects().name(project.get()).commit(revCommit.getName()).cherryPick(input).get();
assertThat(changeInfo.messages).hasSize(1);
Iterator<ChangeMessageInfo> messageIterator = changeInfo.messages.iterator();
String expectedMessage =
String.format("Patch Set 1: Cherry Picked from commit %s.", revCommit.getName());
assertThat(messageIterator.next().message).isEqualTo(expectedMessage);
RevisionInfo revInfo = changeInfo.revisions.get(changeInfo.currentRevision);
assertThat(revInfo).isNotNull();
assertThat(revInfo.commit.message).isEqualTo(input.message + "\n");
}
private RevCommit createNewCommitWithoutChangeId() throws Exception {
try (Repository repo = repoManager.openRepository(project);
RevWalk walk = new RevWalk(repo)) {
Ref ref = repo.exactRef("refs/heads/master");
RevCommit tip = null;
if (ref != null) {
tip = walk.parseCommit(ref.getObjectId());
}
TestRepository<?> testSrcRepo = new TestRepository<>(repo);
TestRepository<?>.BranchBuilder builder = testSrcRepo.branch("refs/heads/master");
RevCommit revCommit =
tip == null
? builder.commit().message("commit 1").add("a.txt", "content").create()
: builder.commit().parent(tip).message("commit 1").add("a.txt", "content").create();
assertThat(GitUtil.getChangeId(testSrcRepo, revCommit).isPresent()).isFalse();
return revCommit;
}
}
private ChangeInput newChangeInput(ChangeStatus status) {
ChangeInput in = new ChangeInput();
in.project = project.get();

View File

@@ -0,0 +1,33 @@
// Copyright (C) 2017 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.extensions.api.projects;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.CherryPickInput;
import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
public interface CommitApi {
ChangeApi cherryPick(CherryPickInput input) throws RestApiException;
/** A default implementation for source compatibility when adding new methods to the interface. */
class NotImplemented implements CommitApi {
@Override
public ChangeApi cherryPick(CherryPickInput input) {
throw new NotImplementedException();
}
}
}

View File

@@ -124,6 +124,14 @@ public interface ProjectApi {
*/
TagApi tag(String ref) throws RestApiException;
/**
* Lookup a commit by its {@Code ObjectId} string.
*
* @param commit the {@Code ObjectId} string.
* @return API for accessing the commit.
*/
CommitApi commit(String commit) throws RestApiException;
/**
* A default implementation which allows source compatibility when adding new methods to the
* interface.
@@ -218,5 +226,10 @@ public interface ProjectApi {
public void deleteTags(DeleteTagsInput in) {
throw new NotImplementedException();
}
@Override
public CommitApi commit(String commit) {
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,55 @@
// Copyright (C) 2017 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.api.projects;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.Changes;
import com.google.gerrit.extensions.api.changes.CherryPickInput;
import com.google.gerrit.extensions.api.projects.CommitApi;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.change.CherryPickCommit;
import com.google.gerrit.server.project.CommitResource;
import com.google.gerrit.server.update.UpdateException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
public class CommitApiImpl implements CommitApi {
public interface Factory {
CommitApiImpl create(CommitResource r);
}
private final Changes changes;
private final CherryPickCommit cherryPickCommit;
private final CommitResource commitResource;
@Inject
CommitApiImpl(
Changes changes, CherryPickCommit cherryPickCommit, @Assisted CommitResource commitResource) {
this.changes = changes;
this.cherryPickCommit = cherryPickCommit;
this.commitResource = commitResource;
}
@Override
public ChangeApi cherryPick(CherryPickInput input) throws RestApiException {
try {
return changes.id(cherryPickCommit.apply(commitResource, input)._number);
} catch (OrmException | IOException | UpdateException e) {
throw new RestApiException("Cannot cherry pick", e);
}
}
}

View File

@@ -26,5 +26,6 @@ public class Module extends FactoryModule {
factory(TagApiImpl.Factory.class);
factory(ProjectApiImpl.Factory.class);
factory(ChildProjectApiImpl.Factory.class);
factory(CommitApiImpl.Factory.class);
}
}

View File

@@ -21,6 +21,7 @@ import com.google.gerrit.extensions.api.access.ProjectAccessInput;
import com.google.gerrit.extensions.api.projects.BranchApi;
import com.google.gerrit.extensions.api.projects.BranchInfo;
import com.google.gerrit.extensions.api.projects.ChildProjectApi;
import com.google.gerrit.extensions.api.projects.CommitApi;
import com.google.gerrit.extensions.api.projects.ConfigInfo;
import com.google.gerrit.extensions.api.projects.ConfigInput;
import com.google.gerrit.extensions.api.projects.DeleteBranchesInput;
@@ -39,6 +40,7 @@ import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.project.ChildProjectsCollection;
import com.google.gerrit.server.project.CommitsCollection;
import com.google.gerrit.server.project.CreateProject;
import com.google.gerrit.server.project.DeleteBranches;
import com.google.gerrit.server.project.DeleteTags;
@@ -89,6 +91,8 @@ public class ProjectApiImpl implements ProjectApi {
private final ListTags listTags;
private final DeleteBranches deleteBranches;
private final DeleteTags deleteTags;
private final CommitsCollection commitsCollection;
private final CommitApiImpl.Factory commitApi;
@AssistedInject
ProjectApiImpl(
@@ -111,6 +115,8 @@ public class ProjectApiImpl implements ProjectApi {
ListTags listTags,
DeleteBranches deleteBranches,
DeleteTags deleteTags,
CommitsCollection commitsCollection,
CommitApiImpl.Factory commitApi,
@Assisted ProjectResource project) {
this(
user,
@@ -133,6 +139,8 @@ public class ProjectApiImpl implements ProjectApi {
deleteBranches,
deleteTags,
project,
commitsCollection,
commitApi,
null);
}
@@ -157,6 +165,8 @@ public class ProjectApiImpl implements ProjectApi {
ListTags listTags,
DeleteBranches deleteBranches,
DeleteTags deleteTags,
CommitsCollection commitsCollection,
CommitApiImpl.Factory commitApi,
@Assisted String name) {
this(
user,
@@ -179,6 +189,8 @@ public class ProjectApiImpl implements ProjectApi {
deleteBranches,
deleteTags,
null,
commitsCollection,
commitApi,
name);
}
@@ -203,6 +215,8 @@ public class ProjectApiImpl implements ProjectApi {
DeleteBranches deleteBranches,
DeleteTags deleteTags,
ProjectResource project,
CommitsCollection commitsCollection,
CommitApiImpl.Factory commitApi,
String name) {
this.user = user;
this.createProjectFactory = createProjectFactory;
@@ -225,6 +239,8 @@ public class ProjectApiImpl implements ProjectApi {
this.listTags = listTags;
this.deleteBranches = deleteBranches;
this.deleteTags = deleteTags;
this.commitsCollection = commitsCollection;
this.commitApi = commitApi;
}
@Override
@@ -393,6 +409,15 @@ public class ProjectApiImpl implements ProjectApi {
}
}
@Override
public CommitApi commit(String commit) throws RestApiException {
try {
return commitApi.create(commitsCollection.parse(checkExists(), IdString.fromDecoded(commit)));
} catch (IOException e) {
throw new RestApiException("Cannot parse commit", e);
}
}
private ProjectResource checkExists() throws ResourceNotFoundException {
if (project == null) {
throw new ResourceNotFoundException(name);

View File

@@ -16,6 +16,7 @@ package com.google.gerrit.server.change;
import com.google.common.base.Strings;
import com.google.gerrit.common.FooterConstants;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.restapi.MergeConflictException;
@@ -42,7 +43,6 @@ import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.git.validators.CommitValidators;
import com.google.gerrit.server.project.ChangeControl;
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.query.change.ChangeData;
@@ -59,9 +59,6 @@ import java.io.IOException;
import java.sql.Timestamp;
import java.util.List;
import java.util.TimeZone;
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;
@@ -114,23 +111,42 @@ public class CherryPickChange {
}
public Change.Id cherryPick(
Change change,
PatchSet patch,
final String message,
final String ref,
final RefControl refControl,
int parent)
throws NoSuchChangeException, OrmException, MissingObjectException,
IncorrectObjectTypeException, IOException, InvalidChangeOperationException,
IntegrationException, UpdateException, RestApiException {
Change change, PatchSet patch, String message, String ref, RefControl refControl, int parent)
throws OrmException, IOException, InvalidChangeOperationException, IntegrationException,
UpdateException, RestApiException {
return cherryPick(
change.getId(),
patch.getId(),
change.getDest(),
change.getTopic(),
change.getProject(),
ObjectId.fromString(patch.getRevision().get()),
message,
ref,
refControl,
parent);
}
if (Strings.isNullOrEmpty(ref)) {
public Change.Id cherryPick(
@Nullable Change.Id sourceChangeId,
@Nullable PatchSet.Id sourcePatchId,
@Nullable Branch.NameKey sourceBranch,
@Nullable String sourceChangeTopic,
Project.NameKey project,
ObjectId sourceCommit,
String message,
String targetRef,
RefControl targetRefControl,
int parent)
throws OrmException, IOException, InvalidChangeOperationException, IntegrationException,
UpdateException, RestApiException {
if (Strings.isNullOrEmpty(targetRef)) {
throw new InvalidChangeOperationException(
"Cherry Pick: Destination branch cannot be null or empty");
}
Project.NameKey project = change.getProject();
String destinationBranch = RefNames.shortName(ref);
String destinationBranch = RefNames.shortName(targetRef);
IdentifiedUser identifiedUser = user.get();
try (Repository git = gitManager.openRepository(project);
// This inserter and revwalk *must* be passed to any BatchUpdates
@@ -138,7 +154,7 @@ public class CherryPickChange {
// before patch sets are updated.
ObjectInserter oi = git.newObjectInserter();
CodeReviewRevWalk revWalk = CodeReviewCommit.newRevWalk(oi.newReader())) {
Ref destRef = git.getRefDatabase().exactRef(ref);
Ref destRef = git.getRefDatabase().exactRef(targetRef);
if (destRef == null) {
throw new InvalidChangeOperationException(
String.format("Branch %s does not exist.", destinationBranch));
@@ -146,8 +162,7 @@ public class CherryPickChange {
CodeReviewCommit mergeTip = revWalk.parseCommit(destRef.getObjectId());
CodeReviewCommit commitToCherryPick =
revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get()));
CodeReviewCommit commitToCherryPick = revWalk.parseCommit(sourceCommit);
if (parent <= 0 || parent > commitToCherryPick.getParentCount()) {
throw new InvalidChangeOperationException(
@@ -171,7 +186,7 @@ public class CherryPickChange {
CodeReviewCommit cherryPickCommit;
try {
ProjectState projectState = refControl.getProjectControl().getProjectState();
ProjectState projectState = targetRefControl.getProjectControl().getProjectState();
cherryPickCommit =
mergeUtilFactory
.create(projectState)
@@ -195,7 +210,7 @@ public class CherryPickChange {
changeKey = new Change.Key("I" + computedChangeId.name());
}
Branch.NameKey newDest = new Branch.NameKey(change.getProject(), destRef.getName());
Branch.NameKey newDest = new Branch.NameKey(project, destRef.getName());
List<ChangeData> destChanges =
queryProvider.get().setLimit(2).byBranchKey(newDest, changeKey);
if (destChanges.size() > 1) {
@@ -205,32 +220,37 @@ public class CherryPickChange {
+ " reside on the same branch. "
+ "Cannot create a new patch set.");
}
try (BatchUpdate bu =
batchUpdateFactory.create(
db.get(), change.getDest().getParentKey(), identifiedUser, now)) {
try (BatchUpdate bu = batchUpdateFactory.create(db.get(), project, identifiedUser, now)) {
bu.setRepository(git, revWalk, oi);
Change.Id result;
if (destChanges.size() == 1) {
// The change key exists on the destination branch. The cherry pick
// will be added as a new patch set.
ChangeControl destCtl =
refControl.getProjectControl().controlFor(destChanges.get(0).notes());
targetRefControl.getProjectControl().controlFor(destChanges.get(0).notes());
result = insertPatchSet(bu, git, destCtl, cherryPickCommit);
} else {
// Change key not found on destination branch. We can create a new
// change.
String newTopic = null;
if (!Strings.isNullOrEmpty(change.getTopic())) {
newTopic = change.getTopic() + "-" + newDest.getShortName();
if (!Strings.isNullOrEmpty(sourceChangeTopic)) {
newTopic = sourceChangeTopic + "-" + newDest.getShortName();
}
result =
createNewChange(
bu, cherryPickCommit, refControl.getRefName(), newTopic, change.getDest());
bu,
cherryPickCommit,
targetRefControl.getRefName(),
newTopic,
sourceBranch,
sourceCommit);
bu.addOp(
change.getId(),
new AddMessageToSourceChangeOp(
changeMessagesUtil, patch.getId(), destinationBranch, cherryPickCommit));
if (sourceChangeId != null && sourcePatchId != null) {
bu.addOp(
sourceChangeId,
new AddMessageToSourceChangeOp(
changeMessagesUtil, sourcePatchId, destinationBranch, cherryPickCommit));
}
}
bu.execute();
return result;
@@ -238,8 +258,6 @@ public class CherryPickChange {
} catch (MergeIdenticalTreeException | MergeConflictException e) {
throw new IntegrationException("Cherry pick failed: " + e.getMessage());
}
} catch (RepositoryNotFoundException e) {
throw new NoSuchChangeException(change.getId(), e);
}
}
@@ -266,7 +284,8 @@ public class CherryPickChange {
CodeReviewCommit cherryPickCommit,
String refName,
String topic,
Branch.NameKey sourceBranch)
Branch.NameKey sourceBranch,
ObjectId sourceCommit)
throws OrmException {
Change.Id changeId = new Change.Id(seq.nextChangeId());
ChangeInserter ins =
@@ -275,7 +294,7 @@ public class CherryPickChange {
.setValidatePolicy(CommitValidators.Policy.GERRIT)
.setTopic(topic);
ins.setMessage(messageForDestinationChange(ins.getPatchSetId(), sourceBranch));
ins.setMessage(messageForDestinationChange(ins.getPatchSetId(), sourceBranch, sourceCommit));
bu.insertChange(ins);
return changeId;
}
@@ -317,12 +336,16 @@ public class CherryPickChange {
}
}
private String messageForDestinationChange(PatchSet.Id patchSetId, Branch.NameKey sourceBranch) {
return new StringBuilder("Patch Set ")
.append(patchSetId.get())
.append(": Cherry Picked from branch ")
.append(sourceBranch.getShortName())
.append(".")
.toString();
private String messageForDestinationChange(
PatchSet.Id patchSetId, Branch.NameKey sourceBranch, ObjectId sourceCommit) {
StringBuilder stringBuilder = new StringBuilder("Patch Set ").append(patchSetId.get());
if (sourceBranch != null) {
stringBuilder.append(": Cherry Picked from branch ").append(sourceBranch.getShortName());
} else {
stringBuilder.append(": Cherry Picked from commit ").append(sourceCommit.getName());
}
return stringBuilder.append(".").toString();
}
}

View File

@@ -0,0 +1,98 @@
// Copyright (C) 2017 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.common.base.Strings;
import com.google.gerrit.common.data.Capable;
import com.google.gerrit.extensions.api.changes.CherryPickInput;
import com.google.gerrit.extensions.common.ChangeInfo;
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.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.git.IntegrationException;
import com.google.gerrit.server.project.CommitResource;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.RefControl;
import com.google.gerrit.server.update.UpdateException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
import org.eclipse.jgit.revwalk.RevCommit;
@Singleton
public class CherryPickCommit implements RestModifyView<CommitResource, CherryPickInput> {
private final CherryPickChange cherryPickChange;
private final ChangeJson.Factory json;
@Inject
CherryPickCommit(CherryPickChange cherryPickChange, ChangeJson.Factory json) {
this.cherryPickChange = cherryPickChange;
this.json = json;
}
@Override
public ChangeInfo apply(CommitResource rsrc, CherryPickInput input)
throws OrmException, IOException, UpdateException, RestApiException {
String message = Strings.nullToEmpty(input.message).trim();
String destination = Strings.nullToEmpty(input.destination).trim();
int parent = input.parent == null ? 1 : input.parent;
if (destination.isEmpty()) {
throw new BadRequestException("destination must be non-empty");
}
ProjectControl projectControl = rsrc.getProject();
Capable capable = projectControl.canPushToAtLeastOneRef();
if (capable != Capable.OK) {
throw new AuthException(capable.getMessage());
}
RevCommit commit = rsrc.getCommit();
String refName = RefNames.fullName(destination);
RefControl refControl = projectControl.controlForRef(refName);
if (!refControl.canUpload()) {
throw new AuthException("Not allowed to cherry pick " + commit + " to " + destination);
}
Project.NameKey project = projectControl.getProject().getNameKey();
try {
Change.Id cherryPickedChangeId =
cherryPickChange.cherryPick(
null,
null,
null,
null,
project,
commit,
message.isEmpty() ? commit.getFullMessage() : message,
refName,
refControl,
parent);
return json.noOptions().format(project, cherryPickedChangeId);
} catch (InvalidChangeOperationException e) {
throw new BadRequestException(e.getMessage());
} catch (IntegrationException e) {
throw new ResourceConflictException(e.getMessage());
}
}
}

View File

@@ -24,6 +24,7 @@ import static com.google.gerrit.server.project.TagResource.TAG_KIND;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.RestApiModule;
import com.google.gerrit.server.change.CherryPickCommit;
public class Module extends RestApiModule {
@Override
@@ -96,6 +97,8 @@ public class Module extends RestApiModule {
get(PROJECT_KIND, "config").to(GetConfig.class);
put(PROJECT_KIND, "config").to(PutConfig.class);
post(COMMIT_KIND, "cherrypick").to(CherryPickCommit.class);
factory(DeleteRef.Factory.class);
}
}