Merge changes Iebe401f1,I84338990,I6d073eb9
* changes:
Document how to measure the test coverage
Warn when a code owner config directly imports itself
Extend change message when code owner override is applied
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java b/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java
index 50e5f68..3bdb518 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java
@@ -47,7 +47,8 @@
install(new CodeOwnerSubmitRule.Module());
DynamicSet.bind(binder(), ExceptionHook.class).to(CodeOwnersExceptionHook.class);
- DynamicSet.bind(binder(), OnPostReview.class).to(CodeOwnersOnPostReview.class);
+ DynamicSet.bind(binder(), OnPostReview.class).to(OnCodeOwnerApproval.class);
+ DynamicSet.bind(binder(), OnPostReview.class).to(OnCodeOwnerOverride.class);
DynamicSet.bind(binder(), ReviewerAddedListener.class).to(CodeOwnersOnAddReviewer.class);
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnPostReview.java b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java
similarity index 97%
rename from java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnPostReview.java
rename to java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java
index 1596e23..e818163 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnPostReview.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java
@@ -35,7 +35,8 @@
import java.util.stream.Stream;
/**
- * Callback that is invoked on post review.
+ * Callback that is invoked on post review and that extends the change message if a code owner
+ * approval was changed.
*
* <p>If a code owner approval was added, removed or changed, include in the change message that is
* being posted on vote, which of the files:
@@ -47,14 +48,14 @@
* </ul>
*/
@Singleton
-class CodeOwnersOnPostReview implements OnPostReview {
+class OnCodeOwnerApproval implements OnPostReview {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
private final CodeOwnerApprovalCheck codeOwnerApprovalCheck;
@Inject
- CodeOwnersOnPostReview(
+ OnCodeOwnerApproval(
CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
CodeOwnerApprovalCheck codeOwnerApprovalCheck) {
this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerOverride.java b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerOverride.java
new file mode 100644
index 0000000..df24fbb
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerOverride.java
@@ -0,0 +1,167 @@
+// Copyright (C) 2021 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.backend;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.backend.config.RequiredApproval;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.restapi.change.OnPostReview;
+import com.google.gerrit.server.util.LabelVote;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Callback that is invoked on post review and that extends the change message if a code owner
+ * override was changed.
+ *
+ * <p>If a code owner override was added, removed or changed, include in the change message that is
+ * being posted on vote, that the vote is a code owner override to let users know about its effect.
+ */
+@Singleton
+class OnCodeOwnerOverride implements OnPostReview {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
+
+ @Inject
+ OnCodeOwnerOverride(CodeOwnersPluginConfiguration codeOwnersPluginConfiguration) {
+ this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
+ }
+
+ @Override
+ public Optional<String> getChangeMessageAddOn(
+ IdentifiedUser user,
+ ChangeNotes changeNotes,
+ PatchSet patchSet,
+ Map<String, Short> oldApprovals,
+ Map<String, Short> approvals) {
+ if (codeOwnersPluginConfiguration.isDisabled(changeNotes.getChange().getDest())) {
+ return Optional.empty();
+ }
+
+ // code owner overrides are only relevant for the current patch set
+ if (!changeNotes.getChange().currentPatchSetId().equals(patchSet.id())) {
+ return Optional.empty();
+ }
+
+ ImmutableSet<RequiredApproval> overrideApprovals =
+ codeOwnersPluginConfiguration.getOverrideApproval(changeNotes.getProjectName());
+
+ List<String> messages = new ArrayList<>();
+ for (RequiredApproval overrideApproval : overrideApprovals) {
+ if (oldApprovals.get(overrideApproval.labelType().getName()) == null) {
+ // If oldApprovals doesn't contain the label or if the labels value in it is null, the label
+ // was not changed.
+ continue;
+ }
+
+ buildMessageForCodeOwnerOverride(
+ user, changeNotes, patchSet, oldApprovals, approvals, overrideApproval)
+ .ifPresent(messages::add);
+ }
+
+ if (messages.isEmpty()) {
+ return Optional.empty();
+ }
+ return Optional.of(Joiner.on("\n\n").join(messages));
+ }
+
+ private Optional<String> buildMessageForCodeOwnerOverride(
+ IdentifiedUser user,
+ ChangeNotes changeNotes,
+ PatchSet patchSet,
+ Map<String, Short> oldApprovals,
+ Map<String, Short> approvals,
+ RequiredApproval overrideApproval) {
+ LabelVote newVote = getNewVote(overrideApproval, approvals);
+
+ if (isCodeOwnerOverrideNewlyApplied(overrideApproval, oldApprovals, newVote)) {
+ return Optional.of(
+ String.format(
+ "By voting %s the code-owners submit requirement is overridden by %s",
+ newVote, user.getName()));
+ } else if (isCodeOwnerOverrideRemoved(overrideApproval, oldApprovals, newVote)) {
+ if (newVote.value() == 0) {
+ return Optional.of(
+ String.format(
+ "By removing the %s vote the code-owners submit requirement is no longer overridden"
+ + " by %s",
+ newVote.label(), user.getName()));
+ }
+ return Optional.of(
+ String.format(
+ "By voting %s the code-owners submit requirement is no longer overridden by %s",
+ newVote, user.getName()));
+ } else if (isCodeOwnerOverrideUpOrDowngraded(overrideApproval, oldApprovals, newVote)) {
+ return Optional.of(
+ String.format(
+ "By voting %s the code-owners submit requirement is still overridden by %s",
+ newVote, user.getName()));
+ }
+ logger.atSevere().log(
+ "code owner override was neither newly applied, removed, upgraded nor downgraded:"
+ + " project = %s, change = %s, patch set = %d, oldApprvals = %s, approvals = %s,"
+ + " requiredApproval = %s",
+ changeNotes.getProjectName(),
+ changeNotes.getChangeId(),
+ patchSet.id().get(),
+ oldApprovals,
+ approvals,
+ overrideApproval);
+ return Optional.empty();
+ }
+
+ private boolean isCodeOwnerOverrideNewlyApplied(
+ RequiredApproval requiredApproval, Map<String, Short> oldApprovals, LabelVote newVote) {
+ String labelName = requiredApproval.labelType().getName();
+ return oldApprovals.get(labelName) < requiredApproval.value()
+ && newVote.value() >= requiredApproval.value();
+ }
+
+ private boolean isCodeOwnerOverrideRemoved(
+ RequiredApproval requiredApproval, Map<String, Short> oldApprovals, LabelVote newVote) {
+ String labelName = requiredApproval.labelType().getName();
+ return oldApprovals.get(labelName) >= requiredApproval.value()
+ && newVote.value() < requiredApproval.value();
+ }
+
+ private boolean isCodeOwnerOverrideUpOrDowngraded(
+ RequiredApproval requiredApproval, Map<String, Short> oldApprovals, LabelVote newVote) {
+ String labelName = requiredApproval.labelType().getName();
+ return oldApprovals.get(labelName) >= requiredApproval.value()
+ && newVote.value() >= requiredApproval.value();
+ }
+
+ private LabelVote getNewVote(RequiredApproval requiredApproval, Map<String, Short> approvals) {
+ String labelName = requiredApproval.labelType().getName();
+ checkState(
+ approvals.containsKey(labelName),
+ "expected that approval on label %s exists (approvals = %s)",
+ labelName,
+ approvals);
+ return LabelVote.create(labelName, approvals.get(labelName));
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
index e4714bd..cef6d52 100644
--- a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
+++ b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
@@ -855,6 +855,14 @@
PathCodeOwners.createKeyForImportedCodeOwnerConfig(
keyOfImportingCodeOwnerConfig, codeOwnerConfigReference);
+ if (isSelfImport(keyOfImportingCodeOwnerConfig, keyOfImportedCodeOwnerConfig)) {
+ return nonResolvableImport(
+ importType,
+ codeOwnerConfigFilePath,
+ "code owner config imports itself",
+ ValidationMessage.Type.WARNING);
+ }
+
Optional<ProjectState> projectState = projectCache.get(keyOfImportedCodeOwnerConfig.project());
if (!projectState.isPresent() || !isProjectReadable(keyOfImportedCodeOwnerConfig)) {
// we intentionally use the same error message for non-existing and non-readable projects so
@@ -944,6 +952,21 @@
return Optional.empty();
}
+ /** Whether the importing code owner config is the same as the imported code owner config. */
+ private boolean isSelfImport(
+ CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig,
+ CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig) {
+ return keyOfImportingCodeOwnerConfig.project().equals(keyOfImportedCodeOwnerConfig.project())
+ && keyOfImportingCodeOwnerConfig.ref().equals(keyOfImportedCodeOwnerConfig.ref())
+ && codeOwnersPluginConfiguration
+ .getBackend(keyOfImportingCodeOwnerConfig.branchNameKey())
+ .getFilePath(keyOfImportingCodeOwnerConfig)
+ .equals(
+ codeOwnersPluginConfiguration
+ .getBackend(keyOfImportedCodeOwnerConfig.branchNameKey())
+ .getFilePath(keyOfImportedCodeOwnerConfig));
+ }
+
private boolean isProjectReadable(CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig) {
try {
return permissionBackend
@@ -994,14 +1017,26 @@
CodeOwnerConfigImportType importType,
Path codeOwnerConfigFilePath,
String message) {
+ return nonResolvableImport(
+ importType,
+ codeOwnerConfigFilePath,
+ message,
+ codeOwnersPluginConfiguration.rejectNonResolvableImports(project)
+ ? ValidationMessage.Type.ERROR
+ : ValidationMessage.Type.WARNING);
+ }
+
+ private Optional<CommitValidationMessage> nonResolvableImport(
+ CodeOwnerConfigImportType importType,
+ Path codeOwnerConfigFilePath,
+ String message,
+ ValidationMessage.Type validationMessageType) {
return Optional.of(
new CommitValidationMessage(
String.format(
"invalid %s import in '%s': %s",
importType.getType(), codeOwnerConfigFilePath, message),
- codeOwnersPluginConfiguration.rejectNonResolvableImports(project)
- ? ValidationMessage.Type.ERROR
- : ValidationMessage.Type.WARNING));
+ validationMessageType));
}
private Optional<CommitValidationMessage> nonResolvableCodeOwner(
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java
index 12b3360..3dfa9db 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java
@@ -1023,6 +1023,102 @@
}
@Test
+ public void uploadConfigWithGlobalSelfImportReportsAWarning() throws Exception {
+ testUploadConfigWithSelfImport(CodeOwnerConfigImportType.GLOBAL);
+ }
+
+ @Test
+ public void uploadConfigWithPerFileSelfImportReportsAWarning() throws Exception {
+ testUploadConfigWithSelfImport(CodeOwnerConfigImportType.PER_FILE);
+ }
+
+ private void testUploadConfigWithSelfImport(CodeOwnerConfigImportType importType)
+ throws Exception {
+ skipTestIfImportsNotSupportedByCodeOwnersBackend();
+
+ // create a code owner config that imports itself
+ CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = createCodeOwnerConfigKey("/");
+ CodeOwnerConfigReference codeOwnerConfigReference =
+ CodeOwnerConfigReference.builder(
+ CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
+ codeOwnerConfigOperations
+ .codeOwnerConfig(keyOfImportingCodeOwnerConfig)
+ .getFilePath())
+ .setProject(project)
+ .build();
+ CodeOwnerConfig codeOwnerConfig =
+ createCodeOwnerConfigWithImport(
+ keyOfImportingCodeOwnerConfig, importType, codeOwnerConfigReference);
+
+ PushOneCommit.Result r =
+ createChange(
+ "Add code owners",
+ codeOwnerConfigOperations
+ .codeOwnerConfig(keyOfImportingCodeOwnerConfig)
+ .getJGitFilePath(),
+ format(codeOwnerConfig));
+ assertOkWithWarnings(
+ r,
+ "invalid code owner config files",
+ String.format(
+ "invalid %s import in '%s': code owner config imports itself",
+ importType.getType(),
+ codeOwnerConfigOperations
+ .codeOwnerConfig(keyOfImportingCodeOwnerConfig)
+ .getFilePath()));
+ }
+
+ @Test
+ public void canUploadConfigWithGlobalImportOfFileWithExtensionFromSameFolder() throws Exception {
+ testUploadConfigWithImportOfFileWithExtensionFromSameFolder(CodeOwnerConfigImportType.GLOBAL);
+ }
+
+ @Test
+ public void canUploadConfigWithPerFileImportOfFileWithExtensionFromSameFolder() throws Exception {
+ testUploadConfigWithImportOfFileWithExtensionFromSameFolder(CodeOwnerConfigImportType.PER_FILE);
+ }
+
+ private void testUploadConfigWithImportOfFileWithExtensionFromSameFolder(
+ CodeOwnerConfigImportType importType) throws Exception {
+ skipTestIfImportsNotSupportedByCodeOwnersBackend();
+
+ // create a code owner config that imports a code owner config from the same folder but with an
+ // extension in the file name
+ CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = createCodeOwnerConfigKey("/");
+ CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .fileName(getCodeOwnerConfigFileName() + "_extension")
+ .addCodeOwnerEmail(user.email())
+ .create();
+ GitUtil.fetch(testRepo, "refs/*:refs/*");
+ testRepo.reset(projectOperations.project(project).getHead("master"));
+ CodeOwnerConfigReference codeOwnerConfigReference =
+ CodeOwnerConfigReference.builder(
+ CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
+ codeOwnerConfigOperations
+ .codeOwnerConfig(keyOfImportedCodeOwnerConfig)
+ .getFilePath())
+ .setProject(project)
+ .build();
+ CodeOwnerConfig codeOwnerConfig =
+ createCodeOwnerConfigWithImport(
+ keyOfImportingCodeOwnerConfig, importType, codeOwnerConfigReference);
+
+ PushOneCommit.Result r =
+ createChange(
+ "Add code owners",
+ codeOwnerConfigOperations
+ .codeOwnerConfig(keyOfImportingCodeOwnerConfig)
+ .getJGitFilePath(),
+ format(codeOwnerConfig));
+ r.assertOkStatus();
+ }
+
+ @Test
public void cannotUploadConfigWithGlobalImportFromNonExistingProject() throws Exception {
testUploadConfigWithImportFromNonExistingProject(CodeOwnerConfigImportType.GLOBAL);
}
@@ -1764,6 +1860,16 @@
return ObjectIds.abbreviateName(id, testRepo.getRevWalk().getObjectReader());
}
+ private String getCodeOwnerConfigFileName() {
+ CodeOwnerBackend backend = backendConfig.getDefaultBackend();
+ if (backend instanceof FindOwnersBackend) {
+ return FindOwnersBackend.CODE_OWNER_CONFIG_FILE_NAME;
+ } else if (backend instanceof ProtoBackend) {
+ return ProtoBackend.CODE_OWNER_CONFIG_FILE_NAME;
+ }
+ throw new IllegalStateException("unknown code owner backend: " + backend.getClass().getName());
+ }
+
private static void assertOkWithoutMessages(PushOneCommit.Result pushResult) {
pushResult.assertOkStatus();
pushResult.assertNotMessage("fatal");
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersOnPostReviewIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerApprovalIT.java
similarity index 99%
rename from javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersOnPostReviewIT.java
rename to javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerApprovalIT.java
index 9bae548..97f426a 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersOnPostReviewIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerApprovalIT.java
@@ -38,9 +38,9 @@
import org.junit.Test;
/**
- * Acceptance test for {@code com.google.gerrit.plugins.codeowners.backend.CodeOwnersOnPostReview}.
+ * Acceptance test for {@code com.google.gerrit.plugins.codeowners.backend.OnCodeOwnerApproval}.
*/
-public class CodeOwnersOnPostReviewIT extends AbstractCodeOwnersIT {
+public class OnCodeOwnerApprovalIT extends AbstractCodeOwnersIT {
@Inject private RequestScopeOperations requestScopeOperations;
@Inject private ProjectOperations projectOperations;
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerOverrrideIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerOverrrideIT.java
new file mode 100644
index 0000000..f094f7b
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerOverrrideIT.java
@@ -0,0 +1,437 @@
+// Copyright (C) 2021 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.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.projects.DeleteBranchesInput;
+import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.common.LabelDefinitionInput;
+import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
+import com.google.inject.Inject;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.regex.Pattern;
+import org.junit.Test;
+
+/** Acceptance test for {@code com.google.gerrit.plugins.codeowners.backend.OnCodeOwnerOverride}. */
+public class OnCodeOwnerOverrrideIT extends AbstractCodeOwnersIT {
+ @Inject private RequestScopeOperations requestScopeOperations;
+ @Inject private ProjectOperations projectOperations;
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.disabled", value = "true")
+ @GerritConfig(name = "plugin.code-owners.overrideApproval", value = "Owners-Override+1")
+ public void changeMessageNotExtendedIfCodeOwnersFuctionalityIsDisabled() throws Exception {
+ createOwnersOverrideLabel();
+
+ String changeId = createChange().getChangeId();
+
+ gApi.changes().id(changeId).current().review(new ReviewInput().label("Owners-Override", 1));
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message).isEqualTo("Patch Set 1: Owners-Override+1");
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.overrideApproval", value = "Owners-Override+1")
+ public void changeMessageExtendedIfCodeOwnersOverrideIsApplied() throws Exception {
+ createOwnersOverrideLabel();
+
+ String changeId = createChange().getChangeId();
+
+ gApi.changes().id(changeId).current().review(new ReviewInput().label("Owners-Override", 1));
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ String.format(
+ "Patch Set 1: Owners-Override+1\n\n"
+ + "By voting Owners-Override+1 the code-owners submit requirement is overridden by %s\n",
+ admin.fullName()));
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.overrideApproval", value = "Owners-Override+1")
+ public void changeMessageExtendedIfCodeOwnersOverrideIsReApplied() throws Exception {
+ createOwnersOverrideLabel();
+
+ String changeId = createChange().getChangeId();
+
+ gApi.changes().id(changeId).current().review(new ReviewInput().label("Owners-Override", 1));
+
+ int messageCount = gApi.changes().id(changeId).get().messages.size();
+
+ // Apply the Owners-Override+1 approval again
+ gApi.changes().id(changeId).current().review(new ReviewInput().label("Owners-Override", 1));
+
+ // Check that a new change message was added.
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(messages.size()).isEqualTo(messageCount + 1);
+
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ String.format(
+ "Patch Set 1: Owners-Override+1\n\n"
+ + "By voting Owners-Override+1 the code-owners submit requirement is still overridden by %s\n",
+ admin.fullName()));
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.overrideApproval", value = "Owners-Override+1")
+ public void changeMessageExtendedIfCodeOwnersOverrideIsAppliedByOtherUser() throws Exception {
+ createOwnersOverrideLabel();
+
+ String changeId = createChange().getChangeId();
+
+ gApi.changes().id(changeId).current().review(new ReviewInput().label("Owners-Override", 1));
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ String.format(
+ "Patch Set 1: Owners-Override+1\n\n"
+ + "By voting Owners-Override+1 the code-owners submit requirement is overridden by %s\n",
+ admin.fullName()));
+
+ // Apply the Owners-Override+1 approval by another user
+ requestScopeOperations.setApiUser(user.id());
+ gApi.changes().id(changeId).current().review(new ReviewInput().label("Owners-Override", 1));
+
+ messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ String.format(
+ "Patch Set 1: Owners-Override+1\n\n"
+ + "By voting Owners-Override+1 the code-owners submit requirement is overridden by %s\n",
+ user.fullName()));
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.overrideApproval", value = "Owners-Override+1")
+ public void changeMessageExtendedIfCodeOwnersOverrideIsUpgraded() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+2", "Override", "+1", "Override", " 0", "No Override");
+ gApi.projects().name(project.get()).label("Owners-Override").create(input).get();
+
+ // Allow to vote on the Owners-Override label.
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ TestProjectUpdate.allowLabel("Owners-Override")
+ .range(0, 2)
+ .ref("refs/*")
+ .group(REGISTERED_USERS)
+ .build())
+ .update();
+
+ String changeId = createChange().getChangeId();
+
+ gApi.changes().id(changeId).current().review(new ReviewInput().label("Owners-Override", 1));
+
+ // Upgrade the approval from Owners-Override+1 to Owners-Override+2
+ gApi.changes().id(changeId).current().review(new ReviewInput().label("Owners-Override", 2));
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ String.format(
+ "Patch Set 1: Owners-Override+2\n\n"
+ + "By voting Owners-Override+2 the code-owners submit requirement is still"
+ + " overridden by %s\n",
+ admin.fullName()));
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.overrideApproval", value = "Owners-Override+1")
+ public void changeMessageExtendedIfCodeOwnersOverrideIsDowngraded() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+2", "Override", "+1", "Override", " 0", "No Override");
+ gApi.projects().name(project.get()).label("Owners-Override").create(input).get();
+
+ // Allow to vote on the Owners-Override label.
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ TestProjectUpdate.allowLabel("Owners-Override")
+ .range(0, 2)
+ .ref("refs/*")
+ .group(REGISTERED_USERS)
+ .build())
+ .update();
+
+ String changeId = createChange().getChangeId();
+
+ gApi.changes().id(changeId).current().review(new ReviewInput().label("Owners-Override", 2));
+
+ // Downgrade the approval from Owners-Override+2 to Owners-Override+1
+ gApi.changes().id(changeId).current().review(new ReviewInput().label("Owners-Override", 1));
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ String.format(
+ "Patch Set 1: Owners-Override+1\n\n"
+ + "By voting Owners-Override+1 the code-owners submit requirement is still"
+ + " overridden by %s\n",
+ admin.fullName()));
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.overrideApproval", value = "Owners-Override+1")
+ public void changeMessageExtendedIfCodeOwnersOverrideIsRemoved() throws Exception {
+ createOwnersOverrideLabel();
+
+ String changeId = createChange().getChangeId();
+
+ gApi.changes().id(changeId).current().review(new ReviewInput().label("Owners-Override", 1));
+
+ // Remove the override approval
+ gApi.changes().id(changeId).current().review(new ReviewInput().label("Owners-Override", 0));
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ String.format(
+ "Patch Set 1: -Owners-Override\n\n"
+ + "By removing the Owners-Override vote the code-owners submit requirement is"
+ + " no longer overridden by %s\n",
+ admin.fullName()));
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.overrideApproval", value = "Owners-Override+1")
+ public void changeMessageExtendedIfCodeOwnersOverrideIsChangedToNegativeValue() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Override", " 0", "No Override", "-1", "No Override");
+ gApi.projects().name(project.get()).label("Owners-Override").create(input).get();
+
+ // Allow to vote on the Owners-Override label.
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ TestProjectUpdate.allowLabel("Owners-Override")
+ .range(-1, 1)
+ .ref("refs/*")
+ .group(REGISTERED_USERS)
+ .build())
+ .update();
+
+ String changeId = createChange().getChangeId();
+
+ gApi.changes().id(changeId).current().review(new ReviewInput().label("Owners-Override", 1));
+
+ // Vote with Owners-Override-1
+ gApi.changes().id(changeId).current().review(new ReviewInput().label("Owners-Override", -1));
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ String.format(
+ "Patch Set 1: Owners-Override-1\n\n"
+ + "By voting Owners-Override-1 the code-owners submit requirement is no longer"
+ + " overridden by %s\n",
+ admin.fullName()));
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.overrideApproval", value = "Owners-Override+1")
+ public void changeMessageNotExtendedIfNonCodeOwnersOverrideIsApplied() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Approval", " 0", "No Approval");
+ gApi.projects().name(project.get()).label("Other").create(input).get();
+
+ // Allow to vote on the Owners-Override label.
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ TestProjectUpdate.allowLabel("Other")
+ .range(0, 1)
+ .ref("refs/*")
+ .group(REGISTERED_USERS)
+ .build())
+ .update();
+
+ String changeId = createChange().getChangeId();
+
+ gApi.changes().id(changeId).current().review(new ReviewInput().label("Other", 1));
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message).isEqualTo("Patch Set 1: Other+1");
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.overrideApproval", value = "Owners-Override+1")
+ public void changeMessageExtendedIfCodeOwnersOverrideIsAppliedTogetherWithComment()
+ throws Exception {
+ createOwnersOverrideLabel();
+
+ String path = "foo/bar.baz";
+ String changeId = createChange("Test Change", path, "file content").getChangeId();
+
+ ReviewInput.CommentInput commentInput = new ReviewInput.CommentInput();
+ commentInput.line = 1;
+ commentInput.message = "some comment";
+ commentInput.path = path;
+ ReviewInput reviewInput = new ReviewInput();
+ reviewInput.labels = new HashMap<>();
+ reviewInput.labels.put("Owners-Override", (short) 1);
+ reviewInput.comments = new HashMap<>();
+ reviewInput.comments.put(commentInput.path, Lists.newArrayList(commentInput));
+ gApi.changes().id(changeId).current().review(reviewInput);
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ String.format(
+ "Patch Set 1: Owners-Override+1\n\n"
+ + "(1 comment)\n\n"
+ + "By voting Owners-Override+1 the code-owners submit requirement is"
+ + " overridden by %s\n",
+ admin.fullName()));
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.overrideApproval", value = "Owners-Override+1")
+ public void changeMessageNotExtendedIfCodeOwnersOverrideIsAppliedOnOldPatchSet()
+ throws Exception {
+ createOwnersOverrideLabel();
+
+ String changeId = createChange().getChangeId();
+
+ // create a second patch set
+ amendChange(changeId);
+
+ // vote on the first patch set
+ gApi.changes().id(changeId).revision(1).review(new ReviewInput().label("Owners-Override", 1));
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message).isEqualTo("Patch Set 1: Owners-Override+1");
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.overrideApproval", value = "Owners-Override+1")
+ public void changeMessageExtendedIfDestinationBranchWasDeleted() throws Exception {
+ createOwnersOverrideLabel();
+
+ String branchName = "tempBranch";
+ createBranch(BranchNameKey.create(project, branchName));
+
+ String changeId = createChange("refs/for/" + branchName).getChangeId();
+
+ DeleteBranchesInput input = new DeleteBranchesInput();
+ input.branches = ImmutableList.of(branchName);
+ gApi.projects().name(project.get()).deleteBranches(input);
+
+ gApi.changes().id(changeId).current().review(new ReviewInput().label("Owners-Override", 1));
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ String.format(
+ "Patch Set 1: Owners-Override+1\n\n"
+ + "By voting Owners-Override+1 the code-owners submit requirement is overridden by %s\n",
+ admin.fullName()));
+ }
+
+ @Test
+ @GerritConfig(
+ name = "plugin.code-owners.overrideApproval",
+ values = {"Owners-Override+1", "Global-Override+1"})
+ public void changeMessageExtendedIfMultipleCodeOwnersOverridesAreAppliedTogether()
+ throws Exception {
+ createOwnersOverrideLabel();
+ createOwnersOverrideLabel("Global-Override");
+
+ String changeId = createChange().getChangeId();
+
+ ReviewInput reviewInput = new ReviewInput();
+ reviewInput.labels = new HashMap<>();
+ reviewInput.labels.put("Owners-Override", (short) 1);
+ reviewInput.labels.put("Global-Override", (short) 1);
+ gApi.changes().id(changeId).current().review(reviewInput);
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message)
+ .matches(
+ Pattern.quote("Patch Set 1: ")
+ + "("
+ + Pattern.quote("Owners-Override+1 Global-Override+1")
+ + "|"
+ + Pattern.quote("Global-Override+1 Owners-Override+1")
+ + ")"
+ + Pattern.quote(
+ String.format(
+ "\n\nBy voting Owners-Override+1 the code-owners submit requirement is"
+ + " overridden by %s\n\n"
+ + "By voting Global-Override+1 the code-owners submit requirement is"
+ + " overridden by %s\n",
+ admin.fullName(), admin.fullName())));
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.overrideApproval", value = "Owners-Override+1")
+ public void changeMessageExtendedIfCodeOwnersOverrideAndCodeOwnerApprovalAreAppliedTogether()
+ throws Exception {
+ createOwnersOverrideLabel();
+
+ String path = "foo/bar.baz";
+ String changeId = createChange("Test Change", path, "file content").getChangeId();
+
+ ReviewInput reviewInput = new ReviewInput();
+ reviewInput.labels = new HashMap<>();
+ reviewInput.labels.put("Owners-Override", (short) 1);
+ reviewInput.labels.put("Code-Review", (short) 1);
+ gApi.changes().id(changeId).current().review(reviewInput);
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message).startsWith("Patch Set 1: ");
+ assertThat(Iterables.getLast(messages).message)
+ .containsMatch(
+ "("
+ + Pattern.quote("Owners-Override+1 Code-Review+1")
+ + "|"
+ + Pattern.quote("Code-Review+1 Owners-Override+1")
+ + ")");
+ assertThat(Iterables.getLast(messages).message)
+ .contains(
+ String.format(
+ "By voting Owners-Override+1 the code-owners submit requirement is"
+ + " overridden by %s\n",
+ admin.fullName()));
+ assertThat(Iterables.getLast(messages).message)
+ .contains(
+ String.format(
+ "By voting Code-Review+1 the following files are now code-owner approved by"
+ + " %s:\n"
+ + "* %s\n",
+ admin.fullName(), path));
+ }
+}
diff --git a/resources/Documentation/build.md b/resources/Documentation/build.md
index 6c2db04..38d44ed 100644
--- a/resources/Documentation/build.md
+++ b/resources/Documentation/build.md
@@ -22,6 +22,14 @@
```
bazel test //plugins/@PLUGIN@/...
```
+\
+To measure the test coverage run:
+
+```
+ bazel coverage --test_output=all plugins/code-owners/... --coverage_report_generator=@bazel_tools//tools/test:coverage_report_generator --combined_report=lcov --instrumentation_filter="^//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/acceptance[/:],^//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite[/:],^//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/api[/:],^//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/api/impl[/:],^//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/backend[/:],^//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/common[/:],,^//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/metrics[/:],^//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/restapi[/:],^//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/testing[/:],^//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/util[/:],^//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/validation[/:]" && genhtml -o . --ignore-errors source bazel-out/_coverage/_coverage_report.dat
+```
+\
+and then open the generated `index.html` in a browser.
---