blob: aaf52a1f060317be231dce8cee04d56bd46d4e04 [file] [log] [blame]
// Copyright (C) 2013 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.rest.change;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.Truth8.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
import static com.google.gerrit.extensions.client.ListChangesOption.SUBMITTABLE;
import static com.google.gerrit.server.group.SystemGroupBackend.CHANGE_OWNER;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import static org.eclipse.jgit.lib.Constants.EMPTY_TREE_ID;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.ExtensionRegistry;
import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
import com.google.gerrit.acceptance.GitUtil;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.TestProjectInput;
import com.google.gerrit.acceptance.UseClockStep;
import com.google.gerrit.acceptance.UseTimezone;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.BooleanProjectConfig;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.api.projects.ConfigInput;
import com.google.gerrit.extensions.api.projects.ProjectInput;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.extensions.common.LabelInfo;
import com.google.gerrit.extensions.events.ChangeIndexedListener;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.webui.UiAction;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.change.TestSubmitInput;
import com.google.gerrit.server.git.validators.OnSubmitValidationListener;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.restapi.change.Submit;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gerrit.server.validators.ValidationException;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gerrit.testing.GerritJUnit.ThrowingRunnable;
import com.google.inject.Inject;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.transport.RefSpec;
import org.junit.Test;
@NoHttpd
@UseClockStep
@UseTimezone(timezone = "US/Eastern")
public abstract class AbstractSubmit extends AbstractDaemonTest {
@ConfigSuite.Config
public static Config submitWholeTopicEnabled() {
return submitWholeTopicEnabledConfig();
}
@Inject private ApprovalsUtil approvalsUtil;
@Inject private IdentifiedUser.GenericFactory userFactory;
@Inject private ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations;
@Inject private Submit submitHandler;
@Inject private ExtensionRegistry extensionRegistry;
protected abstract SubmitType getSubmitType();
@Test
@TestProjectInput(createEmptyCommit = false)
public void submitToEmptyRepo() throws Throwable {
assertThat(projectOperations.project(project).hasHead("master")).isFalse();
PushOneCommit.Result change = createChange();
assertThat(change.getCommit().getParents()).isEmpty();
Map<BranchNameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId());
assertThat(projectOperations.project(project).hasHead("master")).isFalse();
assertThat(actual).hasSize(1);
submit(change.getChangeId());
assertThat(projectOperations.project(project).getHead("master").getId())
.isEqualTo(change.getCommit());
assertTrees(project, actual);
}
@Test
public void submitSingleChange() throws Throwable {
RevCommit initialHead = projectOperations.project(project).getHead("master");
PushOneCommit.Result change = createChange();
Map<BranchNameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId());
RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
assertThat(headAfterSubmit).isEqualTo(initialHead);
assertRefUpdatedEvents();
assertChangeMergedEvents();
if ((getSubmitType() == SubmitType.CHERRY_PICK)
|| (getSubmitType() == SubmitType.REBASE_ALWAYS)) {
// The change is updated as well:
assertThat(actual).hasSize(2);
} else {
assertThat(actual).hasSize(1);
}
submit(change.getChangeId());
assertTrees(project, actual);
}
@Test
public void submitMultipleChangesOtherMergeConflictPreview() throws Throwable {
RevCommit initialHead = projectOperations.project(project).getHead("master");
PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
submit(change.getChangeId());
RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
testRepo.reset(initialHead);
PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "other content");
PushOneCommit.Result change3 = createChange("Change 3", "d", "d");
PushOneCommit.Result change4 = createChange("Change 4", "e", "e");
// change 2 is not approved, but we ignore labels
approve(change3.getChangeId());
try (BinaryResult request =
gApi.changes().id(change4.getChangeId()).current().submitPreview()) {
assertThat(getSubmitType()).isEqualTo(SubmitType.CHERRY_PICK);
submit(change4.getChangeId());
} catch (RestApiException e) {
switch (getSubmitType()) {
case FAST_FORWARD_ONLY:
assertThat(e.getMessage())
.isEqualTo(
"Failed to submit 3 changes due to the following problems:\n"
+ "Change "
+ change2.getChange().getId()
+ ": internal error: "
+ "change not processed by merge strategy\n"
+ "Change "
+ change3.getChange().getId()
+ ": internal error: "
+ "change not processed by merge strategy\n"
+ "Change "
+ change4.getChange().getId()
+ ": Project policy "
+ "requires all submissions to be a fast-forward. Please "
+ "rebase the change locally and upload again for review.");
break;
case REBASE_IF_NECESSARY:
case REBASE_ALWAYS:
String change2hash = change2.getChange().currentPatchSet().commitId().name();
assertThat(e.getMessage())
.isEqualTo(
"Cannot rebase "
+ change2hash
+ ": The change could "
+ "not be rebased due to a conflict during merge.");
break;
case MERGE_ALWAYS:
case MERGE_IF_NECESSARY:
case INHERIT:
assertThat(e.getMessage())
.isEqualTo(
"Failed to submit 3 changes due to the following problems:\n"
+ "Change "
+ change2.getChange().getId()
+ ": Change could not be "
+ "merged due to a path conflict. Please rebase the change "
+ "locally and upload the rebased commit for review.\n"
+ "Change "
+ change3.getChange().getId()
+ ": Change could not be "
+ "merged due to a path conflict. Please rebase the change "
+ "locally and upload the rebased commit for review.\n"
+ "Change "
+ change4.getChange().getId()
+ ": Change could not be "
+ "merged due to a path conflict. Please rebase the change "
+ "locally and upload the rebased commit for review.");
break;
case CHERRY_PICK:
default:
assertWithMessage("Should not reach here.").fail();
break;
}
RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
assertThat(headAfterSubmit).isEqualTo(headAfterFirstSubmit);
assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name());
}
}
@Test
public void submitMultipleChangesPreview() throws Throwable {
RevCommit initialHead = projectOperations.project(project).getHead("master");
PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "other content");
PushOneCommit.Result change3 = createChange("Change 3", "d", "d");
PushOneCommit.Result change4 = createChange("Change 4", "e", "e");
// change 2 is not approved, but we ignore labels
approve(change3.getChangeId());
Map<BranchNameKey, ObjectId> actual = fetchFromSubmitPreview(change4.getChangeId());
Map<String, Map<String, Integer>> expected = new HashMap<>();
expected.put(project.get(), new HashMap<>());
expected.get(project.get()).put("refs/heads/master", 3);
assertThat(actual).containsKey(BranchNameKey.create(project, "refs/heads/master"));
if (getSubmitType() == SubmitType.CHERRY_PICK) {
// CherryPick ignores dependencies, thus only change and destination
// branch refs are modified.
assertThat(actual).hasSize(2);
} else if (getSubmitType() == SubmitType.REBASE_ALWAYS) {
// RebaseAlways takes care of dependencies, therefore Change{2,3,4} and
// destination branch will be modified.
assertThat(actual).hasSize(4);
} else {
assertThat(actual).hasSize(1);
}
// check that the submit preview did not actually submit
RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
assertThat(headAfterSubmit).isEqualTo(initialHead);
assertRefUpdatedEvents();
assertChangeMergedEvents();
// now check we actually have the same content:
approve(change2.getChangeId());
submit(change4.getChangeId());
assertTrees(project, actual);
}
@Test
public void submitNoPermission() throws Throwable {
// create project where submit is blocked
Project.NameKey p = projectOperations.newProject().create();
projectOperations
.project(p)
.forUpdate()
.add(block(Permission.SUBMIT).ref("refs/*").group(REGISTERED_USERS))
.update();
TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
PushOneCommit push = pushFactory.create(admin.newIdent(), repo);
PushOneCommit.Result result = push.to("refs/for/master");
result.assertOkStatus();
submit(result.getChangeId(), new SubmitInput(), AuthException.class, "submit not permitted");
}
@Test
public void noSelfSubmit() throws Throwable {
// create project where submit is blocked for the change owner
Project.NameKey p = projectOperations.newProject().create();
projectOperations
.project(p)
.forUpdate()
.add(block(Permission.SUBMIT).ref("refs/*").group(CHANGE_OWNER))
.add(allow(Permission.SUBMIT).ref("refs/heads/*").group(REGISTERED_USERS))
.add(allowLabel("Code-Review").ref("refs/*").group(REGISTERED_USERS).range(-2, +2))
.update();
TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
PushOneCommit push = pushFactory.create(admin.newIdent(), repo);
PushOneCommit.Result result = push.to("refs/for/master");
result.assertOkStatus();
ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
assertThat(change.owner._accountId).isEqualTo(admin.id().get());
submit(result.getChangeId(), new SubmitInput(), AuthException.class, "submit not permitted");
requestScopeOperations.setApiUser(user.id());
submit(result.getChangeId());
}
@Test
public void onlySelfSubmit() throws Throwable {
// create project where only the change owner can submit
Project.NameKey p = projectOperations.newProject().create();
projectOperations
.project(p)
.forUpdate()
.add(block(Permission.SUBMIT).ref("refs/*").group(REGISTERED_USERS))
.add(allow(Permission.SUBMIT).ref("refs/*").group(CHANGE_OWNER))
.add(allowLabel("Code-Review").ref("refs/*").group(REGISTERED_USERS).range(-2, +2))
.update();
TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
PushOneCommit push = pushFactory.create(admin.newIdent(), repo);
PushOneCommit.Result result = push.to("refs/for/master");
result.assertOkStatus();
ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
assertThat(change.owner._accountId).isEqualTo(admin.id().get());
requestScopeOperations.setApiUser(user.id());
submit(result.getChangeId(), new SubmitInput(), AuthException.class, "submit not permitted");
requestScopeOperations.setApiUser(admin.id());
submit(result.getChangeId());
}
@Test
public void submitWholeTopicMultipleProjects() throws Throwable {
assume().that(isSubmitWholeTopicEnabled()).isTrue();
String topic = "test-topic";
// Create test projects
Project.NameKey keyA = createProjectForPush(getSubmitType());
TestRepository<?> repoA = cloneProject(keyA);
Project.NameKey keyB = createProjectForPush(getSubmitType());
TestRepository<?> repoB = cloneProject(keyB);
// Create changes on project-a
PushOneCommit.Result change1 =
createChange(repoA, "master", "Change 1", "a.txt", "content", topic);
PushOneCommit.Result change2 =
createChange(repoA, "master", "Change 2", "b.txt", "content", topic);
// Create changes on project-b
PushOneCommit.Result change3 =
createChange(repoB, "master", "Change 3", "a.txt", "content", topic);
PushOneCommit.Result change4 =
createChange(repoB, "master", "Change 4", "b.txt", "content", topic);
approve(change1.getChangeId());
approve(change2.getChangeId());
approve(change3.getChangeId());
approve(change4.getChangeId());
submit(change4.getChangeId());
String expectedTopic = name(topic);
change1.assertChange(Change.Status.MERGED, expectedTopic, admin);
change2.assertChange(Change.Status.MERGED, expectedTopic, admin);
change3.assertChange(Change.Status.MERGED, expectedTopic, admin);
change4.assertChange(Change.Status.MERGED, expectedTopic, admin);
}
@Test
public void submitWholeTopicMultipleBranchesOnSameProject() throws Throwable {
assume().that(isSubmitWholeTopicEnabled()).isTrue();
String topic = "test-topic";
// Create test project
Project.NameKey keyA = createProjectForPush(getSubmitType());
TestRepository<?> repoA = cloneProject(keyA);
RevCommit initialHead = projectOperations.project(keyA).getHead("master");
// Create the dev branch on the test project
BranchInput in = new BranchInput();
in.revision = initialHead.name();
gApi.projects().name(keyA.get()).branch("dev").create(in);
// Create changes on master
PushOneCommit.Result change1 =
createChange(repoA, "master", "Change 1", "a.txt", "content", topic);
PushOneCommit.Result change2 =
createChange(repoA, "master", "Change 2", "b.txt", "content", topic);
// Create changes on dev
repoA.reset(initialHead);
PushOneCommit.Result change3 =
createChange(repoA, "dev", "Change 3", "a.txt", "content", topic);
PushOneCommit.Result change4 =
createChange(repoA, "dev", "Change 4", "b.txt", "content", topic);
approve(change1.getChangeId());
approve(change2.getChangeId());
approve(change3.getChangeId());
approve(change4.getChangeId());
submit(change4.getChangeId());
String expectedTopic = name(topic);
change1.assertChange(Change.Status.MERGED, expectedTopic, admin);
change2.assertChange(Change.Status.MERGED, expectedTopic, admin);
change3.assertChange(Change.Status.MERGED, expectedTopic, admin);
change4.assertChange(Change.Status.MERGED, expectedTopic, admin);
}
@Test
public void submitWholeTopic() throws Throwable {
assume().that(isSubmitWholeTopicEnabled()).isTrue();
String topic = "test-topic";
PushOneCommit.Result change1 = createChange("Change 1", "a.txt", "content", topic);
PushOneCommit.Result change2 = createChange("Change 2", "b.txt", "content", topic);
PushOneCommit.Result change3 = createChange("Change 3", "c.txt", "content", topic);
approve(change1.getChangeId());
approve(change2.getChangeId());
approve(change3.getChangeId());
submit(change3.getChangeId());
String expectedTopic = name(topic);
change1.assertChange(Change.Status.MERGED, expectedTopic, admin);
change2.assertChange(Change.Status.MERGED, expectedTopic, admin);
change3.assertChange(Change.Status.MERGED, expectedTopic, admin);
// Check for the exact change to have the correct submitter.
assertSubmitter(change3);
// Also check submitters for changes submitted via the topic relationship.
assertSubmitter(change1);
assertSubmitter(change2);
// Check that the repo has the expected commits
List<RevCommit> log = getRemoteLog();
List<String> commitsInRepo = log.stream().map(RevCommit::getShortMessage).collect(toList());
int expectedCommitCount =
getSubmitType() == SubmitType.MERGE_ALWAYS
? 5 // initial commit + 3 commits + merge commit
: 4; // initial commit + 3 commits
assertThat(log).hasSize(expectedCommitCount);
assertThat(commitsInRepo)
.containsAtLeast("Initial empty repository", "Change 1", "Change 2", "Change 3");
if (getSubmitType() == SubmitType.MERGE_ALWAYS) {
assertThat(commitsInRepo).contains("Merge changes from topic \"" + expectedTopic + "\"");
}
}
@Test
public void submitWholeTopicWithMultipleTopics() throws Throwable {
assume().that(isSubmitWholeTopicEnabled()).isTrue();
String topic1 = "test-topic-1";
String topic2 = "test-topic-2";
PushOneCommit.Result change1 = createChange("Change 1", "a.txt", "content", topic1);
PushOneCommit.Result change2 = createChange("Change 2", "b.txt", "content", topic1);
PushOneCommit.Result change3 = createChange("Change 3", "c.txt", "content", topic2);
PushOneCommit.Result change4 = createChange("Change 4", "d.txt", "content", topic2);
approve(change1.getChangeId());
approve(change2.getChangeId());
approve(change3.getChangeId());
approve(change4.getChangeId());
submit(change4.getChangeId());
String expectedTopic1 = name(topic1);
String expectedTopic2 = name(topic2);
if (getSubmitType() == SubmitType.CHERRY_PICK) {
change1.assertChange(Change.Status.NEW, expectedTopic1, admin);
change2.assertChange(Change.Status.NEW, expectedTopic1, admin);
} else {
change1.assertChange(Change.Status.MERGED, expectedTopic1, admin);
change2.assertChange(Change.Status.MERGED, expectedTopic1, admin);
}
// Check for the exact change to have the correct submitter.
assertSubmitter(change4);
// Also check submitters for changes submitted via the topic relationship.
assertSubmitter(change3);
if (getSubmitType() != SubmitType.CHERRY_PICK) {
assertSubmitter(change1);
assertSubmitter(change2);
}
// Check that the repo has the expected commits
List<RevCommit> log = getRemoteLog();
List<String> commitsInRepo = log.stream().map(RevCommit::getShortMessage).collect(toList());
int expectedCommitCount;
switch (getSubmitType()) {
case MERGE_ALWAYS:
// initial commit + 4 commits + merge commit
expectedCommitCount = 6;
break;
case CHERRY_PICK:
// initial commit + 2 commits
expectedCommitCount = 3;
break;
case FAST_FORWARD_ONLY:
case INHERIT:
case MERGE_IF_NECESSARY:
case REBASE_ALWAYS:
case REBASE_IF_NECESSARY:
default:
// initial commit + 4 commits
expectedCommitCount = 5;
break;
}
assertThat(log).hasSize(expectedCommitCount);
if (getSubmitType() == SubmitType.CHERRY_PICK) {
assertThat(commitsInRepo).containsAtLeast("Initial empty repository", "Change 3", "Change 4");
assertThat(commitsInRepo).doesNotContain("Change 1");
assertThat(commitsInRepo).doesNotContain("Change 2");
} else if (getSubmitType() == SubmitType.MERGE_ALWAYS) {
assertThat(commitsInRepo)
.contains(
String.format(
"Merge changes from topics \"%s\", \"%s\"", expectedTopic1, expectedTopic2));
} else {
assertThat(commitsInRepo)
.containsAtLeast(
"Initial empty repository", "Change 1", "Change 2", "Change 3", "Change 4");
}
}
@Test
public void submitReusingOldTopic() throws Throwable {
assume().that(isSubmitWholeTopicEnabled()).isTrue();
String topic = "test-topic";
PushOneCommit.Result change1 = createChange("Change 1", "a.txt", "content", topic);
PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "content", topic);
String id1 = change1.getChangeId();
String id2 = change2.getChangeId();
approve(id1);
approve(id2);
assertSubmittedTogether(id1, ImmutableList.of(id1, id2));
assertSubmittedTogether(id2, ImmutableList.of(id1, id2));
submit(id2);
String expectedTopic = name(topic);
change1.assertChange(Change.Status.MERGED, expectedTopic, admin);
change2.assertChange(Change.Status.MERGED, expectedTopic, admin);
assertSubmittedTogether(id1, ImmutableList.of(id1, id2));
assertSubmittedTogether(id2, ImmutableList.of(id1, id2));
PushOneCommit.Result change3 = createChange("Change 3", "c.txt", "content", topic);
String id3 = change3.getChangeId();
approve(id3);
assertSubmittedTogether(id3, ImmutableList.of());
submit(id3);
change3.assertChange(Change.Status.MERGED, expectedTopic, admin);
assertSubmittedTogether(id3, ImmutableList.of());
}
private void assertSubmittedTogether(String changeId, Iterable<String> expected)
throws Throwable {
assertThat(gApi.changes().id(changeId).submittedTogether().stream().map(i -> i.changeId))
.containsExactlyElementsIn(expected);
}
@Test
public void submitWorkInProgressChange() throws Throwable {
PushOneCommit.Result change = pushTo("refs/for/master%wip");
Change.Id num = change.getChange().getId();
submitWithConflict(
change.getChangeId(),
"Failed to submit 1 change due to the following problems:\n"
+ "Change "
+ num
+ ": Change "
+ num
+ " is work in progress");
}
@Test
public void submitWithHiddenBranchInSameTopic() throws Throwable {
assume().that(isSubmitWholeTopicEnabled()).isTrue();
PushOneCommit.Result visible = createChange("refs/for/master/" + name("topic"));
Change.Id num = visible.getChange().getId();
createBranch(BranchNameKey.create(project, "hidden"));
PushOneCommit.Result hidden = createChange("refs/for/hidden/" + name("topic"));
approve(hidden.getChangeId());
projectOperations
.project(project)
.forUpdate()
.add(block(Permission.READ).ref("refs/heads/hidden").group(REGISTERED_USERS))
.update();
submit(
visible.getChangeId(),
new SubmitInput(),
AuthException.class,
"A change to be submitted with " + num + " is not visible");
}
@Test
public void submitChangeWhenParentOfOtherBranchTip() throws Throwable {
// Chain of two commits
// Push both to topic-branch
// Push the first commit for review and submit
//
// C2 -- tip of topic branch
// |
// C1 -- pushed for review
// |
// C0 -- Master
//
try (ProjectConfigUpdate u = updateProject(project)) {
u.getConfig()
.getProject()
.setBooleanConfig(
BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET,
InheritableBoolean.TRUE);
u.save();
}
PushOneCommit push1 =
pushFactory.create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
PushOneCommit.Result c1 = push1.to("refs/heads/topic");
c1.assertOkStatus();
PushOneCommit push2 =
pushFactory.create(
admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
PushOneCommit.Result c2 = push2.to("refs/heads/topic");
c2.assertOkStatus();
PushOneCommit.Result change1 = push1.to("refs/for/master");
change1.assertOkStatus();
approve(change1.getChangeId());
submit(change1.getChangeId());
}
@Test
public void submitMergeOfNonChangeBranchTip() throws Throwable {
// Merge a branch with commits that have not been submitted as
// changes.
//
// M -- mergeCommit (pushed for review and submitted)
// | \
// | S -- stable (pushed directly to refs/heads/stable)
// | /
// I -- master
//
RevCommit master = projectOperations.project(project).getHead("master");
PushOneCommit stableTip =
pushFactory.create(admin.newIdent(), testRepo, "Tip of branch stable", "stable.txt", "");
PushOneCommit.Result stable = stableTip.to("refs/heads/stable");
PushOneCommit mergeCommit =
pushFactory.create(admin.newIdent(), testRepo, "The merge commit", "merge.txt", "");
mergeCommit.setParents(ImmutableList.of(master, stable.getCommit()));
PushOneCommit.Result mergeReview = mergeCommit.to("refs/for/master");
approve(mergeReview.getChangeId());
submit(mergeReview.getChangeId());
List<RevCommit> log = getRemoteLog();
assertThat(log).contains(stable.getCommit());
assertThat(log).contains(mergeReview.getCommit());
}
@Test
public void submitMergeOfNonChangeBranchNonTip() throws Throwable {
// Merge a branch with commits that have not been submitted as
// changes.
//
// MC -- merge commit (pushed for review and submitted)
// |\ S2 -- new stable tip (pushed directly to refs/heads/stable)
// M \ /
// | S1 -- stable (pushed directly to refs/heads/stable)
// | /
// I -- master
//
RevCommit initial = projectOperations.project(project).getHead("master");
// push directly to stable to S1
PushOneCommit.Result s1 =
pushFactory
.create(admin.newIdent(), testRepo, "new commit into stable", "stable1.txt", "")
.to("refs/heads/stable");
// move the stable tip ahead to S2
pushFactory
.create(admin.newIdent(), testRepo, "Tip of branch stable", "stable2.txt", "")
.to("refs/heads/stable");
testRepo.reset(initial);
// move the master ahead
PushOneCommit.Result m =
pushFactory
.create(admin.newIdent(), testRepo, "Move master ahead", "master.txt", "")
.to("refs/heads/master");
// create merge change
PushOneCommit mc =
pushFactory.create(admin.newIdent(), testRepo, "The merge commit", "merge.txt", "");
mc.setParents(ImmutableList.of(m.getCommit(), s1.getCommit()));
PushOneCommit.Result mergeReview = mc.to("refs/for/master");
approve(mergeReview.getChangeId());
submit(mergeReview.getChangeId());
List<RevCommit> log = getRemoteLog();
assertThat(log).contains(s1.getCommit());
assertThat(log).contains(mergeReview.getCommit());
}
@Test
public void submitChangeWithCommitThatWasAlreadyMerged() throws Throwable {
// create and submit a change
PushOneCommit.Result change = createChange();
submit(change.getChangeId());
RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
// set the status of the change back to NEW to simulate a failed submit that
// merged the commit but failed to update the change status
setChangeStatusToNew(change);
// submitting the change again should detect that the commit was already
// merged and just fix the change status to be MERGED
submit(change.getChangeId());
assertThat(projectOperations.project(project).getHead("master")).isEqualTo(headAfterSubmit);
}
@Test
public void submitChangesWithCommitsThatWereAlreadyMerged() throws Throwable {
// create and submit 2 changes
PushOneCommit.Result change1 = createChange();
PushOneCommit.Result change2 = createChange();
approve(change1.getChangeId());
if (getSubmitType() == SubmitType.CHERRY_PICK) {
submit(change1.getChangeId());
}
submit(change2.getChangeId());
assertMerged(change1.getChangeId());
RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
// set the status of the changes back to NEW to simulate a failed submit that
// merged the commits but failed to update the change status
setChangeStatusToNew(change1, change2);
// submitting the changes again should detect that the commits were already
// merged and just fix the change status to be MERGED
submit(change1.getChangeId());
submit(change2.getChangeId());
assertThat(projectOperations.project(project).getHead("master")).isEqualTo(headAfterSubmit);
}
@Test
public void submitTopicWithCommitsThatWereAlreadyMerged() throws Throwable {
assume().that(isSubmitWholeTopicEnabled()).isTrue();
// create and submit 2 changes with the same topic
String topic = name("topic");
PushOneCommit.Result change1 = createChange("refs/for/master/" + topic);
PushOneCommit.Result change2 = createChange("refs/for/master/" + topic);
approve(change1.getChangeId());
submit(change2.getChangeId());
assertMerged(change1.getChangeId());
RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
// set the status of the second change back to NEW to simulate a failed
// submit that merged the commits but failed to update the change status of
// some changes in the topic
setChangeStatusToNew(change2);
// submitting the topic again should detect that the commits were already
// merged and just fix the change status to be MERGED
submit(change2.getChangeId());
assertThat(projectOperations.project(project).getHead("master")).isEqualTo(headAfterSubmit);
}
@Test
public void submitWithValidation() throws Throwable {
AtomicBoolean called = new AtomicBoolean(false);
OnSubmitValidationListener listener =
new OnSubmitValidationListener() {
@Override
public void preBranchUpdate(Arguments args) throws ValidationException {
called.set(true);
HashSet<String> refs = Sets.newHashSet(args.getCommands().keySet());
assertThat(refs).contains("refs/heads/master");
refs.remove("refs/heads/master");
if (!refs.isEmpty()) {
// Some submit strategies need to insert new patchset.
assertThat(refs).hasSize(1);
assertThat(refs.iterator().next()).startsWith(RefNames.REFS_CHANGES);
}
}
};
try (Registration registration = extensionRegistry.newRegistration().add(listener)) {
PushOneCommit.Result change = createChange();
approve(change.getChangeId());
submit(change.getChangeId());
assertThat(called.get()).isTrue();
}
}
@Test
public void submitWithValidationMultiRepo() throws Throwable {
assume().that(isSubmitWholeTopicEnabled()).isTrue();
String topic = "test-topic";
// Create test projects
Project.NameKey keyA = createProjectForPush(getSubmitType());
TestRepository<?> repoA = cloneProject(keyA);
Project.NameKey keyB = createProjectForPush(getSubmitType());
TestRepository<?> repoB = cloneProject(keyB);
// Create changes on project-a
PushOneCommit.Result change1 =
createChange(repoA, "master", "Change 1", "a.txt", "content", topic);
PushOneCommit.Result change2 =
createChange(repoA, "master", "Change 2", "b.txt", "content", topic);
// Create changes on project-b
PushOneCommit.Result change3 =
createChange(repoB, "master", "Change 3", "a.txt", "content", topic);
PushOneCommit.Result change4 =
createChange(repoB, "master", "Change 4", "b.txt", "content", topic);
List<PushOneCommit.Result> changes = Lists.newArrayList(change1, change2, change3, change4);
for (PushOneCommit.Result change : changes) {
approve(change.getChangeId());
}
// Construct validator which will throw on a second call.
// Since there are 2 repos, first submit attempt will fail, the second will
// succeed.
List<String> projectsCalled = new ArrayList<>(4);
OnSubmitValidationListener listener =
new OnSubmitValidationListener() {
@Override
public void preBranchUpdate(Arguments args) throws ValidationException {
String master = "refs/heads/master";
assertThat(args.getCommands()).containsKey(master);
ReceiveCommand cmd = args.getCommands().get(master);
ObjectId newMasterId = cmd.getNewId();
try (Repository repo = repoManager.openRepository(args.getProject())) {
assertThat(repo.exactRef(master).getObjectId()).isEqualTo(cmd.getOldId());
assertThat(args.getRef(master)).hasValue(newMasterId);
args.getRevWalk().parseBody(args.getRevWalk().parseCommit(newMasterId));
} catch (IOException e) {
throw new AssertionError("failed checking new ref value", e);
}
projectsCalled.add(args.getProject().get());
if (projectsCalled.size() == 2) {
throw new ValidationException("time to fail");
}
}
};
try (Registration registration = extensionRegistry.newRegistration().add(listener)) {
submitWithConflict(change4.getChangeId(), "time to fail");
assertThat(projectsCalled).containsExactly(keyA.get(), keyB.get());
for (PushOneCommit.Result change : changes) {
change.assertChange(Change.Status.NEW, name(topic), admin);
}
submit(change4.getChangeId());
assertThat(projectsCalled).containsExactly(keyA.get(), keyB.get(), keyA.get(), keyB.get());
for (PushOneCommit.Result change : changes) {
change.assertChange(Change.Status.MERGED, name(topic), admin);
}
}
}
@Test
public void submitWithCommitAndItsMergeCommitTogether() throws Throwable {
assume().that(isSubmitWholeTopicEnabled()).isTrue();
RevCommit initialHead = projectOperations.project(project).getHead("master");
// Create a stable branch and bootstrap it.
gApi.projects().name(project.get()).branch("stable").create(new BranchInput());
PushOneCommit push =
pushFactory.create(user.newIdent(), testRepo, "initial commit", "a.txt", "a");
PushOneCommit.Result change = push.to("refs/heads/stable");
RevCommit stable = projectOperations.project(project).getHead("stable");
RevCommit master = projectOperations.project(project).getHead("master");
assertThat(master).isEqualTo(initialHead);
assertThat(stable).isEqualTo(change.getCommit());
testRepo.git().fetch().call();
testRepo.git().branchCreate().setName("stable").setStartPoint(stable).call();
testRepo.git().branchCreate().setName("master").setStartPoint(master).call();
// Create a fix in stable branch.
testRepo.reset(stable);
RevCommit fix =
testRepo
.commit()
.parent(stable)
.message("small fix")
.add("b.txt", "b")
.insertChangeId()
.create();
testRepo.branch("refs/heads/stable").update(fix);
testRepo
.git()
.push()
.setRefSpecs(new RefSpec("refs/heads/stable:refs/for/stable/" + name("topic")))
.call();
// Merge the fix into master.
testRepo.reset(master);
RevCommit merge =
testRepo
.commit()
.parent(master)
.parent(fix)
.message("Merge stable into master")
.insertChangeId()
.create();
testRepo.branch("refs/heads/master").update(merge);
testRepo
.git()
.push()
.setRefSpecs(new RefSpec("refs/heads/master:refs/for/master/" + name("topic")))
.call();
// Submit together.
String fixId = GitUtil.getChangeId(testRepo, fix).get();
String mergeId = GitUtil.getChangeId(testRepo, merge).get();
approve(fixId);
approve(mergeId);
submit(mergeId);
assertMerged(fixId);
assertMerged(mergeId);
testRepo.git().fetch().call();
RevWalk rw = testRepo.getRevWalk();
master = rw.parseCommit(projectOperations.project(project).getHead("master"));
assertThat(rw.isMergedInto(merge, master)).isTrue();
assertThat(rw.isMergedInto(fix, master)).isTrue();
}
@Test
public void retrySubmitSingleChangeOnLockFailure() throws Throwable {
PushOneCommit.Result change = createChange();
String id = change.getChangeId();
approve(id);
TestSubmitInput input = new TestSubmitInput();
input.generateLockFailures =
new ArrayDeque<>(
ImmutableList.of(
true, // Attempt 1: lock failure
false, // Attempt 2: success
false)); // Leftover value to check total number of calls.
submit(id, input);
assertMerged(id);
testRepo.git().fetch().call();
RevWalk rw = testRepo.getRevWalk();
RevCommit master = rw.parseCommit(projectOperations.project(project).getHead("master"));
RevCommit patchSet = parseCurrentRevision(rw, change.getChangeId());
assertThat(rw.isMergedInto(patchSet, master)).isTrue();
assertThat(input.generateLockFailures).containsExactly(false);
}
@Test
public void retrySubmitAfterTornTopicOnLockFailure() throws Throwable {
assume().that(isSubmitWholeTopicEnabled()).isTrue();
String topic = "test-topic";
Project.NameKey keyA = createProjectForPush(getSubmitType());
Project.NameKey keyB = createProjectForPush(getSubmitType());
TestRepository<?> repoA = cloneProject(keyA);
TestRepository<?> repoB = cloneProject(keyB);
PushOneCommit.Result change1 =
createChange(repoA, "master", "Change 1", "a.txt", "content", topic);
PushOneCommit.Result change2 =
createChange(repoB, "master", "Change 2", "b.txt", "content", topic);
approve(change1.getChangeId());
approve(change2.getChangeId());
TestSubmitInput input = new TestSubmitInput();
input.generateLockFailures =
new ArrayDeque<>(
ImmutableList.of(
false, // Change 1, attempt 1: success
true, // Change 2, attempt 1: lock failure
false, // Change 1, attempt 2: success
false, // Change 2, attempt 2: success
false)); // Leftover value to check total number of calls.
submit(change2.getChangeId(), input);
String expectedTopic = name(topic);
change1.assertChange(Change.Status.MERGED, expectedTopic, admin);
change2.assertChange(Change.Status.MERGED, expectedTopic, admin);
repoA.git().fetch().call();
RevWalk rwA = repoA.getRevWalk();
RevCommit masterA = rwA.parseCommit(projectOperations.project(keyA).getHead("master"));
RevCommit change1Ps = parseCurrentRevision(rwA, change1.getChangeId());
assertThat(rwA.isMergedInto(change1Ps, masterA)).isTrue();
repoB.git().fetch().call();
RevWalk rwB = repoB.getRevWalk();
RevCommit masterB = rwB.parseCommit(projectOperations.project(keyB).getHead("master"));
RevCommit change2Ps = parseCurrentRevision(rwB, change2.getChangeId());
assertThat(rwB.isMergedInto(change2Ps, masterB)).isTrue();
assertThat(input.generateLockFailures).containsExactly(false);
}
@Test
public void authorAndCommitDateAreEqual() throws Throwable {
assume().that(getSubmitType()).isNotEqualTo(SubmitType.FAST_FORWARD_ONLY);
ConfigInput ci = new ConfigInput();
ci.matchAuthorToCommitterDate = InheritableBoolean.TRUE;
gApi.projects().name(project.get()).config(ci);
RevCommit initialHead = projectOperations.project(project).getHead("master");
testRepo.reset(initialHead);
PushOneCommit.Result change = createChange("Change 1", "b", "b");
testRepo.reset(initialHead);
PushOneCommit.Result change2 = createChange("Change 2", "c", "c");
if (getSubmitType() == SubmitType.MERGE_IF_NECESSARY
|| getSubmitType() == SubmitType.REBASE_IF_NECESSARY) {
// Merge another change so that change2 is not a fast-forward
submit(change.getChangeId());
}
submit(change2.getChangeId());
assertAuthorAndCommitDateEquals(projectOperations.project(project).getHead("master"));
}
@Test
@TestProjectInput(rejectEmptyCommit = InheritableBoolean.FALSE)
public void submitEmptyCommitPatchSetCanNotFastForward_emptyCommitAllowed() throws Throwable {
assume().that(getSubmitType()).isNotEqualTo(SubmitType.FAST_FORWARD_ONLY);
PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
submit(change.getChangeId());
ChangeApi revert1 = gApi.changes().id(change.getChangeId()).revert();
approve(revert1.id());
revert1.current().submit();
ChangeApi revert2 = gApi.changes().id(change.getChangeId()).revert();
approve(revert2.id());
revert2.current().submit();
}
@Test
@TestProjectInput(rejectEmptyCommit = InheritableBoolean.TRUE)
public void submitEmptyCommitPatchSetCanNotFastForward_emptyCommitNotAllowed() throws Throwable {
assume().that(getSubmitType()).isNotEqualTo(SubmitType.FAST_FORWARD_ONLY);
PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
submit(change.getChangeId());
ChangeApi revert1 = gApi.changes().id(change.getChangeId()).revert();
approve(revert1.id());
revert1.current().submit();
ChangeApi revert2 = gApi.changes().id(change.getChangeId()).revert();
approve(revert2.id());
ResourceConflictException thrown =
assertThrows(ResourceConflictException.class, () -> revert2.current().submit());
assertThat(thrown)
.hasMessageThat()
.contains(
"Change "
+ revert2.get()._number
+ ": Change could not be merged because the commit is empty. Project policy"
+ " requires all commits to contain modifications to at least one file.");
}
@Test
@TestProjectInput(rejectEmptyCommit = InheritableBoolean.FALSE)
public void submitEmptyCommitPatchSetCanFastForward_emptyCommitAllowed() throws Throwable {
ChangeInput ci = new ChangeInput();
ci.subject = "Empty change";
ci.project = project.get();
ci.branch = "master";
ChangeApi change = gApi.changes().create(ci);
approve(change.id());
change.current().submit();
}
@Test
@TestProjectInput(rejectEmptyCommit = InheritableBoolean.TRUE)
public void submitEmptyCommitPatchSetCanFastForward_emptyCommitNotAllowed() throws Throwable {
ChangeInput ci = new ChangeInput();
ci.subject = "Empty change";
ci.project = project.get();
ci.branch = "master";
ChangeApi change = gApi.changes().create(ci);
approve(change.id());
ResourceConflictException thrown =
assertThrows(ResourceConflictException.class, () -> change.current().submit());
assertThat(thrown)
.hasMessageThat()
.contains(
"Change "
+ change.get()._number
+ ": Change could not be merged because the commit is empty. Project policy"
+ " requires all commits to contain modifications to at least one file.");
}
@Test
@TestProjectInput(createEmptyCommit = false, rejectEmptyCommit = InheritableBoolean.TRUE)
public void submitNonemptyCommitToEmptyRepoWithRejectEmptyCommit_allowed() throws Throwable {
assertThat(projectOperations.project(project).hasHead("master")).isFalse();
PushOneCommit.Result change = createChange();
assertThat(change.getCommit().getParents()).isEmpty();
Map<BranchNameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId());
assertThat(projectOperations.project(project).hasHead("master")).isFalse();
assertThat(actual).hasSize(1);
submit(change.getChangeId());
assertThat(projectOperations.project(project).getHead("master").getId())
.isEqualTo(change.getCommit());
assertTrees(project, actual);
}
@Test
@TestProjectInput(createEmptyCommit = false, rejectEmptyCommit = InheritableBoolean.TRUE)
public void submitEmptyCommitToEmptyRepoWithRejectEmptyCommit_allowed() throws Throwable {
assertThat(projectOperations.project(project).hasHead("master")).isFalse();
PushOneCommit.Result change =
pushFactory
.create(admin.newIdent(), testRepo, "Change 1", ImmutableMap.of())
.to("refs/for/master");
change.assertOkStatus();
assertThat(change.getCommit().getTree()).isEqualTo(EMPTY_TREE_ID);
Map<BranchNameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId());
assertThat(projectOperations.project(project).hasHead("master")).isFalse();
assertThat(actual).hasSize(1);
submit(change.getChangeId());
assertThat(projectOperations.project(project).getHead("master").getId())
.isEqualTo(change.getCommit());
assertTrees(project, actual);
}
private void setChangeStatusToNew(PushOneCommit.Result... changes) throws Throwable {
for (PushOneCommit.Result change : changes) {
try (BatchUpdate bu =
batchUpdateFactory.create(project, userFactory.create(admin.id()), TimeUtil.nowTs())) {
bu.addOp(
change.getChange().getId(),
new BatchUpdateOp() {
@Override
public boolean updateChange(ChangeContext ctx) {
ctx.getChange().setStatus(Change.Status.NEW);
ctx.getUpdate(ctx.getChange().currentPatchSetId()).setStatus(Change.Status.NEW);
return true;
}
});
bu.execute();
}
}
}
@Test
@GerritConfig(name = "index.reindexAfterRefUpdate", value = "true")
public void submitSchedulesOpenChangesOfSameBranchForReindexing() throws Throwable {
// Create a merged change.
PushOneCommit push =
pushFactory.create(admin.newIdent(), testRepo, "Merged Change", "foo.txt", "foo");
PushOneCommit.Result mergedChange = push.to("refs/for/master");
mergedChange.assertOkStatus();
approve(mergedChange.getChangeId());
submit(mergedChange.getChangeId());
// Create some open changes.
PushOneCommit.Result change1 = createChange();
PushOneCommit.Result change2 = createChange();
PushOneCommit.Result change3 = createChange();
// Create a branch with one open change.
BranchInput in = new BranchInput();
in.revision = projectOperations.project(project).getHead("master").name();
gApi.projects().name(project.get()).branch("dev").create(in);
PushOneCommit.Result changeOtherBranch = createChange("refs/for/dev");
ChangeIndexedListener changeIndexedListener = mock(ChangeIndexedListener.class);
try (Registration registration =
extensionRegistry.newRegistration().add(changeIndexedListener)) {
// submit a change, this should trigger asynchronous reindexing of the open changes on the
// same branch
approve(change1.getChangeId());
submit(change1.getChangeId());
assertThat(gApi.changes().id(change1.getChangeId()).get().status)
.isEqualTo(ChangeStatus.MERGED);
// on submit the change that is submitted gets reindexed synchronously
verify(changeIndexedListener, atLeast(1))
.onChangeScheduledForIndexing(project.get(), change1.getChange().getId().get());
verify(changeIndexedListener, atLeast(1))
.onChangeIndexed(project.get(), change1.getChange().getId().get());
// the open changes on the same branch get reindexed asynchronously
verify(changeIndexedListener, times(1))
.onChangeScheduledForIndexing(project.get(), change2.getChange().getId().get());
verify(changeIndexedListener, times(1))
.onChangeScheduledForIndexing(project.get(), change3.getChange().getId().get());
// merged changes don't get reindexed
verify(changeIndexedListener, times(0))
.onChangeScheduledForIndexing(project.get(), mergedChange.getChange().getId().get());
// open changes on other branches don't get reindexed
verify(changeIndexedListener, times(0))
.onChangeScheduledForIndexing(project.get(), changeOtherBranch.getChange().getId().get());
}
}
private void assertSubmitter(PushOneCommit.Result change) throws Throwable {
ChangeInfo info = get(change.getChangeId(), ListChangesOption.MESSAGES);
assertThat(info.messages).isNotNull();
Iterable<String> messages = Iterables.transform(info.messages, i -> i.message);
assertThat(messages).hasSize(3);
String last = Iterables.getLast(messages);
if (getSubmitType() == SubmitType.CHERRY_PICK) {
assertThat(last).startsWith("Change has been successfully cherry-picked as ");
} else if (getSubmitType() == SubmitType.REBASE_ALWAYS) {
assertThat(last).startsWith("Change has been successfully rebased and submitted as");
} else {
assertThat(last).isEqualTo("Change has been successfully merged by Administrator");
}
}
@Override
protected void updateProjectInput(ProjectInput in) {
in.submitType = getSubmitType();
if (in.useContentMerge == InheritableBoolean.INHERIT) {
in.useContentMerge = InheritableBoolean.FALSE;
}
}
protected void submit(String changeId) throws Throwable {
submit(changeId, new SubmitInput(), null, null);
}
protected void submit(String changeId, SubmitInput input) throws Throwable {
submit(changeId, input, null, null);
}
protected void submitWithConflict(String changeId, String expectedError) throws Throwable {
submit(changeId, new SubmitInput(), ResourceConflictException.class, expectedError);
}
protected void submit(
String changeId,
SubmitInput input,
@Nullable Class<? extends RestApiException> expectedExceptionType,
String expectedExceptionMsg)
throws Throwable {
approve(changeId);
if (expectedExceptionType == null) {
assertSubmittable(changeId);
} else {
requireNonNull(expectedExceptionMsg);
}
ThrowingRunnable submit = () -> gApi.changes().id(changeId).current().submit(input);
if (expectedExceptionType != null) {
RestApiException thrown = assertThrows(expectedExceptionType, submit);
assertThat(thrown).hasMessageThat().isEqualTo(expectedExceptionMsg);
return;
}
submit.run();
ChangeInfo change = gApi.changes().id(changeId).info();
assertMerged(change.changeId);
}
protected void assertSubmittable(String changeId) throws Throwable {
assertWithMessage("submit bit on ChangeInfo")
.that(get(changeId, SUBMITTABLE).submittable)
.isTrue();
RevisionResource rsrc = parseCurrentRevisionResource(changeId);
UiAction.Description desc = submitHandler.getDescription(rsrc);
assertWithMessage("visible bit on submit action").that(desc.isVisible()).isTrue();
assertWithMessage("enabled bit on submit action").that(desc.isEnabled()).isTrue();
}
protected void assertChangeMergedEvents(String... expected) throws Throwable {
eventRecorder.assertChangeMergedEvents(project.get(), "refs/heads/master", expected);
}
protected void assertRefUpdatedEvents(RevCommit... expected) throws Throwable {
eventRecorder.assertRefUpdatedEvents(project.get(), "refs/heads/master", expected);
}
protected void assertCurrentRevision(String changeId, int expectedNum, ObjectId expectedId)
throws Throwable {
ChangeInfo c = get(changeId, CURRENT_REVISION);
assertThat(c.currentRevision).isEqualTo(expectedId.name());
assertThat(c.revisions.get(expectedId.name())._number).isEqualTo(expectedNum);
try (Repository repo = repoManager.openRepository(Project.nameKey(c.project))) {
String refName = PatchSet.id(Change.id(c._number), expectedNum).toRefName();
Ref ref = repo.exactRef(refName);
assertWithMessage(refName).that(ref).isNotNull();
assertThat(ref.getObjectId()).isEqualTo(expectedId);
}
}
protected void assertNew(String changeId) throws Throwable {
assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
}
protected void assertApproved(String changeId) throws Throwable {
assertApproved(changeId, admin);
}
protected void assertApproved(String changeId, TestAccount user) throws Throwable {
ChangeInfo c = get(changeId, DETAILED_LABELS);
LabelInfo cr = c.labels.get("Code-Review");
assertThat(cr.all).hasSize(1);
assertThat(cr.all.get(0).value).isEqualTo(2);
assertThat(Account.id(cr.all.get(0)._accountId)).isEqualTo(user.id());
}
protected void assertMerged(String changeId) throws RestApiException {
ChangeStatus status = gApi.changes().id(changeId).info().status;
assertThat(status).isEqualTo(ChangeStatus.MERGED);
}
protected void assertPersonEquals(PersonIdent expected, PersonIdent actual) {
assertThat(actual.getEmailAddress()).isEqualTo(expected.getEmailAddress());
assertThat(actual.getName()).isEqualTo(expected.getName());
assertThat(actual.getTimeZone()).isEqualTo(expected.getTimeZone());
}
protected void assertAuthorAndCommitDateEquals(RevCommit commit) {
assertThat(commit.getAuthorIdent().getWhen()).isEqualTo(commit.getCommitterIdent().getWhen());
assertThat(commit.getAuthorIdent().getTimeZone())
.isEqualTo(commit.getCommitterIdent().getTimeZone());
}
protected void assertSubmitter(String changeId, int psId) throws Throwable {
assertSubmitter(changeId, psId, admin);
}
protected void assertSubmitter(String changeId, int psId, TestAccount user) throws Throwable {
Change c = getOnlyElement(queryProvider.get().byKeyPrefix(changeId)).change();
ChangeNotes cn = notesFactory.createChecked(c);
PatchSetApproval submitter =
approvalsUtil.getSubmitter(cn, PatchSet.id(cn.getChangeId(), psId));
assertThat(submitter).isNotNull();
assertThat(submitter.isLegacySubmit()).isTrue();
assertThat(submitter.accountId()).isEqualTo(user.id());
}
protected void assertNoSubmitter(String changeId, int psId) throws Throwable {
Change c = getOnlyElement(queryProvider.get().byKeyPrefix(changeId)).change();
ChangeNotes cn = notesFactory.createChecked(c);
PatchSetApproval submitter =
approvalsUtil.getSubmitter(cn, PatchSet.id(cn.getChangeId(), psId));
assertThat(submitter).isNull();
}
protected void assertCherryPick(TestRepository<?> testRepo, boolean contentMerge)
throws Throwable {
assertRebase(testRepo, contentMerge);
RevCommit remoteHead = projectOperations.project(project).getHead("master");
assertThat(remoteHead.getFooterLines("Reviewed-On")).isNotEmpty();
assertThat(remoteHead.getFooterLines("Reviewed-By")).isNotEmpty();
}
protected void assertRebase(TestRepository<?> testRepo, boolean contentMerge) throws Throwable {
Repository repo = testRepo.getRepository();
RevCommit localHead = getHead(repo, "HEAD");
RevCommit remoteHead = projectOperations.project(project).getHead("master");
assertThat(localHead.getId()).isNotEqualTo(remoteHead.getId());
assertThat(remoteHead.getParentCount()).isEqualTo(1);
if (!contentMerge) {
assertThat(getLatestRemoteDiff()).isEqualTo(getLatestDiff(repo));
}
assertThat(remoteHead.getShortMessage()).isEqualTo(localHead.getShortMessage());
}
protected List<RevCommit> getRemoteLog(Project.NameKey project, String branch) throws Throwable {
try (Repository repo = repoManager.openRepository(project);
RevWalk rw = new RevWalk(repo)) {
rw.markStart(rw.parseCommit(repo.exactRef("refs/heads/" + branch).getObjectId()));
return Lists.newArrayList(rw);
}
}
protected List<RevCommit> getRemoteLog() throws Throwable {
return getRemoteLog(project, "master");
}
private String getLatestDiff(Repository repo) throws Throwable {
ObjectId oldTreeId = repo.resolve("HEAD~1^{tree}");
ObjectId newTreeId = repo.resolve("HEAD^{tree}");
return getLatestDiff(repo, oldTreeId, newTreeId);
}
private String getLatestRemoteDiff() throws Throwable {
try (Repository repo = repoManager.openRepository(project);
RevWalk rw = new RevWalk(repo)) {
ObjectId oldTreeId = repo.resolve("refs/heads/master~1^{tree}");
ObjectId newTreeId = repo.resolve("refs/heads/master^{tree}");
return getLatestDiff(repo, oldTreeId, newTreeId);
}
}
private String getLatestDiff(Repository repo, ObjectId oldTreeId, ObjectId newTreeId)
throws Throwable {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (DiffFormatter fmt = new DiffFormatter(out)) {
fmt.setRepository(repo);
fmt.format(oldTreeId, newTreeId);
fmt.flush();
return out.toString();
}
}
// TODO(hanwen): the submodule tests have a similar method; maybe we could share code?
protected Project.NameKey createProjectForPush(SubmitType submitType) throws Throwable {
Project.NameKey project = projectOperations.newProject().submitType(submitType).create();
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.PUSH).ref("refs/heads/*").group(adminGroupUuid()))
.add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/*").group(adminGroupUuid()))
.update();
return project;
}
protected PushOneCommit.Result createChange(
String subject, String fileName, String content, String topic) throws Throwable {
PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo, subject, fileName, content);
return push.to("refs/for/master/" + name(topic));
}
}