| // 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.Truth8.assertThat; |
| import static com.google.common.truth.TruthJUnit.assume; |
| 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 java.util.concurrent.TimeUnit.SECONDS; |
| import static java.util.stream.Collectors.toList; |
| |
| 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.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.testsuite.project.ProjectOperations; |
| import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; |
| import com.google.gerrit.common.data.Permission; |
| 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.registration.DynamicSet; |
| import com.google.gerrit.extensions.registration.RegistrationHandle; |
| 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.reviewdb.client.Account; |
| import com.google.gerrit.reviewdb.client.BooleanProjectConfig; |
| import com.google.gerrit.reviewdb.client.Branch; |
| import com.google.gerrit.reviewdb.client.Change; |
| import com.google.gerrit.reviewdb.client.PatchSet; |
| import com.google.gerrit.reviewdb.client.PatchSetApproval; |
| import com.google.gerrit.reviewdb.client.Project; |
| import com.google.gerrit.reviewdb.client.RefNames; |
| 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.project.testing.Util; |
| 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.TestTimeUtil; |
| 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.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| |
| @NoHttpd |
| public abstract class AbstractSubmit extends AbstractDaemonTest { |
| @ConfigSuite.Config |
| public static Config submitWholeTopicEnabled() { |
| return submitWholeTopicEnabledConfig(); |
| } |
| |
| @Inject private ApprovalsUtil approvalsUtil; |
| @Inject private DynamicSet<OnSubmitValidationListener> onSubmitValidationListeners; |
| @Inject private IdentifiedUser.GenericFactory userFactory; |
| @Inject private ProjectOperations projectOperations; |
| @Inject private RequestScopeOperations requestScopeOperations; |
| @Inject private Submit submitHandler; |
| |
| private RegistrationHandle onSubmitValidatorHandle; |
| private String systemTimeZone; |
| |
| @Before |
| public void setTimeForTesting() { |
| systemTimeZone = System.setProperty("user.timezone", "US/Eastern"); |
| TestTimeUtil.resetWithClockStep(1, SECONDS); |
| } |
| |
| @After |
| public void resetTime() { |
| TestTimeUtil.useSystemTime(); |
| System.setProperty("user.timezone", systemTimeZone); |
| } |
| |
| @After |
| public void removeOnSubmitValidator() { |
| if (onSubmitValidatorHandle != null) { |
| onSubmitValidatorHandle.remove(); |
| } |
| } |
| |
| protected abstract SubmitType getSubmitType(); |
| |
| @Test |
| @TestProjectInput(createEmptyCommit = false) |
| public void submitToEmptyRepo() throws Exception { |
| assertThat(projectOperations.project(project).hasHead("master")).isFalse(); |
| PushOneCommit.Result change = createChange(); |
| assertThat(change.getCommit().getParents()).isEmpty(); |
| Map<Branch.NameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId()); |
| assertThat(projectOperations.project(project).hasHead("master")).isFalse(); |
| assertThat(actual).hasSize(1); |
| |
| submit(change.getChangeId()); |
| assertThat(getRemoteHead().getId()).isEqualTo(change.getCommit()); |
| assertTrees(project, actual); |
| } |
| |
| @Test |
| public void submitSingleChange() throws Exception { |
| RevCommit initialHead = getRemoteHead(); |
| PushOneCommit.Result change = createChange(); |
| Map<Branch.NameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId()); |
| RevCommit headAfterSubmit = getRemoteHead(); |
| 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 Exception { |
| RevCommit initialHead = getRemoteHead(); |
| |
| PushOneCommit.Result change = createChange("Change 1", "a.txt", "content"); |
| submit(change.getChangeId()); |
| |
| RevCommit headAfterFirstSubmit = getRemoteHead(); |
| 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().getRevision().get(); |
| 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: |
| fail("Should not reach here."); |
| break; |
| } |
| |
| RevCommit headAfterSubmit = getRemoteHead(); |
| assertThat(headAfterSubmit).isEqualTo(headAfterFirstSubmit); |
| assertRefUpdatedEvents(initialHead, headAfterFirstSubmit); |
| assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name()); |
| } |
| } |
| |
| @Test |
| public void submitMultipleChangesPreview() throws Exception { |
| RevCommit initialHead = getRemoteHead(); |
| 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<Branch.NameKey, 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(new Branch.NameKey(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 = getRemoteHead(); |
| 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 Exception { |
| // create project where submit is blocked |
| Project.NameKey p = projectOperations.newProject().create(); |
| block(p, "refs/*", Permission.SUBMIT, REGISTERED_USERS); |
| |
| 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 Exception { |
| // create project where submit is blocked for the change owner |
| Project.NameKey p = projectOperations.newProject().create(); |
| try (ProjectConfigUpdate u = updateProject(p)) { |
| Util.block(u.getConfig(), Permission.SUBMIT, CHANGE_OWNER, "refs/*"); |
| Util.allow(u.getConfig(), Permission.SUBMIT, REGISTERED_USERS, "refs/heads/*"); |
| Util.allow( |
| u.getConfig(), Permission.forLabel("Code-Review"), -2, +2, REGISTERED_USERS, "refs/*"); |
| u.save(); |
| } |
| |
| 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 Exception { |
| // create project where only the change owner can submit |
| Project.NameKey p = projectOperations.newProject().create(); |
| try (ProjectConfigUpdate u = updateProject(p)) { |
| Util.block(u.getConfig(), Permission.SUBMIT, REGISTERED_USERS, "refs/*"); |
| Util.allow(u.getConfig(), Permission.SUBMIT, CHANGE_OWNER, "refs/*"); |
| Util.allow( |
| u.getConfig(), Permission.forLabel("Code-Review"), -2, +2, REGISTERED_USERS, "refs/*"); |
| u.save(); |
| } |
| |
| 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 Exception { |
| 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 Exception { |
| assume().that(isSubmitWholeTopicEnabled()).isTrue(); |
| String topic = "test-topic"; |
| |
| // Create test project |
| Project.NameKey keyA = createProjectForPush(getSubmitType()); |
| TestRepository<?> repoA = cloneProject(keyA); |
| |
| RevCommit initialHead = getRemoteHead(keyA, "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 Exception { |
| 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 submitReusingOldTopic() throws Exception { |
| 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 Exception { |
| assertThat(gApi.changes().id(changeId).submittedTogether().stream().map(i -> i.changeId)) |
| .containsExactlyElementsIn(expected); |
| } |
| |
| @Test |
| public void submitWorkInProgressChange() throws Exception { |
| 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 Exception { |
| assume().that(isSubmitWholeTopicEnabled()).isTrue(); |
| PushOneCommit.Result visible = createChange("refs/for/master/" + name("topic")); |
| Change.Id num = visible.getChange().getId(); |
| |
| createBranch(new Branch.NameKey(project, "hidden")); |
| PushOneCommit.Result hidden = createChange("refs/for/hidden/" + name("topic")); |
| approve(hidden.getChangeId()); |
| blockRead("refs/heads/hidden"); |
| |
| submit( |
| visible.getChangeId(), |
| new SubmitInput(), |
| AuthException.class, |
| "A change to be submitted with " + num + " is not visible"); |
| } |
| |
| @Test |
| public void submitChangeWhenParentOfOtherBranchTip() throws Exception { |
| // 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 Exception { |
| // 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 = getRemoteHead(project, "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 Exception { |
| // 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 = getRemoteHead(project, "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 Exception { |
| // create and submit a change |
| PushOneCommit.Result change = createChange(); |
| submit(change.getChangeId()); |
| RevCommit headAfterSubmit = getRemoteHead(); |
| |
| // 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(getRemoteHead()).isEqualTo(headAfterSubmit); |
| } |
| |
| @Test |
| public void submitChangesWithCommitsThatWereAlreadyMerged() throws Exception { |
| // 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 = getRemoteHead(); |
| |
| // 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(getRemoteHead()).isEqualTo(headAfterSubmit); |
| } |
| |
| @Test |
| public void submitTopicWithCommitsThatWereAlreadyMerged() throws Exception { |
| 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 = getRemoteHead(); |
| |
| // 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(getRemoteHead()).isEqualTo(headAfterSubmit); |
| } |
| |
| @Test |
| public void submitWithValidation() throws Exception { |
| AtomicBoolean called = new AtomicBoolean(false); |
| this.addOnSubmitValidationListener( |
| args -> { |
| 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); |
| } |
| }); |
| |
| PushOneCommit.Result change = createChange(); |
| approve(change.getChangeId()); |
| submit(change.getChangeId()); |
| assertThat(called.get()).isTrue(); |
| } |
| |
| @Test |
| public void submitWithValidationMultiRepo() throws Exception { |
| 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); |
| this.addOnSubmitValidationListener( |
| args -> { |
| 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"); |
| } |
| }); |
| 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 Exception { |
| assume().that(isSubmitWholeTopicEnabled()).isTrue(); |
| |
| RevCommit initialHead = getRemoteHead(); |
| |
| // 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 = getRemoteHead(project, "stable"); |
| RevCommit master = getRemoteHead(project, "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(getRemoteHead(project, "master")); |
| assertThat(rw.isMergedInto(merge, master)).isTrue(); |
| assertThat(rw.isMergedInto(fix, master)).isTrue(); |
| } |
| |
| @Test |
| public void retrySubmitSingleChangeOnLockFailure() throws Exception { |
| 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(getRemoteHead(project, "master")); |
| RevCommit patchSet = parseCurrentRevision(rw, change.getChangeId()); |
| assertThat(rw.isMergedInto(patchSet, master)).isTrue(); |
| |
| assertThat(input.generateLockFailures).containsExactly(false); |
| } |
| |
| @Test |
| public void retrySubmitAfterTornTopicOnLockFailure() throws Exception { |
| 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(getRemoteHead(keyA, "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(getRemoteHead(keyB, "master")); |
| RevCommit change2Ps = parseCurrentRevision(rwB, change2.getChangeId()); |
| assertThat(rwB.isMergedInto(change2Ps, masterB)).isTrue(); |
| |
| assertThat(input.generateLockFailures).containsExactly(false); |
| } |
| |
| @Test |
| public void authorAndCommitDateAreEqual() throws Exception { |
| 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 = getRemoteHead(); |
| 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(getRemoteHead()); |
| } |
| |
| @Test |
| @TestProjectInput(rejectEmptyCommit = InheritableBoolean.FALSE) |
| public void submitEmptyCommitPatchSetCanNotFastForward_emptyCommitAllowed() throws Exception { |
| 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 Exception { |
| 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()); |
| |
| exception.expect(ResourceConflictException.class); |
| exception.expectMessage( |
| "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."); |
| revert2.current().submit(); |
| } |
| |
| @Test |
| @TestProjectInput(rejectEmptyCommit = InheritableBoolean.FALSE) |
| public void submitEmptyCommitPatchSetCanFastForward_emptyCommitAllowed() throws Exception { |
| 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 Exception { |
| ChangeInput ci = new ChangeInput(); |
| ci.subject = "Empty change"; |
| ci.project = project.get(); |
| ci.branch = "master"; |
| ChangeApi change = gApi.changes().create(ci); |
| approve(change.id()); |
| |
| exception.expect(ResourceConflictException.class); |
| exception.expectMessage( |
| "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."); |
| change.current().submit(); |
| } |
| |
| @Test |
| @TestProjectInput(createEmptyCommit = false, rejectEmptyCommit = InheritableBoolean.TRUE) |
| public void submitNonemptyCommitToEmptyRepoWithRejectEmptyCommit_allowed() throws Exception { |
| assertThat(projectOperations.project(project).hasHead("master")).isFalse(); |
| PushOneCommit.Result change = createChange(); |
| assertThat(change.getCommit().getParents()).isEmpty(); |
| Map<Branch.NameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId()); |
| assertThat(projectOperations.project(project).hasHead("master")).isFalse(); |
| assertThat(actual).hasSize(1); |
| |
| submit(change.getChangeId()); |
| assertThat(getRemoteHead().getId()).isEqualTo(change.getCommit()); |
| assertTrees(project, actual); |
| } |
| |
| @Test |
| @TestProjectInput(createEmptyCommit = false, rejectEmptyCommit = InheritableBoolean.TRUE) |
| public void submitEmptyCommitToEmptyRepoWithRejectEmptyCommit_allowed() throws Exception { |
| 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(); |
| // TODO(dborowitz): Use EMPTY_TREE_ID after upgrading to https://git.eclipse.org/r/127473 |
| assertThat(change.getCommit().getTree()) |
| .isEqualTo(ObjectId.fromString("4b825dc642cb6eb9a060e54bf8d69288fbee4904")); |
| |
| Map<Branch.NameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId()); |
| assertThat(projectOperations.project(project).hasHead("master")).isFalse(); |
| assertThat(actual).hasSize(1); |
| |
| submit(change.getChangeId()); |
| assertThat(getRemoteHead().getId()).isEqualTo(change.getCommit()); |
| assertTrees(project, actual); |
| } |
| |
| private void setChangeStatusToNew(PushOneCommit.Result... changes) throws Exception { |
| 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(); |
| } |
| } |
| } |
| |
| private void assertSubmitter(PushOneCommit.Result change) throws Exception { |
| 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 Exception { |
| submit(changeId, new SubmitInput(), null, null); |
| } |
| |
| protected void submit(String changeId, SubmitInput input) throws Exception { |
| submit(changeId, input, null, null); |
| } |
| |
| protected void submitWithConflict(String changeId, String expectedError) throws Exception { |
| submit(changeId, new SubmitInput(), ResourceConflictException.class, expectedError); |
| } |
| |
| protected void submit( |
| String changeId, |
| SubmitInput input, |
| Class<? extends RestApiException> expectedExceptionType, |
| String expectedExceptionMsg) |
| throws Exception { |
| approve(changeId); |
| if (expectedExceptionType == null) { |
| assertSubmittable(changeId); |
| } |
| try { |
| gApi.changes().id(changeId).current().submit(input); |
| if (expectedExceptionType != null) { |
| fail("Expected exception of type " + expectedExceptionType.getSimpleName()); |
| } |
| } catch (RestApiException e) { |
| if (expectedExceptionType == null) { |
| throw e; |
| } |
| // More verbose than using assertThat and/or ExpectedException, but gives |
| // us the stack trace. |
| if (!expectedExceptionType.isAssignableFrom(e.getClass()) |
| || !e.getMessage().equals(expectedExceptionMsg)) { |
| throw new AssertionError( |
| "Expected exception of type " |
| + expectedExceptionType.getSimpleName() |
| + " with message: \"" |
| + expectedExceptionMsg |
| + "\" but got exception of type " |
| + e.getClass().getSimpleName() |
| + " with message \"" |
| + e.getMessage() |
| + "\"", |
| e); |
| } |
| return; |
| } |
| ChangeInfo change = gApi.changes().id(changeId).info(); |
| assertMerged(change.changeId); |
| } |
| |
| protected void assertSubmittable(String changeId) throws Exception { |
| assertThat(get(changeId, SUBMITTABLE).submittable).named("submit bit on ChangeInfo").isTrue(); |
| RevisionResource rsrc = parseCurrentRevisionResource(changeId); |
| UiAction.Description desc = submitHandler.getDescription(rsrc); |
| assertThat(desc.isVisible()).named("visible bit on submit action").isTrue(); |
| assertThat(desc.isEnabled()).named("enabled bit on submit action").isTrue(); |
| } |
| |
| protected void assertChangeMergedEvents(String... expected) throws Exception { |
| eventRecorder.assertChangeMergedEvents(project.get(), "refs/heads/master", expected); |
| } |
| |
| protected void assertRefUpdatedEvents(RevCommit... expected) throws Exception { |
| eventRecorder.assertRefUpdatedEvents(project.get(), "refs/heads/master", expected); |
| } |
| |
| protected void assertCurrentRevision(String changeId, int expectedNum, ObjectId expectedId) |
| throws Exception { |
| 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(new Project.NameKey(c.project))) { |
| String refName = new PatchSet.Id(new Change.Id(c._number), expectedNum).toRefName(); |
| Ref ref = repo.exactRef(refName); |
| assertThat(ref).named(refName).isNotNull(); |
| assertThat(ref.getObjectId()).isEqualTo(expectedId); |
| } |
| } |
| |
| protected void assertNew(String changeId) throws Exception { |
| assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW); |
| } |
| |
| protected void assertApproved(String changeId) throws Exception { |
| assertApproved(changeId, admin); |
| } |
| |
| protected void assertApproved(String changeId, TestAccount user) throws Exception { |
| 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(new 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 Exception { |
| assertSubmitter(changeId, psId, admin); |
| } |
| |
| protected void assertSubmitter(String changeId, int psId, TestAccount user) throws Exception { |
| Change c = getOnlyElement(queryProvider.get().byKeyPrefix(changeId)).change(); |
| ChangeNotes cn = notesFactory.createChecked(c); |
| PatchSetApproval submitter = |
| approvalsUtil.getSubmitter(cn, new PatchSet.Id(cn.getChangeId(), psId)); |
| assertThat(submitter).isNotNull(); |
| assertThat(submitter.isLegacySubmit()).isTrue(); |
| assertThat(submitter.getAccountId()).isEqualTo(user.id()); |
| } |
| |
| protected void assertNoSubmitter(String changeId, int psId) throws Exception { |
| Change c = getOnlyElement(queryProvider.get().byKeyPrefix(changeId)).change(); |
| ChangeNotes cn = notesFactory.createChecked(c); |
| PatchSetApproval submitter = |
| approvalsUtil.getSubmitter(cn, new PatchSet.Id(cn.getChangeId(), psId)); |
| assertThat(submitter).isNull(); |
| } |
| |
| protected void assertCherryPick(TestRepository<?> testRepo, boolean contentMerge) |
| throws Exception { |
| assertRebase(testRepo, contentMerge); |
| RevCommit remoteHead = getRemoteHead(); |
| assertThat(remoteHead.getFooterLines("Reviewed-On")).isNotEmpty(); |
| assertThat(remoteHead.getFooterLines("Reviewed-By")).isNotEmpty(); |
| } |
| |
| protected void assertRebase(TestRepository<?> testRepo, boolean contentMerge) throws Exception { |
| Repository repo = testRepo.getRepository(); |
| RevCommit localHead = getHead(repo, "HEAD"); |
| RevCommit remoteHead = getRemoteHead(); |
| 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 Exception { |
| 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 Exception { |
| return getRemoteLog(project, "master"); |
| } |
| |
| protected void addOnSubmitValidationListener(OnSubmitValidationListener listener) { |
| assertThat(onSubmitValidatorHandle).isNull(); |
| onSubmitValidatorHandle = onSubmitValidationListeners.add("gerrit", listener); |
| } |
| |
| private String getLatestDiff(Repository repo) throws Exception { |
| ObjectId oldTreeId = repo.resolve("HEAD~1^{tree}"); |
| ObjectId newTreeId = repo.resolve("HEAD^{tree}"); |
| return getLatestDiff(repo, oldTreeId, newTreeId); |
| } |
| |
| private String getLatestRemoteDiff() throws Exception { |
| 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 Exception { |
| 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 Exception { |
| Project.NameKey project = projectOperations.newProject().submitType(submitType).create(); |
| grant(project, "refs/heads/*", Permission.PUSH); |
| grant(project, "refs/for/refs/heads/*", Permission.SUBMIT); |
| return project; |
| } |
| |
| protected PushOneCommit.Result createChange( |
| String subject, String fileName, String content, String topic) throws Exception { |
| PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo, subject, fileName, content); |
| return push.to("refs/for/master/" + name(topic)); |
| } |
| } |