Add new REST api for creating a merge patch set for change

Previously, we have a new feature that allow creating a new merge change
(C2PS1) by merge a change(C1PS1) into a branch (release). If the
original change (C1) is updated (C1PS2), the merged change also need to
update (C2PS2) with a new merge with the updated change.

master: C1PS1 -> C1PS2
            \       \
release: C2PS1 -> C2PS2

Without this new merge patch set API, user have to abandon the old merge
change(C2PS1) and then create a new merge change (C3PS1).

REST api at `POST /changes/{change-id}/merge` and take a
MergePatchSetInput.

Change-Id: Ia3ba1bc5ff30920d1f8270f4fe28ccd7da7ed2a9
This commit is contained in:
Zhen Chen
2016-09-23 12:59:48 -07:00
parent 886c95b98c
commit b1e07e52b2
7 changed files with 418 additions and 0 deletions

View File

@@ -517,6 +517,61 @@ describes the change.
}
----
[[create-merge-patch-set-for-change]]
=== Create Merge Patch Set For Change
--
'POST /changes/link:#change-id[\{change-id\}]/merge'
--
Update an existing change by using a
link:#merge-patch-set-input[MergePatchSetInput] entity.
Gerrit will create a merge commit based on the information of
MergePatchSetInput and add a new patch set to the change corresponding
to the new merge commit.
.Request
----
POST /changes/test~master~Ic5466d107c5294414710935a8ef3b0180fb848dc/merge HTTP/1.0
Content-Type: application/json; charset=UTF-8
{
"source": "refs/12/1234/1"
}
----
As response a link:#change-info[ChangeInfo] entity with current revision is
returned that describes the resulting change.
.Response
----
HTTP/1.1 200 OK
Content-Disposition: attachment
Content-Type: application/json; charset=UTF-8
)]}'
{
"id": "test~master~Ic5466d107c5294414710935a8ef3b0180fb848dc",
"project": "test",
"branch": "master",
"hashtags": [],
"change_id": "Ic5466d107c5294414710935a8ef3b0180fb848dc",
"subject": "Merge dev_branch into master",
"status": "NEW",
"created": "2016-09-23 18:08:53.238000000",
"updated": "2016-09-23 18:09:25.934000000",
"submit_type": "MERGE_IF_NECESSARY",
"mergeable": true,
"insertions": 5,
"deletions": 0,
"_number": 72,
"owner": {
"_account_id": 1000000
},
"current_revision": "27cc4558b5a3d3387dd11ee2df7a117e7e581822"
}
----
[[get-change-detail]]
=== Get Change Detail
--
@@ -5363,6 +5418,25 @@ The strategy of the merge, can be `recursive`, `resolve`,
`simple-two-way-in-core`, `ours` or `theirs`, default will use project settings.
|============================
[[merge-patch-set-input]]
=== MergePatchSetInput
The `MergePatchSetInput` entity contains information about updating a new
change by creating a new merge commit.
[options="header",cols="1,^1,5"]
|==================================
|Field Name ||Description
|`subject` |optional|
The new subject for the change, if not specified, will reuse the current patch
set's subject
|`inheritParent` |optional, default to `false`|
Use the current patch set's first parent as the merge tip when set to `true`.
Otherwise, use the current branch tip of the destination branch.
|`merge` ||
The detail of the source commit for merge as a link:#merge-input[MergeInput]
entity.
|==================================
[[move-input]]
=== MoveInput
The `MoveInput` entity contains information for moving a change to a new branch.

View File

@@ -61,9 +61,11 @@ import com.google.gerrit.extensions.common.ApprovalInfo;
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.MergePatchSetInput;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.common.GitPerson;
import com.google.gerrit.extensions.common.LabelInfo;
import com.google.gerrit.extensions.common.MergeInput;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -71,6 +73,7 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.LabelId;
import com.google.gerrit.reviewdb.client.PatchSet;
@@ -1930,6 +1933,86 @@ public class ChangeIT extends AbstractDaemonTest {
+ r1.getChange().getId().id + ".");
}
@Test
public void testCreateMergePatchSet() throws Exception {
PushOneCommit.Result start = pushTo("refs/heads/master");
start.assertOkStatus();
// create a change for master
PushOneCommit.Result r = createChange();
r.assertOkStatus();
String changeId = r.getChangeId();
testRepo.reset(start.getCommit());
PushOneCommit.Result currentMaster = pushTo("refs/heads/master");
currentMaster.assertOkStatus();
String parent = currentMaster.getCommit().getName();
// push a commit into dev branch
createBranch(new Branch.NameKey(project, "dev"));
PushOneCommit.Result changeA = pushFactory
.create(db, user.getIdent(), testRepo, "change A", "A.txt", "A content")
.to("refs/heads/dev");
changeA.assertOkStatus();
MergeInput mergeInput = new MergeInput();
mergeInput.source = "dev";
MergePatchSetInput in = new MergePatchSetInput();
in.merge = mergeInput;
in.subject = "update change by merge ps2";
gApi.changes().id(changeId).createMergePatchSet(in);
ChangeInfo changeInfo = gApi.changes().id(changeId)
.get(EnumSet.of(ListChangesOption.ALL_REVISIONS,
ListChangesOption.CURRENT_COMMIT,
ListChangesOption.CURRENT_REVISION));
assertThat(changeInfo.revisions.size()).isEqualTo(2);
assertThat(changeInfo.subject).isEqualTo(in.subject);
assertThat(
changeInfo.revisions.get(changeInfo.currentRevision).commit.parents
.get(0).commit).isEqualTo(parent);
}
@Test
public void testCreateMergePatchSetInheritParent() throws Exception {
PushOneCommit.Result start = pushTo("refs/heads/master");
start.assertOkStatus();
// create a change for master
PushOneCommit.Result r = createChange();
r.assertOkStatus();
String changeId = r.getChangeId();
String parent = r.getCommit().getParent(0).getName();
// advance master branch
testRepo.reset(start.getCommit());
PushOneCommit.Result currentMaster = pushTo("refs/heads/master");
currentMaster.assertOkStatus();
// push a commit into dev branch
createBranch(new Branch.NameKey(project, "dev"));
PushOneCommit.Result changeA = pushFactory
.create(db, user.getIdent(), testRepo, "change A", "A.txt", "A content")
.to("refs/heads/dev");
changeA.assertOkStatus();
MergeInput mergeInput = new MergeInput();
mergeInput.source = "dev";
MergePatchSetInput in = new MergePatchSetInput();
in.merge = mergeInput;
in.subject = "update change by merge ps2 inherit parent of ps1";
in.inheritParent = true;
gApi.changes().id(changeId).createMergePatchSet(in);
ChangeInfo changeInfo = gApi.changes().id(changeId)
.get(EnumSet.of(ListChangesOption.ALL_REVISIONS,
ListChangesOption.CURRENT_COMMIT,
ListChangesOption.CURRENT_REVISION));
assertThat(changeInfo.revisions.size()).isEqualTo(2);
assertThat(changeInfo.subject).isEqualTo(in.subject);
assertThat(
changeInfo.revisions.get(changeInfo.currentRevision).commit.parents
.get(0).commit).isEqualTo(parent);
assertThat(
changeInfo.revisions.get(changeInfo.currentRevision).commit.parents
.get(0).commit).isNotEqualTo(currentMaster.getCommit().getName());
}
private static Iterable<Account.Id> getReviewers(
Collection<AccountInfo> r) {
return Iterables.transform(r, a -> new Account.Id(a._accountId));

View File

@@ -17,6 +17,7 @@ package com.google.gerrit.extensions.api.changes;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.MergePatchSetInput;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.EditInfo;
import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
@@ -95,6 +96,9 @@ public interface ChangeApi {
*/
ChangeApi revert(RevertInput in) throws RestApiException;
/** Create a merge patch set for the change. */
ChangeInfo createMergePatchSet(MergePatchSetInput in) throws RestApiException;
List<ChangeInfo> submittedTogether() throws RestApiException;
SubmittedTogetherInfo submittedTogether(
EnumSet<SubmittedTogetherOption> options) throws RestApiException;
@@ -412,5 +416,11 @@ public interface ChangeApi {
EnumSet<SubmittedTogetherOption> b) throws RestApiException {
throw new NotImplementedException();
}
@Override
public ChangeInfo createMergePatchSet(MergePatchSetInput in)
throws RestApiException {
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (C) 2016 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.common;
public class MergePatchSetInput {
public String subject;
public boolean inheritParent;
public MergeInput merge;
}

View File

@@ -31,6 +31,7 @@ import com.google.gerrit.extensions.api.changes.SubmittedTogetherOption;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.MergePatchSetInput;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.EditInfo;
import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
@@ -63,7 +64,9 @@ import com.google.gerrit.server.change.Reviewers;
import com.google.gerrit.server.change.Revisions;
import com.google.gerrit.server.change.SubmittedTogether;
import com.google.gerrit.server.change.SuggestChangeReviewers;
import com.google.gerrit.server.change.CreateMergePatchSet;
import com.google.gerrit.server.git.UpdateException;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -91,6 +94,7 @@ class ChangeApiImpl implements ChangeApi {
private final Abandon abandon;
private final Revert revert;
private final Restore restore;
private final CreateMergePatchSet updateByMerge;
private final Provider<SubmittedTogether> submittedTogether;
private final PublishDraftPatchSet.CurrentRevision
publishDraftChange;
@@ -122,6 +126,7 @@ class ChangeApiImpl implements ChangeApi {
Abandon abandon,
Revert revert,
Restore restore,
CreateMergePatchSet updateByMerge,
Provider<SubmittedTogether> submittedTogether,
PublishDraftPatchSet.CurrentRevision publishDraftChange,
DeleteDraftChange deleteDraftChange,
@@ -151,6 +156,7 @@ class ChangeApiImpl implements ChangeApi {
this.suggestReviewers = suggestReviewers;
this.abandon = abandon;
this.restore = restore;
this.updateByMerge = updateByMerge;
this.submittedTogether = submittedTogether;
this.publishDraftChange = publishDraftChange;
this.deleteDraftChange = deleteDraftChange;
@@ -267,6 +273,17 @@ class ChangeApiImpl implements ChangeApi {
}
}
@Override
public ChangeInfo createMergePatchSet(MergePatchSetInput in)
throws RestApiException {
try {
return updateByMerge.apply(change, in).value();
} catch (IOException | UpdateException | InvalidChangeOperationException
| NoSuchChangeException | OrmException e) {
throw new RestApiException("Cannot update change by merge", e);
}
}
@Override
public List<ChangeInfo> submittedTogether() throws RestApiException {
SubmittedTogetherInfo info = submittedTogether(

View File

@@ -0,0 +1,212 @@
// Copyright (C) 2016 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.MoreObjects;
import com.google.common.base.Strings;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.MergePatchSetInput;
import com.google.gerrit.extensions.common.MergeInput;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MergeConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.git.BatchUpdate;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeIdenticalTreeException;
import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.git.UpdateException;
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.ProjectControl;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
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.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.ChangeIdUtil;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.EnumSet;
import java.util.TimeZone;
@Singleton
public class CreateMergePatchSet implements
RestModifyView<ChangeResource, MergePatchSetInput> {
private final Provider<ReviewDb> db;
private final GitRepositoryManager gitManager;
private final TimeZone serverTimeZone;
private final Provider<CurrentUser> user;
private final ChangeJson.Factory jsonFactory;
private final PatchSetUtil psUtil;
private final MergeUtil.Factory mergeUtilFactory;
private final BatchUpdate.Factory batchUpdateFactory;
private final PatchSetInserter.Factory patchSetInserterFactory;
@Inject
CreateMergePatchSet(Provider<ReviewDb> db,
GitRepositoryManager gitManager,
@GerritPersonIdent PersonIdent myIdent,
Provider<CurrentUser> user,
ChangeJson.Factory json,
PatchSetUtil psUtil,
MergeUtil.Factory mergeUtilFactory,
BatchUpdate.Factory batchUpdateFactory,
PatchSetInserter.Factory patchSetInserterFactory) {
this.db = db;
this.gitManager = gitManager;
this.serverTimeZone = myIdent.getTimeZone();
this.user = user;
this.jsonFactory = json;
this.psUtil = psUtil;
this.mergeUtilFactory = mergeUtilFactory;
this.batchUpdateFactory = batchUpdateFactory;
this.patchSetInserterFactory = patchSetInserterFactory;
}
@Override
public Response<ChangeInfo> apply(ChangeResource req, MergePatchSetInput in)
throws NoSuchChangeException, OrmException, IOException,
InvalidChangeOperationException, RestApiException, UpdateException {
if (in.merge == null) {
throw new BadRequestException("merge field is required");
}
MergeInput merge = in.merge;
if (Strings.isNullOrEmpty(merge.source)) {
throw new BadRequestException("merge.source must be non-empty");
}
ChangeControl ctl = req.getControl();
if (!ctl.isVisible(db.get())) {
throw new InvalidChangeOperationException(
"Base change not found: " + req.getId());
}
PatchSet ps = psUtil.current(db.get(), ctl.getNotes());
if (!ctl.canAddPatchSet(db.get())) {
throw new AuthException("cannot add patch set");
}
ProjectControl projectControl = ctl.getProjectControl();
Change change = ctl.getChange();
Project.NameKey project = change.getProject();
Branch.NameKey dest = change.getDest();
try (Repository git = gitManager.openRepository(project);
ObjectInserter oi = git.newObjectInserter();
RevWalk rw = new RevWalk(oi.newReader())) {
RevCommit sourceCommit =
MergeUtil.resolveCommit(git, rw, merge.source);
if (!projectControl.canReadCommit(db.get(), git, sourceCommit)) {
throw new ResourceNotFoundException(
"cannot find source commit: " + merge.source + " to merge.");
}
RevCommit currentPsCommit =
rw.parseCommit(ObjectId.fromString(ps.getRevision().get()));
Timestamp now = TimeUtil.nowTs();
IdentifiedUser me = user.get().asIdentifiedUser();
PersonIdent author = me.newCommitterIdent(now, serverTimeZone);
RevCommit newCommit =
createMergeCommit(in, projectControl, dest, git, oi, rw,
currentPsCommit, sourceCommit, author,
ObjectId.fromString(change.getKey().get().substring(1)));
PatchSet.Id nextPsId = ChangeUtil.nextPatchSetId(ps.getId());
PatchSetInserter psInserter =
patchSetInserterFactory.create(ctl, nextPsId, newCommit);
try (BatchUpdate bu = batchUpdateFactory
.create(db.get(), project, me, now)) {
bu.setRepository(git, rw, oi);
bu.addOp(ctl.getId(), psInserter
.setMessage("Uploaded patch set " + nextPsId.get() + ".")
.setDraft(ps.isDraft())
.setNotify(NotifyHandling.NONE));
bu.execute();
}
ChangeJson json =
jsonFactory.create(EnumSet.of(ListChangesOption.CURRENT_REVISION));
return Response.ok(json.format(psInserter.getChange()));
}
}
private RevCommit createMergeCommit(MergePatchSetInput in,
ProjectControl projectControl, Branch.NameKey dest, Repository git,
ObjectInserter oi, RevWalk rw, RevCommit currentPsCommit,
RevCommit sourceCommit, PersonIdent author, ObjectId changeId)
throws ResourceNotFoundException, MergeIdenticalTreeException,
MergeConflictException, IOException {
ObjectId parentCommit;
if (in.inheritParent) {
// inherit first parent from previous patch set
parentCommit = currentPsCommit.getParent(0);
} else {
// get the current branch tip of destination branch
Ref destRef = git.getRefDatabase().exactRef(dest.get());
if (destRef != null) {
parentCommit = destRef.getObjectId();
} else {
throw new ResourceNotFoundException("cannot find destination branch");
}
}
RevCommit mergeTip = rw.parseCommit(parentCommit);
String commitMsg;
if (Strings.emptyToNull(in.subject) != null) {
commitMsg = ChangeIdUtil.insertId(in.subject, changeId);
} else {
// reuse previous patch set commit message
commitMsg = currentPsCommit.getFullMessage();
}
String mergeStrategy = MoreObjects.firstNonNull(
Strings.emptyToNull(in.merge.strategy),
mergeUtilFactory.create(projectControl.getProjectState())
.mergeStrategyName());
return MergeUtil.createMergeCommit(git, oi, mergeTip, sourceCommit,
mergeStrategy, author, commitMsg, rw);
}
}

View File

@@ -53,6 +53,7 @@ public class Module extends RestApiModule {
DynamicMap.mapOf(binder(), VOTE_KIND);
get(CHANGE_KIND).to(GetChange.class);
post(CHANGE_KIND, "merge").to(CreateMergePatchSet.class);
get(CHANGE_KIND, "detail").to(GetDetail.class);
get(CHANGE_KIND, "topic").to(GetTopic.class);
get(CHANGE_KIND, "in").to(IncludedIn.class);