CreateMergePatchSet: Implement 'author' to tweak Git commit author

This change implements the same functionality as I54b98be6 but for
CreateMergePatchSet. Setting the newly exposed 'author' field requires
the caller to have 'forgeAuthor' permission.

Change-Id: Ia434ed0ed11489296d8c2aa9f8208faac293b563
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 8bd02a8..6b73482 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -7325,6 +7325,13 @@
 |`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 53f5e07..734d7e9 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 8ac2140..af4bf69 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.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 @@
     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 @@
     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 @@
 
       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 d4affb7..410cb41 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.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.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.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 @@
   }
 
   @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 0000000..8b77d013
--- /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;
+    }
+  }
+}