Allow to enforce implicit approvals even if self approvals are disallowed
At the moment implicit approvals are disabled if the configured required
label disallows self approvals. Add a new option for the
enableImplicitApprovals configuration setting that allows to enable
implicit approvals even if the configured required label disallows self
approvals.
Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: I723097069467ee16f62d7c75d5a4fb4be16e487b
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/EnableImplicitApprovals.java b/java/com/google/gerrit/plugins/codeowners/backend/EnableImplicitApprovals.java
new file mode 100644
index 0000000..bcad5e8
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/EnableImplicitApprovals.java
@@ -0,0 +1,33 @@
+// 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;
+
+/** Enum to control whether implicit code-owner approvals by the patch set uploader are enabled. */
+public enum EnableImplicitApprovals {
+ /** Implicit code-owner approvals of the the patch set uploader are disabled. */
+ FALSE,
+
+ /**
+ * Implicit code-owner approvals of the patch set uploader are enabled, but only if the configured
+ * required label allows self approvals.
+ */
+ TRUE,
+
+ /**
+ * Implicit code-owner approvals of the patch set uploader are enabled, even if the configured
+ * required label disallows self approvals.
+ */
+ FORCED;
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfiguration.java b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfiguration.java
index 4b70846..cc62a94 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfiguration.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfiguration.java
@@ -29,6 +29,7 @@
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
+import com.google.gerrit.plugins.codeowners.backend.EnableImplicitApprovals;
import com.google.gerrit.plugins.codeowners.backend.FallbackCodeOwners;
import com.google.gerrit.plugins.codeowners.common.CodeOwnerConfigValidationPolicy;
import com.google.gerrit.plugins.codeowners.common.MergeCommitStrategy;
@@ -213,15 +214,30 @@
*/
public boolean areImplicitApprovalsEnabled(Project.NameKey project) {
requireNonNull(project, "project");
- LabelType requiredLabel = getRequiredApproval(project).labelType();
- if (requiredLabel.isIgnoreSelfApproval()) {
- logger.atFine().log(
- "ignoring implicit approval configuration on project %s since the label of the required"
- + " approval (%s) is configured to ignore self approvals",
- project, requiredLabel);
- return false;
+ EnableImplicitApprovals enableImplicitApprovals =
+ generalConfig.getEnableImplicitApprovals(project, getPluginConfig(project));
+ switch (enableImplicitApprovals) {
+ case FALSE:
+ logger.atFine().log("implicit approvals on project %s are disabled", project);
+ return false;
+ case TRUE:
+ LabelType requiredLabel = getRequiredApproval(project).labelType();
+ if (requiredLabel.isIgnoreSelfApproval()) {
+ logger.atFine().log(
+ "ignoring implicit approval configuration on project %s since the label of the required"
+ + " approval (%s) is configured to ignore self approvals",
+ project, requiredLabel);
+ return false;
+ }
+ return true;
+ case FORCED:
+ logger.atFine().log("implicit approvals on project %s are enforced", project);
+ return true;
}
- return generalConfig.getEnableImplicitApprovals(getPluginConfig(project));
+ throw new IllegalStateException(
+ String.format(
+ "unknown value %s for enableImplicitApprovals configuration in project %s",
+ enableImplicitApprovals, project));
}
/**
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java b/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java
index 3e5538c..187a7ab 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java
@@ -26,6 +26,7 @@
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
+import com.google.gerrit.plugins.codeowners.backend.EnableImplicitApprovals;
import com.google.gerrit.plugins.codeowners.backend.FallbackCodeOwners;
import com.google.gerrit.plugins.codeowners.common.CodeOwnerConfigValidationPolicy;
import com.google.gerrit.plugins.codeowners.common.MergeCommitStrategy;
@@ -463,17 +464,44 @@
* Gets whether an implicit code owner approval from the last uploader is assumed from the given
* plugin config with fallback to {@code gerrit.config}.
*
+ * @param project the name of the project for which the configuration should be read
* @param pluginConfig the plugin config from which the configuration should be read.
* @return whether an implicit code owner approval from the last uploader is assumed
*/
- boolean getEnableImplicitApprovals(Config pluginConfig) {
+ EnableImplicitApprovals getEnableImplicitApprovals(Project.NameKey project, Config pluginConfig) {
+ requireNonNull(project, "project");
requireNonNull(pluginConfig, "pluginConfig");
- return pluginConfig.getBoolean(
- SECTION_CODE_OWNERS,
- null,
- KEY_ENABLE_IMPLICIT_APPROVALS,
- pluginConfigFromGerritConfig.getBoolean(KEY_ENABLE_IMPLICIT_APPROVALS, false));
+ String enableImplicitApprovalsString =
+ pluginConfig.getString(SECTION_CODE_OWNERS, null, KEY_ENABLE_IMPLICIT_APPROVALS);
+ if (enableImplicitApprovalsString != null) {
+ try {
+ return pluginConfig.getEnum(
+ SECTION_CODE_OWNERS,
+ null,
+ KEY_ENABLE_IMPLICIT_APPROVALS,
+ EnableImplicitApprovals.FALSE);
+ } catch (IllegalArgumentException e) {
+ logger.atWarning().withCause(e).log(
+ "Ignoring invalid value %s for enabling implicit approvals in '%s.config' of project"
+ + " %s. Falling back to global config or default value.",
+ enableImplicitApprovalsString, pluginName, project.get());
+ }
+ }
+
+ try {
+ return pluginConfigFromGerritConfig.getEnum(
+ KEY_ENABLE_IMPLICIT_APPROVALS, EnableImplicitApprovals.FALSE);
+ } catch (IllegalArgumentException e) {
+ logger.atWarning().withCause(e).log(
+ "Ignoring invalid value %s for enabling implict approvals in gerrit.config (parameter"
+ + " plugin.%s.%s). Falling back to default value %s.",
+ pluginConfigFromGerritConfig.getString(KEY_ENABLE_IMPLICIT_APPROVALS),
+ pluginName,
+ KEY_ENABLE_IMPLICIT_APPROVALS,
+ EnableImplicitApprovals.FALSE);
+ return EnableImplicitApprovals.FALSE;
+ }
}
/**
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithSelfApprovalsIgnoredTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithSelfApprovalsIgnoredTest.java
index 3195389..3112b07 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithSelfApprovalsIgnoredTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithSelfApprovalsIgnoredTest.java
@@ -273,6 +273,65 @@
}
@Test
+ @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "forced")
+ public void implicitlyApprovedByUploaderWhoIsChangeOwner() throws Exception {
+ TestAccount codeOwner =
+ accountCreator.create(
+ "codeOwner", "codeOwner@example.com", "CodeOwner", /* displayName= */ null);
+ setAsRootCodeOwners(codeOwner);
+
+ Path path = Paths.get("/foo/bar.baz");
+ String changeId =
+ createChange(codeOwner, "Change Adding A File", JgitPath.of(path).get(), "file content")
+ .getChangeId();
+
+ // Verify that the file is approved.
+ Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses =
+ codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject =
+ assertThatStream(fileCodeOwnerStatuses).onlyElement();
+ fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+ fileCodeOwnerStatusSubject
+ .hasNewPathStatus()
+ .value()
+ .hasStatusThat()
+ .isEqualTo(CodeOwnerStatus.APPROVED);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "forced")
+ public void implicitlyApprovedByUploader() throws Exception {
+ TestAccount changeOwner =
+ accountCreator.create(
+ "changeOwner", "changeOwner@example.com", "ChangeOwner", /* displayName= */ null);
+
+ TestAccount codeOwner =
+ accountCreator.create(
+ "codeOwner", "codeOwner@example.com", "CodeOwner", /* displayName= */ null);
+ setAsRootCodeOwners(codeOwner);
+
+ Path path = Paths.get("/foo/bar.baz");
+ String changeId =
+ createChange(changeOwner, "Change Adding A File", JgitPath.of(path).get(), "file content")
+ .getChangeId();
+
+ // Upload another patch set by a code owner.
+ amendChange(codeOwner, changeId);
+
+ // Verify that the file is approved.
+ Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses =
+ codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject =
+ assertThatStream(fileCodeOwnerStatuses).onlyElement();
+ fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+ fileCodeOwnerStatusSubject
+ .hasNewPathStatus()
+ .value()
+ .hasStatusThat()
+ .isEqualTo(CodeOwnerStatus.APPROVED);
+ }
+
+ @Test
public void notOverriddenByUploaderWhoIsChangeOwner() throws Exception {
TestAccount changeOwner =
accountCreator.create(
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java
index 5a78e50..159ce96 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java
@@ -37,6 +37,7 @@
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
+import com.google.gerrit.plugins.codeowners.backend.EnableImplicitApprovals;
import com.google.gerrit.plugins.codeowners.backend.FallbackCodeOwners;
import com.google.gerrit.plugins.codeowners.common.CodeOwnerConfigValidationPolicy;
import com.google.gerrit.plugins.codeowners.common.MergeCommitStrategy;
@@ -449,16 +450,27 @@
}
@Test
+ public void cannotGetEnableImplicitApprovalsForNullProject() throws Exception {
+ NullPointerException npe =
+ assertThrows(
+ NullPointerException.class,
+ () -> generalConfig.getEnableImplicitApprovals(/* project= */ null, new Config()));
+ assertThat(npe).hasMessageThat().isEqualTo("project");
+ }
+
+ @Test
public void cannotGetEnableImplicitApprovalsForNullPluginConfig() throws Exception {
NullPointerException npe =
assertThrows(
- NullPointerException.class, () -> generalConfig.getEnableImplicitApprovals(null));
+ NullPointerException.class,
+ () -> generalConfig.getEnableImplicitApprovals(project, /* pluginConfig= */ null));
assertThat(npe).hasMessageThat().isEqualTo("pluginConfig");
}
@Test
public void noEnableImplicitApprovalsConfiguration() throws Exception {
- assertThat(generalConfig.getEnableImplicitApprovals(new Config())).isFalse();
+ assertThat(generalConfig.getEnableImplicitApprovals(project, new Config()))
+ .isEqualTo(EnableImplicitApprovals.FALSE);
}
@Test
@@ -466,7 +478,8 @@
public void
enableImplicitApprovalsConfigurationIsRetrievedFromGerritConfigIfNotSpecifiedOnProjectLevel()
throws Exception {
- assertThat(generalConfig.getEnableImplicitApprovals(new Config())).isTrue();
+ assertThat(generalConfig.getEnableImplicitApprovals(project, new Config()))
+ .isEqualTo(EnableImplicitApprovals.TRUE);
}
@Test
@@ -476,7 +489,26 @@
throws Exception {
Config cfg = new Config();
cfg.setString(SECTION_CODE_OWNERS, null, KEY_ENABLE_IMPLICIT_APPROVALS, "false");
- assertThat(generalConfig.getEnableImplicitApprovals(cfg)).isFalse();
+ assertThat(generalConfig.getEnableImplicitApprovals(project, cfg))
+ .isEqualTo(EnableImplicitApprovals.FALSE);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true")
+ public void invalidEnableImplicitApprovalsConfigurationInPluginConfigIsIgnored()
+ throws Exception {
+ Config cfg = new Config();
+ cfg.setString(SECTION_CODE_OWNERS, null, KEY_ENABLE_IMPLICIT_APPROVALS, "INVALID");
+ assertThat(generalConfig.getEnableImplicitApprovals(project, cfg))
+ .isEqualTo(EnableImplicitApprovals.TRUE);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "INVALID")
+ public void invalidEnableImplicitApprovalsConfigurationInGerritConfigIsIgnored()
+ throws Exception {
+ assertThat(generalConfig.getEnableImplicitApprovals(project, cfg))
+ .isEqualTo(EnableImplicitApprovals.FALSE);
}
@Test
diff --git a/resources/Documentation/config.md b/resources/Documentation/config.md
index 8c036fb..b40e939 100644
--- a/resources/Documentation/config.md
+++ b/resources/Documentation/config.md
@@ -91,22 +91,37 @@
<a id="pluginCodeOwnersEnableImplicitApprovals">plugin.@PLUGIN@.enableImplictApprovals</a>
: Whether an implicit code owner approval from the last uploader is
assumed.\
- This setting has no effect if self approvals from the last uploader are
- ignored because the [required label](#pluginCodeOwnersRequiredApproval)
- is configured to [ignore self
- approvals](../../../Documentation/config-labels.html#label_ignoreSelfApproval)
+ \
+ Can be `FALSE`, `TRUE` or `FORCED`.\
+ \
+ `FALSE`:\
+ Implicit code-owner approvals of the the patch set uploader are
+ disabled.\
+ \
+ `TRUE`:\
+ Implicit code-owner approvals of the patch set uploader are enabled, but
+ only if the configured [required
+ label](#pluginCodeOwnersRequiredApproval) is not configured to [ignore
+ self approvals](../../../Documentation/config-labels.html#label_ignoreSelfApproval)
from the uploader.\
- If enabled, code owners need to be aware of their implicit approval when
- they upload new patch sets for other users (e.g. if a contributor pushes
- a change to a wrong branch and a code owner helps them to get it rebased
- onto the correct branch, the rebased change has implicit approvals from
- the code owner, since the code owner is the uploader).\
+ \
+ `FORCED`:\
+ Implicit code-owner approvals of the patch set uploader are enabled,
+ even if the configured [required
+ label](#pluginCodeOwnersRequiredApproval) disallows self approvals.\
+ \
+ If enabled/enforced, code owners need to be aware of their implicit
+ approval when they upload new patch sets for other users (e.g. if a
+ contributor pushes a change to a wrong branch and a code owner helps
+ them to get it rebased onto the correct branch, the rebased change has
+ implicit approvals from the code owner, since the code owner is the
+ uploader).\
If implicit code owner approvals are disabled, code owners can still
self-approve their own changes by voting on the change.\
Can be overridden per project by setting
[codeOwners.enableImplictApprovals](#codeOwnersEnableImplicitApprovals)
in `@PLUGIN@.config`.\
- By default `false`.
+ By default `FALSE`.
<a id="pluginCodeOwnersGlobalCodeOwner">plugin.@PLUGIN@.globalCodeOwner</a>
: The email of a user that should be a code owner globally across all
@@ -454,16 +469,15 @@
<a id="codeOwnersEnableImplicitApprovals">codeOwners.enableImplicitApprovals</a>
: Whether an implicit code owner approval from the last uploader is
assumed.\
- This setting has no effect if self approvals from the last uploader are
- ignored because the [required label](#codeOwnersRequiredApproval)
- is configured to [ignore self
- approvals](../../../Documentation/config-labels.html#label_ignoreSelfApproval)
- from the uploader.\
- If enabled, code owners need to be aware of their implicit approval when
- they upload new patch sets for other users (e.g. if a contributor pushes
- a change to a wrong branch and a code owner helps them to get it rebased
- onto the correct branch, the rebased change has implicit approvals from
- the code owner, since the code owner is the uploader).\
+ Can be `FALSE`, `TRUE` or `FORCED` (see
+ [plugin.@PLUGIN@.enableImplicitApprovals](#pluginCodeOwnersEnableImplicitApprovals)
+ for an explanation of these values).\
+ If enabled/enforced, code owners need to be aware of their implicit
+ approval when they upload new patch sets for other users (e.g. if a
+ contributor pushes a change to a wrong branch and a code owner helps
+ them to get it rebased onto the correct branch, the rebased change has
+ implicit approvals from the code owner, since the code owner is the
+ uploader).\
If implicit code owner approvals are disabled, code owners can still
self-approve their own changes by voting on the change.\
Overrides the global setting