Allow to configure validation flags per branch

Follow the example of change Ifaa955d56 to also make
rejectNonResolvableCodeOwners and rejectNonResolvableImports
configurable per branch.

Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: I502ddd2ca4b56149972ffb2566fff6b592fc96cf
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigSnapshot.java b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigSnapshot.java
index 4595fff..783019b 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigSnapshot.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigSnapshot.java
@@ -118,16 +118,40 @@
   /**
    * Checks whether newly added non-resolvable code owners should be rejected on commit received and
    * submit.
+   *
+   * @param branchName the branch for which it should be checked whether non-resolvable code owners
+   *     should be rejected
    */
-  public boolean rejectNonResolvableCodeOwners() {
+  public boolean rejectNonResolvableCodeOwners(String branchName) {
+    requireNonNull(branchName, "branchName");
+
+    Optional<Boolean> branchSpecificFlag =
+        generalConfig.getRejectNonResolvableCodeOwnersForBranch(
+            BranchNameKey.create(projectName, branchName), pluginConfig);
+    if (branchSpecificFlag.isPresent()) {
+      return branchSpecificFlag.get();
+    }
+
     return generalConfig.getRejectNonResolvableCodeOwners(projectName, pluginConfig);
   }
 
   /**
    * Checks whether newly added non-resolvable imports should be rejected on commit received and
    * submit.
+   *
+   * @param branchName the branch for which it should be checked whether non-resolvable imports
+   *     should be rejected
    */
-  public boolean rejectNonResolvableImports() {
+  public boolean rejectNonResolvableImports(String branchName) {
+    requireNonNull(branchName, "branchName");
+
+    Optional<Boolean> branchSpecificFlag =
+        generalConfig.getRejectNonResolvableImportsForBranch(
+            BranchNameKey.create(projectName, branchName), pluginConfig);
+    if (branchSpecificFlag.isPresent()) {
+      return branchSpecificFlag.get();
+    }
+
     return generalConfig.getRejectNonResolvableImports(projectName, pluginConfig);
   }
 
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 caf8de0..eb84049 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java
@@ -234,15 +234,15 @@
   }
 
   /**
-   * Gets the reject-non-resolvable-code-owners configuration from the given plugin config with
-   * fallback to {@code gerrit.config}.
+   * Gets the reject-non-resolvable-code-owners configuration from the given plugin config for the
+   * specified project with fallback to {@code gerrit.config}.
    *
    * <p>The reject-non-resolvable-code-owners configuration controls whether code owner config files
    * with newly added non-resolvable code owners should be rejected on commit received and on
    * submit.
    *
-   * @param project the project for which the freject-non-resolvable-code-owners configuration
-   *     should be read
+   * @param project the project for which the reject-non-resolvable-code-owners configuration should
+   *     be read
    * @param pluginConfig the plugin config from which the reject-non-resolvable-code-owners
    *     configuration should be read.
    * @return whether code owner config files with newly added non-resolvable code owners should be
@@ -254,13 +254,37 @@
   }
 
   /**
-   * Gets the reject-non-resolvable-imports configuration from the given plugin config with fallback
-   * to {@code gerrit.config}.
+   * Gets the reject-non-resolvable-code-owners configuration from the given plugin config for the
+   * specified branch with fallback to {@code gerrit.config}.
+   *
+   * <p>If multiple branch-specific configurations match the specified branch, it is undefined which
+   * of the matching branch configurations takes precedence.
+   *
+   * <p>The reject-non-resolvable-code-owners configuration controls whether code owner config files
+   * with newly added non-resolvable code owners should be rejected on commit received and on
+   * submit.
+   *
+   * @param branchNameKey the branch and project for which the reject-non-resolvable-code-owners
+   *     configuration should be read
+   * @param pluginConfig the plugin config from which the reject-non-resolvable-code-owners
+   *     configuration should be read.
+   * @return whether code owner config files with newly added non-resolvable code owners should be
+   *     rejected on commit received and on submit
+   */
+  Optional<Boolean> getRejectNonResolvableCodeOwnersForBranch(
+      BranchNameKey branchNameKey, Config pluginConfig) {
+    return getCodeOwnerConfigValidationFlagForBranch(
+        KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS, branchNameKey, pluginConfig);
+  }
+
+  /**
+   * Gets the reject-non-resolvable-imports configuration from the given plugin config for the
+   * specified project with fallback to {@code gerrit.config}.
    *
    * <p>The reject-non-resolvable-imports configuration controls whether code owner config files
    * with newly added non-resolvable imports should be rejected on commit received and on submit.
    *
-   * @param project the project for which the freject-non-resolvable-imports configuration should be
+   * @param project the project for which the reject-non-resolvable-imports configuration should be
    *     read
    * @param pluginConfig the plugin config from which the reject-non-resolvable-imports
    *     configuration should be read.
@@ -272,6 +296,26 @@
         project, pluginConfig, KEY_REJECT_NON_RESOLVABLE_IMPORTS, /* defaultValue= */ true);
   }
 
+  /**
+   * Gets the reject-non-resolvable-imports configuration from the given plugin config for the
+   * specified branch with fallback to {@code gerrit.config}.
+   *
+   * <p>The reject-non-resolvable-imports configuration controls whether code owner config files
+   * with newly added non-resolvable imports should be rejected on commit received and on submit.
+   *
+   * @param branchNameKey the branch and project for which the reject-non-resolvable-imports
+   *     configuration should be read
+   * @param pluginConfig the plugin config from which the reject-non-resolvable-imports
+   *     configuration should be read.
+   * @return whether code owner config files with newly added non-resolvable imports should be
+   *     rejected on commit received and on submit
+   */
+  Optional<Boolean> getRejectNonResolvableImportsForBranch(
+      BranchNameKey branchNameKey, Config pluginConfig) {
+    return getCodeOwnerConfigValidationFlagForBranch(
+        KEY_REJECT_NON_RESOLVABLE_IMPORTS, branchNameKey, pluginConfig);
+  }
+
   private boolean getBooleanConfig(
       Project.NameKey project, Config pluginConfig, String key, boolean defaultValue) {
     requireNonNull(project, "project");
@@ -493,6 +537,22 @@
         validationSectionForBranch.get(), key, branchNameKey.project(), pluginConfig);
   }
 
+  private Optional<Boolean> getCodeOwnerConfigValidationFlagForBranch(
+      String key, BranchNameKey branchNameKey, Config pluginConfig) {
+    requireNonNull(key, "key");
+    requireNonNull(branchNameKey, "branchNameKey");
+    requireNonNull(pluginConfig, "pluginConfig");
+
+    Optional<String> validationSectionForBranch =
+        getValidationSectionForBranch(branchNameKey, pluginConfig);
+    if (!validationSectionForBranch.isPresent()) {
+      return Optional.empty();
+    }
+
+    return getCodeOwnerConfigValidationFlagForBranch(
+        validationSectionForBranch.get(), key, branchNameKey.project(), pluginConfig);
+  }
+
   private CodeOwnerConfigValidationPolicy getCodeOwnerConfigValidationPolicy(
       String key, Project.NameKey project, Config pluginConfig) {
     requireNonNull(key, "key");
@@ -601,6 +661,34 @@
     return Optional.empty();
   }
 
+  private Optional<Boolean> getCodeOwnerConfigValidationFlagForBranch(
+      String branchSubsection, String key, Project.NameKey project, Config pluginConfig) {
+    requireNonNull(branchSubsection, "branchSubsection");
+    requireNonNull(key, "key");
+    requireNonNull(project, "project");
+    requireNonNull(pluginConfig, "pluginConfig");
+
+    String codeOwnerConfigValidationFlagString =
+        pluginConfig.getString(SECTION_VALIDATION, branchSubsection, key);
+    if (codeOwnerConfigValidationFlagString != null) {
+      try {
+        return Optional.of(
+            pluginConfig.getBoolean(SECTION_VALIDATION, branchSubsection, key, true));
+      } catch (IllegalArgumentException e) {
+        logger.atWarning().withCause(e).log(
+            "Ignoring invalid value %s for %s.%s.%s in '%s.config' of project %s."
+                + " Falling back to project-level setting.",
+            codeOwnerConfigValidationFlagString,
+            SECTION_VALIDATION,
+            branchSubsection,
+            key,
+            pluginName,
+            project.get());
+      }
+    }
+    return Optional.empty();
+  }
+
   /**
    * Gets the merge commit strategy from the given plugin config with fallback to {@code
    * gerrit.config}.
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java
index 306442a..1eaaed6 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java
@@ -177,11 +177,7 @@
               problemsByPath.putAll(
                   codeOwnerBackend.getFilePath(codeOwnerConfig.key()).toString(),
                   checkCodeOwnerConfig(
-                      branchNameKey.project(),
-                      revWalk,
-                      codeOwnerBackend,
-                      codeOwnerConfig,
-                      verbosity));
+                      branchNameKey, revWalk, codeOwnerBackend, codeOwnerConfig, verbosity));
               return true;
             },
             (codeOwnerConfigFilePath, configInvalidException) -> {
@@ -196,14 +192,14 @@
   }
 
   private ImmutableList<ConsistencyProblemInfo> checkCodeOwnerConfig(
-      Project.NameKey project,
+      BranchNameKey branchNameKey,
       RevWalk revWalk,
       CodeOwnerBackend codeOwnerBackend,
       CodeOwnerConfig codeOwnerConfig,
       @Nullable ConsistencyProblemInfo.Status verbosity) {
     return codeOwnerConfigValidator
         .validateCodeOwnerConfig(
-            project,
+            branchNameKey,
             revWalk,
             currentUser.get().asIdentifiedUser(),
             codeOwnerBackend,
diff --git a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
index 4f03274..ce10743 100644
--- a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
+++ b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
@@ -494,7 +494,7 @@
         // issues. Hence in this case we downgrade all validation errors in the new version to
         // warnings so that the update is not blocked.
         return validateCodeOwnerConfig(
-                branchNameKey.project(), revWalk, user, codeOwnerBackend, codeOwnerConfig)
+                branchNameKey, revWalk, user, codeOwnerBackend, codeOwnerConfig)
             .map(CodeOwnerConfigValidator::downgradeErrorToWarning);
       }
 
@@ -505,15 +505,14 @@
     // Validate the parsed code owner config.
     if (baseCodeOwnerConfig.isPresent()) {
       return validateCodeOwnerConfig(
-          branchNameKey.project(),
+          branchNameKey,
           revWalk,
           user,
           codeOwnerBackend,
           codeOwnerConfig,
           baseCodeOwnerConfig.get());
     }
-    return validateCodeOwnerConfig(
-        branchNameKey.project(), revWalk, user, codeOwnerBackend, codeOwnerConfig);
+    return validateCodeOwnerConfig(branchNameKey, revWalk, user, codeOwnerBackend, codeOwnerConfig);
   }
 
   /**
@@ -679,7 +678,7 @@
    * <p>Validation errors that exist in both code owner configs are returned as warning (because
    * they are not newly introduced by the given code owner config).
    *
-   * @param project the name of the project
+   * @param branchNameKey the branch and the project
    * @param revWalk rev walk that should be used to load the code owner configs
    * @param user user for which the code owner visibility checks should be performed
    * @param codeOwnerBackend the code owner backend from which the code owner configs were loaded
@@ -689,7 +688,7 @@
    *     empty stream if there are no issues
    */
   private Stream<CommitValidationMessage> validateCodeOwnerConfig(
-      Project.NameKey project,
+      BranchNameKey branchNameKey,
       RevWalk revWalk,
       IdentifiedUser user,
       CodeOwnerBackend codeOwnerBackend,
@@ -699,9 +698,9 @@
     requireNonNull(baseCodeOwnerConfig, "baseCodeOwnerConfig");
 
     ImmutableSet<CommitValidationMessage> issuesInBaseVersion =
-        validateCodeOwnerConfig(project, revWalk, user, codeOwnerBackend, baseCodeOwnerConfig)
+        validateCodeOwnerConfig(branchNameKey, revWalk, user, codeOwnerBackend, baseCodeOwnerConfig)
             .collect(toImmutableSet());
-    return validateCodeOwnerConfig(project, revWalk, user, codeOwnerBackend, codeOwnerConfig)
+    return validateCodeOwnerConfig(branchNameKey, revWalk, user, codeOwnerBackend, codeOwnerConfig)
         .map(
             commitValidationMessage ->
                 issuesInBaseVersion.contains(commitValidationMessage)
@@ -712,7 +711,7 @@
   /**
    * Validates the given code owner config and returns validation issues as stream.
    *
-   * @param project the name of the project
+   * @param branchNameKey the branch and the project
    * @param revWalk rev walk that should be used to load the code owner configs from {@code project}
    * @param user user for which the code owner visibility checks should be performed
    * @param codeOwnerBackend the code owner backend from which the code owner config was loaded
@@ -721,7 +720,7 @@
    *     empty stream if there are no issues
    */
   public Stream<CommitValidationMessage> validateCodeOwnerConfig(
-      Project.NameKey project,
+      BranchNameKey branchNameKey,
       RevWalk revWalk,
       IdentifiedUser user,
       CodeOwnerBackend codeOwnerBackend,
@@ -729,9 +728,12 @@
     requireNonNull(codeOwnerConfig, "codeOwnerConfig");
     return Streams.concat(
         validateCodeOwnerReferences(
-            project, user, codeOwnerBackend.getFilePath(codeOwnerConfig.key()), codeOwnerConfig),
+            branchNameKey,
+            user,
+            codeOwnerBackend.getFilePath(codeOwnerConfig.key()),
+            codeOwnerConfig),
         validateImports(
-            project,
+            branchNameKey,
             revWalk,
             codeOwnerBackend.getFilePath(codeOwnerConfig.key()),
             codeOwnerConfig));
@@ -740,7 +742,7 @@
   /**
    * Validates the code owner references of the given code owner config.
    *
-   * @param project the name of the project
+   * @param branchNameKey the branch and the project
    * @param user user for which the code owner visibility checks should be performed
    * @param codeOwnerConfigFilePath the path of the code owner config file which contains the code
    *     owner references
@@ -750,7 +752,7 @@
    *     empty stream if there are no issues
    */
   private Stream<CommitValidationMessage> validateCodeOwnerReferences(
-      Project.NameKey project,
+      BranchNameKey branchNameKey,
       IdentifiedUser user,
       Path codeOwnerConfigFilePath,
       CodeOwnerConfig codeOwnerConfig) {
@@ -759,7 +761,7 @@
         .map(
             codeOwnerReference ->
                 validateCodeOwnerReference(
-                    project, user, codeOwnerConfigFilePath, codeOwnerReference))
+                    branchNameKey, user, codeOwnerConfigFilePath, codeOwnerReference))
         .filter(Optional::isPresent)
         .map(Optional::get);
   }
@@ -767,7 +769,7 @@
   /**
    * Validates a code owner reference.
    *
-   * @param project the name of the project
+   * @param branchNameKey the branch and the project
    * @param user user for which the code owner visibility checks should be performed
    * @param codeOwnerConfigFilePath the path of the code owner config file which contains the code
    *     owner reference
@@ -776,14 +778,14 @@
    *     Optional#empty()} if there is no issue
    */
   private Optional<CommitValidationMessage> validateCodeOwnerReference(
-      Project.NameKey project,
+      BranchNameKey branchNameKey,
       IdentifiedUser user,
       Path codeOwnerConfigFilePath,
       CodeOwnerReference codeOwnerReference) {
     CodeOwnerResolver codeOwnerResolver = codeOwnerResolverProvider.get().forUser(user);
     if (!codeOwnerResolver.isEmailDomainAllowed(codeOwnerReference.email()).get()) {
       return nonResolvableCodeOwner(
-          project,
+          branchNameKey,
           String.format(
               "the domain of the code owner email '%s' in '%s' is not allowed for code owners",
               codeOwnerReference.email(), codeOwnerConfigFilePath));
@@ -801,7 +803,7 @@
     // cases so that uploaders cannot probe emails for existence (e.g. they cannot add an email and
     // conclude from the error message whether the email exists).
     return nonResolvableCodeOwner(
-        project,
+        branchNameKey,
         String.format(
             "code owner email '%s' in '%s' cannot be resolved for %s",
             codeOwnerReference.email(), codeOwnerConfigFilePath, user.getLoggableName()));
@@ -810,7 +812,7 @@
   /**
    * Validates the imports of the given code owner config.
    *
-   * @param project the name of the project
+   * @param branchNameKey the branch and the project
    * @param revWalk rev walk that should be used to load the code owner configs from {@code project}
    * @param codeOwnerConfigFilePath the path of the code owner config file which contains the code
    *     owner config
@@ -819,7 +821,7 @@
    *     if there are no issues
    */
   private Stream<CommitValidationMessage> validateImports(
-      Project.NameKey project,
+      BranchNameKey branchNameKey,
       RevWalk revWalk,
       Path codeOwnerConfigFilePath,
       CodeOwnerConfig codeOwnerConfig) {
@@ -828,7 +830,7 @@
                 .map(
                     codeOwnerConfigReference ->
                         validateCodeOwnerConfigReference(
-                            project,
+                            branchNameKey,
                             revWalk,
                             codeOwnerConfigFilePath,
                             codeOwnerConfig.key(),
@@ -840,7 +842,7 @@
                 .map(
                     codeOwnerConfigReference ->
                         validateCodeOwnerConfigReference(
-                            project,
+                            branchNameKey,
                             revWalk,
                             codeOwnerConfigFilePath,
                             codeOwnerConfig.key(),
@@ -854,7 +856,7 @@
   /**
    * Validates a code owner config reference.
    *
-   * @param project the name of the project
+   * @param branchNameKey the branch and the project
    * @param revWalk rev walk that should be used to load the code owner configs from {@code project}
    * @param codeOwnerConfigFilePath the path of the code owner config file which contains the code
    *     owner config reference
@@ -867,7 +869,7 @@
    *     Optional#empty()} if there is no issue
    */
   private Optional<CommitValidationMessage> validateCodeOwnerConfigReference(
-      Project.NameKey project,
+      BranchNameKey branchNameKey,
       RevWalk revWalk,
       Path codeOwnerConfigFilePath,
       CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig,
@@ -892,7 +894,7 @@
       // that uploaders cannot probe for the existence of projects (e.g. deduce from the error
       // message whether a project exists)
       return nonResolvableImport(
-          project,
+          branchNameKey,
           importType,
           codeOwnerConfigFilePath,
           String.format("project '%s' not found", keyOfImportedCodeOwnerConfig.project().get()));
@@ -900,7 +902,7 @@
 
     if (!projectState.get().statePermitsRead()) {
       return nonResolvableImport(
-          project,
+          branchNameKey,
           importType,
           codeOwnerConfigFilePath,
           String.format(
@@ -917,7 +919,7 @@
       // that uploaders cannot probe for the existence of branches (e.g. deduce from the error
       // message whether a branch exists)
       return nonResolvableImport(
-          project,
+          branchNameKey,
           importType,
           codeOwnerConfigFilePath,
           String.format(
@@ -933,7 +935,7 @@
     if (!codeOwnerBackend.isCodeOwnerConfigFile(
         keyOfImportedCodeOwnerConfig.project(), codeOwnerConfigReference.fileName())) {
       return nonResolvableImport(
-          project,
+          branchNameKey,
           importType,
           codeOwnerConfigFilePath,
           String.format(
@@ -945,13 +947,13 @@
       // walk, otherwise the revision may not be visible yet and trying to load a code owner config
       // from it could fail with MissingObjectException.
       Optional<CodeOwnerConfig> importedCodeOwnerConfig =
-          keyOfImportedCodeOwnerConfig.project().equals(project)
+          keyOfImportedCodeOwnerConfig.project().equals(branchNameKey.project())
               ? codeOwnerBackend.getCodeOwnerConfig(
                   keyOfImportedCodeOwnerConfig, revWalk, revision.get())
               : codeOwnerBackend.getCodeOwnerConfig(keyOfImportedCodeOwnerConfig, revision.get());
       if (!importedCodeOwnerConfig.isPresent()) {
         return nonResolvableImport(
-            project,
+            branchNameKey,
             importType,
             codeOwnerConfigFilePath,
             String.format(
@@ -965,7 +967,7 @@
       if (getInvalidConfigCause(codeOwnersInternalServerErrorException).isPresent()) {
         // The imported code owner config is non-parseable.
         return nonResolvableImport(
-            project,
+            branchNameKey,
             importType,
             codeOwnerConfigFilePath,
             String.format(
@@ -1047,7 +1049,7 @@
   }
 
   private Optional<CommitValidationMessage> nonResolvableImport(
-      Project.NameKey project,
+      BranchNameKey branchNameKey,
       CodeOwnerConfigImportType importType,
       Path codeOwnerConfigFilePath,
       String message) {
@@ -1055,7 +1057,9 @@
         importType,
         codeOwnerConfigFilePath,
         message,
-        codeOwnersPluginConfiguration.getProjectConfig(project).rejectNonResolvableImports()
+        codeOwnersPluginConfiguration
+                .getProjectConfig(branchNameKey.project())
+                .rejectNonResolvableImports(branchNameKey.branch())
             ? ValidationMessage.Type.ERROR
             : ValidationMessage.Type.WARNING);
   }
@@ -1074,11 +1078,13 @@
   }
 
   private Optional<CommitValidationMessage> nonResolvableCodeOwner(
-      Project.NameKey project, String message) {
+      BranchNameKey branchNameKey, String message) {
     return Optional.of(
         new CommitValidationMessage(
             message,
-            codeOwnersPluginConfiguration.getProjectConfig(project).rejectNonResolvableCodeOwners()
+            codeOwnersPluginConfiguration
+                    .getProjectConfig(branchNameKey.project())
+                    .rejectNonResolvableCodeOwners(branchNameKey.branch())
                 ? ValidationMessage.Type.ERROR
                 : ValidationMessage.Type.WARNING));
   }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java
index b501277..c35280c 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java
@@ -2007,6 +2007,98 @@
     assertThat(gApi.changes().id(r.getChangeId()).get().status).isEqualTo(ChangeStatus.MERGED);
   }
 
+  @Test
+  public void disableRejectionOfNonResolvableCodeOwnersForBranch() throws Exception {
+    setAsDefaultCodeOwners(admin);
+
+    // Disable the rejection of non-resolvable code owners for the master branch.
+    updateCodeOwnersConfig(
+        project,
+        codeOwnersConfig ->
+            codeOwnersConfig.setBoolean(
+                GeneralConfig.SECTION_VALIDATION,
+                "refs/heads/master",
+                GeneralConfig.KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS,
+                false));
+
+    CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/");
+    String unknownEmail = "non-existing-email@example.com";
+    PushOneCommit.Result r =
+        createChange(
+            "Add code owners",
+            codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
+            format(
+                CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
+                    .addCodeOwnerSet(CodeOwnerSet.createWithoutPathExpressions(unknownEmail))
+                    .build()));
+    assertOkWithWarnings(
+        r,
+        "invalid code owner config files",
+        String.format(
+            "code owner email '%s' in '%s' cannot be resolved for %s",
+            unknownEmail,
+            codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
+            identifiedUserFactory.create(admin.id()).getLoggableName()));
+
+    approve(r.getChangeId());
+    gApi.changes().id(r.getChangeId()).current().submit();
+    assertThat(gApi.changes().id(r.getChangeId()).get().status).isEqualTo(ChangeStatus.MERGED);
+  }
+
+  @Test
+  public void disableRejectionOfNonResolvableImportsForBranch() throws Exception {
+    skipTestIfImportsNotSupportedByCodeOwnersBackend();
+
+    setAsDefaultCodeOwners(admin);
+
+    // Disable the rejection of non-resolvable imports for the master branch.
+    updateCodeOwnersConfig(
+        project,
+        codeOwnersConfig ->
+            codeOwnersConfig.setBoolean(
+                GeneralConfig.SECTION_VALIDATION,
+                "refs/heads/master",
+                GeneralConfig.KEY_REJECT_NON_RESOLVABLE_IMPORTS,
+                false));
+
+    // create a code owner config that imports a code owner config from a non-existing project
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = createCodeOwnerConfigKey("/");
+    Project.NameKey nonExistingProject = Project.nameKey("non-existing");
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        CodeOwnerConfigReference.builder(
+                CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
+                codeOwnerConfigOperations
+                    .codeOwnerConfig(CodeOwnerConfig.Key.create(nonExistingProject, "master", "/"))
+                    .getFilePath())
+            .setProject(nonExistingProject)
+            .build();
+    CodeOwnerConfig codeOwnerConfig =
+        createCodeOwnerConfigWithImport(
+            keyOfImportingCodeOwnerConfig,
+            CodeOwnerConfigImportType.GLOBAL,
+            codeOwnerConfigReference);
+
+    PushOneCommit.Result r =
+        createChange(
+            "Add code owners",
+            codeOwnerConfigOperations
+                .codeOwnerConfig(keyOfImportingCodeOwnerConfig)
+                .getJGitFilePath(),
+            format(codeOwnerConfig));
+    assertOkWithWarnings(
+        r,
+        "invalid code owner config files",
+        String.format(
+            "invalid %s import in '%s': project '%s' not found",
+            CodeOwnerConfigImportType.GLOBAL.getType(),
+            codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).getFilePath(),
+            nonExistingProject.get()));
+
+    approve(r.getChangeId());
+    gApi.changes().id(r.getChangeId()).current().submit();
+    assertThat(gApi.changes().id(r.getChangeId()).get().status).isEqualTo(ChangeStatus.MERGED);
+  }
+
   private CodeOwnerConfig createCodeOwnerConfigWithImport(
       CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig,
       CodeOwnerConfigImportType importType,
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/PutCodeOwnerProjectConfigIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/PutCodeOwnerProjectConfigIT.java
index 50114fe..5727b63 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/PutCodeOwnerProjectConfigIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/PutCodeOwnerProjectConfigIT.java
@@ -517,37 +517,52 @@
   @Test
   public void setRejectNonResolvableCodeOwners() throws Exception {
     assertThat(
-            codeOwnersPluginConfiguration.getProjectConfig(project).rejectNonResolvableCodeOwners())
+            codeOwnersPluginConfiguration
+                .getProjectConfig(project)
+                .rejectNonResolvableCodeOwners("master"))
         .isTrue();
 
     CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
     input.rejectNonResolvableCodeOwners = false;
     projectCodeOwnersApiFactory.project(project).updateConfig(input);
     assertThat(
-            codeOwnersPluginConfiguration.getProjectConfig(project).rejectNonResolvableCodeOwners())
+            codeOwnersPluginConfiguration
+                .getProjectConfig(project)
+                .rejectNonResolvableCodeOwners("master"))
         .isFalse();
 
     input.rejectNonResolvableCodeOwners = true;
     projectCodeOwnersApiFactory.project(project).updateConfig(input);
     assertThat(
-            codeOwnersPluginConfiguration.getProjectConfig(project).rejectNonResolvableCodeOwners())
+            codeOwnersPluginConfiguration
+                .getProjectConfig(project)
+                .rejectNonResolvableCodeOwners("master"))
         .isTrue();
   }
 
   @Test
   public void setRejectNonResolvableImports() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).rejectNonResolvableImports())
+    assertThat(
+            codeOwnersPluginConfiguration
+                .getProjectConfig(project)
+                .rejectNonResolvableImports("master"))
         .isTrue();
 
     CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
     input.rejectNonResolvableImports = false;
     projectCodeOwnersApiFactory.project(project).updateConfig(input);
-    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).rejectNonResolvableImports())
+    assertThat(
+            codeOwnersPluginConfiguration
+                .getProjectConfig(project)
+                .rejectNonResolvableImports("master"))
         .isFalse();
 
     input.rejectNonResolvableImports = true;
     projectCodeOwnersApiFactory.project(project).updateConfig(input);
-    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).rejectNonResolvableImports())
+    assertThat(
+            codeOwnersPluginConfiguration
+                .getProjectConfig(project)
+                .rejectNonResolvableImports("master"))
         .isTrue();
   }
 
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigSnapshotTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigSnapshotTest.java
index eff2303..81a9022 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigSnapshotTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigSnapshotTest.java
@@ -1101,6 +1101,143 @@
         .isEqualTo(CodeOwnerConfigValidationPolicy.DRY_RUN);
   }
 
+  @Test
+  public void cannotGetRejectNonResolvableCodeOwnersForNullBranch() throws Exception {
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class,
+            () -> cfgSnapshot().rejectNonResolvableCodeOwners(/* branchName= */ null));
+    assertThat(npe).hasMessageThat().isEqualTo("branchName");
+  }
+
+  @Test
+  public void getRejectNonResolvableCodeOwners_notConfigured() throws Exception {
+    assertThat(cfgSnapshot().rejectNonResolvableCodeOwners("master")).isTrue();
+    assertThat(cfgSnapshot().rejectNonResolvableCodeOwners("non-existing")).isTrue();
+  }
+
+  @Test
+  public void getRejectNonResolvableCodeOwners_configuredOnProjectLevel() throws Exception {
+    configureRejectNonResolvableCodeOwners(project, false);
+    assertThat(cfgSnapshot().rejectNonResolvableCodeOwners("master")).isFalse();
+    assertThat(cfgSnapshot().rejectNonResolvableCodeOwners("non-existing")).isFalse();
+  }
+
+  @Test
+  public void getRejectNonResolvableCodeOwners_configuredOnBranchLevel() throws Exception {
+    configureRejectNonResolvableCodeOwnersForBranch(project, "refs/heads/master", false);
+    assertThat(cfgSnapshot().rejectNonResolvableCodeOwners("master")).isFalse();
+    assertThat(cfgSnapshot().rejectNonResolvableCodeOwners("refs/heads/master")).isFalse();
+    assertThat(cfgSnapshot().rejectNonResolvableCodeOwners("foo")).isTrue();
+  }
+
+  @Test
+  public void getRejectNonResolvableCodeOwners_branchLevelConfigTakesPrecedence() throws Exception {
+    updateCodeOwnersConfig(
+        project,
+        codeOwnersConfig -> {
+          codeOwnersConfig.setBoolean(
+              CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS,
+              /* subsection= */ null,
+              GeneralConfig.KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS,
+              /* value= */ false);
+          codeOwnersConfig.setBoolean(
+              GeneralConfig.SECTION_VALIDATION,
+              "refs/heads/master",
+              GeneralConfig.KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS,
+              /* value= */ true);
+        });
+    assertThat(cfgSnapshot().rejectNonResolvableCodeOwners("master")).isTrue();
+    assertThat(cfgSnapshot().rejectNonResolvableCodeOwners("refs/heads/master")).isTrue();
+    assertThat(cfgSnapshot().rejectNonResolvableCodeOwners("foo")).isFalse();
+  }
+
+  @Test
+  public void getRejectNonResolvableCodeOwners_inheritedBranchLevelConfigTakesPrecedence()
+      throws Exception {
+    configureRejectNonResolvableCodeOwnersForBranch(allProjects, "refs/heads/master", true);
+    configureRejectNonResolvableCodeOwners(project, false);
+    assertThat(cfgSnapshot().rejectNonResolvableCodeOwners("master")).isTrue();
+    assertThat(cfgSnapshot().rejectNonResolvableCodeOwners("refs/heads/master")).isTrue();
+    assertThat(cfgSnapshot().rejectNonResolvableCodeOwners("foo")).isFalse();
+  }
+
+  @Test
+  public void getRejectNonResolvableCodeOwners_inheritedBranchLevelCanBeOverridden()
+      throws Exception {
+    configureRejectNonResolvableCodeOwnersForBranch(allProjects, "refs/heads/master", true);
+    configureRejectNonResolvableCodeOwnersForBranch(project, "refs/heads/master", false);
+    assertThat(cfgSnapshot().rejectNonResolvableCodeOwners("master")).isFalse();
+  }
+
+  @Test
+  public void cannotGetRejectNonResolvableImportsForNullBranch() throws Exception {
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class,
+            () -> cfgSnapshot().rejectNonResolvableImports(/* branchName= */ null));
+    assertThat(npe).hasMessageThat().isEqualTo("branchName");
+  }
+
+  @Test
+  public void getRejectNonResolvableImports_notConfigured() throws Exception {
+    assertThat(cfgSnapshot().rejectNonResolvableImports("master")).isTrue();
+    assertThat(cfgSnapshot().rejectNonResolvableImports("non-existing")).isTrue();
+  }
+
+  @Test
+  public void getRejectNonResolvableImports_configuredOnProjectLevel() throws Exception {
+    configureRejectNonResolvableImports(project, false);
+    assertThat(cfgSnapshot().rejectNonResolvableImports("master")).isFalse();
+    assertThat(cfgSnapshot().rejectNonResolvableImports("non-existing")).isFalse();
+  }
+
+  @Test
+  public void getRejectNonResolvableImports_configuredOnBranchLevel() throws Exception {
+    configureRejectNonResolvableImportsForBranch(project, "refs/heads/master", false);
+    assertThat(cfgSnapshot().rejectNonResolvableImports("master")).isFalse();
+    assertThat(cfgSnapshot().rejectNonResolvableImports("refs/heads/master")).isFalse();
+    assertThat(cfgSnapshot().rejectNonResolvableImports("foo")).isTrue();
+  }
+
+  @Test
+  public void getRejectNonResolvableImports_branchLevelConfigTakesPrecedence() throws Exception {
+    updateCodeOwnersConfig(
+        project,
+        codeOwnersConfig -> {
+          codeOwnersConfig.setBoolean(
+              CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS,
+              /* subsection= */ null,
+              GeneralConfig.KEY_REJECT_NON_RESOLVABLE_IMPORTS,
+              /* value= */ false);
+          codeOwnersConfig.setBoolean(
+              GeneralConfig.SECTION_VALIDATION,
+              "refs/heads/master",
+              GeneralConfig.KEY_REJECT_NON_RESOLVABLE_IMPORTS,
+              /* value= */ true);
+        });
+    assertThat(cfgSnapshot().rejectNonResolvableImports("master")).isTrue();
+    assertThat(cfgSnapshot().rejectNonResolvableImports("refs/heads/master")).isTrue();
+    assertThat(cfgSnapshot().rejectNonResolvableImports("foo")).isFalse();
+  }
+
+  @Test
+  public void getRejectNonResolvableImports_inheritedBranchLevelConfigTakesPrecedence()
+      throws Exception {
+    configureRejectNonResolvableImportsForBranch(allProjects, "refs/heads/master", true);
+    configureRejectNonResolvableImports(project, false);
+    assertThat(cfgSnapshot().rejectNonResolvableImports("master")).isTrue();
+    assertThat(cfgSnapshot().rejectNonResolvableImports("refs/heads/master")).isTrue();
+    assertThat(cfgSnapshot().rejectNonResolvableImports("foo")).isFalse();
+  }
+
+  @Test
+  public void getRejectNonResolvableImports_inheritedBranchLevelCanBeOverridden() throws Exception {
+    configureRejectNonResolvableImportsForBranch(allProjects, "refs/heads/master", true);
+    configureRejectNonResolvableImportsForBranch(project, "refs/heads/master", false);
+    assertThat(cfgSnapshot().rejectNonResolvableImports("master")).isFalse();
+  }
+
   private CodeOwnersPluginConfigSnapshot cfgSnapshot() {
     return codeOwnersPluginConfigSnapshotFactory.create(project);
   }
@@ -1238,6 +1375,48 @@
                 codeOwnerConfigValidationPolicy.name()));
   }
 
+  private void configureRejectNonResolvableCodeOwners(Project.NameKey project, boolean value)
+      throws Exception {
+    setCodeOwnersConfig(
+        project,
+        /* subsection= */ null,
+        GeneralConfig.KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS,
+        Boolean.toString(value));
+  }
+
+  private void configureRejectNonResolvableCodeOwnersForBranch(
+      Project.NameKey project, String branchSubsection, boolean value) throws Exception {
+    updateCodeOwnersConfig(
+        project,
+        codeOwnersConfig ->
+            codeOwnersConfig.setString(
+                GeneralConfig.SECTION_VALIDATION,
+                branchSubsection,
+                GeneralConfig.KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS,
+                Boolean.toString(value)));
+  }
+
+  private void configureRejectNonResolvableImports(Project.NameKey project, boolean value)
+      throws Exception {
+    setCodeOwnersConfig(
+        project,
+        /* subsection= */ null,
+        GeneralConfig.KEY_REJECT_NON_RESOLVABLE_IMPORTS,
+        Boolean.toString(value));
+  }
+
+  private void configureRejectNonResolvableImportsForBranch(
+      Project.NameKey project, String branchSubsection, boolean value) throws Exception {
+    updateCodeOwnersConfig(
+        project,
+        codeOwnersConfig ->
+            codeOwnersConfig.setString(
+                GeneralConfig.SECTION_VALIDATION,
+                branchSubsection,
+                GeneralConfig.KEY_REJECT_NON_RESOLVABLE_IMPORTS,
+                Boolean.toString(value)));
+  }
+
   private AutoCloseable registerTestBackend() {
     RegistrationHandle registrationHandle =
         ((PrivateInternals_DynamicMapImpl<CodeOwnerBackend>) codeOwnerBackends)
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 3c9fc09..5b5b6e9 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java
@@ -281,6 +281,185 @@
   }
 
   @Test
+  public void cannotGetRejectNonResolvableCodeOwnersForBranchForNullBranch() throws Exception {
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class,
+            () ->
+                generalConfig.getRejectNonResolvableCodeOwnersForBranch(
+                    /* branchNameKey= */ null, new Config()));
+    assertThat(npe).hasMessageThat().isEqualTo("branchNameKey");
+  }
+
+  @Test
+  public void cannotGetRejectNonResolvableCodeOwnersForBranchForNullConfig() throws Exception {
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class,
+            () ->
+                generalConfig.getRejectNonResolvableCodeOwnersForBranch(
+                    BranchNameKey.create(project, "master"), /* pluginConfig= */ null));
+    assertThat(npe).hasMessageThat().isEqualTo("pluginConfig");
+  }
+
+  @Test
+  public void noBranchSpecificRejectNonResolvableCodeOwnersConfiguration() throws Exception {
+    assertThat(
+            generalConfig.getRejectNonResolvableCodeOwnersForBranch(
+                BranchNameKey.create(project, "master"), new Config()))
+        .isEmpty();
+  }
+
+  @Test
+  public void noMatchingBranchSpecificRejectNonResolvableCodeOwnersConfiguration_exact()
+      throws Exception {
+    Config cfg = new Config();
+    cfg.setBoolean(
+        SECTION_VALIDATION,
+        "refs/heads/foo",
+        KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS,
+        /* value= */ false);
+    assertThat(
+            generalConfig.getRejectNonResolvableCodeOwnersForBranch(
+                BranchNameKey.create(project, "master"), cfg))
+        .isEmpty();
+  }
+
+  @Test
+  public void noMatchingBranchSpecificRejectNonResolvableCodeOwnersConfiguration_refPattern()
+      throws Exception {
+    Config cfg = new Config();
+    cfg.setBoolean(
+        SECTION_VALIDATION,
+        "refs/heads/foo/*",
+        KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS,
+        /* value= */ false);
+    assertThat(
+            generalConfig.getRejectNonResolvableCodeOwnersForBranch(
+                BranchNameKey.create(project, "master"), cfg))
+        .isEmpty();
+  }
+
+  @Test
+  public void noMatchingBranchSpecificRejectNonResolvableCodeOwnersConfiguration_regEx()
+      throws Exception {
+    Config cfg = new Config();
+    cfg.setBoolean(
+        SECTION_VALIDATION,
+        "^refs/heads/.*foo.*",
+        KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS,
+        /* value= */ false);
+    assertThat(
+            generalConfig.getRejectNonResolvableCodeOwnersForBranch(
+                BranchNameKey.create(project, "master"), cfg))
+        .isEmpty();
+  }
+
+  @Test
+  public void noMatchingBranchSpecificRejectNonResolvableCodeOwnersConfiguration_invalidRegEx()
+      throws Exception {
+    Config cfg = new Config();
+    cfg.setBoolean(
+        SECTION_VALIDATION,
+        "^refs/heads/[",
+        KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS,
+        /* value= */ false);
+    assertThat(
+            generalConfig.getRejectNonResolvableCodeOwnersForBranch(
+                BranchNameKey.create(project, "master"), cfg))
+        .isEmpty();
+  }
+
+  @Test
+  public void matchingBranchSpecificRejectNonResolvableCodeOwnersConfiguration_exact()
+      throws Exception {
+    Config cfg = new Config();
+    cfg.setBoolean(
+        SECTION_VALIDATION,
+        "refs/heads/master",
+        KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS,
+        /* value= */ false);
+    assertThat(
+            generalConfig.getRejectNonResolvableCodeOwnersForBranch(
+                BranchNameKey.create(project, "master"), cfg))
+        .value()
+        .isEqualTo(false);
+  }
+
+  @Test
+  public void matchingBranchSpecificRejectNonResolvableCodeOwnersConfiguration_refPattern()
+      throws Exception {
+    Config cfg = new Config();
+    cfg.setBoolean(
+        SECTION_VALIDATION,
+        "refs/heads/*",
+        KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS,
+        /* value= */ false);
+    assertThat(
+            generalConfig.getRejectNonResolvableCodeOwnersForBranch(
+                BranchNameKey.create(project, "master"), cfg))
+        .value()
+        .isEqualTo(false);
+  }
+
+  @Test
+  public void matchingBranchSpecificRejectNonResolvableCodeOwnersConfiguration_regEx()
+      throws Exception {
+    Config cfg = new Config();
+    cfg.setBoolean(
+        SECTION_VALIDATION,
+        "^refs/heads/.*bar.*",
+        KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS,
+        /* value= */ false);
+    assertThat(
+            generalConfig.getRejectNonResolvableCodeOwnersForBranch(
+                BranchNameKey.create(project, "foobarbaz"), cfg))
+        .value()
+        .isEqualTo(false);
+  }
+
+  @Test
+  public void branchSpecificRejectNonResolvableCodeOwnersConfigurationIsIgnoredIfValueIsInvalid()
+      throws Exception {
+    Config cfg = new Config();
+    cfg.setString(
+        SECTION_VALIDATION, "refs/heads/master", KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS, "INVALID");
+    assertThat(
+            generalConfig.getRejectNonResolvableCodeOwnersForBranch(
+                BranchNameKey.create(project, "master"), cfg))
+        .isEmpty();
+  }
+
+  @Test
+  public void multipleMatchingBranchSpecificRejectNonResolvableCodeOwnersConfiguration()
+      throws Exception {
+    Config cfg = new Config();
+    cfg.setBoolean(
+        SECTION_VALIDATION,
+        "refs/heads/master",
+        KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS,
+        /* value= */ false);
+    cfg.setBoolean(
+        SECTION_VALIDATION,
+        "refs/heads/*",
+        KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS,
+        /* value= */ false);
+    cfg.setBoolean(
+        SECTION_VALIDATION,
+        "^refs/heads/.*",
+        KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS,
+        /* value= */ false);
+
+    // it is non-deterministic which of the branch-specific configurations takes precedence, but
+    // since they all configure the same value it's not important for this assertion
+    assertThat(
+            generalConfig.getRejectNonResolvableCodeOwnersForBranch(
+                BranchNameKey.create(project, "master"), cfg))
+        .value()
+        .isEqualTo(false);
+  }
+
+  @Test
   public void cannotGetRejectNonResolvableImportsForNullProject() throws Exception {
     NullPointerException npe =
         assertThrows(
@@ -339,6 +518,176 @@
   }
 
   @Test
+  public void cannotGetRejectNonResolvableImportsForBranchForNullBranch() throws Exception {
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class,
+            () ->
+                generalConfig.getRejectNonResolvableImportsForBranch(
+                    /* branchNameKey= */ null, new Config()));
+    assertThat(npe).hasMessageThat().isEqualTo("branchNameKey");
+  }
+
+  @Test
+  public void cannotGetRejectNonResolvableImportsForBranchForNullConfig() throws Exception {
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class,
+            () ->
+                generalConfig.getRejectNonResolvableImportsForBranch(
+                    BranchNameKey.create(project, "master"), /* pluginConfig= */ null));
+    assertThat(npe).hasMessageThat().isEqualTo("pluginConfig");
+  }
+
+  @Test
+  public void noBranchSpecificRejectNonResolvableImportsConfiguration() throws Exception {
+    assertThat(
+            generalConfig.getRejectNonResolvableImportsForBranch(
+                BranchNameKey.create(project, "master"), new Config()))
+        .isEmpty();
+  }
+
+  @Test
+  public void noMatchingBranchSpecificRejectNonResolvableImportsConfiguration_exact()
+      throws Exception {
+    Config cfg = new Config();
+    cfg.setBoolean(
+        SECTION_VALIDATION,
+        "refs/heads/foo",
+        KEY_REJECT_NON_RESOLVABLE_IMPORTS,
+        /* value= */ false);
+    assertThat(
+            generalConfig.getRejectNonResolvableImportsForBranch(
+                BranchNameKey.create(project, "master"), cfg))
+        .isEmpty();
+  }
+
+  @Test
+  public void noMatchingBranchSpecificRejectNonResolvableImportsConfiguration_refPattern()
+      throws Exception {
+    Config cfg = new Config();
+    cfg.setBoolean(
+        SECTION_VALIDATION,
+        "refs/heads/foo/*",
+        KEY_REJECT_NON_RESOLVABLE_IMPORTS,
+        /* value= */ false);
+    assertThat(
+            generalConfig.getRejectNonResolvableImportsForBranch(
+                BranchNameKey.create(project, "master"), cfg))
+        .isEmpty();
+  }
+
+  @Test
+  public void noMatchingBranchSpecificRejectNonResolvableImportsConfiguration_regEx()
+      throws Exception {
+    Config cfg = new Config();
+    cfg.setBoolean(
+        SECTION_VALIDATION,
+        "^refs/heads/.*foo.*",
+        KEY_REJECT_NON_RESOLVABLE_IMPORTS,
+        /* value= */ false);
+    assertThat(
+            generalConfig.getRejectNonResolvableImportsForBranch(
+                BranchNameKey.create(project, "master"), cfg))
+        .isEmpty();
+  }
+
+  @Test
+  public void noMatchingBranchSpecificRejectNonResolvableImportsConfiguration_invalidRegEx()
+      throws Exception {
+    Config cfg = new Config();
+    cfg.setBoolean(
+        SECTION_VALIDATION, "^refs/heads/[", KEY_REJECT_NON_RESOLVABLE_IMPORTS, /* value= */ false);
+    assertThat(
+            generalConfig.getRejectNonResolvableImportsForBranch(
+                BranchNameKey.create(project, "master"), cfg))
+        .isEmpty();
+  }
+
+  @Test
+  public void matchingBranchSpecificRejectNonResolvableImportsConfiguration_exact()
+      throws Exception {
+    Config cfg = new Config();
+    cfg.setBoolean(
+        SECTION_VALIDATION,
+        "refs/heads/master",
+        KEY_REJECT_NON_RESOLVABLE_IMPORTS,
+        /* value= */ false);
+    assertThat(
+            generalConfig.getRejectNonResolvableImportsForBranch(
+                BranchNameKey.create(project, "master"), cfg))
+        .value()
+        .isEqualTo(false);
+  }
+
+  @Test
+  public void matchingBranchSpecificRejectNonResolvableImportsConfiguration_refPattern()
+      throws Exception {
+    Config cfg = new Config();
+    cfg.setBoolean(
+        SECTION_VALIDATION, "refs/heads/*", KEY_REJECT_NON_RESOLVABLE_IMPORTS, /* value= */ false);
+    assertThat(
+            generalConfig.getRejectNonResolvableImportsForBranch(
+                BranchNameKey.create(project, "master"), cfg))
+        .value()
+        .isEqualTo(false);
+  }
+
+  @Test
+  public void matchingBranchSpecificRejectNonResolvableImportsConfiguration_regEx()
+      throws Exception {
+    Config cfg = new Config();
+    cfg.setBoolean(
+        SECTION_VALIDATION,
+        "^refs/heads/.*bar.*",
+        KEY_REJECT_NON_RESOLVABLE_IMPORTS,
+        /* value= */ false);
+    assertThat(
+            generalConfig.getRejectNonResolvableImportsForBranch(
+                BranchNameKey.create(project, "foobarbaz"), cfg))
+        .value()
+        .isEqualTo(false);
+  }
+
+  @Test
+  public void branchSpecificRejectNonResolvableImportsConfigurationIsIgnoredIfValueIsInvalid()
+      throws Exception {
+    Config cfg = new Config();
+    cfg.setString(
+        SECTION_VALIDATION, "refs/heads/master", KEY_REJECT_NON_RESOLVABLE_IMPORTS, "INVALID");
+    assertThat(
+            generalConfig.getRejectNonResolvableImportsForBranch(
+                BranchNameKey.create(project, "master"), cfg))
+        .isEmpty();
+  }
+
+  @Test
+  public void multipleMatchingBranchSpecificRejectNonResolvableImportsConfiguration()
+      throws Exception {
+    Config cfg = new Config();
+    cfg.setBoolean(
+        SECTION_VALIDATION,
+        "refs/heads/master",
+        KEY_REJECT_NON_RESOLVABLE_IMPORTS,
+        /* value= */ false);
+    cfg.setBoolean(
+        SECTION_VALIDATION, "refs/heads/*", KEY_REJECT_NON_RESOLVABLE_IMPORTS, /* value= */ false);
+    cfg.setBoolean(
+        SECTION_VALIDATION,
+        "^refs/heads/.*",
+        KEY_REJECT_NON_RESOLVABLE_IMPORTS,
+        /* value= */ false);
+
+    // it is non-deterministic which of the branch-specific configurations takes precedence, but
+    // since they all configure the same value it's not important for this assertion
+    assertThat(
+            generalConfig.getRejectNonResolvableImportsForBranch(
+                BranchNameKey.create(project, "master"), cfg))
+        .value()
+        .isEqualTo(false);
+  }
+
+  @Test
   public void cannotGetEnableValidationOnCommitReceivedForNullProject() throws Exception {
     NullPointerException npe =
         assertThrows(
diff --git a/resources/Documentation/config.md b/resources/Documentation/config.md
index 70e5630..9dbb6a4 100644
--- a/resources/Documentation/config.md
+++ b/resources/Documentation/config.md
@@ -673,10 +673,27 @@
         [plugin.@PLUGIN@.rejectNonResolvableCodeOwners](#pluginCodeOwnersRejectNonResolvableCodeOwners)
         in `gerrit.config` and the `codeOwners.rejectNonResolvableCodeOwners`
         setting from parent projects.\
+        Can be overriden on branch-level by setting
+        [validation.\<branch\>.rejectNonResolvableCodeOwners](#validationBranchRejectNonResolvableCodeOwners).\
         If not set, the global setting
         [plugin.@PLUGIN@.rejectNonResolvableCodeOwners](#pluginCodeOwnersRejectNonResolvableCodeOwners)
         in `gerrit.config` is used.
 
+<a id="validationBranchRejectNonResolvableCodeOwners">validation.\<branch\>.rejectNonResolvableCodeOwners</a>
+:       Branch-level configuration to control whether modifications of code
+        owner config files that newly add non-resolvable code owners should be
+        rejected on commit received and submit.\
+        Applies to all branches that are matched by `<branch>`, which can be
+        an exact ref name (e.g. `refs/heads/master`), a ref pattern (e.g.
+        `refs/heads/*`) or a regular expression (e.g. `^refs/heads/stable-.*`).\
+        If a branches matches multiple validation subsections it is undefined
+        which of the subsections takes precedence.\
+        Overrides the project-level configuration for rejecting non-resolvable
+        code owners that is configured by
+        [codeOwners.rejectNonResolvableCodeOwners](#codeOwnersRejectNonResolvableCodeOwners).\
+        For further details see the description of
+        [codeOwners.rejectNonResolvableCodeOwners](#codeOwnersRejectNonResolvableCodeOwners).
+
 <a id="codeOwnersRejectNonResolvableImports">codeOwners.rejectNonResolvableImports</a>
 :       Whether modifications of code owner config files that newly add
         non-resolvable imports should be rejected on commit received an submit.\
@@ -691,10 +708,27 @@
         [plugin.@PLUGIN@.rejectNonResolvableImports](#pluginCodeOwnersRejectNonResolvableImports)
         in `gerrit.config` and the `codeOwners.rejectNonResolvableImports`
         setting from parent projects.\
+        Can be overriden on branch-level by setting
+        [validation.\<branch\>.rejectNonResolvableImports](#validationBranchRejectNonResolvableImports).\
         If not set, the global setting
         [plugin.@PLUGIN@.rejectNonResolvableImports](#pluginCodeOwnersRejectNonResolvableImports)
         in `gerrit.config` is used.
 
+<a id="validationBranchRejectNonResolvableImports">validation.\<branch\>.rejectNonResolvableImports</a>
+:       Branch-level configuration to control whether modifications of code
+        owner config files that newly add non-resolvable imports should be
+        rejected on commit received and submit.\
+        Applies to all branches that are matched by `<branch>`, which can be
+        an exact ref name (e.g. `refs/heads/master`), a ref pattern (e.g.
+        `refs/heads/*`) or a regular expression (e.g. `^refs/heads/stable-.*`).\
+        If a branches matches multiple validation subsections it is undefined
+        which of the subsections takes precedence.\
+        Overrides the project-level configuration for rejecting non-resolvable
+        imports that is configured by
+        [codeOwners.rejectNonResolvableImports](#codeOwnersRejectNonResolvableImports).\
+        For further details see the description of
+        [codeOwners.rejectNonResolvableImports](#codeOwnersRejectNonResolvableImports).
+
 <a id="codeOwnersRequiredApproval">codeOwners.requiredApproval</a>
 :       Approval that is required from code owners to approve the files in a
         change.\