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
This commit is contained in:
Patrick Hiesel
2020-10-06 13:22:07 +02:00
parent 8fc097141d
commit e8fc7974e8
5 changed files with 670 additions and 407 deletions

View File

@@ -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]]

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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;
@@ -3188,407 +3183,6 @@ public class ChangeIT extends AbstractDaemonTest {
r2.assertOkStatus();
}
@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();

View File

@@ -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;
}
}
}