| // Copyright (C) 2017 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.googlesource.gerrit.plugins.automerger; |
| |
| import static com.google.common.collect.ImmutableList.toImmutableList; |
| import static com.google.common.truth.OptionalSubject.optionals; |
| 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.gerrit.acceptance.testsuite.project.TestProjectUpdate.blockLabel; |
| import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.labelPermissionKey; |
| import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS; |
| import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT; |
| import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION; |
| import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS; |
| import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES; |
| import static com.google.gerrit.testing.GerritJUnit.assertThrows; |
| import static java.util.Comparator.comparing; |
| |
| import com.google.common.base.Charsets; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Iterables; |
| import com.google.common.io.CharStreams; |
| import com.google.gerrit.acceptance.GitUtil; |
| import com.google.gerrit.acceptance.LightweightPluginDaemonTest; |
| import com.google.gerrit.acceptance.PushOneCommit; |
| import com.google.gerrit.acceptance.TestPlugin; |
| import com.google.gerrit.acceptance.testsuite.group.GroupOperations; |
| import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; |
| import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.extensions.api.accounts.AccountApi; |
| import com.google.gerrit.extensions.api.changes.ChangeApi; |
| import com.google.gerrit.extensions.api.changes.RebaseInput; |
| import com.google.gerrit.extensions.api.changes.ReviewInput; |
| import com.google.gerrit.extensions.common.ApprovalInfo; |
| import com.google.gerrit.extensions.common.ChangeInfo; |
| import com.google.gerrit.extensions.common.ChangeMessageInfo; |
| import com.google.gerrit.extensions.restapi.AuthException; |
| import com.google.gerrit.extensions.restapi.BinaryResult; |
| import com.google.gerrit.extensions.restapi.RestApiException; |
| import com.google.gerrit.reviewdb.client.AccountGroup; |
| import com.google.gerrit.reviewdb.client.BranchNameKey; |
| import com.google.gerrit.reviewdb.client.Project; |
| import com.google.gerrit.reviewdb.client.RefNames; |
| import com.google.gerrit.server.group.SystemGroupBackend; |
| import com.google.gerrit.testing.TestTimeUtil; |
| import com.google.inject.Inject; |
| import com.googlesource.gerrit.plugins.automerger.helpers.ConfigOption; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.sql.Timestamp; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Optional; |
| import java.util.concurrent.TimeUnit; |
| 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.junit.Test; |
| |
| @TestPlugin( |
| name = "automerger", |
| sysModule = "com.googlesource.gerrit.plugins.automerger.AutomergerModule") |
| public class DownstreamCreatorIT extends LightweightPluginDaemonTest { |
| @Inject private GroupOperations groupOperations; |
| @Inject private ProjectOperations projectOperations; |
| |
| @Test |
| public void testExpectedFlow() throws Exception { |
| Project.NameKey manifestNameKey = defaultSetup(); |
| // Create initial change |
| PushOneCommit.Result result = |
| createChange(testRepo, "master", "subject", "filename", "content", "testtopic"); |
| // Project name is scoped by test, so we need to get it from our initial change |
| String projectName = result.getChange().project().get(); |
| createBranch(BranchNameKey.create(projectName, "ds_one")); |
| createBranch(BranchNameKey.create(projectName, "ds_two")); |
| pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two"); |
| // After we upload our config, we upload a new patchset to create the downstreams |
| amendChange(result.getChangeId()); |
| result.assertOkStatus(); |
| // Check that there are the correct number of changes in the topic |
| List<ChangeInfo> changesInTopic = |
| gApi.changes() |
| .query("topic: " + gApi.changes().id(result.getChangeId()).topic()) |
| .withOption(CURRENT_REVISION) |
| .get(); |
| assertThat(changesInTopic).hasSize(3); |
| |
| List<ChangeInfo> sortedChanges = sortedChanges(changesInTopic); |
| |
| ChangeInfo dsOneChangeInfo = sortedChanges.get(0); |
| assertThat(dsOneChangeInfo.branch).isEqualTo("ds_one"); |
| assertAutomergerChangeCreatedMessage(dsOneChangeInfo.id); |
| |
| ChangeInfo dsTwoChangeInfo = sortedChanges.get(1); |
| assertThat(dsTwoChangeInfo.branch).isEqualTo("ds_two"); |
| assertAutomergerChangeCreatedMessage(dsTwoChangeInfo.id); |
| |
| ChangeInfo masterChangeInfo = sortedChanges.get(2); |
| assertCodeReview(masterChangeInfo.id, 0, null); |
| assertThat(masterChangeInfo.branch).isEqualTo("master"); |
| |
| // Ensure that commit subjects are correct |
| String masterSubject = masterChangeInfo.subject; |
| String shortMasterSha = masterChangeInfo.currentRevision.substring(0, 10); |
| assertThat(masterChangeInfo.subject).doesNotContainMatch("automerger"); |
| assertThat(dsOneChangeInfo.subject) |
| .isEqualTo("[automerger] " + masterSubject + " am: " + shortMasterSha); |
| assertThat(dsTwoChangeInfo.subject) |
| .isEqualTo("[automerger] " + masterSubject + " am: " + shortMasterSha); |
| |
| // +2 and submit |
| merge(result); |
| assertCodeReview(masterChangeInfo.id, 2, null); |
| assertCodeReview(dsOneChangeInfo.id, 2, "autogenerated:Automerger"); |
| assertCodeReview(dsTwoChangeInfo.id, 2, "autogenerated:Automerger"); |
| } |
| |
| @Test |
| public void testDiamondMerge() throws Exception { |
| Project.NameKey manifestNameKey = defaultSetup(); |
| // Create initial change |
| PushOneCommit.Result initialResult = createChange("subject", "filename", "echo Hello"); |
| // Project name is scoped by test, so we need to get it from our initial change |
| String projectName = initialResult.getChange().project().get(); |
| createBranch(BranchNameKey.create(projectName, "left")); |
| createBranch(BranchNameKey.create(projectName, "right")); |
| initialResult.assertOkStatus(); |
| merge(initialResult); |
| // Reset to create a sibling |
| ObjectId initial = repo().exactRef("HEAD").getLeaf().getObjectId(); |
| testRepo.reset(initial); |
| // Make left != right |
| PushOneCommit.Result left = |
| createChange( |
| testRepo, "left", "subject", "filename", "echo \"Hello asdfsd World\"", "randtopic"); |
| left.assertOkStatus(); |
| merge(left); |
| |
| String leftRevision = gApi.projects().name(projectName).branch("left").get().revision; |
| String rightRevision = gApi.projects().name(projectName).branch("right").get().revision; |
| // For this test, right != left |
| assertThat(leftRevision).isNotEqualTo(rightRevision); |
| createBranch(BranchNameKey.create(projectName, "bottom")); |
| pushDiamondConfig(manifestNameKey.get(), projectName); |
| // After we upload our config, we upload a new patchset to create the downstreams |
| PushOneCommit.Result result = |
| createChange(testRepo, "master", "subject", "filename2", "echo Hello", "sometopic"); |
| result.assertOkStatus(); |
| // Check that there are the correct number of changes in the topic |
| List<ChangeInfo> changesInTopic = |
| gApi.changes() |
| .query("topic: " + gApi.changes().id(result.getChangeId()).topic()) |
| .withOptions(CURRENT_REVISION, CURRENT_COMMIT) |
| .get(); |
| assertThat(changesInTopic).hasSize(5); |
| // +2 and submit |
| merge(result); |
| List<ChangeInfo> sortedChanges = sortedChanges(changesInTopic); |
| |
| // Should create two changes on bottom, since left and right are different. |
| ChangeInfo bottomChangeInfoA = sortedChanges.get(0); |
| assertThat(bottomChangeInfoA.branch).isEqualTo("bottom"); |
| assertCodeReview(bottomChangeInfoA.id, 2, "autogenerated:Automerger"); |
| |
| ChangeInfo bottomChangeInfoB = sortedChanges.get(1); |
| assertThat(bottomChangeInfoB.branch).isEqualTo("bottom"); |
| assertCodeReview(bottomChangeInfoB.id, 2, "autogenerated:Automerger"); |
| |
| ChangeInfo leftChangeInfo = sortedChanges.get(2); |
| assertThat(leftChangeInfo.branch).isEqualTo("left"); |
| assertCodeReview(leftChangeInfo.id, 2, "autogenerated:Automerger"); |
| |
| ChangeInfo masterChangeInfo = sortedChanges.get(3); |
| assertThat(masterChangeInfo.branch).isEqualTo("master"); |
| assertCodeReview(masterChangeInfo.id, 2, null); |
| |
| ChangeInfo rightChangeInfo = sortedChanges.get(4); |
| assertThat(rightChangeInfo.branch).isEqualTo("right"); |
| assertCodeReview(rightChangeInfo.id, 2, "autogenerated:Automerger"); |
| |
| // Ensure that commit subjects are correct |
| String masterSubject = masterChangeInfo.subject; |
| String shortMasterSha = masterChangeInfo.currentRevision.substring(0, 10); |
| String shortLeftSha = leftChangeInfo.currentRevision.substring(0, 10); |
| String shortRightSha = rightChangeInfo.currentRevision.substring(0, 10); |
| assertThat(masterChangeInfo.subject).doesNotContainMatch("automerger"); |
| assertThat(leftChangeInfo.subject) |
| .isEqualTo("[automerger] " + masterSubject + " am: " + shortMasterSha); |
| assertThat(rightChangeInfo.subject) |
| .isEqualTo("[automerger] " + masterSubject + " am: " + shortMasterSha); |
| |
| // Either bottomChangeInfoA came from left and bottomChangeInfoB came from right, or vice versa |
| // We don't know which, so we use the if condition to check |
| String bottomChangeInfoASecondParent = getParent(bottomChangeInfoA, 1); |
| if (bottomChangeInfoASecondParent.equals(leftChangeInfo.currentRevision)) { |
| assertThat(bottomChangeInfoA.subject) |
| .isEqualTo( |
| "[automerger] " + masterSubject + " am: " + shortMasterSha + " am: " + shortLeftSha); |
| assertThat(bottomChangeInfoB.subject) |
| .isEqualTo( |
| "[automerger] " + masterSubject + " am: " + shortMasterSha + " am: " + shortRightSha); |
| } else { |
| assertThat(bottomChangeInfoA.subject) |
| .isEqualTo( |
| "[automerger] " + masterSubject + " am: " + shortMasterSha + " am: " + shortRightSha); |
| assertThat(bottomChangeInfoB.subject) |
| .isEqualTo( |
| "[automerger] " + masterSubject + " am: " + shortMasterSha + " am: " + shortLeftSha); |
| } |
| } |
| |
| @Test |
| public void testDiamondMerge_identicalDiamondSides() throws Exception { |
| Project.NameKey manifestNameKey = defaultSetup(); |
| // Create initial change |
| PushOneCommit.Result initialResult = createChange("subject", "filename", "echo Hello"); |
| // Project name is scoped by test, so we need to get it from our initial change |
| String projectName = initialResult.getChange().project().get(); |
| createBranch(BranchNameKey.create(projectName, "left")); |
| createBranch(BranchNameKey.create(projectName, "right")); |
| initialResult.assertOkStatus(); |
| merge(initialResult); |
| |
| String leftRevision = gApi.projects().name(projectName).branch("left").get().revision; |
| String rightRevision = gApi.projects().name(projectName).branch("right").get().revision; |
| // For this test, right == left |
| assertThat(leftRevision).isEqualTo(rightRevision); |
| createBranch(BranchNameKey.create(projectName, "bottom")); |
| pushDiamondConfig(manifestNameKey.get(), projectName); |
| |
| // Freeze time so that the merge commit from left->bottom and right->bottom have same SHA |
| TestTimeUtil.resetWithClockStep(0, TimeUnit.MILLISECONDS); |
| TestTimeUtil.setClock(new Timestamp(TestTimeUtil.START.toEpochMilli())); |
| // After we upload our config, we upload a new patchset to create the downstreams. |
| PushOneCommit.Result result = |
| createChange(testRepo, "master", "subject", "filename2", "echo Hello", "sometopic"); |
| TestTimeUtil.useSystemTime(); |
| result.assertOkStatus(); |
| |
| List<ChangeInfo> changesInTopic = |
| gApi.changes() |
| .query("topic: " + gApi.changes().id(result.getChangeId()).topic()) |
| .withOption(CURRENT_REVISION) |
| .get(); |
| assertThat(changesInTopic).hasSize(4); |
| // +2 and submit |
| merge(result); |
| List<ChangeInfo> sortedChanges = sortedChanges(changesInTopic); |
| |
| // Should create two changes on bottom, since left and right are different. |
| ChangeInfo bottomChangeInfo = sortedChanges.get(0); |
| assertThat(bottomChangeInfo.branch).isEqualTo("bottom"); |
| assertCodeReview(bottomChangeInfo.id, 2, "autogenerated:Automerger"); |
| |
| ChangeInfo leftChangeInfo = sortedChanges.get(1); |
| assertThat(leftChangeInfo.branch).isEqualTo("left"); |
| assertCodeReview(leftChangeInfo.id, 2, "autogenerated:Automerger"); |
| |
| ChangeInfo masterChangeInfo = sortedChanges.get(2); |
| assertThat(masterChangeInfo.branch).isEqualTo("master"); |
| assertCodeReview(masterChangeInfo.id, 2, null); |
| |
| ChangeInfo rightChangeInfo = sortedChanges.get(3); |
| assertThat(rightChangeInfo.branch).isEqualTo("right"); |
| assertCodeReview(rightChangeInfo.id, 2, "autogenerated:Automerger"); |
| |
| // Ensure that commit subjects are correct |
| String masterSubject = masterChangeInfo.subject; |
| String shortMasterSha = masterChangeInfo.currentRevision.substring(0, 10); |
| String shortLeftSha = leftChangeInfo.currentRevision.substring(0, 10); |
| assertThat(masterChangeInfo.subject).doesNotContainMatch("automerger"); |
| assertThat(leftChangeInfo.subject) |
| .isEqualTo("[automerger] " + masterSubject + " am: " + shortMasterSha); |
| assertThat(rightChangeInfo.subject) |
| .isEqualTo("[automerger] " + masterSubject + " am: " + shortMasterSha); |
| assertThat(bottomChangeInfo.subject) |
| .isEqualTo( |
| "[automerger] " + masterSubject + " am: " + shortMasterSha + " am: " + shortLeftSha); |
| } |
| |
| @Test |
| public void testChangeStack() throws Exception { |
| Project.NameKey manifestNameKey = defaultSetup(); |
| // Create initial change |
| PushOneCommit.Result result = |
| createChange(testRepo, "master", "subject", "filename", "content", "testtopic"); |
| // Project name is scoped by test, so we need to get it from our initial change |
| String projectName = result.getChange().project().get(); |
| createBranch(BranchNameKey.create(projectName, "ds_one")); |
| pushSimpleConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one"); |
| // After we upload our config, we upload a new patchset to create the downstreams |
| amendChange(result.getChangeId()); |
| result.assertOkStatus(); |
| PushOneCommit.Result result2 = |
| createChange(testRepo, "master", "subject2", "filename2", "content2", "testtopic"); |
| result2.assertOkStatus(); |
| // Check that there are the correct number of changes in the topic |
| List<ChangeInfo> changesInTopic = |
| gApi.changes() |
| .query("topic: " + gApi.changes().id(result.getChangeId()).topic()) |
| .withOptions(ALL_REVISIONS, CURRENT_COMMIT) |
| .get(); |
| assertThat(changesInTopic).hasSize(4); |
| List<ChangeInfo> sortedChanges = sortedChanges(changesInTopic); |
| |
| // Change A' |
| ChangeInfo aPrime = sortedChanges.get(0); |
| assertThat(aPrime.branch).isEqualTo("ds_one"); |
| // Change B' |
| ChangeInfo bPrime = sortedChanges.get(1); |
| assertThat(bPrime.branch).isEqualTo("ds_one"); |
| String bPrimeFirstParent = getParent(bPrime, 0); |
| assertThat(aPrime.currentRevision).isEqualTo(bPrimeFirstParent); |
| |
| // Change A |
| ChangeInfo a = sortedChanges.get(2); |
| assertThat(a.branch).isEqualTo("master"); |
| // Change B |
| ChangeInfo b = sortedChanges.get(3); |
| assertThat(b.branch).isEqualTo("master"); |
| String bFirstParent = getParent(b, 0); |
| // Check that first parent of B is A |
| assertThat(bFirstParent).isEqualTo(a.currentRevision); |
| |
| // Ensure that commit subjects are correct |
| String shortASha = a.currentRevision.substring(0, 10); |
| assertThat(a.subject).doesNotContainMatch("automerger"); |
| assertThat(aPrime.subject).isEqualTo("[automerger] test commit am: " + shortASha); |
| } |
| |
| @Test |
| public void testChangeStack_rebaseAfterUpload() throws Exception { |
| Project.NameKey manifestNameKey = defaultSetup(); |
| // Save initial ref at HEAD |
| ObjectId initial = repo().exactRef("HEAD").getLeaf().getObjectId(); |
| // Create initial change |
| PushOneCommit.Result result = |
| createChange(testRepo, "master", "subject", "filename", "content", "testtopic"); |
| // Project name is scoped by test, so we need to get it from our initial change |
| String projectName = result.getChange().project().get(); |
| createBranch(BranchNameKey.create(projectName, "ds_one")); |
| pushSimpleConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one"); |
| // After we upload our config, we upload a new patchset to create the downstreams |
| amendChange(result.getChangeId()); |
| result.assertOkStatus(); |
| |
| // Reset to initial ref to create a sibling |
| testRepo.reset(initial); |
| |
| PushOneCommit.Result result2 = |
| createChange(testRepo, "master", "subject2", "filename2", "content2", "testtopic"); |
| result2.assertOkStatus(); |
| // Check that there are the correct number of changes in the topic |
| List<ChangeInfo> changesInTopic = |
| gApi.changes() |
| .query("topic: " + gApi.changes().id(result.getChangeId()).topic()) |
| .withOptions(ALL_REVISIONS, CURRENT_COMMIT) |
| .get(); |
| assertThat(changesInTopic).hasSize(4); |
| List<ChangeInfo> sortedChanges = sortedChanges(changesInTopic); |
| |
| // Check the first downstream change A' |
| ChangeInfo aPrime = sortedChanges.get(0); |
| assertThat(aPrime.branch).isEqualTo("ds_one"); |
| // Check the second downstream change B' |
| ChangeInfo bPrime = sortedChanges.get(1); |
| assertThat(bPrime.branch).isEqualTo("ds_one"); |
| // Check that B' does not have a first parent of A' yet |
| String bPrimeFirstParent = getParent(bPrime, 0); |
| assertThat(aPrime.currentRevision).isNotEqualTo(bPrimeFirstParent); |
| |
| // Change A |
| ChangeInfo a = sortedChanges.get(2); |
| assertThat(a.branch).isEqualTo("master"); |
| // Change B |
| ChangeInfo b = sortedChanges.get(3); |
| assertThat(b.branch).isEqualTo("master"); |
| String masterChangeInfo2FirstParentSha = getParent(b, 0); |
| // Check that first parent of B is not A |
| assertThat(a.currentRevision).isNotEqualTo(masterChangeInfo2FirstParentSha); |
| |
| // Rebase B on A |
| RebaseInput rebaseInput = new RebaseInput(); |
| rebaseInput.base = a.currentRevision; |
| gApi.changes().id(b.changeId).rebase(rebaseInput); |
| |
| // Check that B is now based on A, and B' is now based on A' |
| ChangeInfo bAfterRebase = gApi.changes().id(b.changeId).get(); |
| String bAfterRebaseFirstParent = getParent(bAfterRebase, 0); |
| assertThat(bAfterRebaseFirstParent).isEqualTo(a.currentRevision); |
| |
| ChangeInfo bPrimeAfterRebase = gApi.changes().id(bPrime.changeId).get(); |
| String bPrimeAfterRebaseFirstParent = getParent(bPrimeAfterRebase, 0); |
| assertThat(bPrimeAfterRebaseFirstParent).isEqualTo(aPrime.currentRevision); |
| } |
| |
| @Test |
| public void testBlankMerge() throws Exception { |
| Project.NameKey manifestNameKey = defaultSetup(); |
| // Create initial change |
| PushOneCommit.Result result = |
| createChange( |
| testRepo, "master", "DO NOT MERGE subject", "filename", "content", "testtopic"); |
| // Project name is scoped by test, so we need to get it from our initial change |
| String projectName = result.getChange().project().get(); |
| createBranch(BranchNameKey.create(projectName, "ds_one")); |
| createBranch(BranchNameKey.create(projectName, "ds_two")); |
| pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two"); |
| // After we upload our config, we upload a new patchset to create the downstreams |
| amendChange(result.getChangeId(), "DO NOT MERGE subject", "filename", "content"); |
| result.assertOkStatus(); |
| |
| ChangeApi change = gApi.changes().id(result.getChangeId()); |
| BinaryResult content = change.current().file("filename").content(); |
| |
| List<ChangeInfo> changesInTopic = |
| gApi.changes().query("topic: " + change.topic()).withOption(CURRENT_REVISION).get(); |
| assertThat(changesInTopic).hasSize(3); |
| |
| List<ChangeInfo> sortedChanges = sortedChanges(changesInTopic); |
| |
| ChangeInfo dsOneChangeInfo = sortedChanges.get(0); |
| assertThat(dsOneChangeInfo.branch).isEqualTo("ds_one"); |
| assertAutomergerChangeCreatedMessage(dsOneChangeInfo.id); |
| // It should skip ds_one, since this is a DO NOT MERGE |
| ChangeApi dsOneChange = gApi.changes().id(dsOneChangeInfo._number); |
| assertThat(dsOneChange.get().subject).contains("skipped:"); |
| assertThat(dsOneChange.current().files().keySet()).contains("filename"); |
| assertThat(dsOneChange.current().files().get("filename").linesDeleted).isEqualTo(1); |
| |
| ChangeInfo dsTwoChangeInfo = sortedChanges.get(1); |
| assertThat(dsTwoChangeInfo.branch).isEqualTo("ds_two"); |
| assertAutomergerChangeCreatedMessage(dsTwoChangeInfo.id); |
| // It should not skip ds_two, since it is marked with mergeAll: true |
| ChangeApi dsTwoChange = gApi.changes().id(dsTwoChangeInfo._number); |
| assertThat(dsTwoChange.get().subject).doesNotContain("skipped:"); |
| BinaryResult dsTwoContent = dsTwoChange.current().file("filename").content(); |
| assertThat(dsTwoContent.asString()).isEqualTo(content.asString()); |
| |
| ChangeInfo masterChangeInfo = sortedChanges.get(2); |
| assertCodeReview(masterChangeInfo.id, 0, null); |
| assertThat(masterChangeInfo.branch).isEqualTo("master"); |
| |
| // Ensure that commit subjects are correct |
| String masterSubject = masterChangeInfo.subject; |
| String shortMasterSha = masterChangeInfo.currentRevision.substring(0, 10); |
| assertThat(masterChangeInfo.subject).doesNotContainMatch("automerger"); |
| assertThat(dsOneChangeInfo.subject) |
| .isEqualTo("[automerger skipped] " + masterSubject + " skipped: " + shortMasterSha); |
| assertThat(dsTwoChangeInfo.subject) |
| .isEqualTo("[automerger] " + masterSubject + " am: " + shortMasterSha); |
| } |
| |
| @Test |
| public void testAlwaysBlankMerge() throws Exception { |
| Project.NameKey manifestNameKey = defaultSetup(); |
| // Create initial change |
| PushOneCommit.Result result = |
| createChange( |
| testRepo, |
| "master", |
| "DO NOT MERGE ANYWHERE subject", |
| "filename", |
| "content", |
| "testtopic"); |
| // Project name is scoped by test, so we need to get it from our initial change |
| String projectName = result.getChange().project().get(); |
| createBranch(BranchNameKey.create(projectName, "ds_one")); |
| createBranch(BranchNameKey.create(projectName, "ds_two")); |
| pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two"); |
| // After we upload our config, we upload a new patchset to create the downstreams |
| amendChange(result.getChangeId(), "DO NOT MERGE ANYWHERE subject", "filename", "content"); |
| result.assertOkStatus(); |
| |
| ChangeApi change = gApi.changes().id(result.getChangeId()); |
| List<ChangeInfo> changesInTopic = |
| gApi.changes().query("topic: " + change.topic()).withOption(CURRENT_REVISION).get(); |
| assertThat(changesInTopic).hasSize(3); |
| |
| List<ChangeInfo> sortedChanges = sortedChanges(changesInTopic); |
| |
| ChangeInfo dsOneChangeInfo = sortedChanges.get(0); |
| assertThat(dsOneChangeInfo.branch).isEqualTo("ds_one"); |
| assertAutomergerChangeCreatedMessage(dsOneChangeInfo.id); |
| ChangeApi dsOneChange = gApi.changes().id(dsOneChangeInfo._number); |
| // It should skip ds_one, since this is a DO NOT MERGE ANYWHERE |
| assertThat(dsOneChange.get().subject).contains("skipped:"); |
| assertThat(dsOneChange.current().files().keySet()).contains("filename"); |
| assertThat(dsOneChange.current().files().get("filename").linesDeleted).isEqualTo(1); |
| |
| ChangeInfo dsTwoChangeInfo = sortedChanges.get(1); |
| assertThat(dsTwoChangeInfo.branch).isEqualTo("ds_two"); |
| assertAutomergerChangeCreatedMessage(dsTwoChangeInfo.id); |
| ChangeApi dsTwoChange = gApi.changes().id(dsTwoChangeInfo._number); |
| // It should skip ds_one, since this is a DO NOT MERGE ANYWHERE |
| assertThat(dsTwoChange.get().subject).contains("skipped:"); |
| assertThat(dsTwoChange.current().files().keySet()).contains("filename"); |
| assertThat(dsTwoChange.current().files().get("filename").linesDeleted).isEqualTo(1); |
| |
| ChangeInfo masterChangeInfo = sortedChanges.get(2); |
| assertCodeReview(masterChangeInfo.id, 0, null); |
| assertThat(masterChangeInfo.branch).isEqualTo("master"); |
| |
| // Ensure that commit subjects are correct |
| String masterSubject = masterChangeInfo.subject; |
| String shortMasterSha = masterChangeInfo.currentRevision.substring(0, 10); |
| assertThat(masterChangeInfo.subject).doesNotContainMatch("automerger"); |
| assertThat(dsOneChangeInfo.subject) |
| .isEqualTo("[automerger skipped] " + masterSubject + " skipped: " + shortMasterSha); |
| assertThat(dsTwoChangeInfo.subject) |
| .isEqualTo("[automerger skipped] " + masterSubject + " skipped: " + shortMasterSha); |
| } |
| |
| @Test |
| public void testDownstreamMergeConflict() throws Exception { |
| Project.NameKey manifestNameKey = defaultSetup(); |
| // Create initial change |
| PushOneCommit.Result result = createChange("subject", "filename", "echo Hello"); |
| // Project name is scoped by test, so we need to get it from our initial change |
| String projectName = result.getChange().project().get(); |
| createBranch(BranchNameKey.create(projectName, "ds_one")); |
| createBranch(BranchNameKey.create(projectName, "ds_two")); |
| result.assertOkStatus(); |
| merge(result); |
| // Reset to create a sibling |
| ObjectId initial = repo().exactRef("HEAD").getLeaf().getObjectId(); |
| testRepo.reset(initial); |
| // Set up a merge conflict between master and ds_one |
| PushOneCommit.Result ds1Result = |
| createChange( |
| testRepo, "ds_one", "subject", "filename", "echo \"Hello asdfsd World\"", "randtopic"); |
| ds1Result.assertOkStatus(); |
| merge(ds1Result); |
| // Reset to allow our merge conflict to come |
| testRepo.reset(initial); |
| pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two"); |
| // After we upload our config, we upload a new change to create the downstreams |
| PushOneCommit.Result masterResult = |
| pushFactory |
| .create(admin.newIdent(), testRepo, "subject", "filename", "echo 'Hello World!'") |
| .to("refs/for/master"); |
| masterResult.assertOkStatus(); |
| |
| // Since there's a conflict with ds_one, there should only be two changes in the topic |
| List<ChangeInfo> changesInTopic = |
| gApi.changes() |
| .query("topic: " + gApi.changes().id(masterResult.getChangeId()).topic()) |
| .withOptions(CURRENT_REVISION, MESSAGES) |
| .get(); |
| assertThat(changesInTopic).hasSize(2); |
| List<ChangeInfo> sortedChanges = sortedChanges(changesInTopic); |
| |
| ChangeInfo dsTwoChangeInfo = sortedChanges.get(0); |
| assertThat(dsTwoChangeInfo.branch).isEqualTo("ds_two"); |
| // This is -2 because the -2 vote from master propagated to ds_two |
| assertCodeReview(dsTwoChangeInfo.id, -2, "autogenerated:Automerger"); |
| |
| ChangeInfo masterChangeInfo = sortedChanges.get(1); |
| assertCodeReview(masterChangeInfo.id, -2, "autogenerated:MergeConflict"); |
| assertThat(masterChangeInfo.branch).isEqualTo("master"); |
| |
| // Make sure that merge conflict message is still added |
| List<String> messages = new ArrayList<>(); |
| for (ChangeMessageInfo cmi : masterChangeInfo.messages) { |
| messages.add(cmi.message); |
| } |
| assertThat(messages).contains("Patch Set 1: Code-Review-2\n\nMerge conflict found on ds_one"); |
| |
| // Ensure that commit subjects are correct |
| String masterSubject = masterChangeInfo.subject; |
| String shortMasterSha = masterChangeInfo.currentRevision.substring(0, 10); |
| assertThat(masterChangeInfo.subject).doesNotContainMatch("automerger"); |
| assertThat(dsTwoChangeInfo.subject) |
| .isEqualTo("[automerger] " + masterSubject + " am: " + shortMasterSha); |
| } |
| |
| @Test |
| public void testRestrictedVotePermissions() throws Exception { |
| Project.NameKey manifestNameKey = defaultSetup(); |
| // Create initial change |
| PushOneCommit.Result result = createChange("subject", "filename", "echo Hello"); |
| // Project name is scoped by test, so we need to get it from our initial change |
| String projectName = result.getChange().project().get(); |
| createBranch(BranchNameKey.create(projectName, "ds_one")); |
| createBranch(BranchNameKey.create(projectName, "ds_two")); |
| result.assertOkStatus(); |
| merge(result); |
| // Reset to create a sibling |
| ObjectId initial = repo().exactRef("HEAD").getLeaf().getObjectId(); |
| testRepo.reset(initial); |
| // Set up a merge conflict between master and ds_one |
| PushOneCommit.Result ds1Result = |
| createChange( |
| testRepo, "ds_one", "subject", "filename", "echo \"Hello asdfsd World\"", "randtopic"); |
| ds1Result.assertOkStatus(); |
| merge(ds1Result); |
| // Reset to allow our merge conflict to come |
| testRepo.reset(initial); |
| pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two"); |
| |
| // Block Code Review label to test restrictions |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| blockLabel("Code-Review") |
| .ref("refs/heads/*") |
| .group(SystemGroupBackend.CHANGE_OWNER) |
| .range(-2, 2)) |
| .update(); |
| |
| // After we upload our config, we upload a new change to create the downstreams |
| PushOneCommit.Result masterResult = |
| pushFactory |
| .create(admin.newIdent(), testRepo, "subject", "filename", "echo 'Hello World!'") |
| .to("refs/for/master"); |
| masterResult.assertOkStatus(); |
| |
| // Since there's a conflict with ds_one, there should only be two changes in the topic |
| List<ChangeInfo> changesInTopic = |
| gApi.changes() |
| .query("topic: " + gApi.changes().id(masterResult.getChangeId()).topic()) |
| .withOptions(CURRENT_REVISION, MESSAGES) |
| .get(); |
| assertThat(changesInTopic).hasSize(2); |
| List<ChangeInfo> sortedChanges = sortedChanges(changesInTopic); |
| |
| ChangeInfo dsTwoChangeInfo = sortedChanges.get(0); |
| assertThat(dsTwoChangeInfo.branch).isEqualTo("ds_two"); |
| assertAutomergerChangeCreatedMessage(dsTwoChangeInfo.id); |
| |
| ChangeInfo masterChangeInfo = sortedChanges.get(1); |
| // This is 0 because the -2 vote on master failed due to permissions |
| assertCodeReview(masterChangeInfo.id, 0, null); |
| assertThat(masterChangeInfo.branch).isEqualTo("master"); |
| assertThat(getLastMessage(masterChangeInfo.id).tag).isEqualTo("autogenerated:MergeConflict"); |
| |
| // Make sure that merge conflict message is still added |
| List<String> messages = new ArrayList<>(); |
| for (ChangeMessageInfo cmi : masterChangeInfo.messages) { |
| messages.add(cmi.message); |
| } |
| assertThat(messages).contains("Patch Set 1:\n\nMerge conflict found on ds_one"); |
| |
| // Ensure that commit subjects are correct |
| String masterSubject = masterChangeInfo.subject; |
| String shortMasterSha = masterChangeInfo.currentRevision.substring(0, 10); |
| assertThat(masterChangeInfo.subject).doesNotContainMatch("automerger"); |
| assertThat(dsTwoChangeInfo.subject) |
| .isEqualTo("[automerger] " + masterSubject + " am: " + shortMasterSha); |
| } |
| |
| @Test |
| public void testTopicEditedListener() throws Exception { |
| Project.NameKey manifestNameKey = defaultSetup(); |
| // Create initial change |
| PushOneCommit.Result result = |
| createChange(testRepo, "master", "subject", "filename", "content", "testtopic"); |
| // Project name is scoped by test, so we need to get it from our initial change |
| String projectName = result.getChange().project().get(); |
| createBranch(BranchNameKey.create(projectName, "ds_one")); |
| createBranch(BranchNameKey.create(projectName, "ds_two")); |
| pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two"); |
| // After we upload our config, we upload a new patchset to create the downstreams |
| amendChange(result.getChangeId()); |
| result.assertOkStatus(); |
| gApi.changes().id(result.getChangeId()).topic("multiple words"); |
| gApi.changes().id(result.getChangeId()).topic("singlewordagain"); |
| // Check that there are the correct number of changes in the topic |
| List<ChangeInfo> changesInTopic = |
| gApi.changes() |
| .query("topic:\"" + gApi.changes().id(result.getChangeId()).topic() + "\"") |
| .get(); |
| assertThat(changesInTopic).hasSize(3); |
| // +2 and submit |
| merge(result); |
| } |
| |
| @Test |
| public void testTopicEditedListener_withQuotes() throws Exception { |
| Project.NameKey manifestNameKey = defaultSetup(); |
| // Create initial change |
| PushOneCommit.Result result = |
| createChange(testRepo, "master", "subject", "filename", "content", "testtopic"); |
| // Project name is scoped by test, so we need to get it from our initial change |
| String projectName = result.getChange().project().get(); |
| createBranch(BranchNameKey.create(projectName, "ds_one")); |
| createBranch(BranchNameKey.create(projectName, "ds_two")); |
| pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two"); |
| // After we upload our config, we upload a new patchset to create the downstreams |
| amendChange(result.getChangeId()); |
| result.assertOkStatus(); |
| gApi.changes().id(result.getChangeId()).topic("multiple words"); |
| gApi.changes().id(result.getChangeId()).topic("with\"quotes\"inside"); |
| // Gerrit fails to submit changes in the same topic together if it contains quotes. |
| gApi.changes().id(result.getChangeId()).topic("without quotes anymore"); |
| // Check that there are the correct number of changes in the topic |
| List<ChangeInfo> changesInTopic = |
| gApi.changes() |
| .query("topic:{" + gApi.changes().id(result.getChangeId()).topic() + "}") |
| .get(); |
| assertThat(changesInTopic).hasSize(3); |
| // +2 and submit |
| merge(result); |
| } |
| |
| @Test |
| public void testTopicEditedListener_withBraces() throws Exception { |
| Project.NameKey manifestNameKey = defaultSetup(); |
| // Create initial change |
| PushOneCommit.Result result = |
| createChange(testRepo, "master", "subject", "filename", "content", "testtopic"); |
| // Project name is scoped by test, so we need to get it from our initial change |
| String projectName = result.getChange().project().get(); |
| createBranch(BranchNameKey.create(projectName, "ds_one")); |
| createBranch(BranchNameKey.create(projectName, "ds_two")); |
| pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two"); |
| // After we upload our config, we upload a new patchset to create the downstreams |
| amendChange(result.getChangeId()); |
| result.assertOkStatus(); |
| gApi.changes().id(result.getChangeId()).topic("multiple words"); |
| gApi.changes().id(result.getChangeId()).topic("with{braces}inside"); |
| // Check that there are the correct number of changes in the topic |
| List<ChangeInfo> changesInTopic = |
| gApi.changes() |
| .query("topic:\"" + gApi.changes().id(result.getChangeId()).topic() + "\"") |
| .get(); |
| assertThat(changesInTopic).hasSize(3); |
| // +2 and submit |
| merge(result); |
| } |
| |
| @Test |
| public void testTopicEditedListener_branchWithBracesAndQuotes() throws Exception { |
| Project.NameKey manifestNameKey = defaultSetup(); |
| // Create initial change |
| PushOneCommit.Result result = |
| createChange(testRepo, "master", "subject", "filename", "content", "testtopic"); |
| // Project name is scoped by test, so we need to get it from our initial change |
| String projectName = result.getChange().project().get(); |
| createBranch(BranchNameKey.create(projectName, "branch{}braces")); |
| createBranch(BranchNameKey.create(projectName, "branch\"quotes")); |
| pushDefaultConfig( |
| "automerger.config", |
| manifestNameKey.get(), |
| projectName, |
| "branch{}braces", |
| "branch\"quotes"); |
| // After we upload our config, we upload a new patchset to create the downstreams |
| amendChange(result.getChangeId()); |
| result.assertOkStatus(); |
| // Check that there are the correct number of changes in the topic |
| List<ChangeInfo> changesInTopic = |
| gApi.changes() |
| .query("topic:\"" + gApi.changes().id(result.getChangeId()).topic() + "\"") |
| .get(); |
| assertThat(changesInTopic).hasSize(3); |
| // +2 and submit |
| merge(result); |
| } |
| |
| @Test |
| public void testTopicEditedListener_emptyTopic() throws Exception { |
| Project.NameKey manifestNameKey = defaultSetup(); |
| // Create initial change |
| PushOneCommit.Result result = |
| createChange(testRepo, "master", "subject", "filename", "content", "testtopic"); |
| // Project name is scoped by test, so we need to get it from our initial change |
| String projectName = result.getChange().project().get(); |
| createBranch(BranchNameKey.create(projectName, "ds_one")); |
| pushSimpleConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one"); |
| // After we upload our config, we upload a new patchset to create the downstreams |
| amendChange(result.getChangeId()); |
| result.assertOkStatus(); |
| // Setting the topic to empty should be a no-op. |
| gApi.changes().id(result.getChangeId()).topic(""); |
| assertThat(gApi.changes().id(result.getChangeId()).topic()) |
| .isEqualTo(gApi.changes().id(result.getChangeId()).topic()); |
| // +2 and submit |
| merge(result); |
| } |
| |
| @Test |
| public void testContextUser() throws Exception { |
| // Branch flow for contextUser is master -> ds_one -> ds_two |
| Project.NameKey manifestNameKey = defaultSetup(); |
| // Create initial change |
| PushOneCommit.Result initialResult = createChange("subject", "filename", "echo Hello"); |
| // Project name is scoped by test, so we need to get it from our initial change |
| Project.NameKey projectNameKey = initialResult.getChange().project(); |
| String projectName = projectNameKey.get(); |
| createBranch(BranchNameKey.create(projectName, "ds_one")); |
| createBranch(BranchNameKey.create(projectName, "ds_two")); |
| initialResult.assertOkStatus(); |
| merge(initialResult); |
| |
| // Create normalUserGroup, containing user, and contextUserGroup, containing contextUser |
| String normalUserGroup = groupOperations.newGroup().name("normalUserGroup").create().get(); |
| gApi.groups().id(normalUserGroup).addMembers(user.id().toString()); |
| AccountApi contextUserApi = gApi.accounts().create("someContextUser"); |
| String contextUserGroup = groupOperations.newGroup().name("contextUserGroup").create().get(); |
| gApi.groups().id(contextUserGroup).addMembers(contextUserApi.get().name); |
| |
| // Grant exclusive +2 to context user |
| projectOperations |
| .project(projectNameKey) |
| .forUpdate() |
| .add( |
| TestProjectUpdate.allowLabel("Code-Review") |
| .ref("refs/heads/ds_one") |
| .group(AccountGroup.UUID.parse(gApi.groups().id(contextUserGroup).get().id)) |
| .range(-2, 2)) |
| .setExclusiveGroup(labelPermissionKey("Code-Review").ref("refs/heads/ds_one"), true) |
| .update(); |
| pushContextUserConfig( |
| manifestNameKey.get(), projectName, contextUserApi.get()._accountId.toString()); |
| |
| // After we upload our config, we upload a new patchset to create the downstreams |
| PushOneCommit.Result result = |
| createChange(testRepo, "master", "subject", "filename2", "echo Hello", "sometopic"); |
| result.assertOkStatus(); |
| // Check that there are the correct number of changes in the topic |
| List<ChangeInfo> changesInTopic = |
| gApi.changes() |
| .query("topic: " + gApi.changes().id(result.getChangeId()).topic()) |
| .withOptions(CURRENT_REVISION, CURRENT_COMMIT) |
| .get(); |
| assertThat(changesInTopic).hasSize(3); |
| |
| List<ChangeInfo> sortedChanges = sortedChanges(changesInTopic); |
| |
| // Check that downstream is at Code-Review 0 |
| ChangeInfo dsOneChangeInfo = sortedChanges.get(0); |
| assertThat(dsOneChangeInfo.branch).isEqualTo("ds_one"); |
| assertCodeReview(dsOneChangeInfo.id, 0, null); |
| |
| // Try to +2 master and see it succeed to +2 master and ds_one |
| ChangeInfo masterChangeInfo = sortedChanges.get(2); |
| assertThat(masterChangeInfo.branch).isEqualTo("master"); |
| assertCodeReviewMissing(masterChangeInfo.id); |
| approve(masterChangeInfo.id); |
| assertCodeReview(masterChangeInfo.id, 2, null); |
| assertCodeReview(dsOneChangeInfo.id, 2, "autogenerated:Automerger"); |
| |
| // Try to +2 downstream and see it fail |
| AuthException thrown = assertThrows(AuthException.class, () -> approve(dsOneChangeInfo.id)); |
| assertThat(thrown).hasMessageThat().contains("Applying label \"Code-Review\": 2 is restricted"); |
| } |
| |
| @Test |
| public void testContextUser_downstreamHighestVote() throws Exception { |
| // Branch flow for contextUser is master -> ds_one -> ds_two |
| Project.NameKey manifestNameKey = defaultSetup(); |
| // Create initial change |
| PushOneCommit.Result initialResult = createChange("subject", "filename", "echo Hello"); |
| // Project name is scoped by test, so we need to get it from our initial change |
| Project.NameKey projectNameKey = initialResult.getChange().project(); |
| String projectName = projectNameKey.get(); |
| createBranch(BranchNameKey.create(projectName, "ds_one")); |
| createBranch(BranchNameKey.create(projectName, "ds_two")); |
| initialResult.assertOkStatus(); |
| merge(initialResult); |
| |
| // Create normalUserGroup, containing user, and contextUserGroup, containing contextUser |
| String normalUserGroup = groupOperations.newGroup().name("normalUserGroup").create().get(); |
| gApi.groups().id(normalUserGroup).addMembers(user.id().toString()); |
| AccountApi contextUserApi = gApi.accounts().create("randomContextUser"); |
| String contextUserGroup = groupOperations.newGroup().name("contextUserGroup").create().get(); |
| gApi.groups().id(contextUserGroup).addMembers(contextUserApi.get().name); |
| |
| // Grant +2 to context user, since it doesn't have it by default |
| projectOperations |
| .project(projectNameKey) |
| .forUpdate() |
| .add( |
| TestProjectUpdate.allowLabel("Code-Review") |
| .ref("refs/heads/*") |
| .group(AccountGroup.UUID.parse(gApi.groups().id(contextUserGroup).get().id)) |
| .range(-2, 2)) |
| .update(); |
| pushContextUserConfig( |
| manifestNameKey.get(), projectName, contextUserApi.get()._accountId.toString()); |
| |
| // After we upload our config, we upload a new patchset to create the downstreams |
| PushOneCommit.Result result = |
| createChange(testRepo, "master", "subject", "filename2", "echo Hello", "sometopic"); |
| result.assertOkStatus(); |
| // Check that there are the correct number of changes in the topic |
| List<ChangeInfo> changesInTopic = |
| gApi.changes() |
| .query("topic: " + gApi.changes().id(result.getChangeId()).topic()) |
| .withOptions(CURRENT_REVISION, CURRENT_COMMIT) |
| .get(); |
| assertThat(changesInTopic).hasSize(3); |
| |
| List<ChangeInfo> sortedChanges = sortedChanges(changesInTopic); |
| |
| // Check that downstream is at Code-Review 0 |
| ChangeInfo dsOneChangeInfo = sortedChanges.get(0); |
| assertThat(dsOneChangeInfo.branch).isEqualTo("ds_one"); |
| assertCodeReview(dsOneChangeInfo.id, 0, null); |
| |
| // Try to +1 master and see it succeed to +1 master and ds_one |
| ChangeInfo masterChangeInfo = sortedChanges.get(2); |
| assertThat(masterChangeInfo.branch).isEqualTo("master"); |
| assertCodeReviewMissing(masterChangeInfo.id); |
| recommend(masterChangeInfo.id); |
| assertCodeReview(masterChangeInfo.id, 1, null); |
| assertCodeReview(dsOneChangeInfo.id, 1, "autogenerated:Automerger"); |
| |
| ChangeInfo dsTwoChangeInfo = sortedChanges.get(1); |
| assertThat(dsTwoChangeInfo.branch).isEqualTo("ds_two"); |
| assertCodeReview(dsTwoChangeInfo.id, 1, "autogenerated:Automerger"); |
| |
| // +2 ds_one and see that it overrides the +1 of the contextUser |
| approve(dsOneChangeInfo.id); |
| assertCodeReview(dsOneChangeInfo.id, 2, null); |
| assertCodeReview(dsTwoChangeInfo.id, 2, "autogenerated:Automerger"); |
| // +0 ds_one and see that it goes back to the +1 of the contextUser |
| gApi.changes().id(dsOneChangeInfo.id).revision("current").review(ReviewInput.noScore()); |
| assertCodeReview(dsOneChangeInfo.id, 1, "autogenerated:Automerger"); |
| assertCodeReview(dsTwoChangeInfo.id, 1, "autogenerated:Automerger"); |
| } |
| |
| @Test |
| public void testContextUser_mergeConflictOnDownstreamVotesOnTopLevel() throws Exception { |
| // Branch flow for contextUser is master -> ds_one -> ds_two |
| Project.NameKey manifestNameKey = defaultSetup(); |
| // Create initial change |
| PushOneCommit.Result initialResult = createChange("subject", "filename", "echo Hello"); |
| // Project name is scoped by test, so we need to get it from our initial change |
| Project.NameKey projectNameKey = initialResult.getChange().project(); |
| String projectName = projectNameKey.get(); |
| createBranch(BranchNameKey.create(projectName, "ds_one")); |
| createBranch(BranchNameKey.create(projectName, "ds_two")); |
| initialResult.assertOkStatus(); |
| merge(initialResult); |
| |
| // Reset to create a sibling |
| ObjectId initial = repo().exactRef("HEAD").getLeaf().getObjectId(); |
| PushOneCommit.Result ds1Result = |
| createChange( |
| testRepo, "ds_one", "subject", "filename", "echo \"Hello asdfsd World\"", "randtopic"); |
| ds1Result.assertOkStatus(); |
| merge(ds1Result); |
| // Reset to allow our merge conflict to come |
| testRepo.reset(initial); |
| // Set up a merge conflict between ds_one and ds_two |
| PushOneCommit.Result ds2Result = |
| createChange( |
| testRepo, "ds_two", "subject", "filename", "echo yo World wutup wutup", "randtopic"); |
| ds2Result.assertOkStatus(); |
| merge(ds2Result); |
| testRepo.reset(initial); |
| |
| // Create normalUserGroup, containing current user, and contextUserGroup, containing contextUser |
| String normalUserGroup = groupOperations.newGroup().name("normalUserGroup").create().get(); |
| gApi.groups().id(normalUserGroup).addMembers(user.id().toString()); |
| AccountApi contextUserApi = gApi.accounts().create("asdfContextUser"); |
| String contextUserGroup = groupOperations.newGroup().name("contextUserGroup").create().get(); |
| gApi.groups().id(contextUserGroup).addMembers(contextUserApi.get().name); |
| |
| // Grant +2 to context user, since it doesn't have it by default |
| projectOperations |
| .project(projectNameKey) |
| .forUpdate() |
| .add( |
| TestProjectUpdate.allowLabel("Code-Review") |
| .ref("refs/heads/*") |
| .group(AccountGroup.UUID.parse(gApi.groups().id(contextUserGroup).get().id)) |
| .range(-2, 2)) |
| .update(); |
| pushContextUserConfig( |
| manifestNameKey.get(), projectName, contextUserApi.get()._accountId.toString()); |
| |
| // After we upload our config, we upload a new patchset to create the downstreams |
| PushOneCommit.Result result = |
| createChange(testRepo, "master", "subject", "filename2", "echo Hello", "sometopic"); |
| result.assertOkStatus(); |
| // Check that there are the correct number of changes in the topic |
| List<ChangeInfo> changesInTopic = |
| gApi.changes() |
| .query("topic: " + gApi.changes().id(result.getChangeId()).topic()) |
| .withOptions(CURRENT_REVISION, CURRENT_COMMIT) |
| .get(); |
| // There should only be two, as ds_one to ds_two should be a merge conflict |
| assertThat(changesInTopic).hasSize(2); |
| |
| List<ChangeInfo> sortedChanges = sortedChanges(changesInTopic); |
| |
| // Check that master is at Code-Review -2 |
| ChangeInfo masterChangeInfo = sortedChanges.get(1); |
| assertThat(masterChangeInfo.branch).isEqualTo("master"); |
| assertCodeReview(masterChangeInfo.id, -2, "autogenerated:MergeConflict"); |
| } |
| |
| private Project.NameKey defaultSetup() throws Exception { |
| Project.NameKey manifestNameKey = |
| projectOperations.newProject().name(name("platform/manifest")).create(); |
| setupTestRepo("default.xml", manifestNameKey, "master", "default.xml"); |
| setupTestRepo("ds_one.xml", manifestNameKey, "ds_one", "default.xml"); |
| setupTestRepo("ds_two.xml", manifestNameKey, "ds_two", "default.xml"); |
| return manifestNameKey; |
| } |
| |
| private void setupTestRepo( |
| String resourceName, Project.NameKey projectNameKey, String branchName, String filename) |
| throws Exception { |
| TestRepository<InMemoryRepository> repo = cloneProject(projectNameKey, admin); |
| try (InputStream in = getClass().getResourceAsStream(resourceName)) { |
| String resourceString = CharStreams.toString(new InputStreamReader(in, Charsets.UTF_8)); |
| |
| PushOneCommit push = |
| pushFactory.create(admin.newIdent(), repo, "some subject", filename, resourceString); |
| push.to("refs/heads/" + branchName).assertOkStatus(); |
| } |
| } |
| |
| private void pushConfig(List<ConfigOption> cfgOptions, String resourceName) throws Exception { |
| TestRepository<InMemoryRepository> allProjectRepo = cloneProject(allProjects, admin); |
| GitUtil.fetch(allProjectRepo, RefNames.REFS_CONFIG + ":config"); |
| allProjectRepo.reset("config"); |
| try (InputStream in = getClass().getResourceAsStream(resourceName)) { |
| String resourceString = CharStreams.toString(new InputStreamReader(in, Charsets.UTF_8)); |
| |
| Config cfg = new Config(); |
| cfg.fromText(resourceString); |
| |
| for (ConfigOption cfgOption : cfgOptions) { |
| cfg.setString(cfgOption.section, cfgOption.subsection, cfgOption.key, cfgOption.value); |
| } |
| |
| PushOneCommit push = |
| pushFactory.create( |
| admin.newIdent(), allProjectRepo, "Subject", "automerger.config", cfg.toText()); |
| push.to(RefNames.REFS_CONFIG).assertOkStatus(); |
| } |
| } |
| |
| private void pushSimpleConfig( |
| String resourceName, String manifestName, String project, String branch1) throws Exception { |
| List<ConfigOption> options = new ArrayList<>(); |
| options.add(new ConfigOption("global", null, "manifestProject", manifestName)); |
| options.add(new ConfigOption("automerger", "master:" + branch1, "setProjects", project)); |
| pushConfig(options, resourceName); |
| } |
| |
| private void pushDefaultConfig( |
| String resourceName, String manifestName, String project, String branch1, String branch2) |
| throws Exception { |
| List<ConfigOption> options = new ArrayList<>(); |
| options.add(new ConfigOption("global", null, "manifestProject", manifestName)); |
| options.add(new ConfigOption("automerger", "master:" + branch1, "setProjects", project)); |
| options.add(new ConfigOption("automerger", "master:" + branch2, "setProjects", project)); |
| pushConfig(options, resourceName); |
| } |
| |
| private void pushDiamondConfig(String manifestName, String project) throws Exception { |
| List<ConfigOption> options = new ArrayList<>(); |
| options.add(new ConfigOption("global", null, "manifestProject", manifestName)); |
| options.add(new ConfigOption("automerger", "master:left", "setProjects", project)); |
| options.add(new ConfigOption("automerger", "master:right", "setProjects", project)); |
| options.add(new ConfigOption("automerger", "left:bottom", "setProjects", project)); |
| options.add(new ConfigOption("automerger", "right:bottom", "setProjects", project)); |
| pushConfig(options, "diamond.config"); |
| } |
| |
| private void pushContextUserConfig(String manifestName, String project, String contextUserId) |
| throws Exception { |
| List<ConfigOption> options = new ArrayList<>(); |
| options.add(new ConfigOption("global", null, "manifestProject", manifestName)); |
| options.add(new ConfigOption("global", null, "contextUserId", contextUserId)); |
| options.add(new ConfigOption("automerger", "master:ds_one", "setProjects", project)); |
| options.add(new ConfigOption("automerger", "ds_one:ds_two", "setProjects", project)); |
| pushConfig(options, "context_user.config"); |
| } |
| |
| private Optional<ApprovalInfo> getCodeReview(String id) throws RestApiException { |
| List<ApprovalInfo> approvals = |
| gApi.changes().id(id).get(DETAILED_LABELS).labels.get("Code-Review").all; |
| if (approvals == null) { |
| return Optional.empty(); |
| } |
| return approvals.stream().max(comparing(a -> a.value)); |
| } |
| |
| private void assertCodeReview(String id, int expectedValue, @Nullable String expectedTag) |
| throws RestApiException { |
| Optional<ApprovalInfo> vote = getCodeReview(id); |
| assertWithMessage("Code-Review vote").about(optionals()).that(vote).isPresent(); |
| assertWithMessage("value of Code-Review vote").that(vote.get().value).isEqualTo(expectedValue); |
| assertWithMessage("tag of Code-Review vote").that(vote.get().tag).isEqualTo(expectedTag); |
| } |
| |
| private void assertCodeReviewMissing(String id) throws RestApiException { |
| assertThat(getCodeReview(id)).isEmpty(); |
| } |
| |
| private void assertAutomergerChangeCreatedMessage(String id) throws RestApiException { |
| ChangeMessageInfo message = getLastMessage(id); |
| assertThat(message.message).contains("Automerger change created!"); |
| assertThat(message.tag).isEqualTo("autogenerated:Automerger"); |
| } |
| |
| private ChangeMessageInfo getLastMessage(String id) throws RestApiException { |
| List<ChangeMessageInfo> messages = gApi.changes().id(id).messages(); |
| assertThat(messages).isNotEmpty(); |
| return Iterables.getLast(messages); |
| } |
| |
| private ImmutableList<ChangeInfo> sortedChanges(List<ChangeInfo> changes) { |
| return changes.stream() |
| .sorted(comparing((ChangeInfo c) -> c.branch).thenComparing(c -> c._number)) |
| .collect(toImmutableList()); |
| } |
| |
| public String getParent(ChangeInfo info, int number) { |
| return info.revisions.get(info.currentRevision).commit.parents.get(number).commit; |
| } |
| } |