blob: f3e7d1b7188a8eb71b5da2db42ed1f1b53c3564d [file] [log] [blame]
// Copyright (C) 2020 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.plugins.codeowners.acceptance.api;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerStatusInfoSubject.assertThat;
import static com.google.gerrit.plugins.codeowners.testing.LegacySubmitRequirementInfoSubject.assertThatCollection;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.TestMetricMaker;
import com.google.gerrit.acceptance.TestProjectInput;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.api.projects.DeleteBranchesInput;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
import com.google.gerrit.plugins.codeowners.api.CodeOwnerStatusInfo;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerSet;
import com.google.gerrit.plugins.codeowners.backend.findowners.FindOwnersBackend;
import com.google.gerrit.plugins.codeowners.common.CodeOwnerStatus;
import com.google.gerrit.plugins.codeowners.testing.LegacySubmitRequirementInfoSubject;
import com.google.gerrit.plugins.codeowners.util.JgitPath;
import com.google.inject.Inject;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Test;
/** Acceptance test for {@code com.google.gerrit.plugins.codeowners.backend.CodeOwnerSubmitRule}. */
public class CodeOwnerSubmitRuleIT extends AbstractCodeOwnersIT {
@Inject private RequestScopeOperations requestScopeOperations;
@Inject private ProjectOperations projectOperations;
@Inject private TestMetricMaker testMetricMaker;
@Test
public void changeIsSubmittableIfCodeOwnersFuctionalityIsDisabled() throws Exception {
disableCodeOwnersForProject(project);
String changeId = createChange("Test Change", "foo/bar.baz", "file content").getChangeId();
// Approve by a non-code-owner.
approve(changeId);
// Check the submittable flag.
ChangeInfo changeInfo = gApi.changes().id(changeId).get(ListChangesOption.SUBMITTABLE);
assertThat(changeInfo.submittable).isTrue();
// Check that there is no submit requirement.
assertThat(changeInfo.requirements).isEmpty();
// Submit the change.
gApi.changes().id(changeId).current().submit();
assertThat(gApi.changes().id(changeId).get().status).isEqualTo(ChangeStatus.MERGED);
}
@Test
@GerritConfig(name = "plugin.code-owners.disabled", value = "true")
@GerritConfig(name = "plugin.code-owners.requiredApproval", value = "INVALID")
public void changeIsSubmittableIfCodeOwnersFuctionalityIsDisabled_invalidPluginConfig()
throws Exception {
String changeId = createChange("Test Change", "foo/bar.baz", "file content").getChangeId();
// Approve by a non-code-owner.
approve(changeId);
// Check the submittable flag.
ChangeInfo changeInfo = gApi.changes().id(changeId).get(ListChangesOption.SUBMITTABLE);
assertThat(changeInfo.submittable).isTrue();
// Check that there is no submit requirement.
assertThat(changeInfo.requirements).isEmpty();
// Submit the change.
gApi.changes().id(changeId).current().submit();
assertThat(gApi.changes().id(changeId).get().status).isEqualTo(ChangeStatus.MERGED);
}
@Test
public void changeWithInsufficientReviewersIsNotSubmittable() throws Exception {
testChangeWithInsufficientReviewersIsNotSubmittable();
}
@Test
@TestProjectInput(createEmptyCommit = false)
public void initialChangeWithInsufficientReviewersIsNotSubmittable() throws Exception {
testChangeWithInsufficientReviewersIsNotSubmittable();
}
private void testChangeWithInsufficientReviewersIsNotSubmittable() throws Exception {
String changeId = createChange("Test Change", "foo/bar.baz", "file content").getChangeId();
// Approve by a non-code-owner.
approve(changeId);
// Verify that the code owner status for the changed file is INSUFFICIENT_REVIEWERS.
CodeOwnerStatusInfo codeOwnerStatus =
changeCodeOwnersApiFactory.change(changeId).getCodeOwnerStatus().get();
assertThat(codeOwnerStatus)
.hasFileCodeOwnerStatusesThat()
.onlyElement()
.hasNewPathStatusThat()
.value()
.hasStatusThat()
.isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
// Check the submittable flag.
ChangeInfo changeInfo = gApi.changes().id(changeId).get(ListChangesOption.SUBMITTABLE);
assertThat(changeInfo.submittable).isFalse();
// Check the submit requirement.
LegacySubmitRequirementInfoSubject submitRequirementInfoSubject =
assertThatCollection(changeInfo.requirements).onlyElement();
submitRequirementInfoSubject.hasStatusThat().isEqualTo("NOT_READY");
submitRequirementInfoSubject.hasFallbackTextThat().isEqualTo("Code-Owners");
submitRequirementInfoSubject.hasTypeThat().isEqualTo("code-owners");
// Try to submit the change.
ResourceConflictException exception =
assertThrows(
ResourceConflictException.class, () -> gApi.changes().id(changeId).current().submit());
assertThat(exception)
.hasMessageThat()
.isEqualTo(
String.format(
"Failed to submit 1 change due to the following problems:\n"
+ "Change %d: submit requirement 'Code-Owners' is unsatisfied.",
changeInfo._number));
}
@Test
public void changeWithPendingCodeOwnerApprovalsIsNotSubmittable() throws Exception {
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/")
.addCodeOwnerEmail(user.email())
.create();
testChangeWithPendingCodeOwnerApprovalsIsNotSubmittable(user);
}
@Test
@TestProjectInput(createEmptyCommit = false)
public void initialChangeWithPendingCodeOwnerApprovalsIsNotSubmittable() throws Exception {
setAsDefaultCodeOwners(user);
testChangeWithPendingCodeOwnerApprovalsIsNotSubmittable(user);
}
private void testChangeWithPendingCodeOwnerApprovalsIsNotSubmittable(TestAccount codeOwner)
throws Exception {
String changeId = createChange("Test Change", "foo/bar.baz", "file content").getChangeId();
// Add a reviewer that is a code owner.
gApi.changes().id(changeId).addReviewer(codeOwner.email());
// Approve by a non-code-owner.
approve(changeId);
// Verify that the code owner status for the changed file is PENDING.
CodeOwnerStatusInfo codeOwnerStatus =
changeCodeOwnersApiFactory.change(changeId).getCodeOwnerStatus().get();
assertThat(codeOwnerStatus)
.hasFileCodeOwnerStatusesThat()
.onlyElement()
.hasNewPathStatusThat()
.value()
.hasStatusThat()
.isEqualTo(CodeOwnerStatus.PENDING);
// Check the submittable flag.
ChangeInfo changeInfo = gApi.changes().id(changeId).get(ListChangesOption.SUBMITTABLE);
assertThat(changeInfo.submittable).isFalse();
// Check the submit requirement.
LegacySubmitRequirementInfoSubject submitRequirementInfoSubject =
assertThatCollection(changeInfo.requirements).onlyElement();
submitRequirementInfoSubject.hasStatusThat().isEqualTo("NOT_READY");
submitRequirementInfoSubject.hasFallbackTextThat().isEqualTo("Code-Owners");
submitRequirementInfoSubject.hasTypeThat().isEqualTo("code-owners");
// Try to submit the change.
ResourceConflictException exception =
assertThrows(
ResourceConflictException.class, () -> gApi.changes().id(changeId).current().submit());
assertThat(exception)
.hasMessageThat()
.isEqualTo(
String.format(
"Failed to submit 1 change due to the following problems:\n"
+ "Change %d: submit requirement 'Code-Owners' is unsatisfied.",
changeInfo._number));
}
@Test
public void changeWithCodeOwnerApprovalsIsSubmittable() throws Exception {
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/")
.addCodeOwnerEmail(user.email())
.create();
testChangeWithCodeOwnerApprovalsIsSubmittable(user);
}
@Test
@TestProjectInput(createEmptyCommit = false)
public void initialChangeWithCodeOwnerApprovalsIsSubmittable() throws Exception {
setAsDefaultCodeOwners(user);
testChangeWithCodeOwnerApprovalsIsSubmittable(user);
}
private void testChangeWithCodeOwnerApprovalsIsSubmittable(TestAccount codeOwner)
throws Exception {
String changeId = createChange("Test Change", "foo/bar.baz", "file content").getChangeId();
// Approve by a code-owner.
projectOperations
.project(project)
.forUpdate()
.add(allowLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-2, +2))
.update();
requestScopeOperations.setApiUser(codeOwner.id());
approve(changeId);
requestScopeOperations.setApiUser(admin.id());
// Verify that the code owner status for the changed file is APPROVED.
CodeOwnerStatusInfo codeOwnerStatus =
changeCodeOwnersApiFactory.change(changeId).getCodeOwnerStatus().get();
assertThat(codeOwnerStatus)
.hasFileCodeOwnerStatusesThat()
.onlyElement()
.hasNewPathStatusThat()
.value()
.hasStatusThat()
.isEqualTo(CodeOwnerStatus.APPROVED);
// Check the submittable flag.
ChangeInfo changeInfo = gApi.changes().id(changeId).get(ListChangesOption.SUBMITTABLE);
assertThat(changeInfo.submittable).isTrue();
// Check the submit requirement.
LegacySubmitRequirementInfoSubject submitRequirementInfoSubject =
assertThatCollection(changeInfo.requirements).onlyElement();
submitRequirementInfoSubject.hasStatusThat().isEqualTo("OK");
submitRequirementInfoSubject.hasFallbackTextThat().isEqualTo("Code-Owners");
submitRequirementInfoSubject.hasTypeThat().isEqualTo("code-owners");
// Submit the change.
gApi.changes().id(changeId).current().submit();
assertThat(gApi.changes().id(changeId).get().status).isEqualTo(ChangeStatus.MERGED);
}
@Test
@GerritConfig(name = "plugin.code-owners.overrideApproval", value = "Owners-Override+1")
public void changeWithOverrideApprovalIsSubmittable() throws Exception {
testChangeWithOverrideApprovalIsSubmittable();
}
@Test
@GerritConfig(name = "plugin.code-owners.overrideApproval", value = "Owners-Override+1")
@TestProjectInput(createEmptyCommit = false)
public void initialChangeWithOverrideApprovalIsSubmittable() throws Exception {
testChangeWithOverrideApprovalIsSubmittable();
}
private void testChangeWithOverrideApprovalIsSubmittable() throws Exception {
createOwnersOverrideLabel();
String changeId = createChange("Test Change", "foo/bar.baz", "file content").getChangeId();
// Check that the change is not submittable.
assertThat(gApi.changes().id(changeId).get(ListChangesOption.SUBMITTABLE).submittable)
.isFalse();
// Add an override approval.
gApi.changes().id(changeId).current().review(new ReviewInput().label("Owners-Override", 1));
// Verify that the code owner status for the changed file is APPROVED.
CodeOwnerStatusInfo codeOwnerStatus =
changeCodeOwnersApiFactory.change(changeId).getCodeOwnerStatus().get();
assertThat(codeOwnerStatus)
.hasFileCodeOwnerStatusesThat()
.onlyElement()
.hasNewPathStatusThat()
.value()
.hasStatusThat()
.isEqualTo(CodeOwnerStatus.APPROVED);
// Approve by a non-code-owner to satisfy the Code-Review+2 requirement.
approve(changeId);
// Check the submit requirement.
LegacySubmitRequirementInfoSubject submitRequirementInfoSubject =
assertThatCollection(gApi.changes().id(changeId).get().requirements).onlyElement();
submitRequirementInfoSubject.hasStatusThat().isEqualTo("OK");
submitRequirementInfoSubject.hasFallbackTextThat().isEqualTo("Code-Owners");
submitRequirementInfoSubject.hasTypeThat().isEqualTo("code-owners");
// Submit the change.
gApi.changes().id(changeId).current().submit();
assertThat(gApi.changes().id(changeId).get().status).isEqualTo(ChangeStatus.MERGED);
}
@Test
public void changeIsNotSubmittableIfDestinationBranchWasDeleted() throws Exception {
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/")
.addCodeOwnerEmail(admin.email())
.create();
String branchName = "tempBranch";
createBranch(BranchNameKey.create(project, branchName));
String changeId = createChange("refs/for/" + branchName).getChangeId();
// Approve by a code-owner.
approve(changeId);
DeleteBranchesInput input = new DeleteBranchesInput();
input.branches = ImmutableList.of(branchName);
gApi.projects().name(project.get()).deleteBranches(input);
ChangeInfo changeInfo = gApi.changes().id(changeId).get(ListChangesOption.SUBMITTABLE);
assertThat(changeInfo.submittable).isFalse();
// Check that the submit requirement.
LegacySubmitRequirementInfoSubject submitRequirementInfoSubject =
assertThatCollection(changeInfo.requirements).onlyElement();
submitRequirementInfoSubject.hasStatusThat().isEqualTo("NOT_READY");
submitRequirementInfoSubject.hasFallbackTextThat().isEqualTo("Code-Owners");
submitRequirementInfoSubject.hasTypeThat().isEqualTo("code-owners");
// Try to submit the change.
ResourceConflictException exception =
assertThrows(
ResourceConflictException.class, () -> gApi.changes().id(changeId).current().submit());
assertThat(exception)
.hasMessageThat()
.isEqualTo(String.format("destination branch \"refs/heads/%s\" not found.", branchName));
}
@Test
@GerritConfig(name = "plugin.code-owners.exemptedUser", value = "exempted-user@example.com")
public void changeIsSubmittableIfUserIsExcempted() throws Exception {
TestAccount exemptedUser =
accountCreator.create(
"exemptedUser", "exempted-user@example.com", "Exempted User", /* displayName= */ null);
PushOneCommit.Result r = createChange(exemptedUser, "Some Change", "foo.txt", "some content");
String changeId = r.getChangeId();
// Apply Code-Review+2 by a non-code-owner to satisfy the MaxWithBlock function of the
// Code-Review label.
approve(changeId);
ChangeInfo changeInfo =
gApi.changes()
.id(changeId)
.get(
ListChangesOption.SUBMITTABLE,
ListChangesOption.ALL_REVISIONS,
ListChangesOption.CURRENT_ACTIONS);
assertThat(changeInfo.submittable).isTrue();
// Check that the submit button is enabled.
assertThat(changeInfo.revisions.get(r.getCommit().getName()).actions.get("submit").enabled)
.isTrue();
// Check the submit requirement.
LegacySubmitRequirementInfoSubject submitRequirementInfoSubject =
assertThatCollection(changeInfo.requirements).onlyElement();
submitRequirementInfoSubject.hasStatusThat().isEqualTo("OK");
submitRequirementInfoSubject.hasFallbackTextThat().isEqualTo("Code-Owners");
submitRequirementInfoSubject.hasTypeThat().isEqualTo("code-owners");
// Submit the change.
gApi.changes().id(changeId).current().submit();
assertThat(gApi.changes().id(changeId).get().status).isEqualTo(ChangeStatus.MERGED);
}
@Test
public void changeIsNotSubmittableIfOwnersFileIsNonParsable() throws Exception {
testChangeIsNotSubmittableIfOwnersFileIsNonParsable(/* invalidCodeOwnerConfigInfoUrl= */ null);
}
@Test
@GerritConfig(name = "plugin.code-owners.invalidCodeOwnerConfigInfoUrl", value = "http://foo.bar")
public void changeIsNotSubmittableIfOwnersFileIsNonParsable_withInvalidCodeOwnerConfigInfoUrl()
throws Exception {
testChangeIsNotSubmittableIfOwnersFileIsNonParsable("http://foo.bar");
}
private void testChangeIsNotSubmittableIfOwnersFileIsNonParsable(
@Nullable String invalidCodeOwnerConfigInfoUrl) throws Exception {
// Add a non-parsable code owner config.
String nameOfInvalidCodeOwnerConfigFile = getCodeOwnerConfigFileName();
createNonParseableCodeOwnerConfig(nameOfInvalidCodeOwnerConfigFile);
PushOneCommit.Result r = createChange("Some Change", "foo.txt", "some content");
String changeId = r.getChangeId();
// Apply Code-Review+2 to satisfy the MaxWithBlock function of the Code-Review label.
approve(changeId);
ChangeInfo changeInfo =
gApi.changes()
.id(changeId)
.get(
ListChangesOption.SUBMITTABLE,
ListChangesOption.ALL_REVISIONS,
ListChangesOption.CURRENT_ACTIONS);
assertThat(changeInfo.submittable).isFalse();
// Check that the submit button is not visible.
assertThat(changeInfo.revisions.get(r.getCommit().getName()).actions.get("submit")).isNull();
// Check the submit requirement.
assertThatCollection(changeInfo.requirements).isEmpty();
// Try to submit the change.
ResourceConflictException exception =
assertThrows(
ResourceConflictException.class, () -> gApi.changes().id(changeId).current().submit());
assertThat(exception)
.hasMessageThat()
.isEqualTo(
String.format(
"Failed to submit 1 change due to the following problems:\n"
+ "Change %s: submit requirement 'Code-Owners' has an"
+ " error: Submittability expression result has an error:"
+ " Failed to evaluate code owner statuses for"
+ " patch set 1 of change %s (cause: invalid code owner config file '%s'"
+ " (project = %s, branch = master):\n %s).%s",
changeInfo._number,
changeInfo._number,
JgitPath.of(nameOfInvalidCodeOwnerConfigFile).getAsAbsolutePath(),
project,
getParsingErrorMessageForNonParseableCodeOwnerConfig(),
invalidCodeOwnerConfigInfoUrl != null
? String.format("\nFor help check %s.", invalidCodeOwnerConfigInfoUrl)
: ""));
}
@Test
@GerritConfig(name = "plugin.code-owners.overrideApproval", value = "Owners-Override+1")
public void changeIsSubmittableWithOverrideIfOwnersFileIsNonParsable() throws Exception {
createOwnersOverrideLabel();
// Add a non-parsable code owner config.
String nameOfInvalidCodeOwnerConfigFile = getCodeOwnerConfigFileName();
createNonParseableCodeOwnerConfig(nameOfInvalidCodeOwnerConfigFile);
PushOneCommit.Result r = createChange("Some Change", "foo.txt", "some content");
String changeId = r.getChangeId();
// Apply Code-Review+2 to satisfy the MaxWithBlock function of the Code-Review label.
approve(changeId);
ChangeInfo changeInfo =
gApi.changes()
.id(changeId)
.get(
ListChangesOption.SUBMITTABLE,
ListChangesOption.ALL_REVISIONS,
ListChangesOption.CURRENT_ACTIONS);
assertThat(changeInfo.submittable).isFalse();
// Check that the submit button is not visible.
assertThat(changeInfo.revisions.get(r.getCommit().getName()).actions.get("submit")).isNull();
// Check the submit requirement.
assertThatCollection(changeInfo.requirements).isEmpty();
// Try to submit the change.
ResourceConflictException exception =
assertThrows(
ResourceConflictException.class, () -> gApi.changes().id(changeId).current().submit());
assertThat(exception)
.hasMessageThat()
.isEqualTo(
String.format(
"Failed to submit 1 change due to the following problems:\n"
+ "Change %s: submit requirement 'Code-Owners' has an"
+ " error: Submittability expression result has an error:"
+ " Failed to evaluate code owner statuses for"
+ " patch set 1 of change %s (cause: invalid code owner config file '%s'"
+ " (project = %s, branch = master):\n %s).",
changeInfo._number,
changeInfo._number,
JgitPath.of(nameOfInvalidCodeOwnerConfigFile).getAsAbsolutePath(),
project,
getParsingErrorMessageForNonParseableCodeOwnerConfig()));
// Apply an override.
gApi.changes().id(changeId).current().review(new ReviewInput().label("Owners-Override", 1));
// Check the submittable flag.
changeInfo =
gApi.changes()
.id(changeId)
.get(
ListChangesOption.SUBMITTABLE,
ListChangesOption.ALL_REVISIONS,
ListChangesOption.CURRENT_ACTIONS);
assertThat(changeInfo.submittable).isTrue();
// Check the submit requirement.
LegacySubmitRequirementInfoSubject submitRequirementInfoSubject =
assertThatCollection(changeInfo.requirements).onlyElement();
submitRequirementInfoSubject.hasStatusThat().isEqualTo("OK");
submitRequirementInfoSubject.hasFallbackTextThat().isEqualTo("Code-Owners");
submitRequirementInfoSubject.hasTypeThat().isEqualTo("code-owners");
// Submit the change.
gApi.changes().id(changeId).current().submit();
assertThat(gApi.changes().id(changeId).get().status).isEqualTo(ChangeStatus.MERGED);
}
@Test
public void changeIsSubmittableIfOwnersFileContainsInvalidPathExpression() throws Exception {
assume().that(backendConfig.getDefaultBackend()).isInstanceOf(FindOwnersBackend.class);
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.addCodeOwnerEmail(admin.email())
.addCodeOwnerSet(
CodeOwnerSet.builder()
.addPathExpression("{foo") // invalid because '{' is not closed
.addCodeOwnerEmail(user.email())
.build())
.create();
PushOneCommit.Result r = createChange("Some Change", "foo.txt", "some content");
String changeId = r.getChangeId();
// Apply Code-Review+2 to satisfy the MaxWithBlock function of the Code-Review label.
approve(changeId);
ChangeInfo changeInfo =
gApi.changes()
.id(changeId)
.get(
ListChangesOption.SUBMITTABLE,
ListChangesOption.ALL_REVISIONS,
ListChangesOption.CURRENT_ACTIONS);
assertThat(changeInfo.submittable).isTrue();
// Check the submit requirement.
LegacySubmitRequirementInfoSubject submitRequirementInfoSubject =
assertThatCollection(changeInfo.requirements).onlyElement();
submitRequirementInfoSubject.hasStatusThat().isEqualTo("OK");
submitRequirementInfoSubject.hasFallbackTextThat().isEqualTo("Code-Owners");
submitRequirementInfoSubject.hasTypeThat().isEqualTo("code-owners");
// Submit the change.
gApi.changes().id(changeId).current().submit();
assertThat(gApi.changes().id(changeId).get().status).isEqualTo(ChangeStatus.MERGED);
}
@Test
@GerritConfig(
name = "plugin.code-owners.mergeCommitStrategy",
value = "FILES_WITH_CONFLICT_RESOLUTION")
public void changeIsSubmittableIfAutoMergeIsNotPresent() throws Exception {
setAsDefaultCodeOwners(admin);
// Create another branch
String branchName = "foo";
BranchInput branchInput = new BranchInput();
branchInput.ref = branchName;
branchInput.revision = projectOperations.project(project).getHead("master").name();
gApi.projects().name(project.get()).branch(branchInput.ref).create(branchInput);
// Create 2 parent commits.
ObjectId initial = projectOperations.project(project).getHead("master");
PushOneCommit.Result p1 =
pushFactory
.create(
admin.newIdent(),
testRepo,
"parent 1",
ImmutableMap.of("foo", "foo-1.2", "bar", "bar-1.2"))
.to("refs/for/master");
RevCommit parent1 = p1.getCommit();
approve(p1.getChangeId());
gApi.changes().id(p1.getChangeId()).current().submit();
testRepo.reset(initial);
PushOneCommit.Result p2 =
pushFactory
.create(
admin.newIdent(),
testRepo,
"parent 2",
ImmutableMap.of("foo", "foo-2.2", "bar", "bar-2.2"))
.to("refs/for/foo");
RevCommit parent2 = p2.getCommit();
approve(p2.getChangeId());
gApi.changes().id(p2.getChangeId()).current().submit();
// Create the merge commit.
PushOneCommit m =
pushFactory.create(
admin.newIdent(), testRepo, "merge", ImmutableMap.of("foo", "foo-1", "bar", "bar-2"));
m.setParents(ImmutableList.of(parent1, parent2));
PushOneCommit.Result r = m.to("refs/for/master");
r.assertOkStatus();
String changeId = r.getChangeId();
// Apply Code-Review+2 by a non-code-owner to satisfy the MaxWithBlock function of the
// Code-Review label.
approve(changeId);
// Delete the auto-merge ref so that the auto-merge needs to be computed in-memory when the
// code-owners submit rule is executed.
deleteAutoMergeBranch(r.getCommit());
ChangeInfo changeInfo =
gApi.changes()
.id(changeId)
.get(
ListChangesOption.SUBMITTABLE,
ListChangesOption.ALL_REVISIONS,
ListChangesOption.CURRENT_ACTIONS);
assertThat(changeInfo.submittable).isTrue();
// Check that the submit button is enabled.
assertThat(changeInfo.revisions.get(r.getCommit().getName()).actions.get("submit").enabled)
.isTrue();
// Check the submit requirement.
LegacySubmitRequirementInfoSubject submitRequirementInfoSubject =
assertThatCollection(changeInfo.requirements).onlyElement();
submitRequirementInfoSubject.hasStatusThat().isEqualTo("OK");
submitRequirementInfoSubject.hasFallbackTextThat().isEqualTo("Code-Owners");
submitRequirementInfoSubject.hasTypeThat().isEqualTo("code-owners");
// Submit the change.
gApi.changes().id(changeId).current().submit();
assertThat(gApi.changes().id(changeId).get().status).isEqualTo(ChangeStatus.MERGED);
}
@Test
public void submitRuleIsInvokedOnlyOnceWhenGettingChangeDetails() throws Exception {
PushOneCommit.Result r = createChange("Some Change", "foo.txt", "some content");
String changeId = r.getChangeId();
testMetricMaker.reset();
gApi.changes()
.id(changeId)
.get(ListChangesOption.ALL_REVISIONS, ListChangesOption.CURRENT_ACTIONS);
// Submit rules are computed freshly, but only once.
assertThat(testMetricMaker.getCount("plugins/code-owners/count_code_owner_submit_rule_runs"))
.isEqualTo(1);
}
@Test
public void submitRuleIsNotInvokedWhenQueryingChange() throws Exception {
PushOneCommit.Result r = createChange("Some Change", "foo.txt", "some content");
String changeId = r.getChangeId();
testMetricMaker.reset();
gApi.changes()
.query(changeId)
.withOptions(ListChangesOption.ALL_REVISIONS, ListChangesOption.CURRENT_ACTIONS)
.get();
// Submit rule evaluation results from the change index are reused
assertThat(testMetricMaker.getCount("plugins/code-owners/count_code_owner_submit_rule_runs"))
.isEqualTo(0);
}
private void deleteAutoMergeBranch(ObjectId mergeCommit) throws Exception {
try (Repository repo = repoManager.openRepository(project)) {
RefUpdate ru = repo.updateRef(RefNames.refsCacheAutomerge(mergeCommit.name()));
ru.setForceUpdate(true);
assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
assertThat(repo.exactRef(RefNames.refsCacheAutomerge(mergeCommit.name()))).isNull();
}
}
}