diff options
author | Patrick Hiesel <hiesel@google.com> | 2020-10-06 15:56:30 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2020-10-06 15:56:30 +0000 |
commit | e1b692cca5a05964575205c089f69f7213cf11fa (patch) | |
tree | 5a090a15c099e8a70c68b60f49fef9469f23358c | |
parent | 980a8f0d1b1a0fcce145c0438bf90ad325f4f229 (diff) | |
parent | e8fc7974e8dd15052034f9e304ab7e4d76b09d53 (diff) |
Merge "CreateMergePatchSet: Implement 'author' to tweak Git commit author"
5 files changed, 670 insertions, 407 deletions
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt index 8bd02a80dd..6b7348248e 100644 --- a/Documentation/rest-api-changes.txt +++ b/Documentation/rest-api-changes.txt @@ -7325,6 +7325,13 @@ it's set. Otherwise, the current branch tip of the destination branch will be us |`merge` || The detail of the source commit for merge as a link:#merge-input[MergeInput] entity. +|`author` |optional| +An link:rest-api-accounts.html#account-input[AccountInput] entity +that will set the author of the commit to create. The author must be +specified as name/email combination. +The caller needs "Forge Author" permission when using this field. +This field does not affect the owner of the change, which will +continue to use the identity of the caller. |================================== [[move-input]] diff --git a/java/com/google/gerrit/extensions/common/MergePatchSetInput.java b/java/com/google/gerrit/extensions/common/MergePatchSetInput.java index 53f5e078c5..734d7e9840 100644 --- a/java/com/google/gerrit/extensions/common/MergePatchSetInput.java +++ b/java/com/google/gerrit/extensions/common/MergePatchSetInput.java @@ -14,9 +14,12 @@ package com.google.gerrit.extensions.common; +import com.google.gerrit.extensions.api.accounts.AccountInput; + public class MergePatchSetInput { public String subject; public boolean inheritParent; public String baseChange; public MergeInput merge; + public AccountInput author; } diff --git a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java index 8ac214001f..af4bf694fc 100644 --- a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java +++ b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java @@ -55,6 +55,7 @@ import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.permissions.ChangePermission; import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.permissions.RefPermission; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectState; import com.google.gerrit.server.restapi.project.CommitsCollection; @@ -128,6 +129,13 @@ public class CreateMergePatchSet implements RestModifyView<ChangeResource, Merge psUtil.checkPatchSetNotLocked(rsrc.getNotes()); rsrc.permissions().check(ChangePermission.ADD_PATCH_SET); + if (in.author != null) { + permissionBackend + .currentUser() + .project(rsrc.getProject()) + .ref(rsrc.getChange().getDest().branch()) + .check(RefPermission.FORGE_AUTHOR); + } ProjectState projectState = projectCache.get(rsrc.getProject()).orElseThrow(illegalState(rsrc.getProject())); @@ -137,6 +145,10 @@ public class CreateMergePatchSet implements RestModifyView<ChangeResource, Merge if (merge == null || Strings.isNullOrEmpty(merge.source)) { throw new BadRequestException("merge.source must be non-empty"); } + if (in.author != null + && (Strings.isNullOrEmpty(in.author.email) || Strings.isNullOrEmpty(in.author.name))) { + throw new BadRequestException("Author must specify name and email"); + } in.baseChange = Strings.nullToEmpty(in.baseChange).trim(); PatchSet ps = psUtil.current(rsrc.getNotes()); @@ -166,7 +178,10 @@ public class CreateMergePatchSet implements RestModifyView<ChangeResource, Merge Timestamp now = TimeUtil.nowTs(); IdentifiedUser me = user.get().asIdentifiedUser(); - PersonIdent author = me.newCommitterIdent(now, serverTimeZone); + PersonIdent author = + in.author == null + ? me.newCommitterIdent(now, serverTimeZone) + : new PersonIdent(in.author.name, in.author.email, now, serverTimeZone); CodeReviewCommit newCommit = createMergeCommit( in, diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java index d4affb7571..410cb41d71 100644 --- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java +++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java @@ -47,7 +47,6 @@ import static com.google.gerrit.extensions.client.ListChangesOption.TRACKING_IDS import static com.google.gerrit.extensions.client.ReviewerState.CC; import static com.google.gerrit.extensions.client.ReviewerState.REMOVED; import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER; -import static com.google.gerrit.git.ObjectIds.abbreviateName; import static com.google.gerrit.server.StarredChangesUtil.DEFAULT_LABEL; import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS; import static com.google.gerrit.server.group.SystemGroupBackend.CHANGE_OWNER; @@ -142,14 +141,11 @@ import com.google.gerrit.extensions.common.CommentInfo; 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.MergePatchSetInput; import com.google.gerrit.extensions.common.RevisionInfo; import com.google.gerrit.extensions.common.TrackingIdInfo; import com.google.gerrit.extensions.events.WorkInProgressStateChangedListener; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.BadRequestException; -import com.google.gerrit.extensions.restapi.BinaryResult; import com.google.gerrit.extensions.restapi.MethodNotAllowedException; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; @@ -183,7 +179,6 @@ import com.google.gerrit.testing.FakeEmailSender.Message; import com.google.inject.AbstractModule; import com.google.inject.Inject; import com.google.inject.name.Named; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.sql.Timestamp; import java.util.ArrayList; @@ -3189,407 +3184,6 @@ public class ChangeIT extends AbstractDaemonTest { } @Test - public void createMergePatchSet() throws Exception { - RevCommit initialHead = projectOperations.project(project).getHead("master"); - createBranch("dev"); - - // create a change for master - String changeId = createChange().getChangeId(); - - testRepo.reset(initialHead); - PushOneCommit.Result currentMaster = pushTo("refs/heads/master"); - currentMaster.assertOkStatus(); - String parent = currentMaster.getCommit().getName(); - - // push a commit into dev branch - testRepo.reset(initialHead); - PushOneCommit.Result changeA = - pushFactory - .create(user.newIdent(), 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; - String subject = "update change by merge ps2"; - in.subject = subject; - - TestWorkInProgressStateChangedListener wipStateChangedListener = - new TestWorkInProgressStateChangedListener(); - try (Registration registration = - extensionRegistry.newRegistration().add(wipStateChangedListener)) { - ChangeInfo changeInfo = gApi.changes().id(changeId).createMergePatchSet(in); - assertThat(changeInfo.subject).isEqualTo(in.subject); - assertThat(changeInfo.containsGitConflicts).isNull(); - assertThat(changeInfo.workInProgress).isNull(); - } - assertThat(wipStateChangedListener.invoked).isFalse(); - - // To get the revisions, we must retrieve the change with more change options. - ChangeInfo changeInfo = - gApi.changes().id(changeId).get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION); - assertThat(changeInfo.revisions).hasSize(2); - assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit) - .isEqualTo(parent); - - // Verify the message that has been posted on the change. - List<ChangeMessageInfo> messages = gApi.changes().id(changeId).messages(); - assertThat(messages).hasSize(2); - assertThat(Iterables.getLast(messages).message).isEqualTo("Uploaded patch set 2."); - - assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.message) - .contains(subject); - } - - @Test - public void createMergePatchSet_SubjectCarriesOverByDefault() throws Exception { - RevCommit initialHead = projectOperations.project(project).getHead("master"); - createBranch("dev"); - - // create a change for master - PushOneCommit.Result result = createChange(); - String changeId = result.getChangeId(); - String subject = result.getChange().change().getSubject(); - - // push a commit into dev branch - testRepo.reset(initialHead); - PushOneCommit.Result pushResult = - pushFactory.create(user.newIdent(), testRepo).to("refs/heads/dev"); - pushResult.assertOkStatus(); - MergeInput mergeInput = new MergeInput(); - mergeInput.source = "dev"; - MergePatchSetInput in = new MergePatchSetInput(); - in.merge = mergeInput; - in.subject = null; - - // Ensure subject carries over - gApi.changes().id(changeId).createMergePatchSet(in); - ChangeInfo changeInfo = gApi.changes().id(changeId).get(); - assertThat(changeInfo.subject).isEqualTo(subject); - } - - @Test - public void createMergePatchSet_Conflict() throws Exception { - RevCommit initialHead = projectOperations.project(project).getHead("master"); - createBranch("dev"); - - // create a change for master - String changeId = createChange().getChangeId(); - - String fileName = "shared.txt"; - testRepo.reset(initialHead); - PushOneCommit.Result currentMaster = - pushFactory - .create(admin.newIdent(), testRepo, "change 1", fileName, "content 1") - .to("refs/heads/master"); - currentMaster.assertOkStatus(); - - // push a commit into dev branch - testRepo.reset(initialHead); - PushOneCommit.Result changeA = - pushFactory - .create(user.newIdent(), testRepo, "change 2", fileName, "content 2") - .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"; - ResourceConflictException thrown = - assertThrows( - ResourceConflictException.class, - () -> gApi.changes().id(changeId).createMergePatchSet(in)); - assertThat(thrown).hasMessageThat().isEqualTo("merge conflict(s):\n" + fileName); - } - - @Test - public void createMergePatchSet_ConflictAllowed() throws Exception { - RevCommit initialHead = projectOperations.project(project).getHead("master"); - createBranch("dev"); - - // create a change for master - String changeId = createChange().getChangeId(); - - String fileName = "shared.txt"; - String sourceSubject = "source change"; - String sourceContent = "source content"; - String targetSubject = "target change"; - String targetContent = "target content"; - testRepo.reset(initialHead); - PushOneCommit.Result currentMaster = - pushFactory - .create(admin.newIdent(), testRepo, targetSubject, fileName, targetContent) - .to("refs/heads/master"); - currentMaster.assertOkStatus(); - String parent = currentMaster.getCommit().getName(); - - // push a commit into dev branch - testRepo.reset(initialHead); - PushOneCommit.Result changeA = - pushFactory - .create(user.newIdent(), testRepo, sourceSubject, fileName, sourceContent) - .to("refs/heads/dev"); - changeA.assertOkStatus(); - MergeInput mergeInput = new MergeInput(); - mergeInput.source = "dev"; - mergeInput.allowConflicts = true; - MergePatchSetInput in = new MergePatchSetInput(); - in.merge = mergeInput; - in.subject = "update change by merge ps2"; - - TestWorkInProgressStateChangedListener wipStateChangedListener = - new TestWorkInProgressStateChangedListener(); - try (Registration registration = - extensionRegistry.newRegistration().add(wipStateChangedListener)) { - ChangeInfo changeInfo = gApi.changes().id(changeId).createMergePatchSet(in); - assertThat(changeInfo.subject).isEqualTo(in.subject); - assertThat(changeInfo.containsGitConflicts).isTrue(); - assertThat(changeInfo.workInProgress).isTrue(); - } - assertThat(wipStateChangedListener.invoked).isTrue(); - assertThat(wipStateChangedListener.wip).isTrue(); - - // To get the revisions, we must retrieve the change with more change options. - ChangeInfo changeInfo = - gApi.changes().id(changeId).get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION); - assertThat(changeInfo.revisions).hasSize(2); - assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit) - .isEqualTo(parent); - - // Verify that the file content in the created patch set is correct. - // We expect that it has conflict markers to indicate the conflict. - BinaryResult bin = gApi.changes().id(changeId).current().file(fileName).content(); - ByteArrayOutputStream os = new ByteArrayOutputStream(); - bin.writeTo(os); - String fileContent = new String(os.toByteArray(), UTF_8); - String sourceSha1 = abbreviateName(changeA.getCommit(), 6); - String targetSha1 = abbreviateName(currentMaster.getCommit(), 6); - assertThat(fileContent) - .isEqualTo( - "<<<<<<< TARGET BRANCH (" - + targetSha1 - + " " - + targetSubject - + ")\n" - + targetContent - + "\n" - + "=======\n" - + sourceContent - + "\n" - + ">>>>>>> SOURCE BRANCH (" - + sourceSha1 - + " " - + sourceSubject - + ")\n"); - - // Verify the message that has been posted on the change. - List<ChangeMessageInfo> messages = gApi.changes().id(changeId).messages(); - assertThat(messages).hasSize(2); - assertThat(Iterables.getLast(messages).message) - .isEqualTo( - "Uploaded patch set 2.\n\n" - + "The following files contain Git conflicts:\n" - + "* " - + fileName - + "\n"); - } - - @Test - public void createMergePatchSet_ConflictAllowedNotSupportedByMergeStrategy() throws Exception { - RevCommit initialHead = projectOperations.project(project).getHead("master"); - createBranch("dev"); - - // create a change for master - String changeId = createChange().getChangeId(); - - String fileName = "shared.txt"; - String sourceSubject = "source change"; - String sourceContent = "source content"; - String targetSubject = "target change"; - String targetContent = "target content"; - testRepo.reset(initialHead); - PushOneCommit.Result currentMaster = - pushFactory - .create(admin.newIdent(), testRepo, targetSubject, fileName, targetContent) - .to("refs/heads/master"); - currentMaster.assertOkStatus(); - - // push a commit into dev branch - testRepo.reset(initialHead); - PushOneCommit.Result changeA = - pushFactory - .create(user.newIdent(), testRepo, sourceSubject, fileName, sourceContent) - .to("refs/heads/dev"); - changeA.assertOkStatus(); - MergeInput mergeInput = new MergeInput(); - mergeInput.source = "dev"; - mergeInput.allowConflicts = true; - mergeInput.strategy = "simple-two-way-in-core"; - MergePatchSetInput in = new MergePatchSetInput(); - in.merge = mergeInput; - in.subject = "update change by merge ps2"; - - BadRequestException ex = - assertThrows( - BadRequestException.class, () -> gApi.changes().id(changeId).createMergePatchSet(in)); - assertThat(ex) - .hasMessageThat() - .isEqualTo( - "merge with conflicts is not supported with merge strategy: " + mergeInput.strategy); - } - - @Test - public void createMergePatchSetInheritParent() throws Exception { - RevCommit initialHead = projectOperations.project(project).getHead("master"); - createBranch("dev"); - - // create a change for master - PushOneCommit.Result r = createChange(); - String changeId = r.getChangeId(); - String parent = r.getCommit().getParent(0).getName(); - - // advance master branch - testRepo.reset(initialHead); - PushOneCommit.Result currentMaster = pushTo("refs/heads/master"); - currentMaster.assertOkStatus(); - - // push a commit into dev branch - testRepo.reset(initialHead); - PushOneCommit.Result changeA = - pushFactory - .create(user.newIdent(), 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(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION); - - assertThat(changeInfo.revisions).hasSize(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()); - } - - @Test - public void createMergePatchSetCannotBaseOnInvisibleChange() throws Exception { - RevCommit initialHead = projectOperations.project(project).getHead("master"); - createBranch("foo"); - createBranch("bar"); - - // Create a merged commit on 'foo' branch. - merge(createChange("refs/for/foo")); - - // Create the base change on 'bar' branch. - testRepo.reset(initialHead); - String baseChange = createChange("refs/for/bar").getChangeId(); - gApi.changes().id(baseChange).setPrivate(true, "set private"); - - // Create the destination change on 'master' branch. - requestScopeOperations.setApiUser(user.id()); - testRepo.reset(initialHead); - String changeId = createChange().getChangeId(); - - UnprocessableEntityException thrown = - assertThrows( - UnprocessableEntityException.class, - () -> - gApi.changes() - .id(changeId) - .createMergePatchSet(createMergePatchSetInput(baseChange))); - assertThat(thrown).hasMessageThat().contains("Read not permitted for " + baseChange); - } - - @Test - public void createMergePatchSetBaseOnChange() throws Exception { - RevCommit initialHead = projectOperations.project(project).getHead("master"); - createBranch("foo"); - createBranch("bar"); - - // Create a merged commit on 'foo' branch. - merge(createChange("refs/for/foo")); - - // Create the base change on 'bar' branch. - testRepo.reset(initialHead); - PushOneCommit.Result result = createChange("refs/for/bar"); - String baseChange = result.getChangeId(); - String expectedParent = result.getCommit().getName(); - - // Create the destination change on 'master' branch. - testRepo.reset(initialHead); - String changeId = createChange().getChangeId(); - - gApi.changes().id(changeId).createMergePatchSet(createMergePatchSetInput(baseChange)); - - ChangeInfo changeInfo = - gApi.changes().id(changeId).get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION); - assertThat(changeInfo.revisions).hasSize(2); - assertThat(changeInfo.subject).isEqualTo("create ps2"); - assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit) - .isEqualTo(expectedParent); - } - - @Test - public void createMergePatchSetWithUnupportedMergeStrategy() throws Exception { - RevCommit initialHead = projectOperations.project(project).getHead("master"); - createBranch("dev"); - - // create a change for master - String changeId = createChange().getChangeId(); - - String fileName = "shared.txt"; - String sourceSubject = "source change"; - String sourceContent = "source content"; - String targetSubject = "target change"; - String targetContent = "target content"; - testRepo.reset(initialHead); - PushOneCommit.Result currentMaster = - pushFactory - .create(admin.newIdent(), testRepo, targetSubject, fileName, targetContent) - .to("refs/heads/master"); - currentMaster.assertOkStatus(); - - // push a commit into dev branch - testRepo.reset(initialHead); - PushOneCommit.Result changeA = - pushFactory - .create(user.newIdent(), testRepo, sourceSubject, fileName, sourceContent) - .to("refs/heads/dev"); - changeA.assertOkStatus(); - MergeInput mergeInput = new MergeInput(); - mergeInput.source = "dev"; - mergeInput.strategy = "unsupported-strategy"; - MergePatchSetInput in = new MergePatchSetInput(); - in.merge = mergeInput; - in.subject = "update change by merge ps2"; - - BadRequestException ex = - assertThrows( - BadRequestException.class, () -> gApi.changes().id(changeId).createMergePatchSet(in)); - assertThat(ex).hasMessageThat().isEqualTo("invalid merge strategy: " + mergeInput.strategy); - } - - private MergePatchSetInput createMergePatchSetInput(String baseChange) { - MergeInput mergeInput = new MergeInput(); - mergeInput.source = "foo"; - MergePatchSetInput in = new MergePatchSetInput(); - in.merge = mergeInput; - in.subject = "create ps2"; - in.inheritParent = false; - in.baseChange = baseChange; - return in; - } - - @Test public void checkLabelsForUnsubmittedChange() throws Exception { PushOneCommit.Result r = createChange(); ChangeInfo change = gApi.changes().id(r.getChangeId()).get(); diff --git a/javatests/com/google/gerrit/acceptance/api/change/CreateMergePatchSetIT.java b/javatests/com/google/gerrit/acceptance/api/change/CreateMergePatchSetIT.java new file mode 100644 index 0000000000..8b77d01349 --- /dev/null +++ b/javatests/com/google/gerrit/acceptance/api/change/CreateMergePatchSetIT.java @@ -0,0 +1,644 @@ +package com.google.gerrit.acceptance.api.change; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow; +import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block; +import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS; +import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT; +import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION; +import static com.google.gerrit.git.ObjectIds.abbreviateName; +import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; +import static com.google.gerrit.testing.GerritJUnit.assertThrows; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.collect.Iterables; +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.ExtensionRegistry; +import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; +import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate; +import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; +import com.google.gerrit.entities.BranchNameKey; +import com.google.gerrit.entities.Permission; +import com.google.gerrit.extensions.api.accounts.AccountInput; +import com.google.gerrit.extensions.common.ChangeInfo; +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.MergePatchSetInput; +import com.google.gerrit.extensions.events.WorkInProgressStateChangedListener; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.BinaryResult; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.UnprocessableEntityException; +import com.google.inject.Inject; +import java.io.ByteArrayOutputStream; +import java.util.List; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.Before; +import org.junit.Test; + +public class CreateMergePatchSetIT extends AbstractDaemonTest { + + @Inject private ProjectOperations projectOperations; + @Inject private RequestScopeOperations requestScopeOperations; + @Inject private ExtensionRegistry extensionRegistry; + + @Before + public void setUp() { + projectOperations + .project(project) + .forUpdate() + .add(allow(Permission.FORGE_AUTHOR).ref("refs/*").group(REGISTERED_USERS)) + .update(); + } + + @Test + public void createMergePatchSet() throws Exception { + RevCommit initialHead = projectOperations.project(project).getHead("master"); + createBranch(BranchNameKey.create(project, "dev")); + + // create a change for master + String changeId = createChange().getChangeId(); + + testRepo.reset(initialHead); + PushOneCommit.Result currentMaster = pushTo("refs/heads/master"); + currentMaster.assertOkStatus(); + String parent = currentMaster.getCommit().getName(); + + // push a commit into dev branch + testRepo.reset(initialHead); + PushOneCommit.Result changeA = + pushFactory + .create(user.newIdent(), 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; + String subject = "update change by merge ps2"; + in.subject = subject; + + TestWorkInProgressStateChangedListener wipStateChangedListener = + new TestWorkInProgressStateChangedListener(); + try (ExtensionRegistry.Registration registration = + extensionRegistry.newRegistration().add(wipStateChangedListener)) { + ChangeInfo changeInfo = gApi.changes().id(changeId).createMergePatchSet(in); + assertThat(changeInfo.subject).isEqualTo(in.subject); + assertThat(changeInfo.containsGitConflicts).isNull(); + assertThat(changeInfo.workInProgress).isNull(); + } + assertThat(wipStateChangedListener.invoked).isFalse(); + + // To get the revisions, we must retrieve the change with more change options. + ChangeInfo changeInfo = + gApi.changes().id(changeId).get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION); + assertThat(changeInfo.revisions).hasSize(2); + assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit) + .isEqualTo(parent); + + // Verify the message that has been posted on the change. + List<ChangeMessageInfo> messages = gApi.changes().id(changeId).messages(); + assertThat(messages).hasSize(2); + assertThat(Iterables.getLast(messages).message).isEqualTo("Uploaded patch set 2."); + + assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.message) + .contains(subject); + } + + @Test + public void createMergePatchSet_SubjectCarriesOverByDefault() throws Exception { + RevCommit initialHead = projectOperations.project(project).getHead("master"); + createBranch(BranchNameKey.create(project, "dev")); + + // create a change for master + PushOneCommit.Result result = createChange(); + String changeId = result.getChangeId(); + String subject = result.getChange().change().getSubject(); + + // push a commit into dev branch + testRepo.reset(initialHead); + PushOneCommit.Result pushResult = + pushFactory.create(user.newIdent(), testRepo).to("refs/heads/dev"); + pushResult.assertOkStatus(); + MergeInput mergeInput = new MergeInput(); + mergeInput.source = "dev"; + MergePatchSetInput in = new MergePatchSetInput(); + in.merge = mergeInput; + in.subject = null; + + // Ensure subject carries over + gApi.changes().id(changeId).createMergePatchSet(in); + ChangeInfo changeInfo = gApi.changes().id(changeId).get(); + assertThat(changeInfo.subject).isEqualTo(subject); + } + + @Test + public void createMergePatchSet_Conflict() throws Exception { + RevCommit initialHead = projectOperations.project(project).getHead("master"); + createBranch(BranchNameKey.create(project, "dev")); + + // create a change for master + String changeId = createChange().getChangeId(); + + String fileName = "shared.txt"; + testRepo.reset(initialHead); + PushOneCommit.Result currentMaster = + pushFactory + .create(admin.newIdent(), testRepo, "change 1", fileName, "content 1") + .to("refs/heads/master"); + currentMaster.assertOkStatus(); + + // push a commit into dev branch + testRepo.reset(initialHead); + PushOneCommit.Result changeA = + pushFactory + .create(user.newIdent(), testRepo, "change 2", fileName, "content 2") + .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"; + ResourceConflictException thrown = + assertThrows( + ResourceConflictException.class, + () -> gApi.changes().id(changeId).createMergePatchSet(in)); + assertThat(thrown).hasMessageThat().isEqualTo("merge conflict(s):\n" + fileName); + } + + @Test + public void createMergePatchSet_ConflictAllowed() throws Exception { + RevCommit initialHead = projectOperations.project(project).getHead("master"); + createBranch(BranchNameKey.create(project, "dev")); + + // create a change for master + String changeId = createChange().getChangeId(); + + String fileName = "shared.txt"; + String sourceSubject = "source change"; + String sourceContent = "source content"; + String targetSubject = "target change"; + String targetContent = "target content"; + testRepo.reset(initialHead); + PushOneCommit.Result currentMaster = + pushFactory + .create(admin.newIdent(), testRepo, targetSubject, fileName, targetContent) + .to("refs/heads/master"); + currentMaster.assertOkStatus(); + String parent = currentMaster.getCommit().getName(); + + // push a commit into dev branch + testRepo.reset(initialHead); + PushOneCommit.Result changeA = + pushFactory + .create(user.newIdent(), testRepo, sourceSubject, fileName, sourceContent) + .to("refs/heads/dev"); + changeA.assertOkStatus(); + MergeInput mergeInput = new MergeInput(); + mergeInput.source = "dev"; + mergeInput.allowConflicts = true; + MergePatchSetInput in = new MergePatchSetInput(); + in.merge = mergeInput; + in.subject = "update change by merge ps2"; + + TestWorkInProgressStateChangedListener wipStateChangedListener = + new TestWorkInProgressStateChangedListener(); + try (ExtensionRegistry.Registration registration = + extensionRegistry.newRegistration().add(wipStateChangedListener)) { + ChangeInfo changeInfo = gApi.changes().id(changeId).createMergePatchSet(in); + assertThat(changeInfo.subject).isEqualTo(in.subject); + assertThat(changeInfo.containsGitConflicts).isTrue(); + assertThat(changeInfo.workInProgress).isTrue(); + } + assertThat(wipStateChangedListener.invoked).isTrue(); + assertThat(wipStateChangedListener.wip).isTrue(); + + // To get the revisions, we must retrieve the change with more change options. + ChangeInfo changeInfo = + gApi.changes().id(changeId).get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION); + assertThat(changeInfo.revisions).hasSize(2); + assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit) + .isEqualTo(parent); + + // Verify that the file content in the created patch set is correct. + // We expect that it has conflict markers to indicate the conflict. + BinaryResult bin = gApi.changes().id(changeId).current().file(fileName).content(); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + bin.writeTo(os); + String fileContent = new String(os.toByteArray(), UTF_8); + String sourceSha1 = abbreviateName(changeA.getCommit(), 6); + String targetSha1 = abbreviateName(currentMaster.getCommit(), 6); + assertThat(fileContent) + .isEqualTo( + "<<<<<<< TARGET BRANCH (" + + targetSha1 + + " " + + targetSubject + + ")\n" + + targetContent + + "\n" + + "=======\n" + + sourceContent + + "\n" + + ">>>>>>> SOURCE BRANCH (" + + sourceSha1 + + " " + + sourceSubject + + ")\n"); + + // Verify the message that has been posted on the change. + List<ChangeMessageInfo> messages = gApi.changes().id(changeId).messages(); + assertThat(messages).hasSize(2); + assertThat(Iterables.getLast(messages).message) + .isEqualTo( + "Uploaded patch set 2.\n\n" + + "The following files contain Git conflicts:\n" + + "* " + + fileName + + "\n"); + } + + @Test + public void createMergePatchSet_ConflictAllowedNotSupportedByMergeStrategy() throws Exception { + RevCommit initialHead = projectOperations.project(project).getHead("master"); + createBranch(BranchNameKey.create(project, "dev")); + + // create a change for master + String changeId = createChange().getChangeId(); + + String fileName = "shared.txt"; + String sourceSubject = "source change"; + String sourceContent = "source content"; + String targetSubject = "target change"; + String targetContent = "target content"; + testRepo.reset(initialHead); + PushOneCommit.Result currentMaster = + pushFactory + .create(admin.newIdent(), testRepo, targetSubject, fileName, targetContent) + .to("refs/heads/master"); + currentMaster.assertOkStatus(); + + // push a commit into dev branch + testRepo.reset(initialHead); + PushOneCommit.Result changeA = + pushFactory + .create(user.newIdent(), testRepo, sourceSubject, fileName, sourceContent) + .to("refs/heads/dev"); + changeA.assertOkStatus(); + MergeInput mergeInput = new MergeInput(); + mergeInput.source = "dev"; + mergeInput.allowConflicts = true; + mergeInput.strategy = "simple-two-way-in-core"; + MergePatchSetInput in = new MergePatchSetInput(); + in.merge = mergeInput; + in.subject = "update change by merge ps2"; + + BadRequestException ex = + assertThrows( + BadRequestException.class, () -> gApi.changes().id(changeId).createMergePatchSet(in)); + assertThat(ex) + .hasMessageThat() + .isEqualTo( + "merge with conflicts is not supported with merge strategy: " + mergeInput.strategy); + } + + @Test + public void createMergePatchSetInheritParent() throws Exception { + RevCommit initialHead = projectOperations.project(project).getHead("master"); + createBranch(BranchNameKey.create(project, "dev")); + + // create a change for master + PushOneCommit.Result r = createChange(); + String changeId = r.getChangeId(); + String parent = r.getCommit().getParent(0).getName(); + + // advance master branch + testRepo.reset(initialHead); + PushOneCommit.Result currentMaster = pushTo("refs/heads/master"); + currentMaster.assertOkStatus(); + + // push a commit into dev branch + testRepo.reset(initialHead); + PushOneCommit.Result changeA = + pushFactory + .create(user.newIdent(), 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(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION); + + assertThat(changeInfo.revisions).hasSize(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()); + } + + @Test + public void createMergePatchSetCannotBaseOnInvisibleChange() throws Exception { + RevCommit initialHead = projectOperations.project(project).getHead("master"); + createBranch(BranchNameKey.create(project, "foo")); + createBranch(BranchNameKey.create(project, "bar")); + + // Create a merged commit on 'foo' branch. + merge(createChange("refs/for/foo")); + + // Create the base change on 'bar' branch. + testRepo.reset(initialHead); + String baseChange = createChange("refs/for/bar").getChangeId(); + gApi.changes().id(baseChange).setPrivate(true, "set private"); + + // Create the destination change on 'master' branch. + requestScopeOperations.setApiUser(user.id()); + testRepo.reset(initialHead); + String changeId = createChange().getChangeId(); + + UnprocessableEntityException thrown = + assertThrows( + UnprocessableEntityException.class, + () -> + gApi.changes() + .id(changeId) + .createMergePatchSet(createMergePatchSetInput(baseChange))); + assertThat(thrown).hasMessageThat().contains("Read not permitted for " + baseChange); + } + + @Test + public void createMergePatchSetBaseOnChange() throws Exception { + RevCommit initialHead = projectOperations.project(project).getHead("master"); + createBranch(BranchNameKey.create(project, "foo")); + createBranch(BranchNameKey.create(project, "bar")); + + // Create a merged commit on 'foo' branch. + merge(createChange("refs/for/foo")); + + // Create the base change on 'bar' branch. + testRepo.reset(initialHead); + PushOneCommit.Result result = createChange("refs/for/bar"); + String baseChange = result.getChangeId(); + String expectedParent = result.getCommit().getName(); + + // Create the destination change on 'master' branch. + testRepo.reset(initialHead); + String changeId = createChange().getChangeId(); + + gApi.changes().id(changeId).createMergePatchSet(createMergePatchSetInput(baseChange)); + + ChangeInfo changeInfo = + gApi.changes().id(changeId).get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION); + assertThat(changeInfo.revisions).hasSize(2); + assertThat(changeInfo.subject).isEqualTo("create ps2"); + assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit) + .isEqualTo(expectedParent); + } + + @Test + public void createMergePatchSetWithUnupportedMergeStrategy() throws Exception { + RevCommit initialHead = projectOperations.project(project).getHead("master"); + createBranch(BranchNameKey.create(project, "dev")); + + // create a change for master + String changeId = createChange().getChangeId(); + + String fileName = "shared.txt"; + String sourceSubject = "source change"; + String sourceContent = "source content"; + String targetSubject = "target change"; + String targetContent = "target content"; + testRepo.reset(initialHead); + PushOneCommit.Result currentMaster = + pushFactory + .create(admin.newIdent(), testRepo, targetSubject, fileName, targetContent) + .to("refs/heads/master"); + currentMaster.assertOkStatus(); + + // push a commit into dev branch + testRepo.reset(initialHead); + PushOneCommit.Result changeA = + pushFactory + .create(user.newIdent(), testRepo, sourceSubject, fileName, sourceContent) + .to("refs/heads/dev"); + changeA.assertOkStatus(); + MergeInput mergeInput = new MergeInput(); + mergeInput.source = "dev"; + mergeInput.strategy = "unsupported-strategy"; + MergePatchSetInput in = new MergePatchSetInput(); + in.merge = mergeInput; + in.subject = "update change by merge ps2"; + + BadRequestException ex = + assertThrows( + BadRequestException.class, () -> gApi.changes().id(changeId).createMergePatchSet(in)); + assertThat(ex).hasMessageThat().isEqualTo("invalid merge strategy: " + mergeInput.strategy); + } + + @Test + public void createMergePatchSetWithOtherAuthor() throws Exception { + RevCommit initialHead = projectOperations.project(project).getHead("master"); + createBranch(BranchNameKey.create(project, "dev")); + + // create a change for master + String changeId = createChange().getChangeId(); + + testRepo.reset(initialHead); + PushOneCommit.Result currentMaster = pushTo("refs/heads/master"); + currentMaster.assertOkStatus(); + String parent = currentMaster.getCommit().getName(); + + // push a commit into dev branch + testRepo.reset(initialHead); + PushOneCommit.Result changeA = + pushFactory + .create(user.newIdent(), 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; + String subject = "update change by merge ps2"; + in.subject = subject; + in.author = new AccountInput(); + in.author.name = "Other Author"; + in.author.email = "otherauthor@example.com"; + gApi.changes().id(changeId).createMergePatchSet(in); + + // To get the revisions, we must retrieve the change with more change options. + ChangeInfo changeInfo = + gApi.changes().id(changeId).get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION); + assertThat(changeInfo.revisions).hasSize(2); + assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit) + .isEqualTo(parent); + + // Verify the message that has been posted on the change. + List<ChangeMessageInfo> messages = gApi.changes().id(changeId).messages(); + assertThat(messages).hasSize(2); + assertThat(Iterables.getLast(messages).message).isEqualTo("Uploaded patch set 2."); + + CommitInfo commitInfo = changeInfo.revisions.get(changeInfo.currentRevision).commit; + assertThat(commitInfo.message).contains(subject); + assertThat(commitInfo.author.name).isEqualTo("Other Author"); + assertThat(commitInfo.author.email).isEqualTo("otherauthor@example.com"); + } + + @Test + public void createMergePatchSetWithSpecificAuthorButNoForgeAuthorPermission() throws Exception { + RevCommit initialHead = projectOperations.project(project).getHead("master"); + createBranch(BranchNameKey.create(project, "dev")); + + // create a change for master + String changeId = createChange().getChangeId(); + + testRepo.reset(initialHead); + PushOneCommit.Result currentMaster = pushTo("refs/heads/master"); + currentMaster.assertOkStatus(); + String parent = currentMaster.getCommit().getName(); + + // push a commit into dev branch + testRepo.reset(initialHead); + PushOneCommit.Result changeA = + pushFactory + .create(user.newIdent(), 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; + String subject = "update change by merge ps2"; + in.subject = subject; + in.author = new AccountInput(); + in.author.name = "Foo"; + in.author.email = "foo@example.com"; + + projectOperations + .project(project) + .forUpdate() + .remove( + TestProjectUpdate.permissionKey(Permission.FORGE_AUTHOR) + .ref("refs/*") + .group(REGISTERED_USERS)) + .add(block(Permission.FORGE_AUTHOR).ref("refs/*").group(REGISTERED_USERS)) + .update(); + AuthException ex = + assertThrows( + AuthException.class, () -> gApi.changes().id(changeId).createMergePatchSet(in)); + assertThat(ex).hasMessageThat().isEqualTo("not permitted: forge author on refs/heads/master"); + } + + @Test + public void createMergePatchSetWithMissingNameFailsWithBadRequestException() throws Exception { + RevCommit initialHead = projectOperations.project(project).getHead("master"); + createBranch(BranchNameKey.create(project, "dev")); + + // create a change for master + String changeId = createChange().getChangeId(); + + testRepo.reset(initialHead); + PushOneCommit.Result currentMaster = pushTo("refs/heads/master"); + currentMaster.assertOkStatus(); + String parent = currentMaster.getCommit().getName(); + + // push a commit into dev branch + testRepo.reset(initialHead); + PushOneCommit.Result changeA = + pushFactory + .create(user.newIdent(), 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; + String subject = "update change by merge ps2"; + in.subject = subject; + in.author = new AccountInput(); + in.author.name = "Foo"; + + projectOperations + .project(project) + .forUpdate() + .add(block(Permission.FORGE_AUTHOR).ref("refs/*").group(REGISTERED_USERS)) + .update(); + BadRequestException ex = + assertThrows( + BadRequestException.class, () -> gApi.changes().id(changeId).createMergePatchSet(in)); + assertThat(ex).hasMessageThat().isEqualTo("Author must specify name and email"); + } + + @Test + public void createMergePatchSetWithMissingEmailFailsWithBadRequestException() throws Exception { + RevCommit initialHead = projectOperations.project(project).getHead("master"); + createBranch(BranchNameKey.create(project, "dev")); + + // create a change for master + String changeId = createChange().getChangeId(); + + testRepo.reset(initialHead); + PushOneCommit.Result currentMaster = pushTo("refs/heads/master"); + currentMaster.assertOkStatus(); + String parent = currentMaster.getCommit().getName(); + + // push a commit into dev branch + testRepo.reset(initialHead); + PushOneCommit.Result changeA = + pushFactory + .create(user.newIdent(), 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; + String subject = "update change by merge ps2"; + in.subject = subject; + in.author = new AccountInput(); + in.author.email = "Foo"; + + projectOperations + .project(project) + .forUpdate() + .add(block(Permission.FORGE_AUTHOR).ref("refs/*").group(REGISTERED_USERS)) + .update(); + BadRequestException ex = + assertThrows( + BadRequestException.class, () -> gApi.changes().id(changeId).createMergePatchSet(in)); + assertThat(ex).hasMessageThat().isEqualTo("Author must specify name and email"); + } + + private MergePatchSetInput createMergePatchSetInput(String baseChange) { + MergeInput mergeInput = new MergeInput(); + mergeInput.source = "foo"; + MergePatchSetInput in = new MergePatchSetInput(); + in.merge = mergeInput; + in.subject = "create ps2"; + in.inheritParent = false; + in.baseChange = baseChange; + return in; + } + + private static class TestWorkInProgressStateChangedListener + implements WorkInProgressStateChangedListener { + boolean invoked; + Boolean wip; + + @Override + public void onWorkInProgressStateChanged(Event event) { + this.invoked = true; + this.wip = + event.getChange().workInProgress != null ? event.getChange().workInProgress : false; + } + } +} |