Merge branch 'stable-3.3'
* stable-3.3:
Split up the api and backend tests
Change-Id: I1b49b2f9972bee39f0af0044d2bf483a2cffe21d
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java b/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java
index adccb0f..a93c20d 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java
@@ -22,6 +22,7 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ServerInitiated;
import com.google.gerrit.server.UserInitiated;
+import com.google.gerrit.server.restapi.change.OnPostReview;
import com.google.inject.Provides;
/** Guice module to bind code owner backends. */
@@ -45,6 +46,7 @@
install(new CodeOwnerSubmitRule.Module());
DynamicSet.bind(binder(), ExceptionHook.class).to(CodeOwnersExceptionHook.class);
+ DynamicSet.bind(binder(), OnPostReview.class).to(CodeOwnersOnPostReview.class);
}
@Provides
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnPostReview.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnPostReview.java
new file mode 100644
index 0000000..61574f4
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnPostReview.java
@@ -0,0 +1,255 @@
+// 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.backend;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static java.util.Comparator.comparing;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.plugins.codeowners.JgitPath;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnerStatus;
+import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.config.RequiredApproval;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
+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.io.IOException;
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+/**
+ * Callback that is invoked on post review.
+ *
+ * <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:
+ *
+ * <ul>
+ * <li>are approved now
+ * <li>are no longer approved
+ * <li>are still approved
+ * </ul>
+ */
+@Singleton
+class CodeOwnersOnPostReview implements OnPostReview {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
+ private final CodeOwnerApprovalCheck codeOwnerApprovalCheck;
+
+ @Inject
+ CodeOwnersOnPostReview(
+ CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
+ CodeOwnerApprovalCheck codeOwnerApprovalCheck) {
+ this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
+ this.codeOwnerApprovalCheck = codeOwnerApprovalCheck;
+ }
+
+ @Override
+ public Optional<String> getChangeMessageAddOn(
+ IdentifiedUser user,
+ ChangeNotes changeNotes,
+ PatchSet patchSet,
+ Map<String, Short> oldApprovals,
+ Map<String, Short> approvals) {
+ // code owner approvals are only computed for the current patch set
+ if (!changeNotes.getChange().currentPatchSetId().equals(patchSet.id())) {
+ return Optional.empty();
+ }
+
+ RequiredApproval requiredApproval =
+ codeOwnersPluginConfiguration.getRequiredApproval(changeNotes.getProjectName());
+
+ if (!oldApprovals.containsKey(requiredApproval.labelType().getName())) {
+ // if oldApprovals doesn't contain the label, the label was not changed
+ return Optional.empty();
+ }
+
+ return buildMessageForCodeOwnerApproval(
+ user, changeNotes, patchSet, oldApprovals, approvals, requiredApproval);
+ }
+
+ private Optional<String> buildMessageForCodeOwnerApproval(
+ IdentifiedUser user,
+ ChangeNotes changeNotes,
+ PatchSet patchSet,
+ Map<String, Short> oldApprovals,
+ Map<String, Short> approvals,
+ RequiredApproval requiredApproval) {
+ int maxPathsInChangeMessage =
+ codeOwnersPluginConfiguration.getMaxPathsInChangeMessages(changeNotes.getProjectName());
+ if (maxPathsInChangeMessage <= 0) {
+ return Optional.empty();
+ }
+
+ LabelVote newVote = getNewVote(requiredApproval, approvals);
+
+ ImmutableList<Path> ownedPaths = getOwnedPaths(changeNotes, user.getAccountId());
+ if (ownedPaths.isEmpty()) {
+ // the user doesn't own any of the modified paths
+ return Optional.empty();
+ }
+
+ boolean hasImplicitApprovalByUser =
+ codeOwnersPluginConfiguration.areImplicitApprovalsEnabled(changeNotes.getProjectName())
+ && patchSet.uploader().equals(user.getAccountId());
+
+ boolean noLongerExplicitlyApproved = false;
+ StringBuilder message = new StringBuilder();
+ if (isCodeOwnerApprovalNewlyApplied(requiredApproval, oldApprovals, newVote)) {
+ if (hasImplicitApprovalByUser) {
+ message.append(
+ String.format(
+ "By voting %s the following files are now explicitly code-owner approved by %s:\n",
+ newVote, user.getName()));
+ } else {
+ message.append(
+ String.format(
+ "By voting %s the following files are now code-owner approved by %s:\n",
+ newVote, user.getName()));
+ }
+ } else if (isCodeOwnerApprovalRemoved(requiredApproval, oldApprovals, newVote)) {
+ if (newVote.value() == 0) {
+ if (hasImplicitApprovalByUser) {
+ noLongerExplicitlyApproved = true;
+ message.append(
+ String.format(
+ "By removing the %s vote the following files are no longer explicitly code-owner"
+ + " approved by %s:\n",
+ newVote.label(), user.getName()));
+ } else {
+ message.append(
+ String.format(
+ "By removing the %s vote the following files are no longer code-owner approved"
+ + " by %s:\n",
+ newVote.label(), user.getName()));
+ }
+ } else {
+ if (hasImplicitApprovalByUser) {
+ noLongerExplicitlyApproved = true;
+ message.append(
+ String.format(
+ "By voting %s the following files are no longer explicitly code-owner approved by"
+ + " %s:\n",
+ newVote, user.getName()));
+ } else {
+ message.append(
+ String.format(
+ "By voting %s the following files are no longer code-owner approved by %s:\n",
+ newVote, user.getName()));
+ }
+ }
+ } else if (isCodeOwnerApprovalUpOrDowngraded(requiredApproval, oldApprovals, newVote)) {
+ if (hasImplicitApprovalByUser) {
+ message.append(
+ String.format(
+ "By voting %s the following files are still explicitly code-owner approved by"
+ + " %s:\n",
+ newVote, user.getName()));
+ } else {
+ message.append(
+ String.format(
+ "By voting %s the following files are still code-owner approved by %s:\n",
+ newVote, user.getName()));
+ }
+ } else {
+ return Optional.empty();
+ }
+
+ if (ownedPaths.size() <= maxPathsInChangeMessage) {
+ appendPaths(message, ownedPaths.stream());
+ } else {
+ // -1 so that we never show "(1 more files)"
+ int limit = maxPathsInChangeMessage - 1;
+ appendPaths(message, ownedPaths.stream().limit(limit));
+ message.append(String.format("(%s more files)\n", ownedPaths.size() - limit));
+ }
+
+ if (hasImplicitApprovalByUser && noLongerExplicitlyApproved) {
+ message.append(
+ String.format(
+ "\nThe listed files are still implicitly approved by %s.\n", user.getName()));
+ }
+
+ return Optional.of(message.toString());
+ }
+
+ private void appendPaths(StringBuilder message, Stream<Path> pathsToAppend) {
+ pathsToAppend.forEach(path -> message.append(String.format("* %s\n", JgitPath.of(path).get())));
+ }
+
+ private boolean isCodeOwnerApprovalNewlyApplied(
+ 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 isCodeOwnerApprovalRemoved(
+ 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 isCodeOwnerApprovalUpOrDowngraded(
+ 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));
+ }
+
+ private ImmutableList<Path> getOwnedPaths(ChangeNotes changeNotes, Account.Id accountId) {
+ try {
+ return codeOwnerApprovalCheck
+ .getFileStatusesForAccount(changeNotes, accountId)
+ .flatMap(
+ fileCodeOwnerStatus ->
+ Stream.of(
+ fileCodeOwnerStatus.newPathStatus(), fileCodeOwnerStatus.oldPathStatus())
+ .filter(Optional::isPresent)
+ .map(Optional::get))
+ .filter(PathCodeOwnerStatus -> PathCodeOwnerStatus.status() == CodeOwnerStatus.APPROVED)
+ .map(PathCodeOwnerStatus::path)
+ .sorted(comparing(Path::toString))
+ .collect(toImmutableList());
+ } catch (IOException | ResourceConflictException | PatchListNotAvailableException e) {
+ logger.atSevere().withCause(e).log(
+ "Failed to compute owned paths of change %s for account %s",
+ changeNotes.getChangeId(), accountId.get());
+ return ImmutableList.of();
+ }
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigValidator.java b/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigValidator.java
index 8f20fc1..6a76360 100644
--- a/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigValidator.java
+++ b/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigValidator.java
@@ -104,7 +104,7 @@
String.format(
"failed to validate file %s for revision %s in ref %s of project %s",
fileName, receiveEvent.commit.getName(), RefNames.REFS_CONFIG, project);
- logger.atSevere().log(errorMessage);
+ logger.atSevere().withCause(e).log(errorMessage);
throw new CommitValidationException(errorMessage, e);
}
}
diff --git a/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfiguration.java b/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfiguration.java
index 1a7985d..f55642b 100644
--- a/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfiguration.java
+++ b/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfiguration.java
@@ -166,6 +166,17 @@
}
/**
+ * Gets the max paths in change messages for the given project.
+ *
+ * @param project the project for which the fallback code owners should be retrieved
+ * @return the fallback code owners for the given project
+ */
+ public int getMaxPathsInChangeMessages(Project.NameKey project) {
+ requireNonNull(project, "project");
+ return generalConfig.getMaxPathsInChangeMessages(project, getPluginConfig(project));
+ }
+
+ /**
* Checks whether an implicit code owner approval from the last uploader is assumed.
*
* @param project the project for it should be checked whether implict approvals are enabled
diff --git a/java/com/google/gerrit/plugins/codeowners/config/GeneralConfig.java b/java/com/google/gerrit/plugins/codeowners/config/GeneralConfig.java
index 85fd19a..cafe8e7 100644
--- a/java/com/google/gerrit/plugins/codeowners/config/GeneralConfig.java
+++ b/java/com/google/gerrit/plugins/codeowners/config/GeneralConfig.java
@@ -69,6 +69,9 @@
@VisibleForTesting
public static final String KEY_ENABLE_VALIDATION_ON_SUBMIT = "enableValidationOnSubmit";
+ @VisibleForTesting
+ public static final String KEY_MAX_PATHS_IN_CHANGE_MESSAGES = "maxPathsInChangeMessages";
+
@VisibleForTesting public static final String KEY_MERGE_COMMIT_STRATEGY = "mergeCommitStrategy";
@VisibleForTesting public static final String KEY_GLOBAL_CODE_OWNER = "globalCodeOwner";
@@ -77,6 +80,8 @@
@VisibleForTesting public static final String KEY_OVERRIDE_INFO_URL = "overrideInfoUrl";
+ @VisibleForTesting static final int DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES = 100;
+
private final String pluginName;
private final PluginConfig pluginConfigFromGerritConfig;
@@ -141,6 +146,29 @@
ValidationMessage.Type.ERROR));
}
+ try {
+ projectLevelConfig
+ .getConfig()
+ .getInt(
+ SECTION_CODE_OWNERS,
+ null,
+ KEY_MAX_PATHS_IN_CHANGE_MESSAGES,
+ DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES);
+ } catch (IllegalArgumentException e) {
+ validationMessages.add(
+ new CommitValidationMessage(
+ String.format(
+ "The value for max paths in change messages '%s' that is configured in %s"
+ + " (parameter %s.%s) is invalid.",
+ projectLevelConfig
+ .getConfig()
+ .getString(SECTION_CODE_OWNERS, null, KEY_MAX_PATHS_IN_CHANGE_MESSAGES),
+ fileName,
+ SECTION_CODE_OWNERS,
+ KEY_MAX_PATHS_IN_CHANGE_MESSAGES),
+ ValidationMessage.Type.ERROR));
+ }
+
return ImmutableList.copyOf(validationMessages);
}
@@ -237,6 +265,51 @@
}
/**
+ * Gets the maximum number of paths that should be incuded in change messages.
+ *
+ * @param project the project for which the maximum number of paths in change messages should be
+ * read
+ * @param pluginConfig the plugin config from which the maximum number of paths in change messages
+ * should be read
+ * @return the maximum number of paths in change messages
+ */
+ int getMaxPathsInChangeMessages(Project.NameKey project, Config pluginConfig) {
+ requireNonNull(project, "project");
+ requireNonNull(pluginConfig, "pluginConfig");
+
+ String maxPathInChangeMessagesString =
+ pluginConfig.getString(SECTION_CODE_OWNERS, null, KEY_MAX_PATHS_IN_CHANGE_MESSAGES);
+ if (maxPathInChangeMessagesString != null) {
+ try {
+ return pluginConfig.getInt(
+ SECTION_CODE_OWNERS,
+ null,
+ KEY_MAX_PATHS_IN_CHANGE_MESSAGES,
+ DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES);
+ } catch (IllegalArgumentException e) {
+ logger.atWarning().log(
+ "Ignoring invalid value %s for max paths in change messages in '%s.config' of"
+ + " project %s. Falling back to global config.",
+ maxPathInChangeMessagesString, pluginName, project.get());
+ }
+ }
+
+ try {
+ return pluginConfigFromGerritConfig.getInt(
+ KEY_MAX_PATHS_IN_CHANGE_MESSAGES, DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES);
+ } catch (IllegalArgumentException e) {
+ logger.atWarning().log(
+ "Ignoring invalid value %s for max paths in change messages in gerrit.config (parameter"
+ + " plugin.%s.%s). Falling back to default value %s.",
+ pluginConfigFromGerritConfig.getString(KEY_MAX_PATHS_IN_CHANGE_MESSAGES),
+ pluginName,
+ KEY_MAX_PATHS_IN_CHANGE_MESSAGES,
+ DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES);
+ return DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES;
+ }
+ }
+
+ /**
* Gets the enable validation on commit received configuration from the given plugin config with
* fallback to {@code gerrit.config} and default to {@code true}.
*
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersOnPostReviewIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersOnPostReviewIT.java
new file mode 100644
index 0000000..e17570a
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersOnPostReviewIT.java
@@ -0,0 +1,569 @@
+// 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 com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.common.LabelDefinitionInput;
+import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
+import java.util.Collection;
+import org.junit.Test;
+
+/**
+ * Acceptance test for {@code com.google.gerrit.plugins.codeowners.backend.CodeOwnersOnPostReview}.
+ */
+public class CodeOwnersOnPostReviewIT extends AbstractCodeOwnersIT {
+ @Test
+ public void changeMessageListsNewlyApprovedPaths() throws Exception {
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+
+ String path = "foo/bar.baz";
+ String changeId = createChange("Test Change", path, "file content").getChangeId();
+
+ recommend(changeId);
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ String.format(
+ "Patch Set 1: Code-Review+1\n\n"
+ + "By voting Code-Review+1 the following files are now code-owner approved by"
+ + " %s:\n"
+ + "* %s\n",
+ admin.fullName(), path));
+ }
+
+ @Test
+ public void changeMessageListsPathsThatAreStillApproved() throws Exception {
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+
+ String path = "foo/bar.baz";
+ String changeId = createChange("Test Change", path, "file content").getChangeId();
+
+ recommend(changeId);
+
+ // Upgrade the approval from Code-Review+1 to Code-Review+2
+ approve(changeId);
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ String.format(
+ "Patch Set 1: Code-Review+2\n\n"
+ + "By voting Code-Review+2 the following files are still code-owner approved by"
+ + " %s:\n"
+ + "* %s\n",
+ admin.fullName(), path));
+ }
+
+ @Test
+ public void changeMessageListsPathsThatAreNoLongerApproved_voteRemoved() throws Exception {
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+
+ String path = "foo/bar.baz";
+ String changeId = createChange("Test Change", path, "file content").getChangeId();
+
+ recommend(changeId);
+
+ // Remove the approval.
+ gApi.changes().id(changeId).current().review(new ReviewInput().label("Code-Review", 0));
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ String.format(
+ "Patch Set 1: -Code-Review\n\n"
+ + "By removing the Code-Review vote the following files are no longer"
+ + " code-owner approved by %s:\n"
+ + "* %s\n",
+ admin.fullName(), path));
+ }
+
+ @Test
+ public void changeMessageListsPathsThatAreNoLongerApproved_voteChangedToNegativeValue()
+ throws Exception {
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+
+ String path = "foo/bar.baz";
+ String changeId = createChange("Test Change", path, "file content").getChangeId();
+
+ recommend(changeId);
+
+ // Vote with a negative value.
+ gApi.changes().id(changeId).current().review(new ReviewInput().label("Code-Review", -1));
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ String.format(
+ "Patch Set 1: Code-Review-1\n\n"
+ + "By voting Code-Review-1 the following files are no longer code-owner"
+ + " approved by %s:\n"
+ + "* %s\n",
+ admin.fullName(), path));
+ }
+
+ @Test
+ public void changeMessageListsOnlyApprovedPaths() throws Exception {
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+
+ String path1 = "foo/bar.baz";
+ String path2 = "foo/baz.bar";
+ String changeId =
+ createChange(
+ "Test Change",
+ ImmutableMap.of(
+ path1,
+ "file content",
+ path2,
+ "file content",
+ "bar/foo.baz",
+ "file content",
+ "bar/baz.foo",
+ "file content"))
+ .getChangeId();
+
+ recommend(changeId);
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ String.format(
+ "Patch Set 1: Code-Review+1\n\n"
+ + "By voting Code-Review+1 the following files are now code-owner approved by"
+ + " %s:\n"
+ + "* %s\n"
+ + "* %s\n",
+ admin.fullName(), path1, path2));
+ }
+
+ @Test
+ public void changeMessageListsOnlyApprovedPaths_fileRenamed() throws Exception {
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+
+ // createChangeWithFileRename creates a change with 2 patch sets
+ String oldPath = "foo/bar.baz";
+ String newPath = "bar/baz.bar";
+ String changeId = createChangeWithFileRename(oldPath, newPath);
+
+ recommend(changeId);
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ String.format(
+ "Patch Set 2: Code-Review+1\n\n"
+ + "By voting Code-Review+1 the following files are now code-owner approved by"
+ + " %s:\n"
+ + "* %s\n",
+ admin.fullName(), oldPath));
+ }
+
+ @Test
+ public void changeMessageNotExtendedIfUserOwnsNoneOfTheFiles() throws Exception {
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+
+ String changeId =
+ createChange(
+ "Test Change",
+ ImmutableMap.of("bar/foo.baz", "file content", "bar/baz.foo", "file content"))
+ .getChangeId();
+
+ recommend(changeId);
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message).isEqualTo("Patch Set 1: Code-Review+1");
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.requiredApproval", value = "Owners-Approval+1")
+ public void changeMessageNotExtendedForNonCodeOwnerApproval() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Owner Approval", " 0", "No Owner Approval");
+ gApi.projects().name(project.get()).label("Owners-Approval").create(input).get();
+
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+
+ String changeId =
+ createChange(
+ "Test Change",
+ ImmutableMap.of("bar/foo.baz", "file content", "bar/baz.foo", "file content"))
+ .getChangeId();
+
+ recommend(changeId);
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message).isEqualTo("Patch Set 1: Code-Review+1");
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true")
+ public void changeMessageListsNewlyApprovedPathsIfTheyWereAlreadyImplicitlyApproved()
+ throws Exception {
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+
+ String path = "foo/bar.baz";
+ String changeId = createChange("Test Change", path, "file content").getChangeId();
+
+ recommend(changeId);
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ String.format(
+ "Patch Set 1: Code-Review+1\n\n"
+ + "By voting Code-Review+1 the following files are now explicitly code-owner"
+ + " approved by %s:\n"
+ + "* %s\n",
+ admin.fullName(), path));
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true")
+ public void changeMessageListsPathsThatAreNoLongerExplicitlyApproved_voteRemoved()
+ throws Exception {
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+
+ String path = "foo/bar.baz";
+ String changeId = createChange("Test Change", path, "file content").getChangeId();
+
+ recommend(changeId);
+
+ // Remove the approval.
+ gApi.changes().id(changeId).current().review(new ReviewInput().label("Code-Review", 0));
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ String.format(
+ "Patch Set 1: -Code-Review\n\n"
+ + "By removing the Code-Review vote the following files are no longer"
+ + " explicitly code-owner approved by %s:\n"
+ + "* %s\n"
+ + "\n"
+ + "The listed files are still implicitly approved by %s.\n",
+ admin.fullName(), path, admin.fullName()));
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true")
+ public void changeMessageListsPathsThatAreNoLongerExplicitlyApproved_voteChangedToNegativeValue()
+ throws Exception {
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+
+ String path = "foo/bar.baz";
+ String changeId = createChange("Test Change", path, "file content").getChangeId();
+
+ recommend(changeId);
+
+ // Vote with a negative value.
+ gApi.changes().id(changeId).current().review(new ReviewInput().label("Code-Review", -1));
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ String.format(
+ "Patch Set 1: Code-Review-1\n\n"
+ + "By voting Code-Review-1 the following files are no longer explicitly"
+ + " code-owner approved by %s:\n"
+ + "* %s\n"
+ + "\n"
+ + "The listed files are still implicitly approved by %s.\n",
+ admin.fullName(), path, admin.fullName()));
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true")
+ public void changeMessageListsNewlyApprovedPaths_noImplicitApprovalButImplicitApprovalsEnabled()
+ throws Exception {
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+
+ String path = "foo/bar.baz";
+ String changeId = createChange(user, "Test Change", path, "file content").getChangeId();
+
+ recommend(changeId);
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ String.format(
+ "Patch Set 1: Code-Review+1\n\n"
+ + "By voting Code-Review+1 the following files are now code-owner approved by"
+ + " %s:\n"
+ + "* %s\n",
+ admin.fullName(), path));
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true")
+ public void
+ changeMessageListsPathsThatAreNoLongerApproved_voteRemoved_noImplicitApprovalButImplicitApprovalsEnabled()
+ throws Exception {
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+
+ String path = "foo/bar.baz";
+ String changeId = createChange(user, "Test Change", path, "file content").getChangeId();
+
+ recommend(changeId);
+
+ // Remove the approval.
+ gApi.changes().id(changeId).current().review(new ReviewInput().label("Code-Review", 0));
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ String.format(
+ "Patch Set 1: -Code-Review\n\n"
+ + "By removing the Code-Review vote the following files are no longer"
+ + " code-owner approved by %s:\n"
+ + "* %s\n",
+ admin.fullName(), path));
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true")
+ public void
+ changeMessageListsPathsThatAreNoLongerApproved_voteChangedToNegativeValue_noImplicitApprovalButImplicitApprovalsEnabled()
+ throws Exception {
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+
+ String path = "foo/bar.baz";
+ String changeId = createChange(user, "Test Change", path, "file content").getChangeId();
+
+ recommend(changeId);
+
+ // Vote with a negative value.
+ gApi.changes().id(changeId).current().review(new ReviewInput().label("Code-Review", -1));
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ String.format(
+ "Patch Set 1: Code-Review-1\n\n"
+ + "By voting Code-Review-1 the following files are no longer code-owner"
+ + " approved by %s:\n"
+ + "* %s\n",
+ admin.fullName(), path));
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.maxPathsInChangeMessages", value = "4")
+ public void pathsInChangeMessageAreLimited_limitNotReached() throws Exception {
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+
+ String path1 = "foo/bar.baz";
+ String path2 = "foo/baz.bar";
+ String path3 = "bar/foo.baz";
+ String path4 = "bar/baz.foo";
+ String changeId =
+ createChange(
+ "Test Change",
+ ImmutableMap.of(
+ path1,
+ "file content",
+ path2,
+ "file content",
+ path3,
+ "file content",
+ path4,
+ "file content"))
+ .getChangeId();
+
+ recommend(changeId);
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ String.format(
+ "Patch Set 1: Code-Review+1\n\n"
+ + "By voting Code-Review+1 the following files are now code-owner approved by"
+ + " %s:\n"
+ + "* %s\n"
+ + "* %s\n"
+ + "* %s\n"
+ + "* %s\n",
+ admin.fullName(), path4, path3, path1, path2));
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.maxPathsInChangeMessages", value = "3")
+ public void pathsInChangeMessageAreLimited_limitReached() throws Exception {
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+
+ String path1 = "foo/bar.baz";
+ String path2 = "foo/baz.bar";
+ String path3 = "bar/foo.baz";
+ String path4 = "bar/baz.foo";
+ String changeId =
+ createChange(
+ "Test Change",
+ ImmutableMap.of(
+ path1,
+ "file content",
+ path2,
+ "file content",
+ path3,
+ "file content",
+ path4,
+ "file content"))
+ .getChangeId();
+
+ recommend(changeId);
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ String.format(
+ "Patch Set 1: Code-Review+1\n\n"
+ + "By voting Code-Review+1 the following files are now code-owner approved by"
+ + " %s:\n"
+ + "* %s\n"
+ + "* %s\n"
+ + "(2 more files)\n",
+ admin.fullName(), path4, path3));
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.maxPathsInChangeMessages", value = "0")
+ public void pathsInChangeMessagesDisabled() throws Exception {
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+
+ String changeId =
+ createChange(
+ "Test Change",
+ ImmutableMap.of(
+ "foo/bar.baz",
+ "file content",
+ "foo/baz.bar",
+ "file content",
+ "bar/foo.baz",
+ "file content",
+ "bar/baz.foo",
+ "file content"))
+ .getChangeId();
+
+ recommend(changeId);
+
+ Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+ assertThat(Iterables.getLast(messages).message).isEqualTo("Patch Set 1: Code-Review+1");
+ }
+}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersPluginConfigValidatorIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersPluginConfigValidatorIT.java
index 1ea4183..e22062e 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersPluginConfigValidatorIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersPluginConfigValidatorIT.java
@@ -494,6 +494,44 @@
+ " (parameter codeOwners.fallbackCodeOwners) is invalid.");
}
+ @Test
+ public void configureMaxPathsInChangeMessages() throws Exception {
+ fetchRefsMetaConfig();
+
+ Config cfg = new Config();
+ cfg.setInt(
+ CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS,
+ /* subsection= */ null,
+ GeneralConfig.KEY_MAX_PATHS_IN_CHANGE_MESSAGES,
+ 50);
+ setCodeOwnersConfig(cfg);
+
+ PushResult r = pushRefsMetaConfig();
+ assertThat(r.getRemoteUpdate(RefNames.REFS_CONFIG).getStatus()).isEqualTo(Status.OK);
+ assertThat(codeOwnersPluginConfiguration.getMaxPathsInChangeMessages(project)).isEqualTo(50);
+ }
+
+ @Test
+ public void cannotSetInvalidMaxPathsInChangeMessages() throws Exception {
+ fetchRefsMetaConfig();
+
+ Config cfg = new Config();
+ cfg.setString(
+ CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS,
+ /* subsection= */ null,
+ GeneralConfig.KEY_MAX_PATHS_IN_CHANGE_MESSAGES,
+ "INVALID");
+ setCodeOwnersConfig(cfg);
+
+ PushResult r = pushRefsMetaConfig();
+ assertThat(r.getRemoteUpdate(RefNames.REFS_CONFIG).getStatus())
+ .isEqualTo(Status.REJECTED_OTHER_REASON);
+ assertThat(r.getMessages())
+ .contains(
+ "The value for max paths in change messages 'INVALID' that is configured in"
+ + " code-owners.config (parameter codeOwners.maxPathsInChangeMessages) is invalid.");
+ }
+
private void fetchRefsMetaConfig() throws Exception {
fetch(testRepo, RefNames.REFS_CONFIG + ":" + RefNames.REFS_CONFIG);
testRepo.reset(RefNames.REFS_CONFIG);
diff --git a/javatests/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigurationTest.java b/javatests/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigurationTest.java
index 641bf10..f03427d 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigurationTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigurationTest.java
@@ -922,6 +922,50 @@
assertThat(codeOwnersPluginConfiguration.areImplicitApprovalsEnabled(project)).isFalse();
}
+ @Test
+ public void cannotGetMaxPathsInChangeMessagesForNullProject() throws Exception {
+ NullPointerException npe =
+ assertThrows(
+ NullPointerException.class,
+ () -> codeOwnersPluginConfiguration.getMaxPathsInChangeMessages(/* project= */ null));
+ assertThat(npe).hasMessageThat().isEqualTo("project");
+ }
+
+ @Test
+ public void getMaxPathsInChangeMessagesIfNoneIsConfigured() throws Exception {
+ assertThat(codeOwnersPluginConfiguration.getMaxPathsInChangeMessages(project))
+ .isEqualTo(GeneralConfig.DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.maxPathsInChangeMessages", value = "50")
+ public void getMaxPathsInChangeMessagesIfNoneIsConfiguredOnProjectLevel() throws Exception {
+ assertThat(codeOwnersPluginConfiguration.getMaxPathsInChangeMessages(project)).isEqualTo(50);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.maxPathsInChangeMessages", value = "50")
+ public void maxPathInChangeMessagesOnProjectLevelOverridesGlobalMaxPathInChangeMessages()
+ throws Exception {
+ configureFallbackCodeOwners(project, FallbackCodeOwners.NONE);
+ assertThat(codeOwnersPluginConfiguration.getFallbackCodeOwners(project))
+ .isEqualTo(FallbackCodeOwners.NONE);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.maxPathsInChangeMessages", value = "50")
+ public void maxPathInChangeMessagesIsInheritedFromParentProject() throws Exception {
+ configureMaxPathsInChangeMessages(allProjects, 20);
+ assertThat(codeOwnersPluginConfiguration.getMaxPathsInChangeMessages(project)).isEqualTo(20);
+ }
+
+ @Test
+ public void inheritedMaxPathInChangeMessagesCanBeOverridden() throws Exception {
+ configureMaxPathsInChangeMessages(allProjects, 50);
+ configureMaxPathsInChangeMessages(project, 20);
+ assertThat(codeOwnersPluginConfiguration.getMaxPathsInChangeMessages(project)).isEqualTo(20);
+ }
+
private void configureDisabled(Project.NameKey project, String disabled) throws Exception {
setCodeOwnersConfig(project, /* subsection= */ null, StatusConfig.KEY_DISABLED, disabled);
}
@@ -987,6 +1031,15 @@
fallbackCodeOwners.name());
}
+ private void configureMaxPathsInChangeMessages(
+ Project.NameKey project, int maxPathsInChangeMessages) throws Exception {
+ setCodeOwnersConfig(
+ project,
+ /* subsection= */ null,
+ GeneralConfig.KEY_MAX_PATHS_IN_CHANGE_MESSAGES,
+ Integer.toString(maxPathsInChangeMessages));
+ }
+
private AutoCloseable registerTestBackend() {
RegistrationHandle registrationHandle =
((PrivateInternals_DynamicMapImpl<CodeOwnerBackend>) codeOwnerBackends)
diff --git a/javatests/com/google/gerrit/plugins/codeowners/config/GeneralConfigTest.java b/javatests/com/google/gerrit/plugins/codeowners/config/GeneralConfigTest.java
index 6bc8ac5..78adae8 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/config/GeneralConfigTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/config/GeneralConfigTest.java
@@ -16,12 +16,14 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS;
+import static com.google.gerrit.plugins.codeowners.config.GeneralConfig.DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES;
import static com.google.gerrit.plugins.codeowners.config.GeneralConfig.KEY_ENABLE_IMPLICIT_APPROVALS;
import static com.google.gerrit.plugins.codeowners.config.GeneralConfig.KEY_ENABLE_VALIDATION_ON_COMMIT_RECEIVED;
import static com.google.gerrit.plugins.codeowners.config.GeneralConfig.KEY_ENABLE_VALIDATION_ON_SUBMIT;
import static com.google.gerrit.plugins.codeowners.config.GeneralConfig.KEY_FALLBACK_CODE_OWNERS;
import static com.google.gerrit.plugins.codeowners.config.GeneralConfig.KEY_FILE_EXTENSION;
import static com.google.gerrit.plugins.codeowners.config.GeneralConfig.KEY_GLOBAL_CODE_OWNER;
+import static com.google.gerrit.plugins.codeowners.config.GeneralConfig.KEY_MAX_PATHS_IN_CHANGE_MESSAGES;
import static com.google.gerrit.plugins.codeowners.config.GeneralConfig.KEY_MERGE_COMMIT_STRATEGY;
import static com.google.gerrit.plugins.codeowners.config.GeneralConfig.KEY_OVERRIDE_INFO_URL;
import static com.google.gerrit.plugins.codeowners.config.GeneralConfig.KEY_READ_ONLY;
@@ -500,4 +502,61 @@
assertThat(generalConfig.getFallbackCodeOwners(project, new Config()))
.isEqualTo(FallbackCodeOwners.NONE);
}
+
+ @Test
+ public void cannotGetMaxPathsInChangeMessagesForNullProject() throws Exception {
+ NullPointerException npe =
+ assertThrows(
+ NullPointerException.class,
+ () -> generalConfig.getMaxPathsInChangeMessages(/* project= */ null, new Config()));
+ assertThat(npe).hasMessageThat().isEqualTo("project");
+ }
+
+ @Test
+ public void cannotGetMaxPathsInChangeMessagesForNullPluginConfig() throws Exception {
+ NullPointerException npe =
+ assertThrows(
+ NullPointerException.class,
+ () -> generalConfig.getMaxPathsInChangeMessages(project, /* pluginConfig= */ null));
+ assertThat(npe).hasMessageThat().isEqualTo("pluginConfig");
+ }
+
+ @Test
+ public void noMaxPathsInChangeMessagesConfigured() throws Exception {
+ assertThat(generalConfig.getMaxPathsInChangeMessages(project, new Config()))
+ .isEqualTo(DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.maxPathsInChangeMessages", value = "50")
+ public void maxPathsInChangeMessagesIsRetrievedFromGerritConfigIfNotSpecifiedOnProjectLevel()
+ throws Exception {
+ assertThat(generalConfig.getMaxPathsInChangeMessages(project, new Config())).isEqualTo(50);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.maxPathsInChangeMessages", value = "50")
+ public void
+ maxPathsInChangeMessagesInPluginConfigOverridesMaxPathsInChangeMessagesInGerritConfig()
+ throws Exception {
+ Config cfg = new Config();
+ cfg.setString(SECTION_CODE_OWNERS, null, KEY_MAX_PATHS_IN_CHANGE_MESSAGES, "10");
+ assertThat(generalConfig.getMaxPathsInChangeMessages(project, cfg)).isEqualTo(10);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.maxPathsInChangeMessages", value = "50")
+ public void globalMaxPathsInChangeMessagesUsedIfInvalidMaxPathsInChangeMessagesConfigured()
+ throws Exception {
+ Config cfg = new Config();
+ cfg.setString(SECTION_CODE_OWNERS, null, KEY_MAX_PATHS_IN_CHANGE_MESSAGES, "INVALID");
+ assertThat(generalConfig.getMaxPathsInChangeMessages(project, cfg)).isEqualTo(50);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.maxPathsInChangeMessages", value = "INVALID")
+ public void defaultValueUsedIfInvalidMaxPathsInChangeMessagesConfigured() throws Exception {
+ assertThat(generalConfig.getMaxPathsInChangeMessages(project, new Config()))
+ .isEqualTo(DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES);
+ }
}
diff --git a/resources/Documentation/config.md b/resources/Documentation/config.md
index 28c65b9..d546374 100644
--- a/resources/Documentation/config.md
+++ b/resources/Documentation/config.md
@@ -283,6 +283,21 @@
`@PLUGIN@.config`.\
By default `NONE`.
+<a id="pluginCodeOwnersMaxPathsInChangeMessages">plugin.@PLUGIN@.maxPathsInChangeMessages</a>
+: When a user votes on the [code owners
+ label](#pluginCodeOwnersRequiredApproval) the paths that are affected by
+ the vote are listed in the change message that is posted when the vote
+ is applied.\
+ This configuration parameter controls the maximum number of paths that
+ are included in change messages. This is to prevent that the change
+ messages become too big for large changes that touch many files.\
+ Setting the value to `0` disables including affected paths into change
+ messages.\
+ Can be overridden per project by setting
+ [codeOwners.maxPathsInChangeMessages](#codeOwnersMaxPathsInChangeMessages)
+ in `@PLUGIN@.config`.\
+ By default `100`.
+
# <a id="projectConfiguration">Project configuration in @PLUGIN@.config</a>
<a id="codeOwnersDisabled">codeOwners.disabled</a>
@@ -530,6 +545,24 @@
[plugin.@PLUGIN@.fallbackCodeOwners](#pluginCodeOwnersFallbackCodeOwners)
in `gerrit.config` is used.
+<a id="codeOwnersMaxPathsInChangeMessages">codeOwners.maxPathsInChangeMessages</a>
+: When a user votes on the [code owners
+ label](#codeOwnersRequiredApproval) the paths that are affected by the
+ vote are listed in the change message that is posted when the vote is
+ applied.\
+ This configuration parameter controls the maximum number of paths that
+ are included in change messages. This is to prevent that the change
+ messages become too big for large changes that touch many files.\
+ Setting the value to `0` disables including affected paths into change
+ messages.\
+ Overrides the global setting
+ [plugin.@PLUGIN@.maxPathsInChangeMessages](#pluginCodeOwnersMaxPathsInChangeMessages)
+ in `gerrit.config`.\
+ If not set, the global setting
+ [plugin.@PLUGIN@.maxPathsInChangeMessages](#pluginCodeOwnersMaxPathsInChangeMessages)
+ in `gerrit.config` is used.\
+ By default `100`.
+
---
Back to [@PLUGIN@ documentation index](index.html)
diff --git a/resources/Documentation/validation.md b/resources/Documentation/validation.md
index ef1b4ce..746221e 100644
--- a/resources/Documentation/validation.md
+++ b/resources/Documentation/validation.md
@@ -154,6 +154,8 @@
configuration is valid
* the [codeOwners.fallbackCodeOwners](config.html#codeOwnersFallbackCodeOwners)
configuration is valid
+* the [codeOwners.maxPathsInChangeMessages](config.html#codeOwnersMaxPathsInChangeMessages)
+ configuration is valid
---