| // 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.assert_; |
| 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 org.junit.Assert.fail; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Lists; |
| import com.google.gerrit.acceptance.AbstractDaemonTest; |
| 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.common.TimeUtil; |
| import com.google.gerrit.common.data.Permission; |
| import com.google.gerrit.extensions.api.changes.SubmitInput; |
| import com.google.gerrit.extensions.api.projects.BranchInput; |
| 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.LabelInfo; |
| 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.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.server.ApprovalsUtil; |
| import com.google.gerrit.server.IdentifiedUser; |
| import com.google.gerrit.server.change.RevisionResource; |
| import com.google.gerrit.server.change.Submit; |
| import com.google.gerrit.server.git.BatchUpdate; |
| import com.google.gerrit.server.git.BatchUpdate.ChangeContext; |
| import com.google.gerrit.server.git.ProjectConfig; |
| import com.google.gerrit.server.notedb.ChangeNotes; |
| import com.google.gerrit.server.project.Util; |
| import com.google.gerrit.testutil.ConfigSuite; |
| import com.google.gerrit.testutil.TestTimeUtil; |
| import com.google.gwtorm.server.OrmException; |
| import com.google.inject.Inject; |
| |
| 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.RevTree; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.stream.Collectors; |
| |
| @NoHttpd |
| public abstract class AbstractSubmit extends AbstractDaemonTest { |
| @ConfigSuite.Config |
| public static Config submitWholeTopicEnabled() { |
| return submitWholeTopicEnabledConfig(); |
| } |
| |
| @Inject |
| private ApprovalsUtil approvalsUtil; |
| |
| @Inject |
| private Submit submitHandler; |
| |
| @Inject |
| private IdentifiedUser.GenericFactory userFactory; |
| |
| @Inject |
| private BatchUpdate.Factory updateFactory; |
| |
| 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 cleanup() { |
| db.close(); |
| } |
| |
| protected abstract SubmitType getSubmitType(); |
| |
| @Test |
| @TestProjectInput(createEmptyCommit = false) |
| public void submitToEmptyRepo() throws Exception { |
| RevCommit initialHead = getRemoteHead(); |
| PushOneCommit.Result change = createChange(); |
| BinaryResult request = submitPreview(change.getChangeId()); |
| RevCommit headAfterSubmitPreview = getRemoteHead(); |
| assertThat(headAfterSubmitPreview).isEqualTo(initialHead); |
| Map<Branch.NameKey, RevTree> actual = |
| fetchFromBundles(request); |
| assertThat(actual).hasSize(1); |
| |
| submit(change.getChangeId()); |
| assertThat(getRemoteHead().getId()).isEqualTo(change.getCommit()); |
| assertRevTrees(project, actual); |
| } |
| |
| @Test |
| public void submitSingleChange() throws Exception { |
| RevCommit initialHead = getRemoteHead(); |
| PushOneCommit.Result change = createChange(); |
| BinaryResult request = submitPreview(change.getChangeId()); |
| RevCommit headAfterSubmit = getRemoteHead(); |
| assertThat(headAfterSubmit).isEqualTo(initialHead); |
| assertRefUpdatedEvents(); |
| assertChangeMergedEvents(); |
| |
| Map<Branch.NameKey, RevTree> actual = |
| fetchFromBundles(request); |
| |
| 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()); |
| assertRevTrees(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()); |
| BinaryResult request = null; |
| String msg = null; |
| try { |
| request = submitPreview(change4.getChangeId()); |
| } catch (Exception e) { |
| msg = e.getMessage(); |
| } |
| |
| if (getSubmitType() == SubmitType.CHERRY_PICK) { |
| Map<Branch.NameKey, RevTree> s = |
| fetchFromBundles(request); |
| submit(change4.getChangeId()); |
| assertRevTrees(project, s); |
| } else if (getSubmitType() == SubmitType.FAST_FORWARD_ONLY) { |
| assertThat(msg).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."); |
| RevCommit headAfterSubmit = getRemoteHead(); |
| assertThat(headAfterSubmit).isEqualTo(headAfterFirstSubmit); |
| assertRefUpdatedEvents(initialHead, headAfterFirstSubmit); |
| assertChangeMergedEvents(change.getChangeId(), |
| headAfterFirstSubmit.name()); |
| } else if ((getSubmitType() == SubmitType.REBASE_IF_NECESSARY) |
| || (getSubmitType() == SubmitType.REBASE_ALWAYS)) { |
| String change2hash = change2.getChange().currentPatchSet() |
| .getRevision().get(); |
| assertThat(msg).isEqualTo( |
| "Cannot rebase " + change2hash + ": The change could " + |
| "not be rebased due to a conflict during merge."); |
| RevCommit headAfterSubmit = getRemoteHead(); |
| assertThat(headAfterSubmit).isEqualTo(headAfterFirstSubmit); |
| assertRefUpdatedEvents(initialHead, headAfterFirstSubmit); |
| assertChangeMergedEvents(change.getChangeId(), |
| headAfterFirstSubmit.name()); |
| } else { |
| assertThat(msg).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."); |
| 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()); |
| BinaryResult request = submitPreview(change4.getChangeId()); |
| |
| Map<String, Map<String, Integer>> expected = new HashMap<>(); |
| expected.put(project.get(), new HashMap<String, Integer>()); |
| expected.get(project.get()).put("refs/heads/master", 3); |
| Map<Branch.NameKey, RevTree> actual = |
| fetchFromBundles(request); |
| |
| 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()); |
| assertRevTrees(project, actual); |
| } |
| |
| @Test |
| public void submitNoPermission() throws Exception { |
| // create project where submit is blocked |
| Project.NameKey p = createProject("p"); |
| block(Permission.SUBMIT, REGISTERED_USERS, "refs/*", p); |
| |
| TestRepository<InMemoryRepository> repo = cloneProject(p, admin); |
| PushOneCommit push = pushFactory.create(db, admin.getIdent(), 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 = createProject("p"); |
| ProjectConfig cfg = projectCache.checkedGet(p).getConfig(); |
| Util.block(cfg, Permission.SUBMIT, CHANGE_OWNER, "refs/*"); |
| Util.allow(cfg, Permission.SUBMIT, REGISTERED_USERS, "refs/heads/*"); |
| Util.allow(cfg, Permission.forLabel("Code-Review"), -2, +2, |
| REGISTERED_USERS, "refs/*"); |
| saveProjectConfig(p, cfg); |
| |
| TestRepository<InMemoryRepository> repo = cloneProject(p, admin); |
| PushOneCommit push = pushFactory.create(db, admin.getIdent(), 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"); |
| |
| setApiUser(user); |
| submit(result.getChangeId()); |
| } |
| |
| @Test |
| public void onlySelfSubmit() throws Exception { |
| // create project where only the change owner can submit |
| Project.NameKey p = createProject("p"); |
| ProjectConfig cfg = projectCache.checkedGet(p).getConfig(); |
| Util.block(cfg, Permission.SUBMIT, REGISTERED_USERS, "refs/*"); |
| Util.allow(cfg, Permission.SUBMIT, CHANGE_OWNER, "refs/*"); |
| Util.allow(cfg, Permission.forLabel("Code-Review"), -2, +2, |
| REGISTERED_USERS, "refs/*"); |
| saveProjectConfig(p, cfg); |
| |
| TestRepository<InMemoryRepository> repo = cloneProject(p, admin); |
| PushOneCommit push = pushFactory.create(db, admin.getIdent(), 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()); |
| |
| setApiUser(user); |
| submit(result.getChangeId(), new SubmitInput(), AuthException.class, |
| "submit not permitted"); |
| |
| setApiUser(admin); |
| submit(result.getChangeId()); |
| } |
| |
| @Test |
| public void submitWholeTopicMultipleProjects() throws Exception { |
| assume().that(isSubmitWholeTopicEnabled()).isTrue(); |
| String topic = "test-topic"; |
| |
| // Create test projects |
| TestRepository<?> repoA = createProjectWithPush( |
| "project-a", null, getSubmitType()); |
| TestRepository<?> repoB = createProjectWithPush( |
| "project-b", null, getSubmitType()); |
| |
| // 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 |
| String projectName = "project-a"; |
| TestRepository<?> repoA = createProjectWithPush( |
| projectName, null, getSubmitType()); |
| |
| RevCommit initialHead = |
| getRemoteHead(new Project.NameKey(name(projectName)), "master"); |
| |
| // Create the dev branch on the test project |
| BranchInput in = new BranchInput(); |
| in.revision = initialHead.name(); |
| gApi.projects().name(name(projectName)).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(c -> c.getShortMessage()) |
| .collect(Collectors.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).containsAllOf( |
| "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 submitDraftChange() throws Exception { |
| PushOneCommit.Result draft = createDraftChange(); |
| Change.Id num = draft.getChange().getId(); |
| submitWithConflict(draft.getChangeId(), |
| "Failed to submit 1 change due to the following problems:\n" |
| + "Change " + num + ": Change " + num + " is draft"); |
| } |
| |
| @Test |
| public void submitDraftPatchSet() throws Exception { |
| PushOneCommit.Result change = createChange(); |
| PushOneCommit.Result draft = amendChangeAsDraft(change.getChangeId()); |
| Change.Id num = draft.getChange().getId(); |
| |
| submitWithConflict(draft.getChangeId(), |
| "Failed to submit 1 change due to the following problems:\n" |
| + "Change " + num + ": submit rule error: " |
| + "Cannot submit draft patch sets"); |
| } |
| |
| @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 |
| // |
| ProjectConfig config = projectCache.checkedGet(project).getConfig(); |
| config.getProject().setCreateNewChangeForAllNotInTarget( |
| InheritableBoolean.TRUE); |
| saveProjectConfig(project, config); |
| |
| PushOneCommit push1 = pushFactory.create(db, admin.getIdent(), testRepo, |
| PushOneCommit.SUBJECT, "a.txt", "content"); |
| PushOneCommit.Result c1 = push1.to("refs/heads/topic"); |
| c1.assertOkStatus(); |
| PushOneCommit push2 = pushFactory.create(db, admin.getIdent(), 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(db, admin.getIdent(), testRepo, |
| "Tip of branch stable", "stable.txt", ""); |
| PushOneCommit.Result stable = stableTip.to("refs/heads/stable"); |
| PushOneCommit mergeCommit = pushFactory.create(db, admin.getIdent(), |
| 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 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); |
| } |
| |
| private void setChangeStatusToNew(PushOneCommit.Result... changes) |
| throws Exception { |
| for (PushOneCommit.Result change : changes) { |
| try (BatchUpdate bu = updateFactory.create(db, project, |
| userFactory.create(admin.id), TimeUtil.nowTs())) { |
| bu.addOp(change.getChange().getId(), new BatchUpdate.Op() { |
| @Override |
| public boolean updateChange(ChangeContext ctx) throws OrmException { |
| 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 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 BinaryResult submitPreview(String changeId) throws Exception { |
| return gApi.changes().id(changeId).current().submitPreview(); |
| } |
| |
| protected BinaryResult submitPreview(String changeId, String format) |
| throws Exception { |
| return gApi.changes().id(changeId).current().submitPreview(format); |
| } |
| |
| protected void assertSubmittable(String changeId) throws Exception { |
| assertThat(get(changeId, SUBMITTABLE).submittable) |
| .named("submit bit on ChangeInfo") |
| .isEqualTo(true); |
| 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(get(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.getId()); |
| } |
| |
| 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 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(db, c); |
| PatchSetApproval submitter = approvalsUtil.getSubmitter(db, cn, |
| new PatchSet.Id(cn.getChangeId(), psId)); |
| assertThat(submitter).isNotNull(); |
| assertThat(submitter.isLegacySubmit()).isTrue(); |
| assertThat(submitter.getAccountId()).isEqualTo(user.getId()); |
| } |
| |
| protected void assertNoSubmitter(String changeId, int psId) |
| throws Exception { |
| Change c = |
| getOnlyElement(queryProvider.get().byKeyPrefix(changeId)).change(); |
| ChangeNotes cn = notesFactory.createChecked(db, c); |
| PatchSetApproval submitter = approvalsUtil.getSubmitter( |
| db, 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); |
| RevCommit remoteHead = getRemoteHead(); |
| assert_().withFailureMessage( |
| String.format("%s not equal %s", localHead.name(), remoteHead.name())) |
| .that(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"); |
| } |
| |
| 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(); |
| } |
| } |
| } |