blob: 2bde16520e52bac840eff074c69c6b4d6bacc03e [file] [log] [blame]
// Copyright (C) 2020 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.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();
// 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();
// 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();
// 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;
}
}
}