diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt index ae02475a1f..5bcb5ede85 100644 --- a/Documentation/rest-api-changes.txt +++ b/Documentation/rest-api-changes.txt @@ -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. diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java index 3dd1ff3fb9..bb2e141e10 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java @@ -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 getReviewers( Collection r) { return Iterables.transform(r, a -> new Account.Id(a._accountId)); diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java index d9cd562d04..8e36a77653 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java @@ -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 submittedTogether() throws RestApiException; SubmittedTogetherInfo submittedTogether( EnumSet options) throws RestApiException; @@ -412,5 +416,11 @@ public interface ChangeApi { EnumSet b) throws RestApiException { throw new NotImplementedException(); } + + @Override + public ChangeInfo createMergePatchSet(MergePatchSetInput in) + throws RestApiException { + throw new NotImplementedException(); + } } } diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergePatchSetInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergePatchSetInput.java new file mode 100644 index 0000000000..263b6c4023 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergePatchSetInput.java @@ -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; +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java index f7cb8f4ced..7532a11db2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java @@ -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; private final PublishDraftPatchSet.CurrentRevision publishDraftChange; @@ -122,6 +126,7 @@ class ChangeApiImpl implements ChangeApi { Abandon abandon, Revert revert, Restore restore, + CreateMergePatchSet updateByMerge, Provider 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 submittedTogether() throws RestApiException { SubmittedTogetherInfo info = submittedTogether( diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java new file mode 100644 index 0000000000..f117179ebe --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java @@ -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 { + + private final Provider db; + private final GitRepositoryManager gitManager; + private final TimeZone serverTimeZone; + private final Provider 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 db, + GitRepositoryManager gitManager, + @GerritPersonIdent PersonIdent myIdent, + Provider 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 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); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java index a52920a08f..bb760845d1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java @@ -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);