Support fallback code owner for paths that have no code owners defined
It's now possible to configure a policy for fallback code owners that
applies for paths for which no code owners have been defined.
The supported options are NONE (no fallback code owners, current
behaviour) and ALL_USERS (files with no defined code owners are owned by
all users). It's possible that we will add support for further options,
e.g. PROJECT_OWNERS (files with no defined code owners are owned by the
project owners), in future.
If ALL_USERS is configured we have the following behaviour:
1. if for a path no code owners are defined, the path is owned by all
users
2. if for a path code owners are defined, only these users own the path
3. if for a path code owners are defined, but none of them is resolvable
the path is owned by no one and changes require an override for
submission
The ALL_USERS option should only be used with care as it means that any
path that is not covered by the code owner config files is automatically
opened up to everyone and mistakes with configuring code owners can
easily happen. This is why this option is intended to be only used if
requiring code owner approvals should not be enforced.
The CodeOwnerApprovalCheck tests for fallback code owners are in a
separate test class
(CodeOwnerApprovalCheckWithAllUsersAsFallbackCodeOwnersTest) since the
CodeOwnerApprovalCheckTest test class is already way too large.
Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: I42774589cfd1898780379b76cdd6daf1bcb7ff4a
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
index f7f7170..436c45e 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
@@ -48,6 +48,7 @@
import java.nio.file.Path;
import java.util.Collections;
import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
@@ -360,6 +361,20 @@
codeOwnerStatus = CodeOwnerStatus.PENDING;
}
+ // Since there are no code owner config files in bootstrapping mode, fallback code owners also
+ // apply if they are configured. We can skip checking them if we already found that the file was
+ // approved.
+ if (codeOwnerStatus != CodeOwnerStatus.APPROVED) {
+ codeOwnerStatus =
+ getCodeOwnerStatusForFallbackCodeOwners(
+ codeOwnerStatus,
+ branch.project(),
+ enableImplicitApprovalFromUploader,
+ reviewerAccountIds,
+ approverAccountIds,
+ absolutePath);
+ }
+
PathCodeOwnerStatus pathCodeOwnerStatus =
PathCodeOwnerStatus.create(absolutePath, codeOwnerStatus);
logger.atFine().log("pathCodeOwnerStatus = %s", pathCodeOwnerStatus);
@@ -485,6 +500,7 @@
codeOwnerStatus.set(CodeOwnerStatus.PENDING);
}
+ AtomicBoolean hasRevelantCodeOwnerDefinitions = new AtomicBoolean(false);
codeOwnerConfigHierarchy.visit(
branch,
revision,
@@ -497,6 +513,10 @@
codeOwnerConfig.key().folderPath(),
codeOwnerConfig.key().fileName().orElse("<default>"));
+ if (codeOwners.hasRevelantCodeOwnerDefinitions()) {
+ hasRevelantCodeOwnerDefinitions.set(true);
+ }
+
if (isApproved(
absolutePath,
codeOwners,
@@ -517,6 +537,21 @@
// change or is a reviewer.
return true;
});
+
+ // If no code owners have been defined for the file, the fallback code owners apply if they
+ // are configured. We can skip checking them if we already found that the file was
+ // approved.
+ if (codeOwnerStatus.get() != CodeOwnerStatus.APPROVED
+ && !hasRevelantCodeOwnerDefinitions.get()) {
+ codeOwnerStatus.set(
+ getCodeOwnerStatusForFallbackCodeOwners(
+ codeOwnerStatus.get(),
+ branch.project(),
+ enableImplicitApprovalFromUploader,
+ reviewerAccountIds,
+ approverAccountIds,
+ absolutePath));
+ }
}
PathCodeOwnerStatus pathCodeOwnerStatus =
@@ -525,6 +560,66 @@
return pathCodeOwnerStatus;
}
+ /**
+ * Computes the code owner status for the given path based on the configured fallback code owners.
+ */
+ private CodeOwnerStatus getCodeOwnerStatusForFallbackCodeOwners(
+ CodeOwnerStatus codeOwnerStatus,
+ Project.NameKey project,
+ boolean enableImplicitApprovalFromUploader,
+ ImmutableSet<Account.Id> reviewerAccountIds,
+ ImmutableSet<Account.Id> approverAccountIds,
+ Path absolutePath) {
+ FallbackCodeOwners fallbackCodeOwners =
+ codeOwnersPluginConfiguration.getFallbackCodeOwners(project);
+ logger.atFine().log(
+ "getting code owner status for fallback code owners (fallback code owners = %s)",
+ fallbackCodeOwners);
+ switch (fallbackCodeOwners) {
+ case NONE:
+ logger.atFine().log("no fallback code owners");
+ return codeOwnerStatus;
+ case ALL_USERS:
+ return getCodeOwnerStatusIfAllUsersAreCodeOwners(
+ enableImplicitApprovalFromUploader,
+ reviewerAccountIds,
+ approverAccountIds,
+ absolutePath);
+ }
+
+ throw new StorageException(
+ String.format("unknown fallback code owners configured: %s", fallbackCodeOwners));
+ }
+
+ /** Computes the code owner status for the given path assuming that all users are code owners. */
+ private CodeOwnerStatus getCodeOwnerStatusIfAllUsersAreCodeOwners(
+ boolean enableImplicitApprovalFromUploader,
+ ImmutableSet<Account.Id> reviewerAccountIds,
+ ImmutableSet<Account.Id> approverAccountIds,
+ Path absolutePath) {
+ logger.atFine().log(
+ "getting code owner status for fallback code owners (all users are fallback code owners)");
+
+ if (enableImplicitApprovalFromUploader) {
+ logger.atFine().log(
+ "%s was implicitly approved by the patch set uploader since the uploader is a fallback"
+ + " code owner",
+ absolutePath);
+ return CodeOwnerStatus.APPROVED;
+ }
+
+ if (!approverAccountIds.isEmpty()) {
+ logger.atFine().log("%s was approved by a fallback code owner", absolutePath);
+ return CodeOwnerStatus.APPROVED;
+ } else if (!reviewerAccountIds.isEmpty()) {
+ logger.atFine().log("%s has a fallback code owner as reviewer", absolutePath);
+ return CodeOwnerStatus.PENDING;
+ }
+
+ logger.atFine().log("%s has no fallback code owner as a reviewer", absolutePath);
+ return CodeOwnerStatus.INSUFFICIENT_REVIEWERS;
+ }
+
private boolean isApproved(
Path absolutePath,
CodeOwnerResolverResult codeOwners,
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java
index cd11262..4d034c3 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java
@@ -46,6 +46,14 @@
/** Whether there are code owner references which couldn't be resolved. */
public abstract boolean hasUnresolvedCodeOwners();
+ /**
+ * Whether there are any code owners defined for the path, regardless of whether they can be
+ * resolved or not.
+ */
+ public boolean hasRevelantCodeOwnerDefinitions() {
+ return !codeOwners().isEmpty() || ownedByAllUsers() || hasUnresolvedCodeOwners();
+ }
+
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/FallbackCodeOwners.java b/java/com/google/gerrit/plugins/codeowners/backend/FallbackCodeOwners.java
new file mode 100644
index 0000000..5ad4079
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/FallbackCodeOwners.java
@@ -0,0 +1,36 @@
+// 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;
+
+/** Defines who owns paths for which no code owners are defined. */
+public enum FallbackCodeOwners {
+ /**
+ * Paths for which no code owners are defined are owned by no one. This means changes that touch
+ * these files can only be submitted with a code owner override.
+ */
+ NONE,
+
+ /**
+ * Paths for which no code owners are defined are owned by all users. This means changes to these
+ * paths can be approved by anyone. If implicit approvals are enabled, these files are always
+ * automatically approved.
+ *
+ * <p>The {@code ALL_USERS} option should only be used with care as it means that any path that is
+ * not covered by the code owner config files is automatically opened up to everyone and mistakes
+ * with configuring code owners can easily happen. This is why this option is intended to be only
+ * used if requiring code owner approvals should not be enforced.
+ */
+ ALL_USERS;
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfiguration.java b/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfiguration.java
index 9f088a7..fe9017b 100644
--- a/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfiguration.java
+++ b/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfiguration.java
@@ -30,6 +30,7 @@
import com.google.gerrit.plugins.codeowners.api.MergeCommitStrategy;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
+import com.google.gerrit.plugins.codeowners.backend.FallbackCodeOwners;
import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectCache;
@@ -137,6 +138,17 @@
}
/**
+ * Gets the fallback code owners 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 FallbackCodeOwners getFallbackCodeOwners(Project.NameKey project) {
+ requireNonNull(project, "project");
+ return generalConfig.getFallbackCodeOwners(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 bd137ac..54ddf7c 100644
--- a/java/com/google/gerrit/plugins/codeowners/config/GeneralConfig.java
+++ b/java/com/google/gerrit/plugins/codeowners/config/GeneralConfig.java
@@ -28,6 +28,7 @@
import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigValidationPolicy;
import com.google.gerrit.plugins.codeowners.api.MergeCommitStrategy;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
+import com.google.gerrit.plugins.codeowners.backend.FallbackCodeOwners;
import com.google.gerrit.server.config.PluginConfig;
import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
@@ -59,6 +60,7 @@
@VisibleForTesting public static final String KEY_ALLOWED_EMAIL_DOMAIN = "allowedEmailDomain";
@VisibleForTesting public static final String KEY_FILE_EXTENSION = "fileExtension";
@VisibleForTesting public static final String KEY_READ_ONLY = "readOnly";
+ @VisibleForTesting public static final String KEY_FALLBACK_CODE_OWNERS = "fallbackCodeOwners";
@VisibleForTesting
public static final String KEY_ENABLE_VALIDATION_ON_COMMIT_RECEIVED =
@@ -118,6 +120,24 @@
ValidationMessage.Type.ERROR));
}
+ try {
+ projectLevelConfig
+ .getConfig()
+ .getEnum(SECTION_CODE_OWNERS, null, KEY_FALLBACK_CODE_OWNERS, FallbackCodeOwners.NONE);
+ } catch (IllegalArgumentException e) {
+ validationMessages.add(
+ new CommitValidationMessage(
+ String.format(
+ "The value for fallback code owners '%s' that is configured in %s (parameter %s.%s) is invalid.",
+ projectLevelConfig
+ .getConfig()
+ .getString(SECTION_CODE_OWNERS, null, KEY_FALLBACK_CODE_OWNERS),
+ fileName,
+ SECTION_CODE_OWNERS,
+ KEY_FALLBACK_CODE_OWNERS),
+ ValidationMessage.Type.ERROR));
+ }
+
return ImmutableList.copyOf(validationMessages);
}
@@ -174,6 +194,46 @@
}
/**
+ * Gets the fallback code owners that own paths that have no defined code owners.
+ *
+ * @param project the project for which the fallback code owners should be read
+ * @param pluginConfig the plugin config from which the fallback code owners should be read
+ * @return the fallback code owners that own paths that have no defined code owners
+ */
+ FallbackCodeOwners getFallbackCodeOwners(Project.NameKey project, Config pluginConfig) {
+ requireNonNull(project, "project");
+ requireNonNull(pluginConfig, "pluginConfig");
+
+ String fallbackCodeOwnersString =
+ pluginConfig.getString(SECTION_CODE_OWNERS, null, KEY_FALLBACK_CODE_OWNERS);
+ if (fallbackCodeOwnersString != null) {
+ try {
+ return pluginConfig.getEnum(
+ SECTION_CODE_OWNERS, null, KEY_FALLBACK_CODE_OWNERS, FallbackCodeOwners.NONE);
+ } catch (IllegalArgumentException e) {
+ logger.atWarning().log(
+ "Ignoring invalid value %s for fallback code owners in '%s.config' of project %s."
+ + " Falling back to global config.",
+ fallbackCodeOwnersString, pluginName, project.get());
+ }
+ }
+
+ try {
+ return pluginConfigFromGerritConfig.getEnum(
+ KEY_FALLBACK_CODE_OWNERS, FallbackCodeOwners.NONE);
+ } catch (IllegalArgumentException e) {
+ logger.atWarning().log(
+ "Ignoring invalid value %s for fallback code owners in gerrit.config (parameter"
+ + " plugin.%s.%s). Falling back to default value %s.",
+ pluginConfigFromGerritConfig.getString(KEY_FALLBACK_CODE_OWNERS),
+ pluginName,
+ KEY_ENABLE_VALIDATION_ON_COMMIT_RECEIVED,
+ FallbackCodeOwners.NONE);
+ return FallbackCodeOwners.NONE;
+ }
+ }
+
+ /**
* 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/CodeOwnersPluginConfigValidatorIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersPluginConfigValidatorIT.java
index daf08e2..610a6b1 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersPluginConfigValidatorIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersPluginConfigValidatorIT.java
@@ -25,6 +25,7 @@
import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
import com.google.gerrit.plugins.codeowners.api.MergeCommitStrategy;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackendId;
+import com.google.gerrit.plugins.codeowners.backend.FallbackCodeOwners;
import com.google.gerrit.plugins.codeowners.backend.proto.ProtoBackend;
import com.google.gerrit.plugins.codeowners.config.BackendConfig;
import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
@@ -406,6 +407,45 @@
+ " (parameter codeOwners.mergeCommitStrategy) is invalid.");
}
+ @Test
+ public void configureFallbackCodeOwners() throws Exception {
+ fetchRefsMetaConfig();
+
+ Config cfg = new Config();
+ cfg.setEnum(
+ CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS,
+ /* subsection= */ null,
+ GeneralConfig.KEY_FALLBACK_CODE_OWNERS,
+ FallbackCodeOwners.ALL_USERS);
+ setCodeOwnersConfig(cfg);
+
+ PushResult r = pushRefsMetaConfig();
+ assertThat(r.getRemoteUpdate(RefNames.REFS_CONFIG).getStatus()).isEqualTo(Status.OK);
+ assertThat(codeOwnersPluginConfiguration.getFallbackCodeOwners(project))
+ .isEqualTo(FallbackCodeOwners.ALL_USERS);
+ }
+
+ @Test
+ public void cannotSetInvalidFallbackCodeOwners() throws Exception {
+ fetchRefsMetaConfig();
+
+ Config cfg = new Config();
+ cfg.setString(
+ CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS,
+ /* subsection= */ null,
+ GeneralConfig.KEY_FALLBACK_CODE_OWNERS,
+ "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 fallback code owners 'INVALID' that is configured in code-owners.config"
+ + " (parameter codeOwners.fallbackCodeOwners) 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/backend/CodeOwnerApprovalCheckTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java
index da58fca..84626e4 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java
@@ -49,7 +49,10 @@
import org.junit.Before;
import org.junit.Test;
-/** Tests for {@link CodeOwnerApprovalCheck}. */
+/**
+ * Tests for {@link CodeOwnerApprovalCheck}. Further tests with fallback code owners are implemented
+ * in {@link CodeOwnerApprovalCheckWithAllUsersAsFallbackCodeOwnersTest}}.
+ */
public class CodeOwnerApprovalCheckTest extends AbstractCodeOwnersTest {
@Inject private ChangeNotes.Factory changeNotesFactory;
@Inject private RequestScopeOperations requestScopeOperations;
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithAllUsersAsFallbackCodeOwnersTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithAllUsersAsFallbackCodeOwnersTest.java
new file mode 100644
index 0000000..d05245c
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithAllUsersAsFallbackCodeOwnersTest.java
@@ -0,0 +1,401 @@
+// 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.gerrit.plugins.codeowners.testing.FileCodeOwnerStatusSubject.assertThatStream;
+
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.plugins.codeowners.JgitPath;
+import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest;
+import com.google.gerrit.plugins.codeowners.acceptance.testsuite.CodeOwnerConfigOperations;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnerStatus;
+import com.google.gerrit.plugins.codeowners.config.GeneralConfig;
+import com.google.gerrit.plugins.codeowners.testing.FileCodeOwnerStatusSubject;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.inject.Inject;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.stream.Stream;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
+import org.junit.Test;
+
+/** Tests for {@link CodeOwnerApprovalCheck} with fallback code owners. */
+public class CodeOwnerApprovalCheckWithAllUsersAsFallbackCodeOwnersTest
+ extends AbstractCodeOwnersTest {
+ @Inject private ChangeNotes.Factory changeNotesFactory;
+ @Inject private RequestScopeOperations requestScopeOperations;
+
+ private CodeOwnerApprovalCheck codeOwnerApprovalCheck;
+ private CodeOwnerConfigOperations codeOwnerConfigOperations;
+
+ /** Returns a {@code gerrit.config} that configures all users as fallback code owners. */
+ @ConfigSuite.Default
+ public static Config defaultConfig() {
+ Config cfg = new Config();
+ cfg.setEnum(
+ "plugin",
+ "code-owners",
+ GeneralConfig.KEY_FALLBACK_CODE_OWNERS,
+ FallbackCodeOwners.ALL_USERS);
+ return cfg;
+ }
+
+ @Before
+ public void setUpCodeOwnersPlugin() throws Exception {
+ codeOwnerApprovalCheck = plugin.getSysInjector().getInstance(CodeOwnerApprovalCheck.class);
+ codeOwnerConfigOperations =
+ plugin.getSysInjector().getInstance(CodeOwnerConfigOperations.class);
+ }
+
+ @Test
+ public void notApprovedByFallbackCodeOwnerIfCodeOwnersDefined() throws Exception {
+ TestAccount codeOwner =
+ accountCreator.create(
+ "codeOwner", "codeOwner@example.com", "CodeOwner", /* displayName= */ null);
+
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerEmail(codeOwner.email())
+ .create();
+
+ Path path = Paths.get("/foo/bar.baz");
+ String changeId =
+ createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId();
+
+ // Verify that the file is not approved yet.
+ Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses =
+ codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject =
+ assertThatStream(fileCodeOwnerStatuses).onlyElement();
+ fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+ fileCodeOwnerStatusSubject
+ .hasNewPathStatus()
+ .value()
+ .hasStatusThat()
+ .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
+
+ // Add a fallback code owner as reviewer.
+ gApi.changes().id(changeId).addReviewer(user.email());
+
+ // Verify that the file is not approved (fallback code owner doesn't apply since a code owner is
+ // defined).
+ fileCodeOwnerStatuses = codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement();
+ fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+ fileCodeOwnerStatusSubject
+ .hasNewPathStatus()
+ .value()
+ .hasStatusThat()
+ .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
+
+ // Add a Code-Review+1 (= code owner approval) from a fallback code owner.
+ requestScopeOperations.setApiUser(user.id());
+ recommend(changeId);
+
+ // Verify that the file is not approved (fallback code owner doesn't apply since a code owner is
+ // defined).
+ fileCodeOwnerStatuses = codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement();
+ fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+ fileCodeOwnerStatusSubject
+ .hasNewPathStatus()
+ .value()
+ .hasStatusThat()
+ .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true")
+ public void notImplicitlyApprovedByFallbackCodeOwnerIfCodeOwnersDefined() throws Exception {
+ TestAccount codeOwner =
+ accountCreator.create(
+ "codeOwner", "codeOwner@example.com", "CodeOwner", /* displayName= */ null);
+
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerEmail(codeOwner.email())
+ .create();
+
+ Path path = Paths.get("/foo/bar.baz");
+ String changeId =
+ createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId();
+
+ // Verify that the file is not approved yet.
+ Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses =
+ codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject =
+ assertThatStream(fileCodeOwnerStatuses).onlyElement();
+ fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+ fileCodeOwnerStatusSubject
+ .hasNewPathStatus()
+ .value()
+ .hasStatusThat()
+ .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
+ }
+
+ @Test
+ public void notApprovedByFallbackCodeOwnerIfNonResolvableCodeOwnersDefined() throws Exception {
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerEmail("non-resolvable-code-owner@example.com")
+ .create();
+
+ Path path = Paths.get("/foo/bar.baz");
+ String changeId =
+ createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId();
+
+ // Verify that the file is not approved yet.
+ Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses =
+ codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject =
+ assertThatStream(fileCodeOwnerStatuses).onlyElement();
+ fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+ fileCodeOwnerStatusSubject
+ .hasNewPathStatus()
+ .value()
+ .hasStatusThat()
+ .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
+
+ // Add a fallback code owner as reviewer.
+ gApi.changes().id(changeId).addReviewer(user.email());
+
+ // Verify that the file is not approved (fallback code owner doesn't apply since a code owner is
+ // defined).
+ fileCodeOwnerStatuses = codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement();
+ fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+ fileCodeOwnerStatusSubject
+ .hasNewPathStatus()
+ .value()
+ .hasStatusThat()
+ .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
+
+ // Add a Code-Review+1 (= code owner approval) from a fallback code owner.
+ requestScopeOperations.setApiUser(user.id());
+ recommend(changeId);
+
+ // Verify that the file is not approved (fallback code owner doesn't apply since a code owner is
+ // defined).
+ fileCodeOwnerStatuses = codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement();
+ fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+ fileCodeOwnerStatusSubject
+ .hasNewPathStatus()
+ .value()
+ .hasStatusThat()
+ .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true")
+ public void notImplicitlyApprovedByFallbackCodeOwnerIfNonResolvableCodeOwnersDefined()
+ throws Exception {
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerEmail("non-resolvable-code-owner@example.com")
+ .create();
+
+ Path path = Paths.get("/foo/bar.baz");
+ String changeId =
+ createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId();
+
+ // Verify that the file is not approved yet.
+ Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses =
+ codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject =
+ assertThatStream(fileCodeOwnerStatuses).onlyElement();
+ fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+ fileCodeOwnerStatusSubject
+ .hasNewPathStatus()
+ .value()
+ .hasStatusThat()
+ .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
+ }
+
+ @Test
+ public void approvedByFallbackCodeOwner() throws Exception {
+ // create arbitrary code owner config to avoid entering the bootstrapping code path in
+ // CodeOwnerApprovalCheck
+ createArbitraryCodeOwnerConfigFile();
+
+ Path path = Paths.get("/foo/bar.baz");
+ String changeId =
+ createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId();
+
+ // Verify that the file is not approved yet (the change owner is a code owner, but
+ // implicit approvals are disabled).
+ Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses =
+ codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject =
+ assertThatStream(fileCodeOwnerStatuses).onlyElement();
+ fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+ fileCodeOwnerStatusSubject
+ .hasNewPathStatus()
+ .value()
+ .hasStatusThat()
+ .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
+
+ // Add a user a fallback code owner as reviewer.
+ gApi.changes().id(changeId).addReviewer(user.email());
+
+ // Verify that the status is pending now .
+ fileCodeOwnerStatuses = codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement();
+ fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+ fileCodeOwnerStatusSubject
+ .hasNewPathStatus()
+ .value()
+ .hasStatusThat()
+ .isEqualTo(CodeOwnerStatus.PENDING);
+
+ // Add a Code-Review+1 (= code owner approval) from a fallback code owner.
+ requestScopeOperations.setApiUser(user.id());
+ recommend(changeId);
+
+ // Verify that the status is approved now
+ fileCodeOwnerStatuses = codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ 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 = "true")
+ public void implicitlyApprovedByFallbackCodeOwner() throws Exception {
+ // create arbitrary code owner config to avoid entering the bootstrapping code path in
+ // CodeOwnerApprovalCheck
+ createArbitraryCodeOwnerConfigFile();
+
+ Path path = Paths.get("/foo/bar.baz");
+ String changeId =
+ createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId();
+
+ // Verify that the file is approved (the change owner is a code owner and implicit approvals are
+ // enabled).
+ 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 approvedByFallbackCodeOwner_bootstrappingMode() throws Exception {
+ // since no code owner config exists we are entering the bootstrapping code path in
+ // CodeOwnerApprovalCheck
+
+ TestAccount user2 = accountCreator.user2();
+
+ Path path = Paths.get("/foo/bar.baz");
+ String changeId =
+ createChange(user, "Change Adding A File", JgitPath.of(path).get(), "file content")
+ .getChangeId();
+
+ // Verify that the file is not approved yet (the change owner is a code owner, but
+ // implicit approvals are disabled).
+ Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses =
+ codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject =
+ assertThatStream(fileCodeOwnerStatuses).onlyElement();
+ fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+ fileCodeOwnerStatusSubject
+ .hasNewPathStatus()
+ .value()
+ .hasStatusThat()
+ .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
+
+ // Add a user a fallback code owner as reviewer.
+ gApi.changes().id(changeId).addReviewer(user2.email());
+
+ // Verify that the status is pending now .
+ fileCodeOwnerStatuses = codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement();
+ fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+ fileCodeOwnerStatusSubject
+ .hasNewPathStatus()
+ .value()
+ .hasStatusThat()
+ .isEqualTo(CodeOwnerStatus.PENDING);
+
+ // Add a Code-Review+1 (= code owner approval) from a fallback code owner.
+ requestScopeOperations.setApiUser(user2.id());
+ recommend(changeId);
+
+ // Verify that the status is approved now
+ fileCodeOwnerStatuses = codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ 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 = "true")
+ public void implicitlyApprovedByFallbackCodeOwner_bootstrappingMode() throws Exception {
+ // since no code owner config exists we are entering the bootstrapping code path in
+ // CodeOwnerApprovalCheck
+
+ Path path = Paths.get("/foo/bar.baz");
+ String changeId =
+ createChange(user, "Change Adding A File", JgitPath.of(path).get(), "file content")
+ .getChangeId();
+
+ // Verify that the file is approved (the change owner is a code owner and implicit approvals are
+ // enabled).
+ 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);
+ }
+
+ private ChangeNotes getChangeNotes(String changeId) throws Exception {
+ return changeNotesFactory.create(project, Change.id(gApi.changes().id(changeId).get()._number));
+ }
+}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigurationTest.java b/javatests/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigurationTest.java
index dab2b0d..d40fba6 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigurationTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigurationTest.java
@@ -36,6 +36,7 @@
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigUpdate;
+import com.google.gerrit.plugins.codeowners.backend.FallbackCodeOwners;
import com.google.gerrit.plugins.codeowners.backend.findowners.FindOwnersBackend;
import com.google.gerrit.server.IdentifiedUser;
import com.google.inject.Inject;
@@ -841,6 +842,52 @@
.isEqualTo(MergeCommitStrategy.ALL_CHANGED_FILES);
}
+ @Test
+ public void cannotGetFallbackCodeOwnersForNullProject() throws Exception {
+ NullPointerException npe =
+ assertThrows(
+ NullPointerException.class,
+ () -> codeOwnersPluginConfiguration.getFallbackCodeOwners(/* project= */ null));
+ assertThat(npe).hasMessageThat().isEqualTo("project");
+ }
+
+ @Test
+ public void getFallbackCodeOwnersIfNoneIsConfigured() throws Exception {
+ assertThat(codeOwnersPluginConfiguration.getFallbackCodeOwners(project))
+ .isEqualTo(FallbackCodeOwners.NONE);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.fallbackCodeOwners", value = "ALL_USERS")
+ public void getFallbackCodeOwnersIfNoneIsConfiguredOnProjectLevel() throws Exception {
+ assertThat(codeOwnersPluginConfiguration.getFallbackCodeOwners(project))
+ .isEqualTo(FallbackCodeOwners.ALL_USERS);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.fallbackCodeOwners", value = "ALL_USERS")
+ public void fallbackCodeOnwersOnProjectLevelOverridesGlobalFallbackCodeOwners() throws Exception {
+ configureFallbackCodeOwners(project, FallbackCodeOwners.NONE);
+ assertThat(codeOwnersPluginConfiguration.getFallbackCodeOwners(project))
+ .isEqualTo(FallbackCodeOwners.NONE);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.fallbackCodeOwners", value = "ALL_USERS")
+ public void fallbackCodeOwnersIsInheritedFromParentProject() throws Exception {
+ configureFallbackCodeOwners(allProjects, FallbackCodeOwners.NONE);
+ assertThat(codeOwnersPluginConfiguration.getFallbackCodeOwners(project))
+ .isEqualTo(FallbackCodeOwners.NONE);
+ }
+
+ @Test
+ public void inheritedFallbackCodeOwnersCanBeOverridden() throws Exception {
+ configureFallbackCodeOwners(allProjects, FallbackCodeOwners.ALL_USERS);
+ configureFallbackCodeOwners(project, FallbackCodeOwners.NONE);
+ assertThat(codeOwnersPluginConfiguration.getFallbackCodeOwners(project))
+ .isEqualTo(FallbackCodeOwners.NONE);
+ }
+
private void configureDisabled(Project.NameKey project, String disabled) throws Exception {
setCodeOwnersConfig(project, /* subsection= */ null, StatusConfig.KEY_DISABLED, disabled);
}
@@ -897,6 +944,15 @@
mergeCommitStrategy.name());
}
+ private void configureFallbackCodeOwners(
+ Project.NameKey project, FallbackCodeOwners fallbackCodeOwners) throws Exception {
+ setCodeOwnersConfig(
+ project,
+ /* subsection= */ null,
+ GeneralConfig.KEY_FALLBACK_CODE_OWNERS,
+ fallbackCodeOwners.name());
+ }
+
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 19e3300..2a92d16 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/config/GeneralConfigTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/config/GeneralConfigTest.java
@@ -18,6 +18,7 @@
import static com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS;
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_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_MERGE_COMMIT_STRATEGY;
@@ -33,6 +34,7 @@
import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigValidationPolicy;
import com.google.gerrit.plugins.codeowners.api.MergeCommitStrategy;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
+import com.google.gerrit.plugins.codeowners.backend.FallbackCodeOwners;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.gerrit.server.git.validators.ValidationMessage;
import com.google.gerrit.server.project.ProjectLevelConfig;
@@ -395,4 +397,62 @@
cfg.setString(SECTION_CODE_OWNERS, null, KEY_OVERRIDE_INFO_URL, "http://bar.example.com");
assertThat(generalConfig.getOverrideInfoUrl(cfg)).value().isEqualTo("http://bar.example.com");
}
+
+ @Test
+ public void cannotGetFallbackCodeOwnersForNullProject() throws Exception {
+ NullPointerException npe =
+ assertThrows(
+ NullPointerException.class,
+ () -> generalConfig.getFallbackCodeOwners(/* project= */ null, new Config()));
+ assertThat(npe).hasMessageThat().isEqualTo("project");
+ }
+
+ @Test
+ public void cannotGetFallbackCodeOwnersForNullPluginConfig() throws Exception {
+ NullPointerException npe =
+ assertThrows(
+ NullPointerException.class,
+ () -> generalConfig.getFallbackCodeOwners(project, /* pluginConfig= */ null));
+ assertThat(npe).hasMessageThat().isEqualTo("pluginConfig");
+ }
+
+ @Test
+ public void noFallbackCodeOwnersConfigured() throws Exception {
+ assertThat(generalConfig.getFallbackCodeOwners(project, new Config()))
+ .isEqualTo(FallbackCodeOwners.NONE);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.fallbackCodeOwners", value = "ALL_USERS")
+ public void fallbackCodeOwnersIsRetrievedFromGerritConfigIfNotSpecifiedOnProjectLevel()
+ throws Exception {
+ assertThat(generalConfig.getFallbackCodeOwners(project, new Config()))
+ .isEqualTo(FallbackCodeOwners.ALL_USERS);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.fallbackCodeOwners", value = "ALL_USERS")
+ public void fallbackCodeOwnersInPluginConfigOverridesFallbackCodeOwnersInGerritConfig()
+ throws Exception {
+ Config cfg = new Config();
+ cfg.setString(SECTION_CODE_OWNERS, null, KEY_FALLBACK_CODE_OWNERS, "NONE");
+ assertThat(generalConfig.getFallbackCodeOwners(project, cfg))
+ .isEqualTo(FallbackCodeOwners.NONE);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.fallbackCodeOwners", value = "ALL_USERS")
+ public void globalFallbackOnwersUsedIfInvalidFallbackCodeOwnersConfigured() throws Exception {
+ Config cfg = new Config();
+ cfg.setString(SECTION_CODE_OWNERS, null, KEY_FALLBACK_CODE_OWNERS, "INVALID");
+ assertThat(generalConfig.getFallbackCodeOwners(project, cfg))
+ .isEqualTo(FallbackCodeOwners.ALL_USERS);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.fallbackCodeOwners", value = "INVALID")
+ public void defaultValueUsedIfInvalidGlobalFallbackCodeOwnersConfigured() throws Exception {
+ assertThat(generalConfig.getFallbackCodeOwners(project, new Config()))
+ .isEqualTo(FallbackCodeOwners.NONE);
+ }
}
diff --git a/resources/Documentation/config.md b/resources/Documentation/config.md
index b67693b..0d2789f 100644
--- a/resources/Documentation/config.md
+++ b/resources/Documentation/config.md
@@ -213,6 +213,33 @@
`@PLUGIN@.config`.\
By default `ALL_CHANGED_FILES`.
+<a id="pluginCodeOwnersFallbackCodeOwners">plugin.@PLUGIN@.fallbackCodeOwners</a>
+: Policy that controls who should own paths that have no code owners
+ defined.\
+ \
+ Can be `NONE` or `ALL_USERS`.\
+ \
+ `NONE`:\
+ Paths for which no code owners are defined are owned by no one. This
+ means changes that touch these files can only be submitted with a code
+ owner override.\
+ \
+ `ALL_USERS`:\
+ Paths for which no code owners are defined are owned by all users. This
+ means changes to these paths can be approved by anyone. If [implicit
+ approvals](#pluginCodeOwnersEnableImplicitApprovals) are enabled, these
+ files are always automatically approved. The `ALL_USERS` option should
+ only be used with care as it means that any path that is not covered by
+ the code owner config files is automatically opened up to everyone and
+ mistakes with configuring code owners can easily happen. This is why
+ this option is intended to be only used if requiring code owner
+ approvals should not be enforced.\
+ \
+ Can be overridden per project by setting
+ [codeOwners.fallbackCodeOwners](#codeOwnersFallbackCodeOwners) in
+ `@PLUGIN@.config`.\
+ By default `NONE`.
+
# <a id="projectConfiguration">Project configuration in @PLUGIN@.config</a>
<a id="codeOwnersDisabled">codeOwners.disabled</a>
@@ -405,6 +432,19 @@
[plugin.@PLUGIN@.mergeCommitStrategy](#pluginCodeOwnersMergeCommitStrategy)
in `gerrit.config` is used.
+<a id="codeOwnersFallbackCodeOwners">codeOwners.fallbackCodeOwners</a>
+: Policy that controls who should own paths that have no code owners
+ defined.\
+ Can be `NONE` or `ALL_USERS` (see
+ [plugin.@PLUGIN@.fallbackCodeOwners](#pluginCodeOwnersFallbackCodeOwners)
+ for an explanation of these values).\
+ Overrides the global setting
+ [plugin.@PLUGIN@.fallbackCodeOwners](#pluginCodeOwnersFallbackCodeOwners)
+ in `gerrit.config`.\
+ If not set, the global setting
+ [plugin.@PLUGIN@.fallbackCodeOwners](#pluginCodeOwnersFallbackCodeOwners)
+ in `gerrit.config` is used.
+
---
Back to [@PLUGIN@ documentation index](index.html)
diff --git a/resources/Documentation/validation.md b/resources/Documentation/validation.md
index d06f5c8..d6160d6 100644
--- a/resources/Documentation/validation.md
+++ b/resources/Documentation/validation.md
@@ -145,6 +145,8 @@
before)
* the [codeOwners.mergeCommitStrategy](config.html#codeOwnersMergeCommitStrategy)
configuration is valid
+* the [codeOwners.fallbackCodeOwners](config.html#codeOwnersFallbackCodeOwners)
+ configuration is valid
---