Refactor config reading as preparation to reduce plugin config loads

Loading a plugin config with inheritance via
pluginConfigFactory#getProjectPluginConfigWithInheritance(projectName,
pluginName) is not free as it needs to overlay the config with the
configs of the parent projects. All configs are cached in the project
cache but doing the overlaying too often can still get expensive. At the
moment we do the overlaying each time a config parameter is read and
since we have a lot of config parameters that get read all over the
code this sums up.

At the moment to read a config parameter we do:
1. get CodeOwnersPluginConfiguration injected
2. call getFoo(Project.NameKey) or getFoo(BranchNameKey) on the
   CodeOwnersPluginConfiguration

Each codeOwnersPluginConfiguration.getFoo(...) call
invokes pluginConfigFactory#getProjectPluginConfigWithInheritance(projectName,
pluginName).

With this change we refactor these calls to:
codeOwnersPluginConfiguration.getProjectConfig(Project.NameKey).getFoo()
and codeOwnersPluginConfiguration.getProjectConfig(Project.NameKey).getFoo(branchName).

codeOwnersPluginConfiguration.getProjectConfig(Project.NameKey) returns
a CodeOwnersPluginConfigSnapshot.

The advantage of the new CodeOwnersPluginConfigSnapshot class is that it
can cache the loaded plugin config and reuse it for subsequent get
calls. With this change we do not benefit from this yet, since all
places that read a config call
codeOwnersPluginConfiguration.getProjectConfig(Project.NameKey) newly
which each time creates a new CodeOwnersPluginConfigSnapshot instance.
However this will change with the follow-up changes. It's not done in
this change yet, so that the refactoring (this change) is separated from
doing improvements (follow-up changes) and hence is hopefully easier to
review.

Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: I3ac34367fdbcdaac299f2c76725e48415683b82b
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java b/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java
index 34f4e0d..2824246 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java
@@ -171,7 +171,8 @@
     String quotedFileExtension =
         Pattern.quote(
             codeOwnersPluginConfiguration
-                .getFileExtension(project)
+                .getProjectConfig(project)
+                .getFileExtension()
                 .map(ext -> "." + ext)
                 .orElse(""));
     String nameExtension = "(\\w)+";
@@ -188,7 +189,11 @@
 
   private String getFileName(Project.NameKey project) {
     return defaultFileName
-        + codeOwnersPluginConfiguration.getFileExtension(project).map(ext -> "." + ext).orElse("");
+        + codeOwnersPluginConfiguration
+            .getProjectConfig(project)
+            .getFileExtension()
+            .map(ext -> "." + ext)
+            .orElse("");
   }
 
   @Override
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java b/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java
index 3bdb518..c22dafe 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.extensions.events.ReviewerAddedListener;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfigSnapshot;
 import com.google.gerrit.server.ExceptionHook;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ServerInitiated;
@@ -32,6 +33,7 @@
   protected void configure() {
     factory(CodeOwnersUpdate.Factory.class);
     factory(CodeOwnerConfigScanner.Factory.class);
+    factory(CodeOwnersPluginConfigSnapshot.Factory.class);
 
     DynamicMap.mapOf(binder(), CodeOwnerBackend.class);
 
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/ChangedFiles.java b/java/com/google/gerrit/plugins/codeowners/backend/ChangedFiles.java
index 787e452..cf88e62 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/ChangedFiles.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/ChangedFiles.java
@@ -127,7 +127,7 @@
         repoConfig,
         revWalk,
         revCommit,
-        codeOwnersPluginConfiguration.getMergeCommitStrategy(project));
+        codeOwnersPluginConfiguration.getProjectConfig(project).getMergeCommitStrategy());
   }
 
   public ImmutableSet<ChangedFile> compute(
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
index 45023fc..453ef46 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
@@ -234,13 +234,17 @@
           "prepare stream to compute file statuses (project = %s, change = %d)",
           changeNotes.getProjectName(), changeNotes.getChangeId().get());
 
-      if (codeOwnersPluginConfiguration.arePureRevertsExempted(changeNotes.getProjectName())
+      if (codeOwnersPluginConfiguration
+              .getProjectConfig(changeNotes.getProjectName())
+              .arePureRevertsExempted()
           && isPureRevert(changeNotes)) {
         return getAllPathsAsApproved(changeNotes, changeNotes.getCurrentPatchSet());
       }
 
       boolean enableImplicitApprovalFromUploader =
-          codeOwnersPluginConfiguration.areImplicitApprovalsEnabled(changeNotes.getProjectName());
+          codeOwnersPluginConfiguration
+              .getProjectConfig(changeNotes.getProjectName())
+              .areImplicitApprovalsEnabled();
       Account.Id patchSetUploader = changeNotes.getCurrentPatchSet().uploader();
       logger.atFine().log(
           "patchSetUploader = %d, implicit approval from uploader is %s",
@@ -250,11 +254,15 @@
           getCurrentPatchSetApprovals(changeNotes);
 
       RequiredApproval requiredApproval =
-          codeOwnersPluginConfiguration.getRequiredApproval(changeNotes.getProjectName());
+          codeOwnersPluginConfiguration
+              .getProjectConfig(changeNotes.getProjectName())
+              .getRequiredApproval();
       logger.atFine().log("requiredApproval = %s", requiredApproval);
 
       ImmutableSet<RequiredApproval> overrideApprovals =
-          codeOwnersPluginConfiguration.getOverrideApproval(changeNotes.getProjectName());
+          codeOwnersPluginConfiguration
+              .getProjectConfig(changeNotes.getProjectName())
+              .getOverrideApproval();
       boolean hasOverride =
           hasOverride(currentPatchSetApprovals, overrideApprovals, changeNotes, patchSetUploader);
       logger.atFine().log(
@@ -287,7 +295,7 @@
       logger.atFine().log("reviewers = %s, approvers = %s", reviewerAccountIds, approverAccountIds);
 
       FallbackCodeOwners fallbackCodeOwners =
-          codeOwnersPluginConfiguration.getFallbackCodeOwners(branch.project());
+          codeOwnersPluginConfiguration.getProjectConfig(branch.project()).getFallbackCodeOwners();
 
       CodeOwnerConfigHierarchy codeOwnerConfigHierarchy = codeOwnerConfigHierarchyProvider.get();
       return changedFiles
@@ -340,7 +348,9 @@
           patchSet.id().get());
 
       RequiredApproval requiredApproval =
-          codeOwnersPluginConfiguration.getRequiredApproval(changeNotes.getProjectName());
+          codeOwnersPluginConfiguration
+              .getProjectConfig(changeNotes.getProjectName())
+              .getRequiredApproval();
       logger.atFine().log("requiredApproval = %s", requiredApproval);
 
       BranchNameKey branch = changeNotes.getChange().getDest();
@@ -349,7 +359,7 @@
 
       boolean isProjectOwner = isProjectOwner(changeNotes.getProjectName(), accountId);
       FallbackCodeOwners fallbackCodeOwners =
-          codeOwnersPluginConfiguration.getFallbackCodeOwners(branch.project());
+          codeOwnersPluginConfiguration.getProjectConfig(branch.project()).getFallbackCodeOwners();
       logger.atFine().log(
           "fallbackCodeOwner = %s, isProjectOwner = %s", fallbackCodeOwners, isProjectOwner);
       if (fallbackCodeOwners.equals(FallbackCodeOwners.PROJECT_OWNERS) && isProjectOwner) {
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java
index 60d1220..9e15746 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java
@@ -92,7 +92,10 @@
     requireNonNull(commitMessage, "commitMessage");
     requireNonNull(codeOwnerConfigFileUpdater, "codeOwnerConfigFileUpdater");
 
-    CodeOwnerBackend codeOwnerBackend = codeOwnersPluginConfiguration.getBackend(branchNameKey);
+    CodeOwnerBackend codeOwnerBackend =
+        codeOwnersPluginConfiguration
+            .getProjectConfig(branchNameKey.project())
+            .getBackend(branchNameKey.branch());
     logger.atFine().log(
         "updating code owner files in branch %s of project %s",
         branchNameKey.branch(), branchNameKey.project());
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java
index e28625e..4df1696 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java
@@ -106,7 +106,10 @@
     requireNonNull(codeOwnerConfigVisitor, "codeOwnerConfigVisitor");
     requireNonNull(invalidCodeOwnerConfigCallback, "invalidCodeOwnerConfigCallback");
 
-    CodeOwnerBackend codeOwnerBackend = codeOwnersPluginConfiguration.getBackend(branchNameKey);
+    CodeOwnerBackend codeOwnerBackend =
+        codeOwnersPluginConfiguration
+            .getProjectConfig(branchNameKey.project())
+            .getBackend(branchNameKey.branch());
     logger.atFine().log(
         "scanning code owner files in branch %s of project %s (path glob = %s)",
         branchNameKey.branch(), branchNameKey.project(), pathGlob);
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
index 1fece0d..e7e18e5 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
@@ -191,7 +191,8 @@
    * @return the resolved global code owners of the given project
    */
   public CodeOwnerResolverResult resolveGlobalCodeOwners(Project.NameKey projectName) {
-    return resolve(codeOwnersPluginConfiguration.getGlobalCodeOwners(projectName));
+    return resolve(
+        codeOwnersPluginConfiguration.getProjectConfig(projectName).getGlobalCodeOwners());
   }
 
   /**
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSubmitRule.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSubmitRule.java
index 2dc2d37..0683b25 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSubmitRule.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSubmitRule.java
@@ -82,7 +82,9 @@
             "run code owner submit rule (project = %s, change = %d)",
             changeData.project().get(), changeData.getId().get());
 
-        if (codeOwnersPluginConfiguration.isDisabled(changeData.change().getDest())) {
+        if (codeOwnersPluginConfiguration
+            .getProjectConfig(changeData.project())
+            .isDisabled(changeData.change().getDest().branch())) {
           logger.atFine().log(
               "code owners functionality is disabled for branch %s", changeData.change().getDest());
           return Optional.empty();
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwners.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwners.java
index 9315523..4247849 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwners.java
@@ -54,7 +54,9 @@
     requireNonNull(revision, "revision");
     codeOwnerMetrics.countCodeOwnerConfigReads.increment();
     CodeOwnerBackend codeOwnerBackend =
-        codeOwnersPluginConfiguration.getBackend(codeOwnerConfigKey.branchNameKey());
+        codeOwnersPluginConfiguration
+            .getProjectConfig(codeOwnerConfigKey.project())
+            .getBackend(codeOwnerConfigKey.branchNameKey().branch());
     return codeOwnerBackend.getCodeOwnerConfig(codeOwnerConfigKey, revision);
   }
 
@@ -63,7 +65,9 @@
     requireNonNull(codeOwnerConfigKey, "codeOwnerConfigKey");
     codeOwnerMetrics.countCodeOwnerConfigReads.increment();
     CodeOwnerBackend codeOwnerBackend =
-        codeOwnersPluginConfiguration.getBackend(codeOwnerConfigKey.branchNameKey());
+        codeOwnersPluginConfiguration
+            .getProjectConfig(codeOwnerConfigKey.project())
+            .getBackend(codeOwnerConfigKey.branchNameKey().branch());
     return codeOwnerBackend.getCodeOwnerConfig(codeOwnerConfigKey, /* revision= */ null);
   }
 
@@ -82,7 +86,9 @@
   public Path getFilePath(CodeOwnerConfig.Key codeOwnerConfigKey) {
     requireNonNull(codeOwnerConfigKey, "codeOwnerConfigKey");
     CodeOwnerBackend codeOwnerBackend =
-        codeOwnersPluginConfiguration.getBackend(codeOwnerConfigKey.branchNameKey());
+        codeOwnersPluginConfiguration
+            .getProjectConfig(codeOwnerConfigKey.project())
+            .getBackend(codeOwnerConfigKey.branchNameKey().branch());
     return codeOwnerBackend.getFilePath(codeOwnerConfigKey);
   }
 
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java
index 8e57412..294fd69 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java
@@ -19,7 +19,6 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.ChangeMessage;
 import com.google.gerrit.entities.Project;
@@ -88,10 +87,12 @@
   public void onReviewersAdded(Event event) {
     Change.Id changeId = Change.id(event.getChange()._number);
     Project.NameKey projectName = Project.nameKey(event.getChange().project);
-    BranchNameKey branchNameKey = BranchNameKey.create(projectName, event.getChange().branch);
 
-    if (codeOwnersPluginConfiguration.isDisabled(branchNameKey)
-        || codeOwnersPluginConfiguration.getMaxPathsInChangeMessages(projectName) <= 0) {
+    if (codeOwnersPluginConfiguration
+            .getProjectConfig(projectName)
+            .isDisabled(event.getChange().branch)
+        || codeOwnersPluginConfiguration.getProjectConfig(projectName).getMaxPathsInChangeMessages()
+            <= 0) {
       return;
     }
 
@@ -176,7 +177,7 @@
               reviewerAccount.getName()));
 
       int maxPathsInChangeMessage =
-          codeOwnersPluginConfiguration.getMaxPathsInChangeMessages(projectName);
+          codeOwnersPluginConfiguration.getProjectConfig(projectName).getMaxPathsInChangeMessages();
       if (ownedPaths.size() <= maxPathsInChangeMessage) {
         appendPaths(message, ownedPaths.stream());
       } else {
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersUpdate.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersUpdate.java
index d2a7f01..5e41d70 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersUpdate.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersUpdate.java
@@ -96,7 +96,9 @@
   public Optional<CodeOwnerConfig> upsertCodeOwnerConfig(
       CodeOwnerConfig.Key codeOwnerConfigKey, CodeOwnerConfigUpdate codeOwnerConfigUpdate) {
     CodeOwnerBackend codeOwnerBackend =
-        codeOwnersPluginConfiguration.getBackend(codeOwnerConfigKey.branchNameKey());
+        codeOwnersPluginConfiguration
+            .getProjectConfig(codeOwnerConfigKey.project())
+            .getBackend(codeOwnerConfigKey.branchNameKey().branch());
     return codeOwnerBackend.upsertCodeOwnerConfig(
         codeOwnerConfigKey, codeOwnerConfigUpdate, currentUser.orElse(null));
   }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java
index 1cf59ce..5cbde72 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java
@@ -69,7 +69,9 @@
       PatchSet patchSet,
       Map<String, Short> oldApprovals,
       Map<String, Short> approvals) {
-    if (codeOwnersPluginConfiguration.isDisabled(changeNotes.getChange().getDest())) {
+    if (codeOwnersPluginConfiguration
+        .getProjectConfig(changeNotes.getProjectName())
+        .isDisabled(changeNotes.getChange().getDest().branch())) {
       return Optional.empty();
     }
 
@@ -79,7 +81,9 @@
     }
 
     RequiredApproval requiredApproval =
-        codeOwnersPluginConfiguration.getRequiredApproval(changeNotes.getProjectName());
+        codeOwnersPluginConfiguration
+            .getProjectConfig(changeNotes.getProjectName())
+            .getRequiredApproval();
 
     if (oldApprovals.get(requiredApproval.labelType().getName()) == null) {
       // If oldApprovals doesn't contain the label or if the labels value in it is null, the label
@@ -100,7 +104,9 @@
       Map<String, Short> approvals,
       RequiredApproval requiredApproval) {
     int maxPathsInChangeMessage =
-        codeOwnersPluginConfiguration.getMaxPathsInChangeMessages(changeNotes.getProjectName());
+        codeOwnersPluginConfiguration
+            .getProjectConfig(changeNotes.getProjectName())
+            .getMaxPathsInChangeMessages();
     if (maxPathsInChangeMessage <= 0) {
       return Optional.empty();
     }
@@ -137,7 +143,9 @@
     }
 
     boolean hasImplicitApprovalByUser =
-        codeOwnersPluginConfiguration.areImplicitApprovalsEnabled(changeNotes.getProjectName())
+        codeOwnersPluginConfiguration
+                .getProjectConfig(changeNotes.getProjectName())
+                .areImplicitApprovalsEnabled()
             && patchSet.uploader().equals(user.getAccountId());
 
     boolean noLongerExplicitlyApproved = false;
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerOverride.java b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerOverride.java
index 8a8c702..bf15ef2 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerOverride.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerOverride.java
@@ -57,7 +57,9 @@
       PatchSet patchSet,
       Map<String, Short> oldApprovals,
       Map<String, Short> approvals) {
-    if (codeOwnersPluginConfiguration.isDisabled(changeNotes.getChange().getDest())) {
+    if (codeOwnersPluginConfiguration
+        .getProjectConfig(changeNotes.getProjectName())
+        .isDisabled(changeNotes.getChange().getDest().branch())) {
       return Optional.empty();
     }
 
@@ -67,7 +69,8 @@
     }
 
     ImmutableList<RequiredApproval> overrideApprovals =
-        codeOwnersPluginConfiguration.getOverrideApproval(changeNotes.getProjectName()).stream()
+        codeOwnersPluginConfiguration.getProjectConfig(changeNotes.getProjectName())
+            .getOverrideApproval().stream()
             .sorted(comparing(RequiredApproval::toString))
             .collect(toImmutableList());
 
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
index c382801..91b0f6f 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
@@ -127,7 +127,9 @@
      */
     private PathExpressionMatcher getMatcher(CodeOwnerConfig.Key codeOwnerConfigKey) {
       CodeOwnerBackend codeOwnerBackend =
-          codeOwnersPluginConfiguration.getBackend(codeOwnerConfigKey.branchNameKey());
+          codeOwnersPluginConfiguration
+              .getProjectConfig(codeOwnerConfigKey.project())
+              .getBackend(codeOwnerConfigKey.branchNameKey().branch());
       return codeOwnerBackend
           .getPathExpressionMatcher()
           .orElse((pathExpression, relativePath) -> false);
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportFormatter.java b/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportFormatter.java
index 0029d9e..52958d9 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportFormatter.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportFormatter.java
@@ -61,7 +61,9 @@
    */
   private CodeOwnerBackend getBackend(CodeOwnerConfig.Key codeOwnerConfigKey) {
     if (projectCache.get(codeOwnerConfigKey.project()).isPresent()) {
-      return codeOwnersPluginConfiguration.getBackend(codeOwnerConfigKey.branchNameKey());
+      return codeOwnersPluginConfiguration
+          .getProjectConfig(codeOwnerConfigKey.project())
+          .getBackend(codeOwnerConfigKey.branchNameKey().branch());
     }
     return backendConfig.getDefaultBackend();
   }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigSnapshot.java b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigSnapshot.java
new file mode 100644
index 0000000..666b2c1
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigSnapshot.java
@@ -0,0 +1,416 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.backend.config;
+
+import static com.google.gerrit.server.project.ProjectCache.illegalState;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
+import com.google.gerrit.plugins.codeowners.backend.EnableImplicitApprovals;
+import com.google.gerrit.plugins.codeowners.backend.FallbackCodeOwners;
+import com.google.gerrit.plugins.codeowners.common.CodeOwnerConfigValidationPolicy;
+import com.google.gerrit.plugins.codeowners.common.MergeCommitStrategy;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import org.eclipse.jgit.lib.Config;
+
+/** Snapshot of the code-owners plugin configuration for one project. */
+public class CodeOwnersPluginConfigSnapshot {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  public interface Factory {
+    CodeOwnersPluginConfigSnapshot create(Project.NameKey projectName);
+  }
+
+  private final String pluginName;
+  private final PluginConfigFactory pluginConfigFactory;
+  private final ProjectCache projectCache;
+  private final BackendConfig backendConfig;
+  private final GeneralConfig generalConfig;
+  private final OverrideApprovalConfig overrideApprovalConfig;
+  private final RequiredApprovalConfig requiredApprovalConfig;
+  private final StatusConfig statusConfig;
+  private final Project.NameKey projectName;
+  private final Config pluginConfig;
+
+  @Inject
+  CodeOwnersPluginConfigSnapshot(
+      @PluginName String pluginName,
+      PluginConfigFactory pluginConfigFactory,
+      ProjectCache projectCache,
+      BackendConfig backendConfig,
+      GeneralConfig generalConfig,
+      OverrideApprovalConfig overrideApprovalConfig,
+      RequiredApprovalConfig requiredApprovalConfig,
+      StatusConfig statusConfig,
+      @Assisted Project.NameKey projectName) {
+    this.pluginName = pluginName;
+    this.pluginConfigFactory = pluginConfigFactory;
+    this.projectCache = projectCache;
+    this.backendConfig = backendConfig;
+    this.generalConfig = generalConfig;
+    this.overrideApprovalConfig = overrideApprovalConfig;
+    this.requiredApprovalConfig = requiredApprovalConfig;
+    this.statusConfig = statusConfig;
+    this.projectName = projectName;
+    this.pluginConfig = loadPluginConfig();
+  }
+
+  /** Gets the file extension of code owner config files, if any configured. */
+  public Optional<String> getFileExtension() {
+    return generalConfig.getFileExtension(pluginConfig);
+  }
+
+  /** Checks whether code owner configs are read-only. */
+  public boolean areCodeOwnerConfigsReadOnly() {
+    return generalConfig.getReadOnly(projectName, pluginConfig);
+  }
+
+  /**
+   * Checks whether pure revert changes are exempted from needing code owner approvals for submit.
+   */
+  public boolean arePureRevertsExempted() {
+    return generalConfig.getExemptPureReverts(projectName, pluginConfig);
+  }
+
+  /**
+   * Checks whether newly added non-resolvable code owners should be rejected on commit received and
+   * submit.
+   */
+  public boolean rejectNonResolvableCodeOwners() {
+    return generalConfig.getRejectNonResolvableCodeOwners(projectName, pluginConfig);
+  }
+
+  /**
+   * Checks whether newly added non-resolvable imports should be rejected on commit received and
+   * submit.
+   */
+  public boolean rejectNonResolvableImports() {
+    return generalConfig.getRejectNonResolvableImports(projectName, pluginConfig);
+  }
+
+  /** Whether code owner configs should be validated when a commit is received. */
+  public CodeOwnerConfigValidationPolicy getCodeOwnerConfigValidationPolicyForCommitReceived() {
+    return generalConfig.getCodeOwnerConfigValidationPolicyForCommitReceived(
+        projectName, pluginConfig);
+  }
+
+  /** Whether code owner configs should be validated when a change is submitted. */
+  public CodeOwnerConfigValidationPolicy getCodeOwnerConfigValidationPolicyForSubmit() {
+    return generalConfig.getCodeOwnerConfigValidationPolicyForSubmit(projectName, pluginConfig);
+  }
+
+  /** Gets the merge commit strategy. */
+  public MergeCommitStrategy getMergeCommitStrategy() {
+    return generalConfig.getMergeCommitStrategy(projectName, pluginConfig);
+  }
+
+  /** Gets the fallback code owners. */
+  public FallbackCodeOwners getFallbackCodeOwners() {
+    return generalConfig.getFallbackCodeOwners(projectName, pluginConfig);
+  }
+
+  /** Gets the max paths in change messages. */
+  public int getMaxPathsInChangeMessages() {
+    return generalConfig.getMaxPathsInChangeMessages(projectName, pluginConfig);
+  }
+
+  /** Gets the global code owners. */
+  public ImmutableSet<CodeOwnerReference> getGlobalCodeOwners() {
+    return generalConfig.getGlobalCodeOwners(pluginConfig);
+  }
+
+  /** Gets the override info URL that is configured. */
+  public Optional<String> getOverrideInfoUrl() {
+    return generalConfig.getOverrideInfoUrl(pluginConfig);
+  }
+
+  /**
+   * Whether the code owners functionality is disabled for the given branch.
+   *
+   * <p>Callers must ensure that the project of the specified branch exists. If the project doesn't
+   * exist the call fails with {@link IllegalStateException}.
+   *
+   * <p>The configuration is evaluated in the following order:
+   *
+   * <ul>
+   *   <li>disabled configuration for the branch (with inheritance)
+   *   <li>disabled configuration for the project (with inheritance)
+   *   <li>hard-coded default (not disabled)
+   * </ul>
+   *
+   * <p>The first disabled configuration that exists counts and the evaluation is stopped.
+   *
+   * @param branchName the branch for which it should be checked whether the code owners
+   *     functionality is disabled
+   * @return {@code true} if the code owners functionality is disabled for the given branch,
+   *     otherwise {@code false}
+   */
+  public boolean isDisabled(String branchName) {
+    requireNonNull(branchName, "branchName");
+
+    boolean isDisabled =
+        statusConfig.isDisabledForBranch(
+            pluginConfig, BranchNameKey.create(projectName, branchName));
+    if (isDisabled) {
+      return true;
+    }
+
+    return isDisabled();
+  }
+
+  /**
+   * Whether the code owners functionality is disabled for the given project.
+   *
+   * <p>The configuration is evaluated in the following order:
+   *
+   * <ul>
+   *   <li>disabled configuration for the project (with inheritance)
+   *   <li>hard-coded default (not disabled)
+   * </ul>
+   *
+   * <p>The first disabled configuration that exists counts and the evaluation is stopped.
+   *
+   * @return {@code true} if the code owners functionality is disabled, otherwise {@code false}
+   */
+  public boolean isDisabled() {
+    return statusConfig.isDisabledForProject(pluginConfig, projectName);
+  }
+
+  /**
+   * Returns the configured {@link CodeOwnerBackend} for the given branch.
+   *
+   * <p>The code owner backend configuration is evaluated in the following order:
+   *
+   * <ul>
+   *   <li>backend configuration for branch (with inheritance, first by full branch name, then by
+   *       short branch name)
+   *   <li>backend configuration for project (with inheritance)
+   *   <li>default backend (first globally configured backend, then hard-coded default backend)
+   * </ul>
+   *
+   * <p>The first code owner backend configuration that exists counts and the evaluation is stopped.
+   *
+   * @param branchName the branch for which the configured code owner backend should be returned
+   * @return the {@link CodeOwnerBackend} that should be used for the branch
+   */
+  public CodeOwnerBackend getBackend(String branchName) {
+    // check if a branch specific backend is configured
+    Optional<CodeOwnerBackend> codeOwnerBackend =
+        backendConfig.getBackendForBranch(
+            pluginConfig, BranchNameKey.create(projectName, branchName));
+    if (codeOwnerBackend.isPresent()) {
+      return codeOwnerBackend.get();
+    }
+
+    return getBackend();
+  }
+
+  /**
+   * Returns the configured {@link CodeOwnerBackend}.
+   *
+   * <p>The code owner backend configuration is evaluated in the following order:
+   *
+   * <ul>
+   *   <li>backend configuration for project (with inheritance)
+   *   <li>default backend (first globally configured backend, then hard-coded default backend)
+   * </ul>
+   *
+   * <p>The first code owner backend configuration that exists counts and the evaluation is stopped.
+   *
+   * @return the {@link CodeOwnerBackend} that should be used
+   */
+  public CodeOwnerBackend getBackend() {
+    // check if a project specific backend is configured
+    Optional<CodeOwnerBackend> codeOwnerBackend =
+        backendConfig.getBackendForProject(pluginConfig, projectName);
+    if (codeOwnerBackend.isPresent()) {
+      return codeOwnerBackend.get();
+    }
+
+    // fall back to the default backend
+    return backendConfig.getDefaultBackend();
+  }
+
+  /** Checks whether an implicit code owner approval from the last uploader is assumed. */
+  public boolean areImplicitApprovalsEnabled() {
+    EnableImplicitApprovals enableImplicitApprovals =
+        generalConfig.getEnableImplicitApprovals(projectName, pluginConfig);
+    switch (enableImplicitApprovals) {
+      case FALSE:
+        logger.atFine().log("implicit approvals on project %s are disabled", projectName);
+        return false;
+      case TRUE:
+        LabelType requiredLabel = getRequiredApproval().labelType();
+        if (requiredLabel.isIgnoreSelfApproval()) {
+          logger.atFine().log(
+              "ignoring implicit approval configuration on project %s since the label of the required"
+                  + " approval (%s) is configured to ignore self approvals",
+              projectName, requiredLabel);
+          return false;
+        }
+        return true;
+      case FORCED:
+        logger.atFine().log("implicit approvals on project %s are enforced", projectName);
+        return true;
+    }
+    throw new IllegalStateException(
+        String.format(
+            "unknown value %s for enableImplicitApprovals configuration in project %s",
+            enableImplicitApprovals, projectName));
+  }
+
+  /**
+   * Returns the approval that is required from code owners to approve the files in a change.
+   *
+   * <p>Defines which approval counts as code owner approval.
+   *
+   * <p>The code owner required approval configuration is evaluated in the following order:
+   *
+   * <ul>
+   *   <li>required approval configuration for project (with inheritance)
+   *   <li>globally configured required approval
+   *   <li>hard-coded default required approval
+   * </ul>
+   *
+   * <p>The first required code owner approval configuration that exists counts and the evaluation
+   * is stopped.
+   *
+   * <p>If the code owner configuration contains multiple required approvals values, the last value
+   * is used.
+   *
+   * @return the required code owner approval that should be used
+   */
+  public RequiredApproval getRequiredApproval() {
+    ImmutableList<RequiredApproval> configuredRequiredApprovalConfig =
+        getConfiguredRequiredApproval(requiredApprovalConfig);
+    if (!configuredRequiredApprovalConfig.isEmpty()) {
+      // There can be only one required approval. If multiple ones are configured just use the last
+      // one, this is also what Config#getString(String, String, String) does.
+      return Iterables.getLast(configuredRequiredApprovalConfig);
+    }
+
+    // fall back to hard-coded default required approval
+    ProjectState projectState =
+        projectCache.get(projectName).orElseThrow(illegalState(projectName));
+    return requiredApprovalConfig.createDefault(projectState);
+  }
+
+  /**
+   * Returns the approvals that are required to override the code owners submit check for a change.
+   *
+   * <p>If multiple approvals are returned, any of them is sufficient to override the code owners
+   * submit check.
+   *
+   * <p>The override approval configuration is evaluated in the following order:
+   *
+   * <ul>
+   *   <li>override approval configuration for project (with inheritance)
+   *   <li>globally configured override approval
+   * </ul>
+   *
+   * <p>The first override approval configuration that exists counts and the evaluation is stopped.
+   *
+   * @return the override approvals that should be used, an empty set if no override approval is
+   *     configured, in this case the override functionality is disabled
+   */
+  public ImmutableSet<RequiredApproval> getOverrideApproval() {
+    try {
+      return filterOutDuplicateRequiredApprovals(
+          getConfiguredRequiredApproval(overrideApprovalConfig));
+    } catch (InvalidPluginConfigurationException e) {
+      logger.atWarning().withCause(e).log(
+          "Ignoring invalid override approval configuration for project %s."
+              + " Overrides are disabled.",
+          projectName.get());
+    }
+
+    return ImmutableSet.of();
+  }
+
+  /**
+   * Filters out duplicate required approvals from the input list.
+   *
+   * <p>The following entries are considered as duplicate:
+   *
+   * <ul>
+   *   <li>exact identical required approvals (e.g. "Code-Review+2" and "Code-Review+2")
+   *   <li>required approvals with the same label name and a higher value (e.g. "Code-Review+2" is
+   *       not needed if "Code-Review+1" is already contained, since "Code-Review+1" covers all
+   *       "Code-Review" approvals >= 1)
+   * </ul>
+   */
+  private ImmutableSet<RequiredApproval> filterOutDuplicateRequiredApprovals(
+      ImmutableList<RequiredApproval> requiredApprovals) {
+    Map<String, RequiredApproval> requiredApprovalsByLabel = new HashMap<>();
+    for (RequiredApproval requiredApproval : requiredApprovals) {
+      String labelName = requiredApproval.labelType().getName();
+      RequiredApproval otherRequiredApproval = requiredApprovalsByLabel.get(labelName);
+      if (otherRequiredApproval != null
+          && otherRequiredApproval.value() <= requiredApproval.value()) {
+        continue;
+      }
+      requiredApprovalsByLabel.put(labelName, requiredApproval);
+    }
+    return ImmutableSet.copyOf(requiredApprovalsByLabel.values());
+  }
+
+  /**
+   * Gets the required approvals that are configured.
+   *
+   * @param requiredApprovalConfig the config from which the required approvals should be read
+   * @return the required approvals that is configured, an empty list if no required approvals are
+   *     configured
+   */
+  private ImmutableList<RequiredApproval> getConfiguredRequiredApproval(
+      AbstractRequiredApprovalConfig requiredApprovalConfig) {
+    ProjectState projectState =
+        projectCache.get(projectName).orElseThrow(illegalState(projectName));
+    return requiredApprovalConfig.get(projectState, pluginConfig);
+  }
+
+  /**
+   * Reads and returns the config from the {@code code-owners.config} file in {@code
+   * refs/meta/config} branch.
+   *
+   * @return the code owners configurations
+   */
+  private Config loadPluginConfig() {
+    try {
+      return pluginConfigFactory.getProjectPluginConfigWithInheritance(projectName, pluginName);
+    } catch (NoSuchProjectException e) {
+      throw new IllegalStateException(
+          String.format(
+              "cannot get %s plugin config for non-existing project %s", pluginName, projectName),
+          e);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfiguration.java b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfiguration.java
index a9b5ea6..f833542 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfiguration.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfiguration.java
@@ -14,35 +14,17 @@
 
 package com.google.gerrit.plugins.codeowners.backend.config;
 
-import static com.google.gerrit.server.project.ProjectCache.illegalState;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.entities.BranchNameKey;
-import com.google.gerrit.entities.LabelType;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
-import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
-import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
-import com.google.gerrit.plugins.codeowners.backend.EnableImplicitApprovals;
-import com.google.gerrit.plugins.codeowners.backend.FallbackCodeOwners;
-import com.google.gerrit.plugins.codeowners.common.CodeOwnerConfigValidationPolicy;
-import com.google.gerrit.plugins.codeowners.common.MergeCommitStrategy;
 import com.google.gerrit.server.config.PluginConfigFactory;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-import org.eclipse.jgit.lib.Config;
 
 /**
  * The configuration of the code-owners plugin.
@@ -65,213 +47,32 @@
   @VisibleForTesting
   static final String KEY_ENABLE_EXPERIMENTAL_REST_ENDPOINTS = "enableExperimentalRestEndpoints";
 
+  private final CodeOwnersPluginConfigSnapshot.Factory codeOwnersPluginConfigSnapshotFactory;
   private final String pluginName;
   private final PluginConfigFactory pluginConfigFactory;
-  private final ProjectCache projectCache;
   private final GeneralConfig generalConfig;
-  private final StatusConfig statusConfig;
-  private final BackendConfig backendConfig;
-  private final RequiredApprovalConfig requiredApprovalConfig;
-  private final OverrideApprovalConfig overrideApprovalConfig;
 
   @Inject
   CodeOwnersPluginConfiguration(
+      CodeOwnersPluginConfigSnapshot.Factory codeOwnersPluginConfigSnapshotFactory,
       @PluginName String pluginName,
       PluginConfigFactory pluginConfigFactory,
-      ProjectCache projectCache,
-      GeneralConfig generalConfig,
-      StatusConfig statusConfig,
-      BackendConfig backendConfig,
-      RequiredApprovalConfig requiredApprovalConfig,
-      OverrideApprovalConfig overrideApprovalConfig) {
+      GeneralConfig generalConfig) {
+    this.codeOwnersPluginConfigSnapshotFactory = codeOwnersPluginConfigSnapshotFactory;
     this.pluginName = pluginName;
     this.pluginConfigFactory = pluginConfigFactory;
-    this.projectCache = projectCache;
     this.generalConfig = generalConfig;
-    this.statusConfig = statusConfig;
-    this.backendConfig = backendConfig;
-    this.requiredApprovalConfig = requiredApprovalConfig;
-    this.overrideApprovalConfig = overrideApprovalConfig;
   }
 
   /**
-   * Gets the file extension that is configured for the given project.
+   * Returns the code-owner plugin configuration for the given projects.
    *
-   * @param project the project for which the configured file extension should be returned
-   * @return the file extension that is configured for the given project
+   * <p>Callers must ensure that the project of the specified branch exists. If the project doesn't
+   * exist the call fails with {@link IllegalStateException}.
    */
-  public Optional<String> getFileExtension(Project.NameKey project) {
-    requireNonNull(project, "project");
-    return generalConfig.getFileExtension(getPluginConfig(project));
-  }
-
-  /**
-   * Checks whether code owner configs in the given project are read-only.
-   *
-   * @param project the project for which it should be checked whether code owner configs are
-   *     read-only
-   * @return whether code owner configs in the given project are read-only
-   */
-  public boolean areCodeOwnerConfigsReadOnly(Project.NameKey project) {
-    requireNonNull(project, "project");
-    return generalConfig.getReadOnly(project, getPluginConfig(project));
-  }
-
-  /**
-   * Checks whether pure revert changes are exempted from needing code owner approvals for submit.
-   *
-   * @param project the project for which it should be checked whether pure revert changes are
-   *     exempted from needing code owner approvals for submit
-   * @return whether pure revert changes are exempted from needing code owner approvals for submit
-   */
-  public boolean arePureRevertsExempted(Project.NameKey project) {
-    requireNonNull(project, "project");
-    return generalConfig.getExemptPureReverts(project, getPluginConfig(project));
-  }
-
-  /**
-   * Checks whether newly added non-resolvable code owners should be rejected on commit received and
-   * submit.
-   *
-   * @param project the project for which it should be checked whether non-resolvable code owners
-   *     should be rejected
-   * @return whether newly added non-resolvable code owners should be rejected on commit received
-   *     and submit
-   */
-  public boolean rejectNonResolvableCodeOwners(Project.NameKey project) {
-    requireNonNull(project, "project");
-    return generalConfig.getRejectNonResolvableCodeOwners(project, getPluginConfig(project));
-  }
-
-  /**
-   * Checks whether newly added non-resolvable imports should be rejected on commit received and
-   * submit.
-   *
-   * @param project the project for which it should be checked whether non-resolvable imports should
-   *     be rejected
-   * @return whether newly added non-resolvable imports should be rejected on commit received and
-   *     submit
-   */
-  public boolean rejectNonResolvableImports(Project.NameKey project) {
-    requireNonNull(project, "project");
-    return generalConfig.getRejectNonResolvableImports(project, getPluginConfig(project));
-  }
-
-  /**
-   * Whether code owner configs should be validated when a commit is received.
-   *
-   * @param project the project for it should be checked whether code owner configs should be
-   *     validated when a commit is received
-   * @return whether code owner configs should be validated when a commit is received
-   */
-  public CodeOwnerConfigValidationPolicy getCodeOwnerConfigValidationPolicyForCommitReceived(
-      Project.NameKey project) {
-    requireNonNull(project, "project");
-    return generalConfig.getCodeOwnerConfigValidationPolicyForCommitReceived(
-        project, getPluginConfig(project));
-  }
-
-  /**
-   * Whether code owner configs should be validated when a change is submitted.
-   *
-   * @param project the project for it should be checked whether code owner configs should be
-   *     validated when a change is submitted
-   * @return whether code owner configs should be validated when a change is submitted
-   */
-  public CodeOwnerConfigValidationPolicy getCodeOwnerConfigValidationPolicyForSubmit(
-      Project.NameKey project) {
-    requireNonNull(project, "project");
-    return generalConfig.getCodeOwnerConfigValidationPolicyForSubmit(
-        project, getPluginConfig(project));
-  }
-
-  /**
-   * Gets the merge commit strategy for the given project.
-   *
-   * @param project the project for which the merge commit strategy should be retrieved
-   * @return the merge commit strategy for the given project
-   */
-  public MergeCommitStrategy getMergeCommitStrategy(Project.NameKey project) {
-    requireNonNull(project, "project");
-    return generalConfig.getMergeCommitStrategy(project, getPluginConfig(project));
-  }
-
-  /**
-   * 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));
-  }
-
-  /**
-   * Gets the max paths in change messages for the given project.
-   *
-   * @param project the project for which the fallback code owners should be retrieved
-   * @return the fallback code owners for the given project
-   */
-  public int getMaxPathsInChangeMessages(Project.NameKey project) {
-    requireNonNull(project, "project");
-    return generalConfig.getMaxPathsInChangeMessages(project, getPluginConfig(project));
-  }
-
-  /**
-   * Checks whether an implicit code owner approval from the last uploader is assumed.
-   *
-   * @param project the project for it should be checked whether implicit approvals are enabled
-   * @return whether an implicit code owner approval from the last uploader is assumed
-   */
-  public boolean areImplicitApprovalsEnabled(Project.NameKey project) {
-    requireNonNull(project, "project");
-    EnableImplicitApprovals enableImplicitApprovals =
-        generalConfig.getEnableImplicitApprovals(project, getPluginConfig(project));
-    switch (enableImplicitApprovals) {
-      case FALSE:
-        logger.atFine().log("implicit approvals on project %s are disabled", project);
-        return false;
-      case TRUE:
-        LabelType requiredLabel = getRequiredApproval(project).labelType();
-        if (requiredLabel.isIgnoreSelfApproval()) {
-          logger.atFine().log(
-              "ignoring implicit approval configuration on project %s since the label of the required"
-                  + " approval (%s) is configured to ignore self approvals",
-              project, requiredLabel);
-          return false;
-        }
-        return true;
-      case FORCED:
-        logger.atFine().log("implicit approvals on project %s are enforced", project);
-        return true;
-    }
-    throw new IllegalStateException(
-        String.format(
-            "unknown value %s for enableImplicitApprovals configuration in project %s",
-            enableImplicitApprovals, project));
-  }
-
-  /**
-   * Gets the global code owners of the given project.
-   *
-   * @param project the project for which the global code owners should be returned
-   * @return the global code owners of the given project
-   */
-  public ImmutableSet<CodeOwnerReference> getGlobalCodeOwners(Project.NameKey project) {
-    requireNonNull(project, "project");
-    return generalConfig.getGlobalCodeOwners(getPluginConfig(project));
-  }
-
-  /**
-   * Gets the override info URL that is configured for the given project.
-   *
-   * @param project the project for which the configured override info URL should be returned
-   * @return the override info URL that is configured for the given project
-   */
-  public Optional<String> getOverrideInfoUrl(Project.NameKey project) {
-    requireNonNull(project, "project");
-    return generalConfig.getOverrideInfoUrl(getPluginConfig(project));
+  public CodeOwnersPluginConfigSnapshot getProjectConfig(Project.NameKey projectName) {
+    requireNonNull(projectName, "projectName");
+    return codeOwnersPluginConfigSnapshotFactory.create(projectName);
   }
 
   /**
@@ -286,252 +87,6 @@
   }
 
   /**
-   * Whether the code owners functionality is disabled for the given branch.
-   *
-   * <p>Callers must ensure that the project of the specified branch exists. If the project doesn't
-   * exist the call fails with {@link IllegalStateException}.
-   *
-   * <p>The configuration is evaluated in the following order:
-   *
-   * <ul>
-   *   <li>disabled configuration for the branch (with inheritance)
-   *   <li>disabled configuration for the project (with inheritance)
-   *   <li>hard-coded default (not disabled)
-   * </ul>
-   *
-   * <p>The first disabled configuration that exists counts and the evaluation is stopped.
-   *
-   * @param branchNameKey the branch and project for which it should be checked whether the code
-   *     owners functionality is disabled
-   * @return {@code true} if the code owners functionality is disabled for the given branch,
-   *     otherwise {@code false}
-   */
-  public boolean isDisabled(BranchNameKey branchNameKey) {
-    requireNonNull(branchNameKey, "branchNameKey");
-
-    Config pluginConfig = getPluginConfig(branchNameKey.project());
-
-    boolean isDisabled = statusConfig.isDisabledForBranch(pluginConfig, branchNameKey);
-    if (isDisabled) {
-      return true;
-    }
-
-    return isDisabled(branchNameKey.project());
-  }
-
-  /**
-   * Whether the code owners functionality is disabled for the given project.
-   *
-   * <p>Callers must ensure that the project of the specified branch exists. If the project doesn't
-   * exist the call fails with {@link IllegalStateException}.
-   *
-   * <p>The configuration is evaluated in the following order:
-   *
-   * <ul>
-   *   <li>disabled configuration for the project (with inheritance)
-   *   <li>hard-coded default (not disabled)
-   * </ul>
-   *
-   * <p>The first disabled configuration that exists counts and the evaluation is stopped.
-   *
-   * @param project the project for which it should be checked whether the code owners functionality
-   *     is disabled
-   * @return {@code true} if the code owners functionality is disabled for the given project,
-   *     otherwise {@code false}
-   */
-  public boolean isDisabled(Project.NameKey project) {
-    requireNonNull(project, "project");
-
-    Config pluginConfig = getPluginConfig(project);
-    return statusConfig.isDisabledForProject(pluginConfig, project);
-  }
-
-  /**
-   * Returns the configured {@link CodeOwnerBackend} for the given branch.
-   *
-   * <p>Callers must ensure that the project of the specified branch exists. If the project doesn't
-   * exist the call fails with {@link IllegalStateException}.
-   *
-   * <p>The code owner backend configuration is evaluated in the following order:
-   *
-   * <ul>
-   *   <li>backend configuration for branch (with inheritance, first by full branch name, then by
-   *       short branch name)
-   *   <li>backend configuration for project (with inheritance)
-   *   <li>default backend (first globally configured backend, then hard-coded default backend)
-   * </ul>
-   *
-   * <p>The first code owner backend configuration that exists counts and the evaluation is stopped.
-   *
-   * @param branchNameKey project and branch for which the configured code owner backend should be
-   *     returned
-   * @return the {@link CodeOwnerBackend} that should be used for the branch
-   */
-  public CodeOwnerBackend getBackend(BranchNameKey branchNameKey) {
-    Config pluginConfig = getPluginConfig(branchNameKey.project());
-
-    // check if a branch specific backend is configured
-    Optional<CodeOwnerBackend> codeOwnerBackend =
-        backendConfig.getBackendForBranch(pluginConfig, branchNameKey);
-    if (codeOwnerBackend.isPresent()) {
-      return codeOwnerBackend.get();
-    }
-
-    return getBackend(branchNameKey.project());
-  }
-
-  /**
-   * Returns the configured {@link CodeOwnerBackend} for the given project.
-   *
-   * <p>Callers must ensure that the project exists. If the project doesn't exist the call fails
-   * with {@link IllegalStateException}.
-   *
-   * <p>The code owner backend configuration is evaluated in the following order:
-   *
-   * <ul>
-   *   <li>backend configuration for project (with inheritance)
-   *   <li>default backend (first globally configured backend, then hard-coded default backend)
-   * </ul>
-   *
-   * <p>The first code owner backend configuration that exists counts and the evaluation is stopped.
-   *
-   * @param project project for which the configured code owner backend should be returned
-   * @return the {@link CodeOwnerBackend} that should be used for the project
-   */
-  public CodeOwnerBackend getBackend(Project.NameKey project) {
-    Config pluginConfig = getPluginConfig(project);
-
-    // check if a project specific backend is configured
-    Optional<CodeOwnerBackend> codeOwnerBackend =
-        backendConfig.getBackendForProject(pluginConfig, project);
-    if (codeOwnerBackend.isPresent()) {
-      return codeOwnerBackend.get();
-    }
-
-    // fall back to the default backend
-    return backendConfig.getDefaultBackend();
-  }
-
-  /**
-   * Returns the approval that is required from code owners to approve the files in a change of the
-   * given project.
-   *
-   * <p>Defines which approval counts as code owner approval.
-   *
-   * <p>Callers must ensure that the project of the specified branch exists. If the project doesn't
-   * exist the call fails with {@link IllegalStateException}.
-   *
-   * <p>The code owner required approval configuration is evaluated in the following order:
-   *
-   * <ul>
-   *   <li>required approval configuration for project (with inheritance)
-   *   <li>globally configured required approval
-   *   <li>hard-coded default required approval
-   * </ul>
-   *
-   * <p>The first required code owner approval configuration that exists counts and the evaluation
-   * is stopped.
-   *
-   * <p>If the code owner configuration contains multiple required approvals values, the last value
-   * is used.
-   *
-   * @param project project for which the required approval should be returned
-   * @return the required code owner approval that should be used for the given project
-   */
-  public RequiredApproval getRequiredApproval(Project.NameKey project) {
-    ImmutableList<RequiredApproval> configuredRequiredApprovalConfig =
-        getConfiguredRequiredApproval(requiredApprovalConfig, project);
-    if (!configuredRequiredApprovalConfig.isEmpty()) {
-      // There can be only one required approval. If multiple ones are configured just use the last
-      // one, this is also what Config#getString(String, String, String) does.
-      return Iterables.getLast(configuredRequiredApprovalConfig);
-    }
-
-    // fall back to hard-coded default required approval
-    ProjectState projectState = projectCache.get(project).orElseThrow(illegalState(project));
-    return requiredApprovalConfig.createDefault(projectState);
-  }
-
-  /**
-   * Returns the approvals that are required to override the code owners submit check for a change
-   * of the given project.
-   *
-   * <p>If multiple approvals are returned, any of them is sufficient to override the code owners
-   * submit check.
-   *
-   * <p>Callers must ensure that the project of the specified branch exists. If the project doesn't
-   * exist the call fails with {@link IllegalStateException}.
-   *
-   * <p>The override approval configuration is evaluated in the following order:
-   *
-   * <ul>
-   *   <li>override approval configuration for project (with inheritance)
-   *   <li>globally configured override approval
-   * </ul>
-   *
-   * <p>The first override approval configuration that exists counts and the evaluation is stopped.
-   *
-   * @param project project for which the override approval should be returned
-   * @return the override approvals that should be used for the given project, an empty set if no
-   *     override approval is configured, in this case the override functionality is disabled
-   */
-  public ImmutableSet<RequiredApproval> getOverrideApproval(Project.NameKey project) {
-    try {
-      return filterOutDuplicateRequiredApprovals(
-          getConfiguredRequiredApproval(overrideApprovalConfig, project));
-    } catch (InvalidPluginConfigurationException e) {
-      logger.atWarning().withCause(e).log(
-          "Ignoring invalid override approval configuration for project %s."
-              + " Overrides are disabled.",
-          project.get());
-    }
-
-    return ImmutableSet.of();
-  }
-
-  /**
-   * Filters out duplicate required approvals from the input list.
-   *
-   * <p>The following entries are considered as duplicate:
-   *
-   * <ul>
-   *   <li>exact identical required approvals (e.g. "Code-Review+2" and "Code-Review+2")
-   *   <li>required approvals with the same label name and a higher value (e.g. "Code-Review+2" is
-   *       not needed if "Code-Review+1" is already contained, since "Code-Review+1" covers all
-   *       "Code-Review" approvals >= 1)
-   * </ul>
-   */
-  private ImmutableSet<RequiredApproval> filterOutDuplicateRequiredApprovals(
-      ImmutableList<RequiredApproval> requiredApprovals) {
-    Map<String, RequiredApproval> requiredApprovalsByLabel = new HashMap<>();
-    for (RequiredApproval requiredApproval : requiredApprovals) {
-      String labelName = requiredApproval.labelType().getName();
-      RequiredApproval otherRequiredApproval = requiredApprovalsByLabel.get(labelName);
-      if (otherRequiredApproval != null
-          && otherRequiredApproval.value() <= requiredApproval.value()) {
-        continue;
-      }
-      requiredApprovalsByLabel.put(labelName, requiredApproval);
-    }
-    return ImmutableSet.copyOf(requiredApprovalsByLabel.values());
-  }
-
-  /**
-   * Gets the required approvals that are configured for the given project.
-   *
-   * @param requiredApprovalConfig the config from which the required approvals should be read
-   * @param project the project for which the configured required approvals should be returned
-   * @return the required approvals that is configured for the given project, an empty list if no
-   *     required approvals are configured
-   */
-  private ImmutableList<RequiredApproval> getConfiguredRequiredApproval(
-      AbstractRequiredApprovalConfig requiredApprovalConfig, Project.NameKey project) {
-    Config pluginConfig = getPluginConfig(project);
-    ProjectState projectState = projectCache.get(project).orElseThrow(illegalState(project));
-    return requiredApprovalConfig.get(projectState, pluginConfig);
-  }
-
-  /**
    * Checks whether experimental REST endpoints are enabled.
    *
    * @throws MethodNotAllowedException thrown if experimental REST endpoints are disabled
@@ -559,22 +114,4 @@
       return false;
     }
   }
-
-  /**
-   * Reads and returns the config from the {@code code-owners.config} file in {@code
-   * refs/meta/config} branch of the given project.
-   *
-   * @param project the project for which the code owners configurations should be returned
-   * @return the code owners configurations for the given project
-   */
-  private Config getPluginConfig(Project.NameKey project) {
-    try {
-      return pluginConfigFactory.getProjectPluginConfigWithInheritance(project, pluginName);
-    } catch (NoSuchProjectException e) {
-      throw new IllegalStateException(
-          String.format(
-              "cannot get %s plugin config for non-existing project %s", pluginName, project),
-          e);
-    }
-  }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java b/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
index d406be1..6fb8c15 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
@@ -237,7 +237,8 @@
     CodeOwnerResolverResult globalCodeOwners =
         codeOwnerResolver
             .get()
-            .resolve(codeOwnersPluginConfiguration.getGlobalCodeOwners(projectName));
+            .resolve(
+                codeOwnersPluginConfiguration.getProjectConfig(projectName).getGlobalCodeOwners());
     logger.atFine().log("including global code owners = %s", globalCodeOwners);
     return globalCodeOwners;
   }
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
index d326da6..9ab1ed1 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
@@ -239,7 +239,8 @@
   }
 
   private boolean isGlobalCodeOwner(Project.NameKey projectName) {
-    return codeOwnersPluginConfiguration.getGlobalCodeOwners(projectName).stream()
+    return codeOwnersPluginConfiguration.getProjectConfig(projectName).getGlobalCodeOwners()
+        .stream()
         .filter(cor -> cor.email().equals(email))
         .findAny()
         .isPresent();
@@ -247,7 +248,7 @@
 
   private boolean isFallbackCodeOwner(Project.NameKey projectName) {
     FallbackCodeOwners fallbackCodeOwners =
-        codeOwnersPluginConfiguration.getFallbackCodeOwners(projectName);
+        codeOwnersPluginConfiguration.getProjectConfig(projectName).getFallbackCodeOwners();
     switch (fallbackCodeOwners) {
       case NONE:
         return false;
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java
index ae55e82..306442a 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java
@@ -135,7 +135,9 @@
           .filter(
               branchNameKey ->
                   validateDisabledBranches(input)
-                      || !codeOwnersPluginConfiguration.isDisabled(branchNameKey))
+                      || !codeOwnersPluginConfiguration
+                          .getProjectConfig(branchNameKey.project())
+                          .isDisabled(branchNameKey.branch()))
           .forEach(
               branchNameKey ->
                   resultsByBranchBuilder.put(
@@ -159,7 +161,10 @@
       BranchNameKey branchNameKey,
       @Nullable ConsistencyProblemInfo.Status verbosity) {
     ListMultimap<String, ConsistencyProblemInfo> problemsByPath = LinkedListMultimap.create();
-    CodeOwnerBackend codeOwnerBackend = codeOwnersPluginConfiguration.getBackend(branchNameKey);
+    CodeOwnerBackend codeOwnerBackend =
+        codeOwnersPluginConfiguration
+            .getProjectConfig(branchNameKey.project())
+            .getBackend(branchNameKey.branch());
     codeOwnerConfigScannerFactory
         .create()
         // Do not check the default code owner config file in refs/meta/config, as this config is
@@ -260,7 +265,7 @@
         }
 
         if ((input.validateDisabledBranches == null || !input.validateDisabledBranches)
-            && codeOwnersPluginConfiguration.isDisabled(branchNameKey)) {
+            && codeOwnersPluginConfiguration.getProjectConfig(projectName).isDisabled(branchName)) {
           throw new BadRequestException(
               String.format(
                   "code owners functionality for branch %s is disabled,"
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFilesInRevision.java b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFilesInRevision.java
index d960595..db939c4 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFilesInRevision.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFilesInRevision.java
@@ -84,7 +84,9 @@
         input.path);
 
     CodeOwnerBackend codeOwnerBackend =
-        codeOwnersPluginConfiguration.getBackend(revisionResource.getChange().getDest());
+        codeOwnersPluginConfiguration
+            .getProjectConfig(revisionResource.getProject())
+            .getBackend(revisionResource.getChange().getDest().branch());
 
     IdentifiedUser uploader = genericUserFactory.create(revisionResource.getPatchSet().uploader());
     logger.atFine().log("uploader = %s", uploader.getLoggableName());
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJson.java b/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJson.java
index 2f649f9..2f5856a 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJson.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJson.java
@@ -65,7 +65,7 @@
     CodeOwnerProjectConfigInfo info = new CodeOwnerProjectConfigInfo();
     info.status = formatStatusInfo(projectResource);
 
-    if (codeOwnersPluginConfiguration.isDisabled(projectResource.getNameKey())) {
+    if (codeOwnersPluginConfiguration.getProjectConfig(projectResource.getNameKey()).isDisabled()) {
       return info;
     }
 
@@ -80,7 +80,10 @@
   CodeOwnerBranchConfigInfo format(BranchResource branchResource) {
     CodeOwnerBranchConfigInfo info = new CodeOwnerBranchConfigInfo();
 
-    boolean disabled = codeOwnersPluginConfiguration.isDisabled(branchResource.getBranchKey());
+    boolean disabled =
+        codeOwnersPluginConfiguration
+            .getProjectConfig(branchResource.getNameKey())
+            .isDisabled(branchResource.getBranchKey().branch());
     info.disabled = disabled ? disabled : null;
 
     if (disabled) {
@@ -90,7 +93,10 @@
     info.general = formatGeneralInfo(branchResource.getNameKey());
     info.backendId =
         CodeOwnerBackendId.getBackendId(
-            codeOwnersPluginConfiguration.getBackend(branchResource.getBranchKey()).getClass());
+            codeOwnersPluginConfiguration
+                .getProjectConfig(branchResource.getNameKey())
+                .getBackend(branchResource.getBranchKey().branch())
+                .getClass());
     info.requiredApproval = formatRequiredApprovalInfo(branchResource.getNameKey());
     info.overrideApproval = formatOverrideApprovalInfo(branchResource.getNameKey());
 
@@ -100,15 +106,20 @@
   private GeneralInfo formatGeneralInfo(Project.NameKey projectName) {
     GeneralInfo generalInfo = new GeneralInfo();
     generalInfo.fileExtension =
-        codeOwnersPluginConfiguration.getFileExtension(projectName).orElse(null);
+        codeOwnersPluginConfiguration.getProjectConfig(projectName).getFileExtension().orElse(null);
     generalInfo.mergeCommitStrategy =
-        codeOwnersPluginConfiguration.getMergeCommitStrategy(projectName);
+        codeOwnersPluginConfiguration.getProjectConfig(projectName).getMergeCommitStrategy();
     generalInfo.implicitApprovals =
-        codeOwnersPluginConfiguration.areImplicitApprovalsEnabled(projectName) ? true : null;
+        codeOwnersPluginConfiguration.getProjectConfig(projectName).areImplicitApprovalsEnabled()
+            ? true
+            : null;
     generalInfo.overrideInfoUrl =
-        codeOwnersPluginConfiguration.getOverrideInfoUrl(projectName).orElse(null);
+        codeOwnersPluginConfiguration
+            .getProjectConfig(projectName)
+            .getOverrideInfoUrl()
+            .orElse(null);
     generalInfo.fallbackCodeOwners =
-        codeOwnersPluginConfiguration.getFallbackCodeOwners(projectName);
+        codeOwnersPluginConfiguration.getProjectConfig(projectName).getFallbackCodeOwners();
     return generalInfo;
   }
 
@@ -117,7 +128,9 @@
       throws RestApiException, PermissionBackendException, IOException {
     CodeOwnersStatusInfo info = new CodeOwnersStatusInfo();
     info.disabled =
-        codeOwnersPluginConfiguration.isDisabled(projectResource.getNameKey()) ? true : null;
+        codeOwnersPluginConfiguration.getProjectConfig(projectResource.getNameKey()).isDisabled()
+            ? true
+            : null;
 
     if (info.disabled == null) {
       ImmutableList<BranchNameKey> disabledBranches = getDisabledBranches(projectResource);
@@ -135,7 +148,10 @@
     BackendInfo info = new BackendInfo();
     info.id =
         CodeOwnerBackendId.getBackendId(
-            codeOwnersPluginConfiguration.getBackend(projectResource.getNameKey()).getClass());
+            codeOwnersPluginConfiguration
+                .getProjectConfig(projectResource.getNameKey())
+                .getBackend()
+                .getClass());
 
     ImmutableMap<String, String> idsByBranch =
         getBackendIdsPerBranch(projectResource).entrySet().stream()
@@ -147,14 +163,15 @@
   }
 
   private RequiredApprovalInfo formatRequiredApprovalInfo(Project.NameKey projectName) {
-    return formatRequiredApproval(codeOwnersPluginConfiguration.getRequiredApproval(projectName));
+    return formatRequiredApproval(
+        codeOwnersPluginConfiguration.getProjectConfig(projectName).getRequiredApproval());
   }
 
   @VisibleForTesting
   @Nullable
   ImmutableList<RequiredApprovalInfo> formatOverrideApprovalInfo(Project.NameKey projectName) {
     ImmutableList<RequiredApprovalInfo> overrideApprovalInfos =
-        codeOwnersPluginConfiguration.getOverrideApproval(projectName).stream()
+        codeOwnersPluginConfiguration.getProjectConfig(projectName).getOverrideApproval().stream()
             .sorted(comparing(requiredApproval -> requiredApproval.toString()))
             .map(CodeOwnerProjectConfigJson::formatRequiredApproval)
             .collect(toImmutableList());
@@ -174,7 +191,11 @@
   private ImmutableList<BranchNameKey> getDisabledBranches(ProjectResource projectResource)
       throws RestApiException, PermissionBackendException, IOException {
     return branches(projectResource)
-        .filter(codeOwnersPluginConfiguration::isDisabled)
+        .filter(
+            branchNameKey ->
+                codeOwnersPluginConfiguration
+                    .getProjectConfig(branchNameKey.project())
+                    .isDisabled(branchNameKey.branch()))
         .collect(toImmutableList());
   }
 
@@ -187,7 +208,10 @@
                 Function.identity(),
                 branchNameKey ->
                     CodeOwnerBackendId.getBackendId(
-                        codeOwnersPluginConfiguration.getBackend(branchNameKey).getClass())));
+                        codeOwnersPluginConfiguration
+                            .getProjectConfig(branchNameKey.project())
+                            .getBackend(branchNameKey.branch())
+                            .getClass())));
   }
 
   private Stream<BranchNameKey> branches(ProjectResource projectResource)
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnerConfigFiles.java b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnerConfigFiles.java
index ef08584..54d305a 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnerConfigFiles.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnerConfigFiles.java
@@ -86,11 +86,13 @@
   }
 
   @Override
-  public Response<List<String>> apply(BranchResource resource) throws BadRequestException {
+  public Response<List<String>> apply(BranchResource branchResource) throws BadRequestException {
     validateOptions();
 
     CodeOwnerBackend codeOwnerBackend =
-        codeOwnersPluginConfiguration.getBackend(resource.getBranchKey());
+        codeOwnersPluginConfiguration
+            .getProjectConfig(branchResource.getNameKey())
+            .getBackend(branchResource.getBranchKey().branch());
     ImmutableList.Builder<Path> codeOwnerConfigs = ImmutableList.builder();
 
     if (email != null) {
@@ -106,7 +108,7 @@
         // files in refs/meta/config explicitly.
         .includeDefaultCodeOwnerConfig(false)
         .visit(
-            resource.getBranchKey(),
+            branchResource.getBranchKey(),
             codeOwnerConfig -> {
               Path codeOwnerConfigPath = codeOwnerBackend.getFilePath(codeOwnerConfig.key());
               if (email == null || containsEmail(codeOwnerConfig, codeOwnerConfigPath, email)) {
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/RenameEmail.java b/java/com/google/gerrit/plugins/codeowners/restapi/RenameEmail.java
index 6f597fd..e338543 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/RenameEmail.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/RenameEmail.java
@@ -99,7 +99,9 @@
     validateInput(input);
 
     CodeOwnerBackend codeOwnerBackend =
-        codeOwnersPluginConfiguration.getBackend(branchResource.getBranchKey());
+        codeOwnersPluginConfiguration
+            .getProjectConfig(branchResource.getNameKey())
+            .getBackend(branchResource.getBranchKey().branch());
 
     Account.Id accountOwningOldEmail = resolveEmail(input.oldEmail);
     Account.Id accountOwningNewEmail = resolveEmail(input.newEmail);
diff --git a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
index f230eb9..8af3da0 100644
--- a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
+++ b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
@@ -174,8 +174,9 @@
                 .username(receiveEvent.user.getLoggableName())
                 .build())) {
       CodeOwnerConfigValidationPolicy codeOwnerConfigValidationPolicy =
-          codeOwnersPluginConfiguration.getCodeOwnerConfigValidationPolicyForCommitReceived(
-              receiveEvent.getProjectNameKey());
+          codeOwnersPluginConfiguration
+              .getProjectConfig(receiveEvent.getProjectNameKey())
+              .getCodeOwnerConfigValidationPolicyForCommitReceived();
       logger.atFine().log("codeOwnerConfigValidationPolicy = %s", codeOwnerConfigValidationPolicy);
       Optional<ValidationResult> validationResult;
       if (!codeOwnerConfigValidationPolicy.runValidation()) {
@@ -243,8 +244,9 @@
                 .patchSetId(patchSetId.get())
                 .build())) {
       CodeOwnerConfigValidationPolicy codeOwnerConfigValidationPolicy =
-          codeOwnersPluginConfiguration.getCodeOwnerConfigValidationPolicyForSubmit(
-              branchNameKey.project());
+          codeOwnersPluginConfiguration
+              .getProjectConfig(branchNameKey.project())
+              .getCodeOwnerConfigValidationPolicyForSubmit();
       logger.atFine().log("codeOwnerConfigValidationPolicy = %s", codeOwnerConfigValidationPolicy);
       Optional<ValidationResult> validationResult;
       if (!codeOwnerConfigValidationPolicy.runValidation()) {
@@ -303,7 +305,9 @@
       RevWalk revWalk,
       RevCommit revCommit,
       IdentifiedUser user) {
-    if (codeOwnersPluginConfiguration.isDisabled(branchNameKey)) {
+    if (codeOwnersPluginConfiguration
+        .getProjectConfig(branchNameKey.project())
+        .isDisabled(branchNameKey.branch())) {
       return Optional.of(
           ValidationResult.create(
               pluginName,
@@ -311,7 +315,9 @@
               new CommitValidationMessage(
                   "code-owners functionality is disabled", ValidationMessage.Type.HINT)));
     }
-    if (codeOwnersPluginConfiguration.areCodeOwnerConfigsReadOnly(branchNameKey.project())) {
+    if (codeOwnersPluginConfiguration
+        .getProjectConfig(branchNameKey.project())
+        .areCodeOwnerConfigsReadOnly()) {
       return Optional.of(
           ValidationResult.create(
               pluginName,
@@ -322,7 +328,10 @@
     }
 
     try {
-      CodeOwnerBackend codeOwnerBackend = codeOwnersPluginConfiguration.getBackend(branchNameKey);
+      CodeOwnerBackend codeOwnerBackend =
+          codeOwnersPluginConfiguration
+              .getProjectConfig(branchNameKey.project())
+              .getBackend(branchNameKey.branch());
 
       // For merge commits, always do the comparison against the destination branch
       // (MergeCommitStrategy.ALL_CHANGED_FILES). Doing the comparison against the auto-merge
@@ -922,7 +931,9 @@
     }
 
     CodeOwnerBackend codeOwnerBackend =
-        codeOwnersPluginConfiguration.getBackend(keyOfImportedCodeOwnerConfig.branchNameKey());
+        codeOwnersPluginConfiguration
+            .getProjectConfig(keyOfImportedCodeOwnerConfig.project())
+            .getBackend(keyOfImportedCodeOwnerConfig.branchNameKey().branch());
     if (!codeOwnerBackend.isCodeOwnerConfigFile(
         keyOfImportedCodeOwnerConfig.project(), codeOwnerConfigReference.fileName())) {
       return nonResolvableImport(
@@ -983,11 +994,13 @@
     return keyOfImportingCodeOwnerConfig.project().equals(keyOfImportedCodeOwnerConfig.project())
         && keyOfImportingCodeOwnerConfig.ref().equals(keyOfImportedCodeOwnerConfig.ref())
         && codeOwnersPluginConfiguration
-            .getBackend(keyOfImportingCodeOwnerConfig.branchNameKey())
+            .getProjectConfig(keyOfImportingCodeOwnerConfig.project())
+            .getBackend(keyOfImportingCodeOwnerConfig.branchNameKey().branch())
             .getFilePath(keyOfImportingCodeOwnerConfig)
             .equals(
                 codeOwnersPluginConfiguration
-                    .getBackend(keyOfImportedCodeOwnerConfig.branchNameKey())
+                    .getProjectConfig(keyOfImportedCodeOwnerConfig.project())
+                    .getBackend(keyOfImportedCodeOwnerConfig.branchNameKey().branch())
                     .getFilePath(keyOfImportedCodeOwnerConfig));
   }
 
@@ -1046,7 +1059,7 @@
         importType,
         codeOwnerConfigFilePath,
         message,
-        codeOwnersPluginConfiguration.rejectNonResolvableImports(project)
+        codeOwnersPluginConfiguration.getProjectConfig(project).rejectNonResolvableImports()
             ? ValidationMessage.Type.ERROR
             : ValidationMessage.Type.WARNING);
   }
@@ -1069,7 +1082,7 @@
     return Optional.of(
         new CommitValidationMessage(
             message,
-            codeOwnersPluginConfiguration.rejectNonResolvableCodeOwners(project)
+            codeOwnersPluginConfiguration.getProjectConfig(project).rejectNonResolvableCodeOwners()
                 ? ValidationMessage.Type.ERROR
                 : ValidationMessage.Type.WARNING));
   }
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 be0f13b..90f950a 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersPluginConfigValidatorIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersPluginConfigValidatorIT.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.config.GerritConfig;
-import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.RebaseInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -101,7 +100,7 @@
 
     PushResult r = pushRefsMetaConfig();
     assertThat(r.getRemoteUpdate(RefNames.REFS_CONFIG).getStatus()).isEqualTo(Status.OK);
-    assertThat(codeOwnersPluginConfiguration.isDisabled(project)).isTrue();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).isDisabled()).isTrue();
   }
 
   @Test
@@ -118,7 +117,7 @@
 
     PushResult r = pushRefsMetaConfig();
     assertThat(r.getRemoteUpdate(RefNames.REFS_CONFIG).getStatus()).isEqualTo(Status.OK);
-    assertThat(codeOwnersPluginConfiguration.isDisabled(BranchNameKey.create(project, "master")))
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).isDisabled("master"))
         .isTrue();
   }
 
@@ -178,7 +177,7 @@
 
     PushResult r = pushRefsMetaConfig();
     assertThat(r.getRemoteUpdate(RefNames.REFS_CONFIG).getStatus()).isEqualTo(Status.OK);
-    assertThat(codeOwnersPluginConfiguration.getBackend(BranchNameKey.create(project, "master")))
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getBackend("master"))
         .isInstanceOf(ProtoBackend.class);
   }
 
@@ -196,7 +195,7 @@
 
     PushResult r = pushRefsMetaConfig();
     assertThat(r.getRemoteUpdate(RefNames.REFS_CONFIG).getStatus()).isEqualTo(Status.OK);
-    assertThat(codeOwnersPluginConfiguration.getBackend(BranchNameKey.create(project, "master")))
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getBackend("master"))
         .isInstanceOf(ProtoBackend.class);
   }
 
@@ -256,7 +255,8 @@
 
     PushResult r = pushRefsMetaConfig();
     assertThat(r.getRemoteUpdate(RefNames.REFS_CONFIG).getStatus()).isEqualTo(Status.OK);
-    RequiredApproval requiredApproval = codeOwnersPluginConfiguration.getRequiredApproval(project);
+    RequiredApproval requiredApproval =
+        codeOwnersPluginConfiguration.getProjectConfig(project).getRequiredApproval();
     assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
     assertThat(requiredApproval).hasValueThat().isEqualTo(2);
   }
@@ -327,7 +327,7 @@
     PushResult r = pushRefsMetaConfig();
     assertThat(r.getRemoteUpdate(RefNames.REFS_CONFIG).getStatus()).isEqualTo(Status.OK);
     ImmutableSet<RequiredApproval> overrideApproval =
-        codeOwnersPluginConfiguration.getOverrideApproval(project);
+        codeOwnersPluginConfiguration.getProjectConfig(project).getOverrideApproval();
     assertThat(overrideApproval).hasSize(1);
     assertThat(overrideApproval).element(0).hasLabelNameThat().isEqualTo("Code-Review");
     assertThat(overrideApproval).element(0).hasValueThat().isEqualTo(2);
@@ -424,7 +424,7 @@
     PushResult r = pushRefsMetaConfig();
     assertThat(r.getRemoteUpdate(RefNames.REFS_CONFIG).getStatus()).isEqualTo(Status.OK);
     ImmutableSet<RequiredApproval> overrideApproval =
-        codeOwnersPluginConfiguration.getOverrideApproval(project);
+        codeOwnersPluginConfiguration.getProjectConfig(project).getOverrideApproval();
     assertThat(overrideApproval).hasSize(1);
     assertThat(overrideApproval).element(0).hasLabelNameThat().isEqualTo("Owners-Override");
     assertThat(overrideApproval).element(0).hasValueThat().isEqualTo(1);
@@ -444,7 +444,7 @@
 
     PushResult r = pushRefsMetaConfig();
     assertThat(r.getRemoteUpdate(RefNames.REFS_CONFIG).getStatus()).isEqualTo(Status.OK);
-    assertThat(codeOwnersPluginConfiguration.getMergeCommitStrategy(project))
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getMergeCommitStrategy())
         .isEqualTo(MergeCommitStrategy.ALL_CHANGED_FILES);
   }
 
@@ -483,7 +483,7 @@
 
     PushResult r = pushRefsMetaConfig();
     assertThat(r.getRemoteUpdate(RefNames.REFS_CONFIG).getStatus()).isEqualTo(Status.OK);
-    assertThat(codeOwnersPluginConfiguration.getFallbackCodeOwners(project))
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getFallbackCodeOwners())
         .isEqualTo(FallbackCodeOwners.ALL_USERS);
   }
 
@@ -522,7 +522,9 @@
 
     PushResult r = pushRefsMetaConfig();
     assertThat(r.getRemoteUpdate(RefNames.REFS_CONFIG).getStatus()).isEqualTo(Status.OK);
-    assertThat(codeOwnersPluginConfiguration.getMaxPathsInChangeMessages(project)).isEqualTo(50);
+    assertThat(
+            codeOwnersPluginConfiguration.getProjectConfig(project).getMaxPathsInChangeMessages())
+        .isEqualTo(50);
   }
 
   @Test
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 c09a8ae..d8eefd3 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/PutCodeOwnerProjectConfigIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/PutCodeOwnerProjectConfigIT.java
@@ -85,29 +85,27 @@
 
   @Test
   public void disableAndReenableCodeOwnersFunctionality() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.isDisabled(project)).isFalse();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).isDisabled()).isFalse();
 
     CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
     input.disabled = true;
     CodeOwnerProjectConfigInfo updatedConfig =
         projectCodeOwnersApiFactory.project(project).updateConfig(input);
     assertThat(updatedConfig.status.disabled).isTrue();
-    assertThat(codeOwnersPluginConfiguration.isDisabled(project)).isTrue();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).isDisabled()).isTrue();
 
     input.disabled = false;
     updatedConfig = projectCodeOwnersApiFactory.project(project).updateConfig(input);
     assertThat(updatedConfig.status.disabled).isNull();
-    assertThat(codeOwnersPluginConfiguration.isDisabled(project)).isFalse();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).isDisabled()).isFalse();
   }
 
   @Test
   public void setDisabledBranches() throws Exception {
-    BranchNameKey masterBranch = BranchNameKey.create(project, "master");
-    BranchNameKey fooBranch = BranchNameKey.create(project, "foo");
-
-    createBranch(fooBranch);
-    assertThat(codeOwnersPluginConfiguration.isDisabled(masterBranch)).isFalse();
-    assertThat(codeOwnersPluginConfiguration.isDisabled(fooBranch)).isFalse();
+    createBranch(BranchNameKey.create(project, "foo"));
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).isDisabled("master"))
+        .isFalse();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).isDisabled("foo")).isFalse();
 
     CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
     input.disabledBranches = ImmutableList.of("refs/heads/master", "refs/heads/foo");
@@ -115,25 +113,25 @@
         projectCodeOwnersApiFactory.project(project).updateConfig(input);
     assertThat(updatedConfig.status.disabledBranches)
         .containsExactly("refs/heads/master", "refs/heads/foo");
-    assertThat(codeOwnersPluginConfiguration.isDisabled(masterBranch)).isTrue();
-    assertThat(codeOwnersPluginConfiguration.isDisabled(fooBranch)).isTrue();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).isDisabled("master"))
+        .isTrue();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).isDisabled("foo")).isTrue();
 
     input = new CodeOwnerProjectConfigInput();
     input.disabledBranches = ImmutableList.of();
     updatedConfig = projectCodeOwnersApiFactory.project(project).updateConfig(input);
     assertThat(updatedConfig.status.disabledBranches).isNull();
-    assertThat(codeOwnersPluginConfiguration.isDisabled(masterBranch)).isFalse();
-    assertThat(codeOwnersPluginConfiguration.isDisabled(fooBranch)).isFalse();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).isDisabled("master"))
+        .isFalse();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).isDisabled("foo")).isFalse();
   }
 
   @Test
   public void setDisabledBranchesRegEx() throws Exception {
-    BranchNameKey masterBranch = BranchNameKey.create(project, "master");
-    BranchNameKey fooBranch = BranchNameKey.create(project, "foo");
-
-    createBranch(fooBranch);
-    assertThat(codeOwnersPluginConfiguration.isDisabled(masterBranch)).isFalse();
-    assertThat(codeOwnersPluginConfiguration.isDisabled(fooBranch)).isFalse();
+    createBranch(BranchNameKey.create(project, "foo"));
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).isDisabled("master"))
+        .isFalse();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).isDisabled("foo")).isFalse();
 
     CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
     input.disabledBranches = ImmutableList.of("refs/heads/*");
@@ -141,15 +139,14 @@
         projectCodeOwnersApiFactory.project(project).updateConfig(input);
     assertThat(updatedConfig.status.disabledBranches)
         .containsExactly("refs/heads/master", "refs/heads/foo");
-    assertThat(codeOwnersPluginConfiguration.isDisabled(masterBranch)).isTrue();
-    assertThat(codeOwnersPluginConfiguration.isDisabled(fooBranch)).isTrue();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).isDisabled("master"))
+        .isTrue();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).isDisabled("foo")).isTrue();
   }
 
   @Test
   public void setDisabledBranchThatDoesntExist() throws Exception {
-    BranchNameKey fooBranch = BranchNameKey.create(project, "foo");
-
-    assertThat(codeOwnersPluginConfiguration.isDisabled(fooBranch)).isFalse();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).isDisabled("foo")).isFalse();
 
     CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
     input.disabledBranches = ImmutableList.of("refs/heads/foo");
@@ -157,9 +154,9 @@
         projectCodeOwnersApiFactory.project(project).updateConfig(input);
     // status.disabledBranches does only contain existing branches
     assertThat(updatedConfig.status.disabledBranches).isNull();
-    assertThat(codeOwnersPluginConfiguration.isDisabled(fooBranch)).isTrue();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).isDisabled("foo")).isTrue();
 
-    createBranch(fooBranch);
+    createBranch(BranchNameKey.create(project, "foo"));
     assertThat(projectCodeOwnersApiFactory.project(project).getConfig().status.disabledBranches)
         .containsExactly("refs/heads/foo");
   }
@@ -183,24 +180,29 @@
 
   @Test
   public void setFileExtension() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.getFileExtension(project)).isEmpty();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getFileExtension())
+        .isEmpty();
 
     CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
     input.fileExtension = "foo";
     CodeOwnerProjectConfigInfo updatedConfig =
         projectCodeOwnersApiFactory.project(project).updateConfig(input);
     assertThat(updatedConfig.general.fileExtension).isEqualTo("foo");
-    assertThat(codeOwnersPluginConfiguration.getFileExtension(project)).value().isEqualTo("foo");
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getFileExtension())
+        .value()
+        .isEqualTo("foo");
 
     input.fileExtension = "";
     updatedConfig = projectCodeOwnersApiFactory.project(project).updateConfig(input);
     assertThat(updatedConfig.general.fileExtension).isNull();
-    assertThat(codeOwnersPluginConfiguration.getFileExtension(project)).isEmpty();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getFileExtension())
+        .isEmpty();
   }
 
   @Test
   public void setRequiredApproval() throws Exception {
-    RequiredApproval requiredApproval = codeOwnersPluginConfiguration.getRequiredApproval(project);
+    RequiredApproval requiredApproval =
+        codeOwnersPluginConfiguration.getProjectConfig(project).getRequiredApproval();
     assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
     assertThat(requiredApproval).hasValueThat().isEqualTo(1);
 
@@ -215,7 +217,8 @@
         projectCodeOwnersApiFactory.project(project).updateConfig(input);
     assertThat(updatedConfig.requiredApproval.label).isEqualTo(otherLabel);
     assertThat(updatedConfig.requiredApproval.value).isEqualTo(2);
-    requiredApproval = codeOwnersPluginConfiguration.getRequiredApproval(project);
+    requiredApproval =
+        codeOwnersPluginConfiguration.getProjectConfig(project).getRequiredApproval();
     assertThat(requiredApproval).hasLabelNameThat().isEqualTo(otherLabel);
     assertThat(requiredApproval).hasValueThat().isEqualTo(2);
 
@@ -223,7 +226,8 @@
     updatedConfig = projectCodeOwnersApiFactory.project(project).updateConfig(input);
     assertThat(updatedConfig.requiredApproval.label).isEqualTo("Code-Review");
     assertThat(updatedConfig.requiredApproval.value).isEqualTo(1);
-    requiredApproval = codeOwnersPluginConfiguration.getRequiredApproval(project);
+    requiredApproval =
+        codeOwnersPluginConfiguration.getProjectConfig(project).getRequiredApproval();
     assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
     assertThat(requiredApproval).hasValueThat().isEqualTo(1);
   }
@@ -249,7 +253,8 @@
 
   @Test
   public void setOverrideApproval() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.getOverrideApproval(project)).isEmpty();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getOverrideApproval())
+        .isEmpty();
 
     String overrideLabel1 = "Bypass-Owners";
     String overrideLabel2 = "Owners-Override";
@@ -265,12 +270,14 @@
     assertThat(updatedConfig.overrideApproval.get(0).value).isEqualTo(1);
     assertThat(updatedConfig.overrideApproval.get(1).label).isEqualTo(overrideLabel2);
     assertThat(updatedConfig.overrideApproval.get(1).value).isEqualTo(1);
-    assertThat(codeOwnersPluginConfiguration.getOverrideApproval(project)).hasSize(2);
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getOverrideApproval())
+        .hasSize(2);
 
     input.overrideApprovals = ImmutableList.of();
     updatedConfig = projectCodeOwnersApiFactory.project(project).updateConfig(input);
     assertThat(updatedConfig.overrideApproval).isNull();
-    assertThat(codeOwnersPluginConfiguration.getOverrideApproval(project)).isEmpty();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getOverrideApproval())
+        .isEmpty();
   }
 
   @Test
@@ -294,7 +301,7 @@
 
   @Test
   public void setFallbackCodeOwners() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.getFallbackCodeOwners(project))
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getFallbackCodeOwners())
         .isEqualTo(FallbackCodeOwners.NONE);
 
     CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
@@ -302,37 +309,39 @@
     CodeOwnerProjectConfigInfo updatedConfig =
         projectCodeOwnersApiFactory.project(project).updateConfig(input);
     assertThat(updatedConfig.general.fallbackCodeOwners).isEqualTo(FallbackCodeOwners.ALL_USERS);
-    assertThat(codeOwnersPluginConfiguration.getFallbackCodeOwners(project))
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getFallbackCodeOwners())
         .isEqualTo(FallbackCodeOwners.ALL_USERS);
 
     input.fallbackCodeOwners = FallbackCodeOwners.NONE;
     updatedConfig = projectCodeOwnersApiFactory.project(project).updateConfig(input);
     assertThat(updatedConfig.general.fallbackCodeOwners).isEqualTo(FallbackCodeOwners.NONE);
-    assertThat(codeOwnersPluginConfiguration.getFallbackCodeOwners(project))
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getFallbackCodeOwners())
         .isEqualTo(FallbackCodeOwners.NONE);
   }
 
   @Test
   public void setGlobalCodeOwners() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.getGlobalCodeOwners(project)).isEmpty();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getGlobalCodeOwners())
+        .isEmpty();
 
     CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
     input.globalCodeOwners = ImmutableList.of(user.email(), "foo.bar@example.com");
     projectCodeOwnersApiFactory.project(project).updateConfig(input);
     assertThat(
-            codeOwnersPluginConfiguration.getGlobalCodeOwners(project).stream()
+            codeOwnersPluginConfiguration.getProjectConfig(project).getGlobalCodeOwners().stream()
                 .map(CodeOwnerReference::email)
                 .collect(toImmutableSet()))
         .containsExactly(user.email(), "foo.bar@example.com");
 
     input.globalCodeOwners = ImmutableList.of();
     projectCodeOwnersApiFactory.project(project).updateConfig(input);
-    assertThat(codeOwnersPluginConfiguration.getGlobalCodeOwners(project)).isEmpty();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getGlobalCodeOwners())
+        .isEmpty();
   }
 
   @Test
   public void setMergeCommitStrategy() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.getMergeCommitStrategy(project))
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getMergeCommitStrategy())
         .isEqualTo(MergeCommitStrategy.ALL_CHANGED_FILES);
 
     CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
@@ -341,166 +350,210 @@
         projectCodeOwnersApiFactory.project(project).updateConfig(input);
     assertThat(updatedConfig.general.mergeCommitStrategy)
         .isEqualTo(MergeCommitStrategy.FILES_WITH_CONFLICT_RESOLUTION);
-    assertThat(codeOwnersPluginConfiguration.getMergeCommitStrategy(project))
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getMergeCommitStrategy())
         .isEqualTo(MergeCommitStrategy.FILES_WITH_CONFLICT_RESOLUTION);
 
     input.mergeCommitStrategy = MergeCommitStrategy.ALL_CHANGED_FILES;
     updatedConfig = projectCodeOwnersApiFactory.project(project).updateConfig(input);
     assertThat(updatedConfig.general.mergeCommitStrategy)
         .isEqualTo(MergeCommitStrategy.ALL_CHANGED_FILES);
-    assertThat(codeOwnersPluginConfiguration.getMergeCommitStrategy(project))
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getMergeCommitStrategy())
         .isEqualTo(MergeCommitStrategy.ALL_CHANGED_FILES);
   }
 
   @Test
   public void setImplicitApprovals() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.areImplicitApprovalsEnabled(project)).isFalse();
+    assertThat(
+            codeOwnersPluginConfiguration.getProjectConfig(project).areImplicitApprovalsEnabled())
+        .isFalse();
 
     CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
     input.implicitApprovals = true;
     CodeOwnerProjectConfigInfo updatedConfig =
         projectCodeOwnersApiFactory.project(project).updateConfig(input);
     assertThat(updatedConfig.general.implicitApprovals).isTrue();
-    assertThat(codeOwnersPluginConfiguration.areImplicitApprovalsEnabled(project)).isTrue();
+    assertThat(
+            codeOwnersPluginConfiguration.getProjectConfig(project).areImplicitApprovalsEnabled())
+        .isTrue();
 
     input.implicitApprovals = false;
     updatedConfig = projectCodeOwnersApiFactory.project(project).updateConfig(input);
     assertThat(updatedConfig.general.implicitApprovals).isNull();
-    assertThat(codeOwnersPluginConfiguration.areImplicitApprovalsEnabled(project)).isFalse();
+    assertThat(
+            codeOwnersPluginConfiguration.getProjectConfig(project).areImplicitApprovalsEnabled())
+        .isFalse();
   }
 
   @Test
   public void setOverrideInfoUrl() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.getOverrideInfoUrl(project)).isEmpty();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getOverrideInfoUrl())
+        .isEmpty();
 
     CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
     input.overrideInfoUrl = "http://foo.bar";
     CodeOwnerProjectConfigInfo updatedConfig =
         projectCodeOwnersApiFactory.project(project).updateConfig(input);
     assertThat(updatedConfig.general.overrideInfoUrl).isEqualTo("http://foo.bar");
-    assertThat(codeOwnersPluginConfiguration.getOverrideInfoUrl(project))
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getOverrideInfoUrl())
         .value()
         .isEqualTo("http://foo.bar");
 
     input.overrideInfoUrl = "";
     updatedConfig = projectCodeOwnersApiFactory.project(project).updateConfig(input);
     assertThat(updatedConfig.general.overrideInfoUrl).isNull();
-    assertThat(codeOwnersPluginConfiguration.getOverrideInfoUrl(project)).isEmpty();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getOverrideInfoUrl())
+        .isEmpty();
   }
 
   @Test
   public void setReadOnly() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.areCodeOwnerConfigsReadOnly(project)).isFalse();
+    assertThat(
+            codeOwnersPluginConfiguration.getProjectConfig(project).areCodeOwnerConfigsReadOnly())
+        .isFalse();
 
     CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
     input.readOnly = true;
     projectCodeOwnersApiFactory.project(project).updateConfig(input);
-    assertThat(codeOwnersPluginConfiguration.areCodeOwnerConfigsReadOnly(project)).isTrue();
+    assertThat(
+            codeOwnersPluginConfiguration.getProjectConfig(project).areCodeOwnerConfigsReadOnly())
+        .isTrue();
 
     input.readOnly = false;
     projectCodeOwnersApiFactory.project(project).updateConfig(input);
-    assertThat(codeOwnersPluginConfiguration.areCodeOwnerConfigsReadOnly(project)).isFalse();
+    assertThat(
+            codeOwnersPluginConfiguration.getProjectConfig(project).areCodeOwnerConfigsReadOnly())
+        .isFalse();
   }
 
   @Test
   public void setExemptPureReverts() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.arePureRevertsExempted(project)).isFalse();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).arePureRevertsExempted())
+        .isFalse();
 
     CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
     input.exemptPureReverts = true;
     projectCodeOwnersApiFactory.project(project).updateConfig(input);
-    assertThat(codeOwnersPluginConfiguration.arePureRevertsExempted(project)).isTrue();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).arePureRevertsExempted())
+        .isTrue();
 
     input.exemptPureReverts = false;
     projectCodeOwnersApiFactory.project(project).updateConfig(input);
-    assertThat(codeOwnersPluginConfiguration.arePureRevertsExempted(project)).isFalse();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).arePureRevertsExempted())
+        .isFalse();
   }
 
   @Test
   public void setEnableValidationOnCommitReceived() throws Exception {
     assertThat(
-            codeOwnersPluginConfiguration.getCodeOwnerConfigValidationPolicyForCommitReceived(
-                project))
+            codeOwnersPluginConfiguration
+                .getProjectConfig(project)
+                .getCodeOwnerConfigValidationPolicyForCommitReceived())
         .isEqualTo(CodeOwnerConfigValidationPolicy.TRUE);
 
     CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
     input.enableValidationOnCommitReceived = CodeOwnerConfigValidationPolicy.FALSE;
     projectCodeOwnersApiFactory.project(project).updateConfig(input);
     assertThat(
-            codeOwnersPluginConfiguration.getCodeOwnerConfigValidationPolicyForCommitReceived(
-                project))
+            codeOwnersPluginConfiguration
+                .getProjectConfig(project)
+                .getCodeOwnerConfigValidationPolicyForCommitReceived())
         .isEqualTo(CodeOwnerConfigValidationPolicy.FALSE);
 
     input.enableValidationOnCommitReceived = CodeOwnerConfigValidationPolicy.TRUE;
     projectCodeOwnersApiFactory.project(project).updateConfig(input);
     assertThat(
-            codeOwnersPluginConfiguration.getCodeOwnerConfigValidationPolicyForCommitReceived(
-                project))
+            codeOwnersPluginConfiguration
+                .getProjectConfig(project)
+                .getCodeOwnerConfigValidationPolicyForCommitReceived())
         .isEqualTo(CodeOwnerConfigValidationPolicy.TRUE);
   }
 
   @Test
   public void setEnableValidationOnSubmit() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.getCodeOwnerConfigValidationPolicyForSubmit(project))
+    assertThat(
+            codeOwnersPluginConfiguration
+                .getProjectConfig(project)
+                .getCodeOwnerConfigValidationPolicyForSubmit())
         .isEqualTo(CodeOwnerConfigValidationPolicy.TRUE);
 
     CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
     input.enableValidationOnSubmit = CodeOwnerConfigValidationPolicy.FALSE;
     projectCodeOwnersApiFactory.project(project).updateConfig(input);
-    assertThat(codeOwnersPluginConfiguration.getCodeOwnerConfigValidationPolicyForSubmit(project))
+    assertThat(
+            codeOwnersPluginConfiguration
+                .getProjectConfig(project)
+                .getCodeOwnerConfigValidationPolicyForSubmit())
         .isEqualTo(CodeOwnerConfigValidationPolicy.FALSE);
 
     input.enableValidationOnSubmit = CodeOwnerConfigValidationPolicy.TRUE;
     projectCodeOwnersApiFactory.project(project).updateConfig(input);
-    assertThat(codeOwnersPluginConfiguration.getCodeOwnerConfigValidationPolicyForSubmit(project))
+    assertThat(
+            codeOwnersPluginConfiguration
+                .getProjectConfig(project)
+                .getCodeOwnerConfigValidationPolicyForSubmit())
         .isEqualTo(CodeOwnerConfigValidationPolicy.TRUE);
   }
 
   @Test
   public void setRejectNonResolvableCodeOwners() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.rejectNonResolvableCodeOwners(project)).isTrue();
+    assertThat(
+            codeOwnersPluginConfiguration.getProjectConfig(project).rejectNonResolvableCodeOwners())
+        .isTrue();
 
     CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
     input.rejectNonResolvableCodeOwners = false;
     projectCodeOwnersApiFactory.project(project).updateConfig(input);
-    assertThat(codeOwnersPluginConfiguration.rejectNonResolvableCodeOwners(project)).isFalse();
+    assertThat(
+            codeOwnersPluginConfiguration.getProjectConfig(project).rejectNonResolvableCodeOwners())
+        .isFalse();
 
     input.rejectNonResolvableCodeOwners = true;
     projectCodeOwnersApiFactory.project(project).updateConfig(input);
-    assertThat(codeOwnersPluginConfiguration.rejectNonResolvableCodeOwners(project)).isTrue();
+    assertThat(
+            codeOwnersPluginConfiguration.getProjectConfig(project).rejectNonResolvableCodeOwners())
+        .isTrue();
   }
 
   @Test
   public void setRejectNonResolvableImports() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.rejectNonResolvableImports(project)).isTrue();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).rejectNonResolvableImports())
+        .isTrue();
 
     CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
     input.rejectNonResolvableImports = false;
     projectCodeOwnersApiFactory.project(project).updateConfig(input);
-    assertThat(codeOwnersPluginConfiguration.rejectNonResolvableImports(project)).isFalse();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).rejectNonResolvableImports())
+        .isFalse();
 
     input.rejectNonResolvableImports = true;
     projectCodeOwnersApiFactory.project(project).updateConfig(input);
-    assertThat(codeOwnersPluginConfiguration.rejectNonResolvableImports(project)).isTrue();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).rejectNonResolvableImports())
+        .isTrue();
   }
 
   @Test
   public void setMaxPathsInChangeMessages() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.getMaxPathsInChangeMessages(project))
+    assertThat(
+            codeOwnersPluginConfiguration.getProjectConfig(project).getMaxPathsInChangeMessages())
         .isEqualTo(GeneralConfig.DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES);
 
     CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
     input.maxPathsInChangeMessages = 10;
     projectCodeOwnersApiFactory.project(project).updateConfig(input);
-    assertThat(codeOwnersPluginConfiguration.getMaxPathsInChangeMessages(project)).isEqualTo(10);
+    assertThat(
+            codeOwnersPluginConfiguration.getProjectConfig(project).getMaxPathsInChangeMessages())
+        .isEqualTo(10);
 
     input.maxPathsInChangeMessages = 0;
     projectCodeOwnersApiFactory.project(project).updateConfig(input);
-    assertThat(codeOwnersPluginConfiguration.getMaxPathsInChangeMessages(project)).isEqualTo(0);
+    assertThat(
+            codeOwnersPluginConfiguration.getProjectConfig(project).getMaxPathsInChangeMessages())
+        .isEqualTo(0);
 
     input.maxPathsInChangeMessages = GeneralConfig.DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES;
     projectCodeOwnersApiFactory.project(project).updateConfig(input);
-    assertThat(codeOwnersPluginConfiguration.getMaxPathsInChangeMessages(project))
+    assertThat(
+            codeOwnersPluginConfiguration.getProjectConfig(project).getMaxPathsInChangeMessages())
         .isEqualTo(GeneralConfig.DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES);
   }
 
@@ -544,12 +597,12 @@
     ProjectState projectState = projectCache.get(project).orElseThrow(illegalState(project));
     deleteRef.deleteSingleRef(projectState, RefNames.REFS_CONFIG);
 
-    assertThat(codeOwnersPluginConfiguration.isDisabled(project)).isFalse();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).isDisabled()).isFalse();
 
     CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
     input.disabled = true;
     projectCodeOwnersApiFactory.project(project).updateConfig(input);
 
-    assertThat(codeOwnersPluginConfiguration.isDisabled(project)).isTrue();
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).isDisabled()).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
new file mode 100644
index 0000000..997eb19
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigSnapshotTest.java
@@ -0,0 +1,874 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.backend.config;
+
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.plugins.codeowners.testing.RequiredApprovalSubject.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.truth.OptionalSubject.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.common.LabelDefinitionInput;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.PrivateInternals_DynamicMapImpl;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest;
+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.PathExpressionMatcher;
+import com.google.gerrit.plugins.codeowners.backend.findowners.FindOwnersBackend;
+import com.google.gerrit.plugins.codeowners.common.MergeCommitStrategy;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.inject.Inject;
+import com.google.inject.Key;
+import com.google.inject.util.Providers;
+import java.nio.file.Path;
+import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.Before;
+import org.junit.Test;
+
+/** Tests for {@link CodeOwnersPluginConfigSnapshot}. */
+public class CodeOwnersPluginConfigSnapshotTest extends AbstractCodeOwnersTest {
+  @Inject private ProjectOperations projectOperations;
+
+  private CodeOwnersPluginConfigSnapshot.Factory codeOwnersPluginConfigSnapshotFactory;
+  private DynamicMap<CodeOwnerBackend> codeOwnerBackends;
+
+  @Before
+  public void setUpCodeOwnersPlugin() throws Exception {
+    codeOwnersPluginConfigSnapshotFactory =
+        plugin.getSysInjector().getInstance(CodeOwnersPluginConfigSnapshot.Factory.class);
+    codeOwnerBackends =
+        plugin.getSysInjector().getInstance(new Key<DynamicMap<CodeOwnerBackend>>() {});
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.fileExtension", value = "foo")
+  public void getFileExtensionIfNoneIsConfiguredOnProjectLevel() throws Exception {
+    assertThat(cfgSnapshot().getFileExtension()).value().isEqualTo("foo");
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.fileExtension", value = "foo")
+  public void fileExtensionOnProjectLevelOverridesDefaultFileExtension() throws Exception {
+    configureFileExtension(project, "bar");
+    assertThat(cfgSnapshot().getFileExtension()).value().isEqualTo("bar");
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.fileExtension", value = "foo")
+  public void fileExtensionIsInheritedFromParentProject() throws Exception {
+    configureFileExtension(allProjects, "bar");
+    assertThat(cfgSnapshot().getFileExtension()).value().isEqualTo("bar");
+  }
+
+  @Test
+  public void inheritedFileExtensionCanBeOverridden() throws Exception {
+    configureFileExtension(allProjects, "foo");
+    configureFileExtension(project, "bar");
+    assertThat(cfgSnapshot().getFileExtension()).value().isEqualTo("bar");
+  }
+
+  @Test
+  public void getMergeCommitStrategyIfNoneIsConfigured() throws Exception {
+    assertThat(cfgSnapshot().getMergeCommitStrategy())
+        .isEqualTo(MergeCommitStrategy.ALL_CHANGED_FILES);
+  }
+
+  @Test
+  @GerritConfig(
+      name = "plugin.code-owners.mergeCommitStrategy",
+      value = "FILES_WITH_CONFLICT_RESOLUTION")
+  public void getMergeCommitStrategyIfNoneIsConfiguredOnProjectLevel() throws Exception {
+    assertThat(cfgSnapshot().getMergeCommitStrategy())
+        .isEqualTo(MergeCommitStrategy.FILES_WITH_CONFLICT_RESOLUTION);
+  }
+
+  @Test
+  @GerritConfig(
+      name = "plugin.code-owners.mergeCommitStrategy",
+      value = "FILES_WITH_CONFLICT_RESOLUTION")
+  public void mergeCommitStrategyOnProjectLevelOverridesGlobalMergeCommitStrategy()
+      throws Exception {
+    configureMergeCommitStrategy(project, MergeCommitStrategy.ALL_CHANGED_FILES);
+    assertThat(cfgSnapshot().getMergeCommitStrategy())
+        .isEqualTo(MergeCommitStrategy.ALL_CHANGED_FILES);
+  }
+
+  @Test
+  @GerritConfig(
+      name = "plugin.code-owners.mergeCommitStrategy",
+      value = "FILES_WITH_CONFLICT_RESOLUTION")
+  public void mergeCommitStrategyIsInheritedFromParentProject() throws Exception {
+    configureMergeCommitStrategy(allProjects, MergeCommitStrategy.ALL_CHANGED_FILES);
+    assertThat(cfgSnapshot().getMergeCommitStrategy())
+        .isEqualTo(MergeCommitStrategy.ALL_CHANGED_FILES);
+  }
+
+  @Test
+  public void inheritedMergeCommitStrategyCanBeOverridden() throws Exception {
+    configureMergeCommitStrategy(allProjects, MergeCommitStrategy.FILES_WITH_CONFLICT_RESOLUTION);
+    configureMergeCommitStrategy(project, MergeCommitStrategy.ALL_CHANGED_FILES);
+    assertThat(cfgSnapshot().getMergeCommitStrategy())
+        .isEqualTo(MergeCommitStrategy.ALL_CHANGED_FILES);
+  }
+
+  @Test
+  public void getFallbackCodeOwnersIfNoneIsConfigured() throws Exception {
+    assertThat(cfgSnapshot().getFallbackCodeOwners()).isEqualTo(FallbackCodeOwners.NONE);
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.fallbackCodeOwners", value = "ALL_USERS")
+  public void getFallbackCodeOwnersIfNoneIsConfiguredOnProjectLevel() throws Exception {
+    assertThat(cfgSnapshot().getFallbackCodeOwners()).isEqualTo(FallbackCodeOwners.ALL_USERS);
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.fallbackCodeOwners", value = "ALL_USERS")
+  public void fallbackCodeOnwersOnProjectLevelOverridesGlobalFallbackCodeOwners() throws Exception {
+    configureFallbackCodeOwners(project, FallbackCodeOwners.NONE);
+    assertThat(cfgSnapshot().getFallbackCodeOwners()).isEqualTo(FallbackCodeOwners.NONE);
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.fallbackCodeOwners", value = "ALL_USERS")
+  public void fallbackCodeOwnersIsInheritedFromParentProject() throws Exception {
+    configureFallbackCodeOwners(allProjects, FallbackCodeOwners.NONE);
+    assertThat(cfgSnapshot().getFallbackCodeOwners()).isEqualTo(FallbackCodeOwners.NONE);
+  }
+
+  @Test
+  public void inheritedFallbackCodeOwnersCanBeOverridden() throws Exception {
+    configureFallbackCodeOwners(allProjects, FallbackCodeOwners.ALL_USERS);
+    configureFallbackCodeOwners(project, FallbackCodeOwners.NONE);
+    assertThat(cfgSnapshot().getFallbackCodeOwners()).isEqualTo(FallbackCodeOwners.NONE);
+  }
+
+  @Test
+  public void getMaxPathsInChangeMessagesIfNoneIsConfigured() throws Exception {
+    assertThat(cfgSnapshot().getMaxPathsInChangeMessages())
+        .isEqualTo(GeneralConfig.DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES);
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.maxPathsInChangeMessages", value = "50")
+  public void getMaxPathsInChangeMessagesIfNoneIsConfiguredOnProjectLevel() throws Exception {
+    assertThat(cfgSnapshot().getMaxPathsInChangeMessages()).isEqualTo(50);
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.maxPathsInChangeMessages", value = "50")
+  public void maxPathInChangeMessagesOnProjectLevelOverridesGlobalMaxPathInChangeMessages()
+      throws Exception {
+    configureMaxPathsInChangeMessages(project, 20);
+    assertThat(cfgSnapshot().getMaxPathsInChangeMessages()).isEqualTo(20);
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.maxPathsInChangeMessages", value = "50")
+  public void maxPathInChangeMessagesIsInheritedFromParentProject() throws Exception {
+    configureMaxPathsInChangeMessages(allProjects, 20);
+    assertThat(cfgSnapshot().getMaxPathsInChangeMessages()).isEqualTo(20);
+  }
+
+  @Test
+  public void inheritedMaxPathInChangeMessagesCanBeOverridden() throws Exception {
+    configureMaxPathsInChangeMessages(allProjects, 50);
+    configureMaxPathsInChangeMessages(project, 20);
+    assertThat(cfgSnapshot().getMaxPathsInChangeMessages()).isEqualTo(20);
+  }
+
+  @Test
+  public void cannotCheckForNullBranchIfCodeOwnersFunctionalityIsDisabled() throws Exception {
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class, () -> cfgSnapshot().isDisabled(/* branchName= */ null));
+    assertThat(npe).hasMessageThat().isEqualTo("branchName");
+  }
+
+  @Test
+  public void checkIfCodeOwnersFunctionalityIsDisabledForNonExistingBranch() throws Exception {
+    assertThat(cfgSnapshot().isDisabled("non-existing")).isFalse();
+  }
+
+  @Test
+  public void checkIfCodeOwnersFunctionalityIsDisabledForProjectWithEmptyConfig() throws Exception {
+    assertThat(cfgSnapshot().isDisabled()).isFalse();
+  }
+
+  @Test
+  public void checkIfCodeOwnersFunctionalityIsDisabledForBranchWithEmptyConfig() throws Exception {
+    assertThat(cfgSnapshot().isDisabled("master")).isFalse();
+  }
+
+  @Test
+  public void codeOwnersFunctionalityIsDisabledForProject() throws Exception {
+    disableCodeOwnersForProject(project);
+    assertThat(cfgSnapshot().isDisabled()).isTrue();
+  }
+
+  @Test
+  public void codeOwnersFunctionalityIsDisabledForBranchIfItIsDisabledForProject()
+      throws Exception {
+    disableCodeOwnersForProject(project);
+    assertThat(cfgSnapshot().isDisabled("master")).isTrue();
+  }
+
+  @Test
+  public void codeOwnersFunctionalityIsDisabledForBranch_exactRef() throws Exception {
+    configureDisabledBranch(project, "refs/heads/master");
+    assertThat(cfgSnapshot().isDisabled("master")).isTrue();
+    assertThat(cfgSnapshot().isDisabled("other")).isFalse();
+  }
+
+  @Test
+  public void codeOwnersFunctionalityIsDisabledForBranch_refPattern() throws Exception {
+    configureDisabledBranch(project, "refs/heads/*");
+    assertThat(cfgSnapshot().isDisabled("master")).isTrue();
+    assertThat(cfgSnapshot().isDisabled("other")).isTrue();
+    assertThat(cfgSnapshot().isDisabled(RefNames.REFS_META)).isFalse();
+  }
+
+  @Test
+  public void codeOwnersFunctionalityIsDisabledForBranch_regularExpression() throws Exception {
+    configureDisabledBranch(project, "^refs/heads/.*");
+    assertThat(cfgSnapshot().isDisabled("master")).isTrue();
+    assertThat(cfgSnapshot().isDisabled("other")).isTrue();
+    assertThat(cfgSnapshot().isDisabled(RefNames.REFS_META)).isFalse();
+  }
+
+  @Test
+  public void codeOwnersFunctionalityIsDisabledForBranch_invalidRegularExpression()
+      throws Exception {
+    configureDisabledBranch(project, "^refs/heads/[");
+    assertThat(cfgSnapshot().isDisabled("master")).isFalse();
+  }
+
+  @Test
+  public void disabledIsInheritedFromParentProject() throws Exception {
+    disableCodeOwnersForProject(allProjects);
+    assertThat(cfgSnapshot().isDisabled()).isTrue();
+  }
+
+  @Test
+  public void inheritedDisabledAlsoCountsForBranch() throws Exception {
+    disableCodeOwnersForProject(allProjects);
+    assertThat(cfgSnapshot().isDisabled("master")).isTrue();
+  }
+
+  @Test
+  public void inheritedDisabledValueIsIgnoredIfInvalid() throws Exception {
+    configureDisabled(project, "invalid");
+    assertThat(cfgSnapshot().isDisabled()).isFalse();
+  }
+
+  @Test
+  public void inheritedDisabledValueIsIgnoredForBranchIfInvalid() throws Exception {
+    configureDisabled(project, "invalid");
+    assertThat(cfgSnapshot().isDisabled("master")).isFalse();
+  }
+
+  @Test
+  public void disabledForOtherProjectHasNoEffect() throws Exception {
+    Project.NameKey otherProject = projectOperations.newProject().create();
+    disableCodeOwnersForProject(otherProject);
+    assertThat(cfgSnapshot().isDisabled()).isFalse();
+  }
+
+  @Test
+  public void disabledBranchForOtherProjectHasNoEffect() throws Exception {
+    Project.NameKey otherProject = projectOperations.newProject().create();
+    configureDisabledBranch(otherProject, "refs/heads/master");
+    assertThat(cfgSnapshot().isDisabled("master")).isFalse();
+  }
+
+  @Test
+  public void disabledBranchIsInheritedFromParentProject() throws Exception {
+    configureDisabledBranch(allProjects, "refs/heads/master");
+    assertThat(cfgSnapshot().isDisabled("master")).isTrue();
+  }
+
+  @Test
+  public void inheritedDisabledCanBeOverridden() throws Exception {
+    disableCodeOwnersForProject(allProjects);
+    enableCodeOwnersForProject(project);
+    assertThat(cfgSnapshot().isDisabled("master")).isFalse();
+  }
+
+  @Test
+  public void inheritedDisabledBranchCanBeOverridden() throws Exception {
+    configureDisabledBranch(allProjects, "refs/heads/master");
+    enableCodeOwnersForAllBranches(project);
+    assertThat(cfgSnapshot().isDisabled("master")).isFalse();
+  }
+
+  @Test
+  public void getBackendForNonExistingBranch() throws Exception {
+    assertThat(cfgSnapshot().getBackend("non-existing")).isInstanceOf(FindOwnersBackend.class);
+  }
+
+  @Test
+  public void getDefaultBackendWhenNoBackendIsConfigured() throws Exception {
+    assertThat(cfgSnapshot().getBackend("master")).isInstanceOf(FindOwnersBackend.class);
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.backend", value = TestCodeOwnerBackend.ID)
+  public void getConfiguredDefaultBackend() throws Exception {
+    try (AutoCloseable registration = registerTestBackend()) {
+      assertThat(cfgSnapshot().getBackend("master")).isInstanceOf(TestCodeOwnerBackend.class);
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.backend", value = "non-existing-backend")
+  public void cannotGetBackendIfNonExistingBackendIsConfigured() throws Exception {
+    InvalidPluginConfigurationException exception =
+        assertThrows(
+            InvalidPluginConfigurationException.class, () -> cfgSnapshot().getBackend("master"));
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(
+            "Invalid configuration of the code-owners plugin. Code owner backend"
+                + " 'non-existing-backend' that is configured in gerrit.config (parameter"
+                + " plugin.code-owners.backend) not found.");
+  }
+
+  @Test
+  public void getBackendConfiguredOnProjectLevel() throws Exception {
+    configureBackend(project, TestCodeOwnerBackend.ID);
+    try (AutoCloseable registration = registerTestBackend()) {
+      assertThat(cfgSnapshot().getBackend("master")).isInstanceOf(TestCodeOwnerBackend.class);
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.backend", value = FindOwnersBackend.ID)
+  public void backendConfiguredOnProjectLevelOverridesDefaultBackend() throws Exception {
+    configureBackend(project, TestCodeOwnerBackend.ID);
+    try (AutoCloseable registration = registerTestBackend()) {
+      assertThat(cfgSnapshot().getBackend("master")).isInstanceOf(TestCodeOwnerBackend.class);
+    }
+  }
+
+  @Test
+  public void backendIsInheritedFromParentProject() throws Exception {
+    configureBackend(allProjects, TestCodeOwnerBackend.ID);
+    try (AutoCloseable registration = registerTestBackend()) {
+      assertThat(cfgSnapshot().getBackend("master")).isInstanceOf(TestCodeOwnerBackend.class);
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.backend", value = FindOwnersBackend.ID)
+  public void inheritedBackendOverridesDefaultBackend() throws Exception {
+    configureBackend(allProjects, TestCodeOwnerBackend.ID);
+    try (AutoCloseable registration = registerTestBackend()) {
+      assertThat(cfgSnapshot().getBackend("master")).isInstanceOf(TestCodeOwnerBackend.class);
+    }
+  }
+
+  @Test
+  public void projectLevelBackendOverridesInheritedBackend() throws Exception {
+    configureBackend(allProjects, TestCodeOwnerBackend.ID);
+    configureBackend(project, FindOwnersBackend.ID);
+    try (AutoCloseable registration = registerTestBackend()) {
+      assertThat(cfgSnapshot().getBackend("master")).isInstanceOf(FindOwnersBackend.class);
+    }
+  }
+
+  @Test
+  public void cannotGetBackendIfNonExistingBackendIsConfiguredOnProjectLevel() throws Exception {
+    configureBackend(project, "non-existing-backend");
+    InvalidPluginConfigurationException exception =
+        assertThrows(
+            InvalidPluginConfigurationException.class, () -> cfgSnapshot().getBackend("master"));
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(
+            String.format(
+                "Invalid configuration of the code-owners plugin. Code owner backend"
+                    + " 'non-existing-backend' that is configured for project %s in"
+                    + " code-owners.config (parameter codeOwners.backend) not found.",
+                project));
+  }
+
+  @Test
+  public void projectLevelBackendForOtherProjectHasNoEffect() throws Exception {
+    Project.NameKey otherProject = projectOperations.newProject().create();
+    configureBackend(otherProject, TestCodeOwnerBackend.ID);
+    try (AutoCloseable registration = registerTestBackend()) {
+      assertThat(cfgSnapshot().getBackend("master")).isInstanceOf(FindOwnersBackend.class);
+    }
+  }
+
+  @Test
+  public void getBackendConfiguredOnBranchLevel() throws Exception {
+    configureBackend(project, "refs/heads/master", TestCodeOwnerBackend.ID);
+    try (AutoCloseable registration = registerTestBackend()) {
+      assertThat(cfgSnapshot().getBackend("master")).isInstanceOf(TestCodeOwnerBackend.class);
+    }
+  }
+
+  @Test
+  public void getBackendConfiguredOnBranchLevelShortName() throws Exception {
+    configureBackend(project, "master", TestCodeOwnerBackend.ID);
+    try (AutoCloseable registration = registerTestBackend()) {
+      assertThat(cfgSnapshot().getBackend("master")).isInstanceOf(TestCodeOwnerBackend.class);
+    }
+  }
+
+  @Test
+  public void branchLevelBackendOnFullNameTakesPrecedenceOverBranchLevelBackendOnShortName()
+      throws Exception {
+    configureBackend(project, "master", TestCodeOwnerBackend.ID);
+    configureBackend(project, "refs/heads/master", FindOwnersBackend.ID);
+    try (AutoCloseable registration = registerTestBackend()) {
+      assertThat(cfgSnapshot().getBackend("master")).isInstanceOf(FindOwnersBackend.class);
+    }
+  }
+
+  @Test
+  public void branchLevelBackendOverridesProjectLevelBackend() throws Exception {
+    configureBackend(project, TestCodeOwnerBackend.ID);
+    configureBackend(project, "master", FindOwnersBackend.ID);
+    try (AutoCloseable registration = registerTestBackend()) {
+      assertThat(cfgSnapshot().getBackend("master")).isInstanceOf(FindOwnersBackend.class);
+    }
+  }
+
+  @Test
+  public void cannotGetBackendIfNonExistingBackendIsConfiguredOnBranchLevel() throws Exception {
+    configureBackend(project, "master", "non-existing-backend");
+    InvalidPluginConfigurationException exception =
+        assertThrows(
+            InvalidPluginConfigurationException.class, () -> cfgSnapshot().getBackend("master"));
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(
+            String.format(
+                "Invalid configuration of the code-owners plugin. Code owner backend"
+                    + " 'non-existing-backend' that is configured for project %s in"
+                    + " code-owners.config (parameter codeOwners.master.backend) not found.",
+                project));
+  }
+
+  @Test
+  public void branchLevelBackendForOtherBranchHasNoEffect() throws Exception {
+    configureBackend(project, "foo", TestCodeOwnerBackend.ID);
+    try (AutoCloseable registration = registerTestBackend()) {
+      assertThat(cfgSnapshot().getBackend("master")).isInstanceOf(FindOwnersBackend.class);
+    }
+  }
+
+  @Test
+  public void getDefaultRequiredApprovalWhenNoRequiredApprovalIsConfigured() throws Exception {
+    RequiredApproval requiredApproval = cfgSnapshot().getRequiredApproval();
+    assertThat(requiredApproval).hasLabelNameThat().isEqualTo(RequiredApprovalConfig.DEFAULT_LABEL);
+    assertThat(requiredApproval).hasValueThat().isEqualTo(RequiredApprovalConfig.DEFAULT_VALUE);
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.requiredApproval", value = "Code-Review+2")
+  public void getConfiguredDefaultRequireApproval() throws Exception {
+    RequiredApproval requiredApproval = cfgSnapshot().getRequiredApproval();
+    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).hasValueThat().isEqualTo(2);
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.requiredApproval", value = "Foo-Bar+1")
+  public void cannotGetRequiredApprovalIfNonExistingLabelIsConfiguredAsRequiredApproval()
+      throws Exception {
+    InvalidPluginConfigurationException exception =
+        assertThrows(
+            InvalidPluginConfigurationException.class, () -> cfgSnapshot().getRequiredApproval());
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(
+            String.format(
+                "Invalid configuration of the code-owners plugin. Required approval 'Foo-Bar+1'"
+                    + " that is configured in gerrit.config (parameter"
+                    + " plugin.code-owners.requiredApproval) is invalid: Label Foo-Bar doesn't exist"
+                    + " for project %s.",
+                project.get()));
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.requiredApproval", value = "Code-Review+3")
+  public void cannotGetRequiredApprovalIfNonExistingLabelValueIsConfiguredAsRequiredApproval()
+      throws Exception {
+    InvalidPluginConfigurationException exception =
+        assertThrows(
+            InvalidPluginConfigurationException.class, () -> cfgSnapshot().getRequiredApproval());
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(
+            String.format(
+                "Invalid configuration of the code-owners plugin. Required approval"
+                    + " 'Code-Review+3' that is configured in gerrit.config (parameter"
+                    + " plugin.code-owners.requiredApproval) is invalid: Label Code-Review on"
+                    + " project %s doesn't allow value 3.",
+                project.get()));
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.requiredApproval", value = "INVALID")
+  public void cannotGetRequiredApprovalIfInvalidRequiredApprovalIsConfigured() throws Exception {
+    InvalidPluginConfigurationException exception =
+        assertThrows(
+            InvalidPluginConfigurationException.class, () -> cfgSnapshot().getRequiredApproval());
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(
+            "Invalid configuration of the code-owners plugin. Required approval 'INVALID' that is"
+                + " configured in gerrit.config (parameter plugin.code-owners.requiredApproval) is"
+                + " invalid: Invalid format, expected '<label-name>+<label-value>'.");
+  }
+
+  @Test
+  public void getRequiredApprovalConfiguredOnProjectLevel() throws Exception {
+    configureRequiredApproval(project, "Code-Review+2");
+    RequiredApproval requiredApproval = cfgSnapshot().getRequiredApproval();
+    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).hasValueThat().isEqualTo(2);
+  }
+
+  @Test
+  public void getRequiredApprovalMultipleConfiguredOnProjectLevel() throws Exception {
+    setCodeOwnersConfig(
+        project,
+        /* subsection= */ null,
+        RequiredApprovalConfig.KEY_REQUIRED_APPROVAL,
+        ImmutableList.of("Code-Review+2", "Code-Review+1"));
+
+    // If multiple values are set for a key, the last value wins.
+    RequiredApproval requiredApproval = cfgSnapshot().getRequiredApproval();
+    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).hasValueThat().isEqualTo(1);
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.requiredApproval", value = "Code-Review+1")
+  public void requiredApprovalConfiguredOnProjectLevelOverridesDefaultRequiredApproval()
+      throws Exception {
+    configureRequiredApproval(project, "Code-Review+2");
+    RequiredApproval requiredApproval = cfgSnapshot().getRequiredApproval();
+    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).hasValueThat().isEqualTo(2);
+  }
+
+  @Test
+  public void requiredApprovalIsInheritedFromParentProject() throws Exception {
+    configureRequiredApproval(allProjects, "Code-Review+2");
+    RequiredApproval requiredApproval = cfgSnapshot().getRequiredApproval();
+    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).hasValueThat().isEqualTo(2);
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.backend", value = FindOwnersBackend.ID)
+  public void inheritedRequiredApprovalOverridesDefaultRequiredApproval() throws Exception {
+    configureRequiredApproval(allProjects, "Code-Review+2");
+    RequiredApproval requiredApproval = cfgSnapshot().getRequiredApproval();
+    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).hasValueThat().isEqualTo(2);
+  }
+
+  @Test
+  public void projectLevelRequiredApprovalOverridesInheritedRequiredApproval() throws Exception {
+    configureRequiredApproval(allProjects, "Code-Review+1");
+    configureRequiredApproval(project, "Code-Review+2");
+    RequiredApproval requiredApproval = cfgSnapshot().getRequiredApproval();
+    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).hasValueThat().isEqualTo(2);
+  }
+
+  @Test
+  public void
+      cannotGetRequiredApprovalIfNonExistingLabelIsConfiguredAsRequiredApprovalOnProjectLevel()
+          throws Exception {
+    configureRequiredApproval(project, "Foo-Bar+1");
+    InvalidPluginConfigurationException exception =
+        assertThrows(
+            InvalidPluginConfigurationException.class, () -> cfgSnapshot().getRequiredApproval());
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(
+            String.format(
+                "Invalid configuration of the code-owners plugin. Required approval 'Foo-Bar+1'"
+                    + " that is configured in code-owners.config (parameter"
+                    + " codeOwners.requiredApproval) is invalid: Label Foo-Bar doesn't exist for"
+                    + " project %s.",
+                project.get()));
+  }
+
+  @Test
+  public void
+      cannotGetRequiredApprovalIfNonExistingLabelValueIsConfiguredAsRequiredApprovalOnProjectLevel()
+          throws Exception {
+    configureRequiredApproval(project, "Code-Review+3");
+    InvalidPluginConfigurationException exception =
+        assertThrows(
+            InvalidPluginConfigurationException.class, () -> cfgSnapshot().getRequiredApproval());
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(
+            String.format(
+                "Invalid configuration of the code-owners plugin. Required approval"
+                    + " 'Code-Review+3' that is configured in code-owners.config (parameter"
+                    + " codeOwners.requiredApproval) is invalid: Label Code-Review on project %s"
+                    + " doesn't allow value 3.",
+                project.get()));
+  }
+
+  @Test
+  public void cannotGetRequiredApprovalIfInvalidRequiredApprovalIsConfiguredOnProjectLevel()
+      throws Exception {
+    configureRequiredApproval(project, "INVALID");
+    InvalidPluginConfigurationException exception =
+        assertThrows(
+            InvalidPluginConfigurationException.class, () -> cfgSnapshot().getRequiredApproval());
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(
+            "Invalid configuration of the code-owners plugin. Required approval 'INVALID' that is"
+                + " configured in code-owners.config (parameter codeOwners.requiredApproval) is"
+                + " invalid: Invalid format, expected '<label-name>+<label-value>'.");
+  }
+
+  @Test
+  public void projectLevelRequiredApprovalForOtherProjectHasNoEffect() throws Exception {
+    Project.NameKey otherProject = projectOperations.newProject().create();
+    configureRequiredApproval(otherProject, "Code-Review+2");
+    RequiredApproval requiredApproval = cfgSnapshot().getRequiredApproval();
+    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).hasValueThat().isEqualTo(1);
+  }
+
+  @Test
+  public void getOverrideApprovalWhenNoRequiredApprovalIsConfigured() throws Exception {
+    assertThat(cfgSnapshot().getOverrideApproval()).isEmpty();
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.overrideApproval", value = "Code-Review+2")
+  public void getConfiguredDefaultOverrideApproval() throws Exception {
+    ImmutableSet<RequiredApproval> requiredApproval = cfgSnapshot().getOverrideApproval();
+    assertThat(requiredApproval).hasSize(1);
+    assertThat(requiredApproval).element(0).hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).element(0).hasValueThat().isEqualTo(2);
+  }
+
+  @Test
+  public void getOverrideApprovalConfiguredOnProjectLevel() throws Exception {
+    configureOverrideApproval(project, "Code-Review+2");
+    ImmutableSet<RequiredApproval> requiredApproval = cfgSnapshot().getOverrideApproval();
+    assertThat(requiredApproval).hasSize(1);
+    assertThat(requiredApproval).element(0).hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).element(0).hasValueThat().isEqualTo(2);
+  }
+
+  @Test
+  public void getOverrideApprovalMultipleConfiguredOnProjectLevel() throws Exception {
+    createOwnersOverrideLabel();
+    createOwnersOverrideLabel("Other-Override");
+
+    setCodeOwnersConfig(
+        project,
+        /* subsection= */ null,
+        OverrideApprovalConfig.KEY_OVERRIDE_APPROVAL,
+        ImmutableList.of("Owners-Override+1", "Other-Override+1"));
+
+    ImmutableSet<RequiredApproval> requiredApprovals = cfgSnapshot().getOverrideApproval();
+    assertThat(
+            requiredApprovals.stream()
+                .map(requiredApproval -> requiredApproval.toString())
+                .collect(toImmutableSet()))
+        .containsExactly("Owners-Override+1", "Other-Override+1");
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.overrideApproval", value = "INVALID")
+  public void getOverrideApprovalIfInvalidOverrideApprovalIsConfigured() throws Exception {
+    assertThat(cfgSnapshot().getOverrideApproval()).isEmpty();
+  }
+
+  @Test
+  public void getOverrideApprovalIfInvalidOverrideApprovalIsConfiguredOnProjectLevel()
+      throws Exception {
+    configureOverrideApproval(project, "INVALID");
+    assertThat(cfgSnapshot().getOverrideApproval()).isEmpty();
+  }
+
+  @Test
+  public void getOverrideApprovalDuplicatesAreFilteredOut() throws Exception {
+    setCodeOwnersConfig(
+        project,
+        /* subsection= */ null,
+        OverrideApprovalConfig.KEY_OVERRIDE_APPROVAL,
+        ImmutableList.of("Code-Review+2", "Code-Review+1", "Code-Review+2"));
+
+    // If multiple values are set for a key, the last value wins.
+    ImmutableSet<RequiredApproval> requiredApproval = cfgSnapshot().getOverrideApproval();
+    assertThat(requiredApproval).hasSize(1);
+    assertThat(requiredApproval).element(0).hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).element(0).hasValueThat().isEqualTo(1);
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true")
+  public void implicitApprovalsAreDisabledIfRequiredLabelIgnoresSelfApprovals() throws Exception {
+    assertThat(cfgSnapshot().areImplicitApprovalsEnabled()).isTrue();
+
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.ignoreSelfApproval = true;
+    gApi.projects().name(allProjects.get()).label("Code-Review").update(input);
+    assertThat(cfgSnapshot().areImplicitApprovalsEnabled()).isFalse();
+  }
+
+  private CodeOwnersPluginConfigSnapshot cfgSnapshot() {
+    return codeOwnersPluginConfigSnapshotFactory.create(project);
+  }
+
+  private void configureFileExtension(Project.NameKey project, String fileExtension)
+      throws Exception {
+    setCodeOwnersConfig(
+        project, /* subsection= */ null, GeneralConfig.KEY_FILE_EXTENSION, fileExtension);
+  }
+
+  private void configureMergeCommitStrategy(
+      Project.NameKey project, MergeCommitStrategy mergeCommitStrategy) throws Exception {
+    setCodeOwnersConfig(
+        project,
+        /* subsection= */ null,
+        GeneralConfig.KEY_MERGE_COMMIT_STRATEGY,
+        mergeCommitStrategy.name());
+  }
+
+  private void configureFallbackCodeOwners(
+      Project.NameKey project, FallbackCodeOwners fallbackCodeOwners) throws Exception {
+    setCodeOwnersConfig(
+        project,
+        /* subsection= */ null,
+        GeneralConfig.KEY_FALLBACK_CODE_OWNERS,
+        fallbackCodeOwners.name());
+  }
+
+  private void configureMaxPathsInChangeMessages(
+      Project.NameKey project, int maxPathsInChangeMessages) throws Exception {
+    setCodeOwnersConfig(
+        project,
+        /* subsection= */ null,
+        GeneralConfig.KEY_MAX_PATHS_IN_CHANGE_MESSAGES,
+        Integer.toString(maxPathsInChangeMessages));
+  }
+
+  private void configureDisabled(Project.NameKey project, String disabled) throws Exception {
+    setCodeOwnersConfig(project, /* subsection= */ null, StatusConfig.KEY_DISABLED, disabled);
+  }
+
+  private void configureDisabledBranch(Project.NameKey project, String disabledBranch)
+      throws Exception {
+    setCodeOwnersConfig(
+        project, /* subsection= */ null, StatusConfig.KEY_DISABLED_BRANCH, disabledBranch);
+  }
+
+  private void enableCodeOwnersForAllBranches(Project.NameKey project) throws Exception {
+    setCodeOwnersConfig(project, /* subsection= */ null, StatusConfig.KEY_DISABLED_BRANCH, "");
+  }
+
+  private void configureBackend(Project.NameKey project, String backendName) throws Exception {
+    configureBackend(project, /* branch= */ null, backendName);
+  }
+
+  private void configureBackend(
+      Project.NameKey project, @Nullable String branch, String backendName) throws Exception {
+    setCodeOwnersConfig(project, branch, BackendConfig.KEY_BACKEND, backendName);
+  }
+
+  private void configureRequiredApproval(Project.NameKey project, String requiredApproval)
+      throws Exception {
+    setCodeOwnersConfig(
+        project,
+        /* subsection= */ null,
+        RequiredApprovalConfig.KEY_REQUIRED_APPROVAL,
+        requiredApproval);
+  }
+
+  private void configureOverrideApproval(Project.NameKey project, String requiredApproval)
+      throws Exception {
+    setCodeOwnersConfig(
+        project,
+        /* subsection= */ null,
+        OverrideApprovalConfig.KEY_OVERRIDE_APPROVAL,
+        requiredApproval);
+  }
+
+  private AutoCloseable registerTestBackend() {
+    RegistrationHandle registrationHandle =
+        ((PrivateInternals_DynamicMapImpl<CodeOwnerBackend>) codeOwnerBackends)
+            .put("gerrit", TestCodeOwnerBackend.ID, Providers.of(new TestCodeOwnerBackend()));
+    return registrationHandle::remove;
+  }
+
+  private static class TestCodeOwnerBackend implements CodeOwnerBackend {
+    static final String ID = "test-backend";
+
+    @Override
+    public Optional<CodeOwnerConfig> getCodeOwnerConfig(
+        CodeOwnerConfig.Key codeOwnerConfigKey,
+        @Nullable RevWalk revWalk,
+        @Nullable ObjectId revision) {
+      throw new UnsupportedOperationException("not implemented");
+    }
+
+    @Override
+    public Optional<CodeOwnerConfig> upsertCodeOwnerConfig(
+        CodeOwnerConfig.Key codeOwnerConfigKey,
+        CodeOwnerConfigUpdate codeOwnerConfigUpdate,
+        @Nullable IdentifiedUser currentUser) {
+      throw new UnsupportedOperationException("not implemented");
+    }
+
+    @Override
+    public boolean isCodeOwnerConfigFile(NameKey project, String fileName) {
+      throw new UnsupportedOperationException("not implemented");
+    }
+
+    @Override
+    public Path getFilePath(CodeOwnerConfig.Key codeOwnerConfigKey) {
+      throw new UnsupportedOperationException("not implemented");
+    }
+
+    @Override
+    public Optional<PathExpressionMatcher> getPathExpressionMatcher() {
+      return Optional.empty();
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigurationTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigurationTest.java
index 72d6d75..71c6849 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigurationTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigurationTest.java
@@ -14,42 +14,13 @@
 
 package com.google.gerrit.plugins.codeowners.backend.config;
 
-import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.plugins.codeowners.testing.RequiredApprovalSubject.assertThat;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
-import static com.google.gerrit.truth.OptionalSubject.assertThat;
 
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.acceptance.config.GerritConfig;
-import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.Project.NameKey;
-import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.extensions.common.LabelDefinitionInput;
-import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.extensions.registration.PrivateInternals_DynamicMapImpl;
-import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest;
-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.PathExpressionMatcher;
-import com.google.gerrit.plugins.codeowners.backend.findowners.FindOwnersBackend;
-import com.google.gerrit.plugins.codeowners.common.MergeCommitStrategy;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.inject.Inject;
-import com.google.inject.Key;
-import com.google.inject.util.Providers;
-import java.nio.file.Path;
-import java.util.Optional;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -58,408 +29,30 @@
  * com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration}.
  */
 public class CodeOwnersPluginConfigurationTest extends AbstractCodeOwnersTest {
-  @Inject private ProjectOperations projectOperations;
-
   private CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
-  private DynamicMap<CodeOwnerBackend> codeOwnerBackends;
 
   @Before
   public void setUpCodeOwnersPlugin() throws Exception {
     codeOwnersPluginConfiguration =
         plugin.getSysInjector().getInstance(CodeOwnersPluginConfiguration.class);
-    codeOwnerBackends =
-        plugin.getSysInjector().getInstance(new Key<DynamicMap<CodeOwnerBackend>>() {});
   }
 
   @Test
-  public void cannotCheckForNullProjectIfCodeOwnersFunctionalityIsDisabled() throws Exception {
+  public void cannotGetProjectConfigForNullProject() throws Exception {
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () -> codeOwnersPluginConfiguration.isDisabled(/* project= */ (Project.NameKey) null));
-    assertThat(npe).hasMessageThat().isEqualTo("project");
+            () -> codeOwnersPluginConfiguration.getProjectConfig(/* projectName= */ null));
+    assertThat(npe).hasMessageThat().isEqualTo("projectName");
   }
 
   @Test
-  public void cannotCheckForNullBranchIfCodeOwnersFunctionalityIsDisabled() throws Exception {
-    NullPointerException npe =
-        assertThrows(
-            NullPointerException.class,
-            () ->
-                codeOwnersPluginConfiguration.isDisabled(
-                    /* branchNameKey= */ (BranchNameKey) null));
-    assertThat(npe).hasMessageThat().isEqualTo("branchNameKey");
-  }
-
-  @Test
-  public void cannotCheckIfCodeOwnersFunctionalityIsDisabledForNonExistingProject()
-      throws Exception {
+  public void cannotGetProjectConfigForNonExistingProject() throws Exception {
     IllegalStateException exception =
         assertThrows(
             IllegalStateException.class,
             () ->
-                codeOwnersPluginConfiguration.isDisabled(Project.nameKey("non-existing-project")));
-    assertThat(exception)
-        .hasMessageThat()
-        .isEqualTo(
-            "cannot get code-owners plugin config for non-existing project non-existing-project");
-  }
-
-  @Test
-  public void cannotCheckIfCodeOwnersFunctionalityIsDisabledForBranchOfNonExistingProject()
-      throws Exception {
-    IllegalStateException exception =
-        assertThrows(
-            IllegalStateException.class,
-            () ->
-                codeOwnersPluginConfiguration.isDisabled(
-                    BranchNameKey.create(Project.nameKey("non-existing-project"), "master")));
-    assertThat(exception)
-        .hasMessageThat()
-        .isEqualTo(
-            "cannot get code-owners plugin config for non-existing project non-existing-project");
-  }
-
-  @Test
-  public void checkIfCodeOwnersFunctionalityIsDisabledForNonExistingBranch() throws Exception {
-    assertThat(
-            codeOwnersPluginConfiguration.isDisabled(BranchNameKey.create(project, "non-existing")))
-        .isFalse();
-  }
-
-  @Test
-  public void checkIfCodeOwnersFunctionalityIsDisabledForProjectWithEmptyConfig() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.isDisabled(project)).isFalse();
-  }
-
-  @Test
-  public void checkIfCodeOwnersFunctionalityIsDisabledForBranchWithEmptyConfig() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.isDisabled(BranchNameKey.create(project, "master")))
-        .isFalse();
-  }
-
-  @Test
-  public void codeOwnersFunctionalityIsDisabledForProject() throws Exception {
-    disableCodeOwnersForProject(project);
-    assertThat(codeOwnersPluginConfiguration.isDisabled(project)).isTrue();
-  }
-
-  @Test
-  public void codeOwnersFunctionalityIsDisabledForBranchIfItIsDisabledForProject()
-      throws Exception {
-    disableCodeOwnersForProject(project);
-    assertThat(codeOwnersPluginConfiguration.isDisabled(BranchNameKey.create(project, "master")))
-        .isTrue();
-  }
-
-  @Test
-  public void codeOwnersFunctionalityIsDisabledForBranch_exactRef() throws Exception {
-    configureDisabledBranch(project, "refs/heads/master");
-    assertThat(codeOwnersPluginConfiguration.isDisabled(BranchNameKey.create(project, "master")))
-        .isTrue();
-    assertThat(codeOwnersPluginConfiguration.isDisabled(BranchNameKey.create(project, "other")))
-        .isFalse();
-  }
-
-  @Test
-  public void codeOwnersFunctionalityIsDisabledForBranch_refPattern() throws Exception {
-    configureDisabledBranch(project, "refs/heads/*");
-    assertThat(codeOwnersPluginConfiguration.isDisabled(BranchNameKey.create(project, "master")))
-        .isTrue();
-    assertThat(codeOwnersPluginConfiguration.isDisabled(BranchNameKey.create(project, "other")))
-        .isTrue();
-    assertThat(
-            codeOwnersPluginConfiguration.isDisabled(
-                BranchNameKey.create(project, RefNames.REFS_META)))
-        .isFalse();
-  }
-
-  @Test
-  public void codeOwnersFunctionalityIsDisabledForBranch_regularExpression() throws Exception {
-    configureDisabledBranch(project, "^refs/heads/.*");
-    assertThat(codeOwnersPluginConfiguration.isDisabled(BranchNameKey.create(project, "master")))
-        .isTrue();
-    assertThat(codeOwnersPluginConfiguration.isDisabled(BranchNameKey.create(project, "other")))
-        .isTrue();
-    assertThat(
-            codeOwnersPluginConfiguration.isDisabled(
-                BranchNameKey.create(project, RefNames.REFS_META)))
-        .isFalse();
-  }
-
-  @Test
-  public void codeOwnersFunctionalityIsDisabledForBranch_invalidRegularExpression()
-      throws Exception {
-    configureDisabledBranch(project, "^refs/heads/[");
-    assertThat(codeOwnersPluginConfiguration.isDisabled(BranchNameKey.create(project, "master")))
-        .isFalse();
-  }
-
-  @Test
-  public void disabledIsInheritedFromParentProject() throws Exception {
-    disableCodeOwnersForProject(allProjects);
-    assertThat(codeOwnersPluginConfiguration.isDisabled(project)).isTrue();
-  }
-
-  @Test
-  public void inheritedDisabledAlsoCountsForBranch() throws Exception {
-    disableCodeOwnersForProject(allProjects);
-    assertThat(codeOwnersPluginConfiguration.isDisabled(BranchNameKey.create(project, "master")))
-        .isTrue();
-  }
-
-  @Test
-  public void inheritedDisabledValueIsIgnoredIfInvalid() throws Exception {
-    configureDisabled(project, "invalid");
-    assertThat(codeOwnersPluginConfiguration.isDisabled(project)).isFalse();
-  }
-
-  @Test
-  public void inheritedDisabledValueIsIgnoredForBranchIfInvalid() throws Exception {
-    configureDisabled(project, "invalid");
-    assertThat(codeOwnersPluginConfiguration.isDisabled(BranchNameKey.create(project, "master")))
-        .isFalse();
-  }
-
-  @Test
-  public void disabledForOtherProjectHasNoEffect() throws Exception {
-    Project.NameKey otherProject = projectOperations.newProject().create();
-    disableCodeOwnersForProject(otherProject);
-    assertThat(codeOwnersPluginConfiguration.isDisabled(project)).isFalse();
-  }
-
-  @Test
-  public void disabledBranchForOtherProjectHasNoEffect() throws Exception {
-    Project.NameKey otherProject = projectOperations.newProject().create();
-    configureDisabledBranch(otherProject, "refs/heads/master");
-    assertThat(codeOwnersPluginConfiguration.isDisabled(BranchNameKey.create(project, "master")))
-        .isFalse();
-  }
-
-  @Test
-  public void disabledBranchIsInheritedFromParentProject() throws Exception {
-    configureDisabledBranch(allProjects, "refs/heads/master");
-    assertThat(codeOwnersPluginConfiguration.isDisabled(BranchNameKey.create(project, "master")))
-        .isTrue();
-  }
-
-  @Test
-  public void inheritedDisabledCanBeOverridden() throws Exception {
-    disableCodeOwnersForProject(allProjects);
-    enableCodeOwnersForProject(project);
-    assertThat(codeOwnersPluginConfiguration.isDisabled(BranchNameKey.create(project, "master")))
-        .isFalse();
-  }
-
-  @Test
-  public void inheritedDisabledBranchCanBeOverridden() throws Exception {
-    configureDisabledBranch(allProjects, "refs/heads/master");
-    enableCodeOwnersForAllBranches(project);
-    assertThat(codeOwnersPluginConfiguration.isDisabled(BranchNameKey.create(project, "master")))
-        .isFalse();
-  }
-
-  @Test
-  public void cannotGetBackendForNonExistingProject() throws Exception {
-    IllegalStateException exception =
-        assertThrows(
-            IllegalStateException.class,
-            () ->
-                codeOwnersPluginConfiguration.getBackend(
-                    BranchNameKey.create(Project.nameKey("non-existing-project"), "master")));
-    assertThat(exception)
-        .hasMessageThat()
-        .isEqualTo(
-            "cannot get code-owners plugin config for non-existing project non-existing-project");
-  }
-
-  @Test
-  public void getBackendForNonExistingBranch() throws Exception {
-    assertThat(
-            codeOwnersPluginConfiguration.getBackend(BranchNameKey.create(project, "non-existing")))
-        .isInstanceOf(FindOwnersBackend.class);
-  }
-
-  @Test
-  public void getDefaultBackendWhenNoBackendIsConfigured() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.getBackend(BranchNameKey.create(project, "master")))
-        .isInstanceOf(FindOwnersBackend.class);
-  }
-
-  @Test
-  @GerritConfig(name = "plugin.code-owners.backend", value = TestCodeOwnerBackend.ID)
-  public void getConfiguredDefaultBackend() throws Exception {
-    try (AutoCloseable registration = registerTestBackend()) {
-      assertThat(codeOwnersPluginConfiguration.getBackend(BranchNameKey.create(project, "master")))
-          .isInstanceOf(TestCodeOwnerBackend.class);
-    }
-  }
-
-  @Test
-  @GerritConfig(name = "plugin.code-owners.backend", value = "non-existing-backend")
-  public void cannotGetBackendIfNonExistingBackendIsConfigured() throws Exception {
-    InvalidPluginConfigurationException exception =
-        assertThrows(
-            InvalidPluginConfigurationException.class,
-            () ->
-                codeOwnersPluginConfiguration.getBackend(BranchNameKey.create(project, "master")));
-    assertThat(exception)
-        .hasMessageThat()
-        .isEqualTo(
-            "Invalid configuration of the code-owners plugin. Code owner backend"
-                + " 'non-existing-backend' that is configured in gerrit.config (parameter"
-                + " plugin.code-owners.backend) not found.");
-  }
-
-  @Test
-  public void getBackendConfiguredOnProjectLevel() throws Exception {
-    configureBackend(project, TestCodeOwnerBackend.ID);
-    try (AutoCloseable registration = registerTestBackend()) {
-      assertThat(codeOwnersPluginConfiguration.getBackend(BranchNameKey.create(project, "master")))
-          .isInstanceOf(TestCodeOwnerBackend.class);
-    }
-  }
-
-  @Test
-  @GerritConfig(name = "plugin.code-owners.backend", value = FindOwnersBackend.ID)
-  public void backendConfiguredOnProjectLevelOverridesDefaultBackend() throws Exception {
-    configureBackend(project, TestCodeOwnerBackend.ID);
-    try (AutoCloseable registration = registerTestBackend()) {
-      assertThat(codeOwnersPluginConfiguration.getBackend(BranchNameKey.create(project, "master")))
-          .isInstanceOf(TestCodeOwnerBackend.class);
-    }
-  }
-
-  @Test
-  public void backendIsInheritedFromParentProject() throws Exception {
-    configureBackend(allProjects, TestCodeOwnerBackend.ID);
-    try (AutoCloseable registration = registerTestBackend()) {
-      assertThat(codeOwnersPluginConfiguration.getBackend(BranchNameKey.create(project, "master")))
-          .isInstanceOf(TestCodeOwnerBackend.class);
-    }
-  }
-
-  @Test
-  @GerritConfig(name = "plugin.code-owners.backend", value = FindOwnersBackend.ID)
-  public void inheritedBackendOverridesDefaultBackend() throws Exception {
-    configureBackend(allProjects, TestCodeOwnerBackend.ID);
-    try (AutoCloseable registration = registerTestBackend()) {
-      assertThat(codeOwnersPluginConfiguration.getBackend(BranchNameKey.create(project, "master")))
-          .isInstanceOf(TestCodeOwnerBackend.class);
-    }
-  }
-
-  @Test
-  public void projectLevelBackendOverridesInheritedBackend() throws Exception {
-    configureBackend(allProjects, TestCodeOwnerBackend.ID);
-    configureBackend(project, FindOwnersBackend.ID);
-    try (AutoCloseable registration = registerTestBackend()) {
-      assertThat(codeOwnersPluginConfiguration.getBackend(BranchNameKey.create(project, "master")))
-          .isInstanceOf(FindOwnersBackend.class);
-    }
-  }
-
-  @Test
-  public void cannotGetBackendIfNonExistingBackendIsConfiguredOnProjectLevel() throws Exception {
-    configureBackend(project, "non-existing-backend");
-    InvalidPluginConfigurationException exception =
-        assertThrows(
-            InvalidPluginConfigurationException.class,
-            () ->
-                codeOwnersPluginConfiguration.getBackend(BranchNameKey.create(project, "master")));
-    assertThat(exception)
-        .hasMessageThat()
-        .isEqualTo(
-            String.format(
-                "Invalid configuration of the code-owners plugin. Code owner backend"
-                    + " 'non-existing-backend' that is configured for project %s in"
-                    + " code-owners.config (parameter codeOwners.backend) not found.",
-                project));
-  }
-
-  @Test
-  public void projectLevelBackendForOtherProjectHasNoEffect() throws Exception {
-    Project.NameKey otherProject = projectOperations.newProject().create();
-    configureBackend(otherProject, TestCodeOwnerBackend.ID);
-    try (AutoCloseable registration = registerTestBackend()) {
-      assertThat(codeOwnersPluginConfiguration.getBackend(BranchNameKey.create(project, "master")))
-          .isInstanceOf(FindOwnersBackend.class);
-    }
-  }
-
-  @Test
-  public void getBackendConfiguredOnBranchLevel() throws Exception {
-    configureBackend(project, "refs/heads/master", TestCodeOwnerBackend.ID);
-    try (AutoCloseable registration = registerTestBackend()) {
-      assertThat(codeOwnersPluginConfiguration.getBackend(BranchNameKey.create(project, "master")))
-          .isInstanceOf(TestCodeOwnerBackend.class);
-    }
-  }
-
-  @Test
-  public void getBackendConfiguredOnBranchLevelShortName() throws Exception {
-    configureBackend(project, "master", TestCodeOwnerBackend.ID);
-    try (AutoCloseable registration = registerTestBackend()) {
-      assertThat(codeOwnersPluginConfiguration.getBackend(BranchNameKey.create(project, "master")))
-          .isInstanceOf(TestCodeOwnerBackend.class);
-    }
-  }
-
-  @Test
-  public void branchLevelBackendOnFullNameTakesPrecedenceOverBranchLevelBackendOnShortName()
-      throws Exception {
-    configureBackend(project, "master", TestCodeOwnerBackend.ID);
-    configureBackend(project, "refs/heads/master", FindOwnersBackend.ID);
-    try (AutoCloseable registration = registerTestBackend()) {
-      assertThat(codeOwnersPluginConfiguration.getBackend(BranchNameKey.create(project, "master")))
-          .isInstanceOf(FindOwnersBackend.class);
-    }
-  }
-
-  @Test
-  public void branchLevelBackendOverridesProjectLevelBackend() throws Exception {
-    configureBackend(project, TestCodeOwnerBackend.ID);
-    configureBackend(project, "master", FindOwnersBackend.ID);
-    try (AutoCloseable registration = registerTestBackend()) {
-      assertThat(codeOwnersPluginConfiguration.getBackend(BranchNameKey.create(project, "master")))
-          .isInstanceOf(FindOwnersBackend.class);
-    }
-  }
-
-  @Test
-  public void cannotGetBackendIfNonExistingBackendIsConfiguredOnBranchLevel() throws Exception {
-    configureBackend(project, "master", "non-existing-backend");
-    InvalidPluginConfigurationException exception =
-        assertThrows(
-            InvalidPluginConfigurationException.class,
-            () ->
-                codeOwnersPluginConfiguration.getBackend(BranchNameKey.create(project, "master")));
-    assertThat(exception)
-        .hasMessageThat()
-        .isEqualTo(
-            String.format(
-                "Invalid configuration of the code-owners plugin. Code owner backend"
-                    + " 'non-existing-backend' that is configured for project %s in"
-                    + " code-owners.config (parameter codeOwners.master.backend) not found.",
-                project));
-  }
-
-  @Test
-  public void branchLevelBackendForOtherBranchHasNoEffect() throws Exception {
-    configureBackend(project, "foo", TestCodeOwnerBackend.ID);
-    try (AutoCloseable registration = registerTestBackend()) {
-      assertThat(codeOwnersPluginConfiguration.getBackend(BranchNameKey.create(project, "master")))
-          .isInstanceOf(FindOwnersBackend.class);
-    }
-  }
-
-  @Test
-  public void cannotGetRequiredApprovalForNonExistingProject() throws Exception {
-    IllegalStateException exception =
-        assertThrows(
-            IllegalStateException.class,
-            () ->
-                codeOwnersPluginConfiguration.getRequiredApproval(
+                codeOwnersPluginConfiguration.getProjectConfig(
                     Project.nameKey("non-existing-project")));
     assertThat(exception)
         .hasMessageThat()
@@ -468,285 +61,6 @@
   }
 
   @Test
-  public void getDefaultRequiredApprovalWhenNoRequiredApprovalIsConfigured() throws Exception {
-    RequiredApproval requiredApproval = codeOwnersPluginConfiguration.getRequiredApproval(project);
-    assertThat(requiredApproval).hasLabelNameThat().isEqualTo(RequiredApprovalConfig.DEFAULT_LABEL);
-    assertThat(requiredApproval).hasValueThat().isEqualTo(RequiredApprovalConfig.DEFAULT_VALUE);
-  }
-
-  @Test
-  @GerritConfig(name = "plugin.code-owners.requiredApproval", value = "Code-Review+2")
-  public void getConfiguredDefaultRequireApproval() throws Exception {
-    RequiredApproval requiredApproval = codeOwnersPluginConfiguration.getRequiredApproval(project);
-    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
-    assertThat(requiredApproval).hasValueThat().isEqualTo(2);
-  }
-
-  @Test
-  @GerritConfig(name = "plugin.code-owners.requiredApproval", value = "Foo-Bar+1")
-  public void cannotGetRequiredApprovalIfNonExistingLabelIsConfiguredAsRequiredApproval()
-      throws Exception {
-    InvalidPluginConfigurationException exception =
-        assertThrows(
-            InvalidPluginConfigurationException.class,
-            () -> codeOwnersPluginConfiguration.getRequiredApproval(project));
-    assertThat(exception)
-        .hasMessageThat()
-        .isEqualTo(
-            String.format(
-                "Invalid configuration of the code-owners plugin. Required approval 'Foo-Bar+1'"
-                    + " that is configured in gerrit.config (parameter"
-                    + " plugin.code-owners.requiredApproval) is invalid: Label Foo-Bar doesn't exist"
-                    + " for project %s.",
-                project.get()));
-  }
-
-  @Test
-  @GerritConfig(name = "plugin.code-owners.requiredApproval", value = "Code-Review+3")
-  public void cannotGetRequiredApprovalIfNonExistingLabelValueIsConfiguredAsRequiredApproval()
-      throws Exception {
-    InvalidPluginConfigurationException exception =
-        assertThrows(
-            InvalidPluginConfigurationException.class,
-            () -> codeOwnersPluginConfiguration.getRequiredApproval(project));
-    assertThat(exception)
-        .hasMessageThat()
-        .isEqualTo(
-            String.format(
-                "Invalid configuration of the code-owners plugin. Required approval"
-                    + " 'Code-Review+3' that is configured in gerrit.config (parameter"
-                    + " plugin.code-owners.requiredApproval) is invalid: Label Code-Review on"
-                    + " project %s doesn't allow value 3.",
-                project.get()));
-  }
-
-  @Test
-  @GerritConfig(name = "plugin.code-owners.requiredApproval", value = "INVALID")
-  public void cannotGetRequiredApprovalIfInvalidRequiredApprovalIsConfigured() throws Exception {
-    InvalidPluginConfigurationException exception =
-        assertThrows(
-            InvalidPluginConfigurationException.class,
-            () -> codeOwnersPluginConfiguration.getRequiredApproval(project));
-    assertThat(exception)
-        .hasMessageThat()
-        .isEqualTo(
-            "Invalid configuration of the code-owners plugin. Required approval 'INVALID' that is"
-                + " configured in gerrit.config (parameter plugin.code-owners.requiredApproval) is"
-                + " invalid: Invalid format, expected '<label-name>+<label-value>'.");
-  }
-
-  @Test
-  public void getRequiredApprovalConfiguredOnProjectLevel() throws Exception {
-    configureRequiredApproval(project, "Code-Review+2");
-    RequiredApproval requiredApproval = codeOwnersPluginConfiguration.getRequiredApproval(project);
-    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
-    assertThat(requiredApproval).hasValueThat().isEqualTo(2);
-  }
-
-  @Test
-  public void getRequiredApprovalMultipleConfiguredOnProjectLevel() throws Exception {
-    setCodeOwnersConfig(
-        project,
-        /* subsection= */ null,
-        RequiredApprovalConfig.KEY_REQUIRED_APPROVAL,
-        ImmutableList.of("Code-Review+2", "Code-Review+1"));
-
-    // If multiple values are set for a key, the last value wins.
-    RequiredApproval requiredApproval = codeOwnersPluginConfiguration.getRequiredApproval(project);
-    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
-    assertThat(requiredApproval).hasValueThat().isEqualTo(1);
-  }
-
-  @Test
-  @GerritConfig(name = "plugin.code-owners.requiredApproval", value = "Code-Review+1")
-  public void requiredApprovalConfiguredOnProjectLevelOverridesDefaultRequiredApproval()
-      throws Exception {
-    configureRequiredApproval(project, "Code-Review+2");
-    RequiredApproval requiredApproval = codeOwnersPluginConfiguration.getRequiredApproval(project);
-    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
-    assertThat(requiredApproval).hasValueThat().isEqualTo(2);
-  }
-
-  @Test
-  public void requiredApprovalIsInheritedFromParentProject() throws Exception {
-    configureRequiredApproval(allProjects, "Code-Review+2");
-    RequiredApproval requiredApproval = codeOwnersPluginConfiguration.getRequiredApproval(project);
-    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
-    assertThat(requiredApproval).hasValueThat().isEqualTo(2);
-  }
-
-  @Test
-  @GerritConfig(name = "plugin.code-owners.backend", value = FindOwnersBackend.ID)
-  public void inheritedRequiredApprovalOverridesDefaultRequiredApproval() throws Exception {
-    configureRequiredApproval(allProjects, "Code-Review+2");
-    RequiredApproval requiredApproval = codeOwnersPluginConfiguration.getRequiredApproval(project);
-    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
-    assertThat(requiredApproval).hasValueThat().isEqualTo(2);
-  }
-
-  @Test
-  public void projectLevelRequiredApprovalOverridesInheritedRequiredApproval() throws Exception {
-    configureRequiredApproval(allProjects, "Code-Review+1");
-    configureRequiredApproval(project, "Code-Review+2");
-    RequiredApproval requiredApproval = codeOwnersPluginConfiguration.getRequiredApproval(project);
-    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
-    assertThat(requiredApproval).hasValueThat().isEqualTo(2);
-  }
-
-  @Test
-  public void
-      cannotGetRequiredApprovalIfNonExistingLabelIsConfiguredAsRequiredApprovalOnProjectLevel()
-          throws Exception {
-    configureRequiredApproval(project, "Foo-Bar+1");
-    InvalidPluginConfigurationException exception =
-        assertThrows(
-            InvalidPluginConfigurationException.class,
-            () -> codeOwnersPluginConfiguration.getRequiredApproval(project));
-    assertThat(exception)
-        .hasMessageThat()
-        .isEqualTo(
-            String.format(
-                "Invalid configuration of the code-owners plugin. Required approval 'Foo-Bar+1'"
-                    + " that is configured in code-owners.config (parameter"
-                    + " codeOwners.requiredApproval) is invalid: Label Foo-Bar doesn't exist for"
-                    + " project %s.",
-                project.get()));
-  }
-
-  @Test
-  public void
-      cannotGetRequiredApprovalIfNonExistingLabelValueIsConfiguredAsRequiredApprovalOnProjectLevel()
-          throws Exception {
-    configureRequiredApproval(project, "Code-Review+3");
-    InvalidPluginConfigurationException exception =
-        assertThrows(
-            InvalidPluginConfigurationException.class,
-            () -> codeOwnersPluginConfiguration.getRequiredApproval(project));
-    assertThat(exception)
-        .hasMessageThat()
-        .isEqualTo(
-            String.format(
-                "Invalid configuration of the code-owners plugin. Required approval"
-                    + " 'Code-Review+3' that is configured in code-owners.config (parameter"
-                    + " codeOwners.requiredApproval) is invalid: Label Code-Review on project %s"
-                    + " doesn't allow value 3.",
-                project.get()));
-  }
-
-  @Test
-  public void cannotGetRequiredApprovalIfInvalidRequiredApprovalIsConfiguredOnProjectLevel()
-      throws Exception {
-    configureRequiredApproval(project, "INVALID");
-    InvalidPluginConfigurationException exception =
-        assertThrows(
-            InvalidPluginConfigurationException.class,
-            () -> codeOwnersPluginConfiguration.getRequiredApproval(project));
-    assertThat(exception)
-        .hasMessageThat()
-        .isEqualTo(
-            "Invalid configuration of the code-owners plugin. Required approval 'INVALID' that is"
-                + " configured in code-owners.config (parameter codeOwners.requiredApproval) is"
-                + " invalid: Invalid format, expected '<label-name>+<label-value>'.");
-  }
-
-  @Test
-  public void projectLevelRequiredApprovalForOtherProjectHasNoEffect() throws Exception {
-    Project.NameKey otherProject = projectOperations.newProject().create();
-    configureRequiredApproval(otherProject, "Code-Review+2");
-    RequiredApproval requiredApproval = codeOwnersPluginConfiguration.getRequiredApproval(project);
-    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
-    assertThat(requiredApproval).hasValueThat().isEqualTo(1);
-  }
-
-  @Test
-  public void cannotGetOverrideApprovalForNonExistingProject() throws Exception {
-    IllegalStateException exception =
-        assertThrows(
-            IllegalStateException.class,
-            () ->
-                codeOwnersPluginConfiguration.getOverrideApproval(
-                    Project.nameKey("non-existing-project")));
-    assertThat(exception)
-        .hasMessageThat()
-        .isEqualTo(
-            "cannot get code-owners plugin config for non-existing project non-existing-project");
-  }
-
-  @Test
-  public void getOverrideApprovalWhenNoRequiredApprovalIsConfigured() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.getOverrideApproval(project)).isEmpty();
-  }
-
-  @Test
-  @GerritConfig(name = "plugin.code-owners.overrideApproval", value = "Code-Review+2")
-  public void getConfiguredDefaultOverrideApproval() throws Exception {
-    ImmutableSet<RequiredApproval> requiredApproval =
-        codeOwnersPluginConfiguration.getOverrideApproval(project);
-    assertThat(requiredApproval).hasSize(1);
-    assertThat(requiredApproval).element(0).hasLabelNameThat().isEqualTo("Code-Review");
-    assertThat(requiredApproval).element(0).hasValueThat().isEqualTo(2);
-  }
-
-  @Test
-  public void getOverrideApprovalConfiguredOnProjectLevel() throws Exception {
-    configureOverrideApproval(project, "Code-Review+2");
-    ImmutableSet<RequiredApproval> requiredApproval =
-        codeOwnersPluginConfiguration.getOverrideApproval(project);
-    assertThat(requiredApproval).hasSize(1);
-    assertThat(requiredApproval).element(0).hasLabelNameThat().isEqualTo("Code-Review");
-    assertThat(requiredApproval).element(0).hasValueThat().isEqualTo(2);
-  }
-
-  @Test
-  public void getOverrideApprovalMultipleConfiguredOnProjectLevel() throws Exception {
-    createOwnersOverrideLabel();
-    createOwnersOverrideLabel("Other-Override");
-
-    setCodeOwnersConfig(
-        project,
-        /* subsection= */ null,
-        OverrideApprovalConfig.KEY_OVERRIDE_APPROVAL,
-        ImmutableList.of("Owners-Override+1", "Other-Override+1"));
-
-    ImmutableSet<RequiredApproval> requiredApprovals =
-        codeOwnersPluginConfiguration.getOverrideApproval(project);
-    assertThat(
-            requiredApprovals.stream()
-                .map(requiredApproval -> requiredApproval.toString())
-                .collect(toImmutableSet()))
-        .containsExactly("Owners-Override+1", "Other-Override+1");
-  }
-
-  @Test
-  @GerritConfig(name = "plugin.code-owners.overrideApproval", value = "INVALID")
-  public void getOverrideApprovalIfInvalidOverrideApprovalIsConfigured() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.getOverrideApproval(project)).isEmpty();
-  }
-
-  @Test
-  public void getOverrideApprovalIfInvalidOverrideApprovalIsConfiguredOnProjectLevel()
-      throws Exception {
-    configureOverrideApproval(project, "INVALID");
-    assertThat(codeOwnersPluginConfiguration.getOverrideApproval(project)).isEmpty();
-  }
-
-  @Test
-  public void getOverrideApprovalDuplicatesAreFilteredOut() throws Exception {
-    setCodeOwnersConfig(
-        project,
-        /* subsection= */ null,
-        OverrideApprovalConfig.KEY_OVERRIDE_APPROVAL,
-        ImmutableList.of("Code-Review+2", "Code-Review+1", "Code-Review+2"));
-
-    // If multiple values are set for a key, the last value wins.
-    ImmutableSet<RequiredApproval> requiredApproval =
-        codeOwnersPluginConfiguration.getOverrideApproval(project);
-    assertThat(requiredApproval).hasSize(1);
-    assertThat(requiredApproval).element(0).hasLabelNameThat().isEqualTo("Code-Review");
-    assertThat(requiredApproval).element(0).hasValueThat().isEqualTo(1);
-  }
-
-  @Test
   @GerritConfig(name = "plugin.code-owners.enableExperimentalRestEndpoints", value = "false")
   public void checkExperimentalRestEndpointsEnabledThrowsExceptionIfDisabled() throws Exception {
     MethodNotAllowedException exception =
@@ -774,315 +88,4 @@
   public void experimentalRestEndpointsNotEnabled_invalidConfig() throws Exception {
     assertThat(codeOwnersPluginConfiguration.areExperimentalRestEndpointsEnabled()).isFalse();
   }
-
-  @Test
-  public void cannotGetFileExtensionForNullProject() throws Exception {
-    NullPointerException npe =
-        assertThrows(
-            NullPointerException.class,
-            () -> codeOwnersPluginConfiguration.getFileExtension(/* project= */ null));
-    assertThat(npe).hasMessageThat().isEqualTo("project");
-  }
-
-  @Test
-  public void getFileExtensionIfNoneIsConfigured() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.getFileExtension(project)).isEmpty();
-  }
-
-  @Test
-  @GerritConfig(name = "plugin.code-owners.fileExtension", value = "foo")
-  public void getFileExtensionIfNoneIsConfiguredOnProjectLevel() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.getFileExtension(project)).value().isEqualTo("foo");
-  }
-
-  @Test
-  @GerritConfig(name = "plugin.code-owners.fileExtension", value = "foo")
-  public void fileExtensionOnProjectLevelOverridesDefaultFileExtension() throws Exception {
-    configureFileExtension(project, "bar");
-    assertThat(codeOwnersPluginConfiguration.getFileExtension(project)).value().isEqualTo("bar");
-  }
-
-  @Test
-  @GerritConfig(name = "plugin.code-owners.fileExtension", value = "foo")
-  public void fileExtensionIsInheritedFromParentProject() throws Exception {
-    configureFileExtension(allProjects, "bar");
-    assertThat(codeOwnersPluginConfiguration.getFileExtension(project)).value().isEqualTo("bar");
-  }
-
-  @Test
-  public void inheritedFileExtensionCanBeOverridden() throws Exception {
-    configureFileExtension(allProjects, "foo");
-    configureFileExtension(project, "bar");
-    assertThat(codeOwnersPluginConfiguration.getFileExtension(project)).value().isEqualTo("bar");
-  }
-
-  @Test
-  public void cannotGetMergeCommitStrategyForNullProject() throws Exception {
-    NullPointerException npe =
-        assertThrows(
-            NullPointerException.class,
-            () -> codeOwnersPluginConfiguration.getMergeCommitStrategy(/* project= */ null));
-    assertThat(npe).hasMessageThat().isEqualTo("project");
-  }
-
-  @Test
-  public void getMergeCommitStrategyIfNoneIsConfigured() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.getMergeCommitStrategy(project))
-        .isEqualTo(MergeCommitStrategy.ALL_CHANGED_FILES);
-  }
-
-  @Test
-  @GerritConfig(
-      name = "plugin.code-owners.mergeCommitStrategy",
-      value = "FILES_WITH_CONFLICT_RESOLUTION")
-  public void getMergeCommitStrategyIfNoneIsConfiguredOnProjectLevel() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.getMergeCommitStrategy(project))
-        .isEqualTo(MergeCommitStrategy.FILES_WITH_CONFLICT_RESOLUTION);
-  }
-
-  @Test
-  @GerritConfig(
-      name = "plugin.code-owners.mergeCommitStrategy",
-      value = "FILES_WITH_CONFLICT_RESOLUTION")
-  public void mergeCommitStrategyOnProjectLevelOverridesGlobalMergeCommitStrategy()
-      throws Exception {
-    configureMergeCommitStrategy(project, MergeCommitStrategy.ALL_CHANGED_FILES);
-    assertThat(codeOwnersPluginConfiguration.getMergeCommitStrategy(project))
-        .isEqualTo(MergeCommitStrategy.ALL_CHANGED_FILES);
-  }
-
-  @Test
-  @GerritConfig(
-      name = "plugin.code-owners.mergeCommitStrategy",
-      value = "FILES_WITH_CONFLICT_RESOLUTION")
-  public void mergeCommitStrategyIsInheritedFromParentProject() throws Exception {
-    configureMergeCommitStrategy(allProjects, MergeCommitStrategy.ALL_CHANGED_FILES);
-    assertThat(codeOwnersPluginConfiguration.getMergeCommitStrategy(project))
-        .isEqualTo(MergeCommitStrategy.ALL_CHANGED_FILES);
-  }
-
-  @Test
-  public void inheritedMergeCommitStrategyCanBeOverridden() throws Exception {
-    configureMergeCommitStrategy(allProjects, MergeCommitStrategy.FILES_WITH_CONFLICT_RESOLUTION);
-    configureMergeCommitStrategy(project, MergeCommitStrategy.ALL_CHANGED_FILES);
-    assertThat(codeOwnersPluginConfiguration.getMergeCommitStrategy(project))
-        .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);
-  }
-
-  @Test
-  @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true")
-  public void implicitApprovalsAreDisabledIfRequiredLabelIgnoresSelfApprovals() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.areImplicitApprovalsEnabled(project)).isTrue();
-
-    LabelDefinitionInput input = new LabelDefinitionInput();
-    input.ignoreSelfApproval = true;
-    gApi.projects().name(allProjects.get()).label("Code-Review").update(input);
-    assertThat(codeOwnersPluginConfiguration.areImplicitApprovalsEnabled(project)).isFalse();
-  }
-
-  @Test
-  public void cannotGetMaxPathsInChangeMessagesForNullProject() throws Exception {
-    NullPointerException npe =
-        assertThrows(
-            NullPointerException.class,
-            () -> codeOwnersPluginConfiguration.getMaxPathsInChangeMessages(/* project= */ null));
-    assertThat(npe).hasMessageThat().isEqualTo("project");
-  }
-
-  @Test
-  public void getMaxPathsInChangeMessagesIfNoneIsConfigured() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.getMaxPathsInChangeMessages(project))
-        .isEqualTo(GeneralConfig.DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES);
-  }
-
-  @Test
-  @GerritConfig(name = "plugin.code-owners.maxPathsInChangeMessages", value = "50")
-  public void getMaxPathsInChangeMessagesIfNoneIsConfiguredOnProjectLevel() throws Exception {
-    assertThat(codeOwnersPluginConfiguration.getMaxPathsInChangeMessages(project)).isEqualTo(50);
-  }
-
-  @Test
-  @GerritConfig(name = "plugin.code-owners.maxPathsInChangeMessages", value = "50")
-  public void maxPathInChangeMessagesOnProjectLevelOverridesGlobalMaxPathInChangeMessages()
-      throws Exception {
-    configureFallbackCodeOwners(project, FallbackCodeOwners.NONE);
-    assertThat(codeOwnersPluginConfiguration.getFallbackCodeOwners(project))
-        .isEqualTo(FallbackCodeOwners.NONE);
-  }
-
-  @Test
-  @GerritConfig(name = "plugin.code-owners.maxPathsInChangeMessages", value = "50")
-  public void maxPathInChangeMessagesIsInheritedFromParentProject() throws Exception {
-    configureMaxPathsInChangeMessages(allProjects, 20);
-    assertThat(codeOwnersPluginConfiguration.getMaxPathsInChangeMessages(project)).isEqualTo(20);
-  }
-
-  @Test
-  public void inheritedMaxPathInChangeMessagesCanBeOverridden() throws Exception {
-    configureMaxPathsInChangeMessages(allProjects, 50);
-    configureMaxPathsInChangeMessages(project, 20);
-    assertThat(codeOwnersPluginConfiguration.getMaxPathsInChangeMessages(project)).isEqualTo(20);
-  }
-
-  private void configureDisabled(Project.NameKey project, String disabled) throws Exception {
-    setCodeOwnersConfig(project, /* subsection= */ null, StatusConfig.KEY_DISABLED, disabled);
-  }
-
-  private void configureDisabledBranch(Project.NameKey project, String disabledBranch)
-      throws Exception {
-    setCodeOwnersConfig(
-        project, /* subsection= */ null, StatusConfig.KEY_DISABLED_BRANCH, disabledBranch);
-  }
-
-  private void enableCodeOwnersForAllBranches(Project.NameKey project) throws Exception {
-    setCodeOwnersConfig(project, /* subsection= */ null, StatusConfig.KEY_DISABLED_BRANCH, "");
-  }
-
-  private void configureBackend(Project.NameKey project, String backendName) throws Exception {
-    configureBackend(project, /* branch= */ null, backendName);
-  }
-
-  private void configureBackend(
-      Project.NameKey project, @Nullable String branch, String backendName) throws Exception {
-    setCodeOwnersConfig(project, branch, BackendConfig.KEY_BACKEND, backendName);
-  }
-
-  private void configureRequiredApproval(Project.NameKey project, String requiredApproval)
-      throws Exception {
-    setCodeOwnersConfig(
-        project,
-        /* subsection= */ null,
-        RequiredApprovalConfig.KEY_REQUIRED_APPROVAL,
-        requiredApproval);
-  }
-
-  private void configureOverrideApproval(Project.NameKey project, String requiredApproval)
-      throws Exception {
-    setCodeOwnersConfig(
-        project,
-        /* subsection= */ null,
-        OverrideApprovalConfig.KEY_OVERRIDE_APPROVAL,
-        requiredApproval);
-  }
-
-  private void configureFileExtension(Project.NameKey project, String fileExtension)
-      throws Exception {
-    setCodeOwnersConfig(
-        project, /* subsection= */ null, GeneralConfig.KEY_FILE_EXTENSION, fileExtension);
-  }
-
-  private void configureMergeCommitStrategy(
-      Project.NameKey project, MergeCommitStrategy mergeCommitStrategy) throws Exception {
-    setCodeOwnersConfig(
-        project,
-        /* subsection= */ null,
-        GeneralConfig.KEY_MERGE_COMMIT_STRATEGY,
-        mergeCommitStrategy.name());
-  }
-
-  private void configureFallbackCodeOwners(
-      Project.NameKey project, FallbackCodeOwners fallbackCodeOwners) throws Exception {
-    setCodeOwnersConfig(
-        project,
-        /* subsection= */ null,
-        GeneralConfig.KEY_FALLBACK_CODE_OWNERS,
-        fallbackCodeOwners.name());
-  }
-
-  private void configureMaxPathsInChangeMessages(
-      Project.NameKey project, int maxPathsInChangeMessages) throws Exception {
-    setCodeOwnersConfig(
-        project,
-        /* subsection= */ null,
-        GeneralConfig.KEY_MAX_PATHS_IN_CHANGE_MESSAGES,
-        Integer.toString(maxPathsInChangeMessages));
-  }
-
-  private AutoCloseable registerTestBackend() {
-    RegistrationHandle registrationHandle =
-        ((PrivateInternals_DynamicMapImpl<CodeOwnerBackend>) codeOwnerBackends)
-            .put("gerrit", TestCodeOwnerBackend.ID, Providers.of(new TestCodeOwnerBackend()));
-    return registrationHandle::remove;
-  }
-
-  private static class TestCodeOwnerBackend implements CodeOwnerBackend {
-    static final String ID = "test-backend";
-
-    @Override
-    public Optional<CodeOwnerConfig> getCodeOwnerConfig(
-        CodeOwnerConfig.Key codeOwnerConfigKey,
-        @Nullable RevWalk revWalk,
-        @Nullable ObjectId revision) {
-      throw new UnsupportedOperationException("not implemented");
-    }
-
-    @Override
-    public Optional<CodeOwnerConfig> upsertCodeOwnerConfig(
-        CodeOwnerConfig.Key codeOwnerConfigKey,
-        CodeOwnerConfigUpdate codeOwnerConfigUpdate,
-        @Nullable IdentifiedUser currentUser) {
-      throw new UnsupportedOperationException("not implemented");
-    }
-
-    @Override
-    public boolean isCodeOwnerConfigFile(NameKey project, String fileName) {
-      throw new UnsupportedOperationException("not implemented");
-    }
-
-    @Override
-    public Path getFilePath(CodeOwnerConfig.Key codeOwnerConfigKey) {
-      throw new UnsupportedOperationException("not implemented");
-    }
-
-    @Override
-    public Optional<PathExpressionMatcher> getPathExpressionMatcher() {
-      return Optional.empty();
-    }
-  }
 }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJsonTest.java b/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJsonTest.java
index 13a326d..313bef4 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJsonTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJsonTest.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.plugins.codeowners.api.RequiredApprovalInfo;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackendId;
 import com.google.gerrit.plugins.codeowners.backend.FallbackCodeOwners;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfigSnapshot;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
 import com.google.gerrit.plugins.codeowners.backend.config.RequiredApproval;
 import com.google.gerrit.plugins.codeowners.backend.findowners.FindOwnersBackend;
@@ -63,6 +64,7 @@
   @Rule public final MockitoRule mockito = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
 
   @Mock private CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
+  @Mock private CodeOwnersPluginConfigSnapshot codeOwnersPluginConfigSnapshot;
 
   @Inject private CurrentUser currentUser;
 
@@ -106,14 +108,15 @@
   public void formatBackendIds() throws Exception {
     createBranch(BranchNameKey.create(project, "stable-2.10"));
 
-    when(codeOwnersPluginConfiguration.getBackend(project)).thenReturn(findOwnersBackend);
-    when(codeOwnersPluginConfiguration.getBackend(BranchNameKey.create(project, "master")))
+    when(codeOwnersPluginConfigSnapshot.getBackend()).thenReturn(findOwnersBackend);
+    when(codeOwnersPluginConfigSnapshot.getBackend("refs/heads/master"))
         .thenReturn(findOwnersBackend);
-    when(codeOwnersPluginConfiguration.getBackend(
-            BranchNameKey.create(project, "refs/meta/config")))
+    when(codeOwnersPluginConfigSnapshot.getBackend("refs/meta/config"))
         .thenReturn(findOwnersBackend);
-    when(codeOwnersPluginConfiguration.getBackend(BranchNameKey.create(project, "stable-2.10")))
+    when(codeOwnersPluginConfigSnapshot.getBackend("refs/heads/stable-2.10"))
         .thenReturn(protoBackend);
+    when(codeOwnersPluginConfiguration.getProjectConfig(project))
+        .thenReturn(codeOwnersPluginConfigSnapshot);
 
     BackendInfo backendInfo = codeOwnerProjectConfigJson.formatBackendInfo(createProjectResource());
     assertThat(backendInfo.id).isEqualTo(CodeOwnerBackendId.FIND_OWNERS.getBackendId());
@@ -129,12 +132,13 @@
 
   @Test
   public void idsPerBranchNotSetIfThereIsNoBranchSpecificBackendConfiguration() throws Exception {
-    when(codeOwnersPluginConfiguration.getBackend(project)).thenReturn(findOwnersBackend);
-    when(codeOwnersPluginConfiguration.getBackend(BranchNameKey.create(project, "master")))
+    when(codeOwnersPluginConfigSnapshot.getBackend()).thenReturn(findOwnersBackend);
+    when(codeOwnersPluginConfigSnapshot.getBackend("refs/heads/master"))
         .thenReturn(findOwnersBackend);
-    when(codeOwnersPluginConfiguration.getBackend(
-            BranchNameKey.create(project, "refs/meta/config")))
+    when(codeOwnersPluginConfigSnapshot.getBackend("refs/meta/config"))
         .thenReturn(findOwnersBackend);
+    when(codeOwnersPluginConfiguration.getProjectConfig(project))
+        .thenReturn(codeOwnersPluginConfigSnapshot);
 
     BackendInfo backendInfo = codeOwnerProjectConfigJson.formatBackendInfo(createProjectResource());
     assertThat(backendInfo.id).isEqualTo(CodeOwnerBackendId.FIND_OWNERS.getBackendId());
@@ -146,31 +150,32 @@
     createOwnersOverrideLabel();
     createBranch(BranchNameKey.create(project, "stable-2.10"));
 
-    when(codeOwnersPluginConfiguration.isDisabled(project)).thenReturn(false);
-    when(codeOwnersPluginConfiguration.isDisabled(any(BranchNameKey.class))).thenReturn(false);
-    when(codeOwnersPluginConfiguration.getBackend(project)).thenReturn(findOwnersBackend);
-    when(codeOwnersPluginConfiguration.getBackend(BranchNameKey.create(project, "master")))
-        .thenReturn(findOwnersBackend);
-    when(codeOwnersPluginConfiguration.getBackend(
-            BranchNameKey.create(project, "refs/meta/config")))
-        .thenReturn(findOwnersBackend);
-    when(codeOwnersPluginConfiguration.getBackend(BranchNameKey.create(project, "stable-2.10")))
-        .thenReturn(protoBackend);
-    when(codeOwnersPluginConfiguration.getFileExtension(project)).thenReturn(Optional.of("foo"));
-    when(codeOwnersPluginConfiguration.getOverrideInfoUrl(project))
-        .thenReturn(Optional.of("http://foo.example.com"));
-    when(codeOwnersPluginConfiguration.getMergeCommitStrategy(project))
+    when(codeOwnersPluginConfigSnapshot.getFileExtension()).thenReturn(Optional.of("foo"));
+    when(codeOwnersPluginConfigSnapshot.getMergeCommitStrategy())
         .thenReturn(MergeCommitStrategy.FILES_WITH_CONFLICT_RESOLUTION);
-    when(codeOwnersPluginConfiguration.getFallbackCodeOwners(project))
+    when(codeOwnersPluginConfigSnapshot.getFallbackCodeOwners())
         .thenReturn(FallbackCodeOwners.ALL_USERS);
-    when(codeOwnersPluginConfiguration.areImplicitApprovalsEnabled(project)).thenReturn(true);
-    when(codeOwnersPluginConfiguration.getRequiredApproval(project))
+    when(codeOwnersPluginConfigSnapshot.getOverrideInfoUrl())
+        .thenReturn(Optional.of("http://foo.example.com"));
+    when(codeOwnersPluginConfigSnapshot.isDisabled()).thenReturn(false);
+    when(codeOwnersPluginConfigSnapshot.isDisabled(any(String.class))).thenReturn(false);
+    when(codeOwnersPluginConfigSnapshot.getBackend()).thenReturn(findOwnersBackend);
+    when(codeOwnersPluginConfigSnapshot.getBackend("refs/heads/master"))
+        .thenReturn(findOwnersBackend);
+    when(codeOwnersPluginConfigSnapshot.getBackend("refs/meta/config"))
+        .thenReturn(findOwnersBackend);
+    when(codeOwnersPluginConfigSnapshot.getBackend("refs/heads/stable-2.10"))
+        .thenReturn(protoBackend);
+    when(codeOwnersPluginConfigSnapshot.areImplicitApprovalsEnabled()).thenReturn(true);
+    when(codeOwnersPluginConfigSnapshot.getRequiredApproval())
         .thenReturn(RequiredApproval.create(getDefaultCodeReviewLabel(), (short) 2));
-    when(codeOwnersPluginConfiguration.getOverrideApproval(project))
+    when(codeOwnersPluginConfigSnapshot.getOverrideApproval())
         .thenReturn(
             ImmutableSet.of(
                 RequiredApproval.create(
                     LabelType.withDefaultValues("Owners-Override"), (short) 1)));
+    when(codeOwnersPluginConfiguration.getProjectConfig(project))
+        .thenReturn(codeOwnersPluginConfigSnapshot);
 
     CodeOwnerProjectConfigInfo codeOwnerProjectConfigInfo =
         codeOwnerProjectConfigJson.format(createProjectResource());
@@ -198,7 +203,9 @@
 
   @Test
   public void disabledBranchesNotSetIfDisabledOnProjectLevel() throws Exception {
-    when(codeOwnersPluginConfiguration.isDisabled(project)).thenReturn(true);
+    when(codeOwnersPluginConfigSnapshot.isDisabled()).thenReturn(true);
+    when(codeOwnersPluginConfiguration.getProjectConfig(project))
+        .thenReturn(codeOwnersPluginConfigSnapshot);
     CodeOwnersStatusInfo statusInfo =
         codeOwnerProjectConfigJson.formatStatusInfo(createProjectResource());
     assertThat(statusInfo.disabled).isTrue();
@@ -207,12 +214,11 @@
 
   @Test
   public void emptyStatus() throws Exception {
-    when(codeOwnersPluginConfiguration.isDisabled(project)).thenReturn(false);
-    when(codeOwnersPluginConfiguration.isDisabled(BranchNameKey.create(project, "master")))
-        .thenReturn(false);
-    when(codeOwnersPluginConfiguration.isDisabled(
-            BranchNameKey.create(project, "refs/meta/config")))
-        .thenReturn(false);
+    when(codeOwnersPluginConfigSnapshot.isDisabled()).thenReturn(false);
+    when(codeOwnersPluginConfigSnapshot.isDisabled("refs/heads/master")).thenReturn(false);
+    when(codeOwnersPluginConfigSnapshot.isDisabled("refs/meta/config")).thenReturn(false);
+    when(codeOwnersPluginConfiguration.getProjectConfig(project))
+        .thenReturn(codeOwnersPluginConfigSnapshot);
     CodeOwnersStatusInfo statusInfo =
         codeOwnerProjectConfigJson.formatStatusInfo(createProjectResource());
     assertThat(statusInfo.disabled).isNull();
@@ -221,12 +227,11 @@
 
   @Test
   public void withDisabledBranches() throws Exception {
-    when(codeOwnersPluginConfiguration.isDisabled(project)).thenReturn(false);
-    when(codeOwnersPluginConfiguration.isDisabled(BranchNameKey.create(project, "master")))
-        .thenReturn(true);
-    when(codeOwnersPluginConfiguration.isDisabled(
-            BranchNameKey.create(project, "refs/meta/config")))
-        .thenReturn(false);
+    when(codeOwnersPluginConfigSnapshot.isDisabled()).thenReturn(false);
+    when(codeOwnersPluginConfigSnapshot.isDisabled("refs/heads/master")).thenReturn(true);
+    when(codeOwnersPluginConfigSnapshot.isDisabled("refs/meta/config")).thenReturn(false);
+    when(codeOwnersPluginConfiguration.getProjectConfig(project))
+        .thenReturn(codeOwnersPluginConfigSnapshot);
     CodeOwnersStatusInfo statusInfo =
         codeOwnerProjectConfigJson.formatStatusInfo(createProjectResource());
     assertThat(statusInfo.disabled).isNull();
@@ -237,11 +242,13 @@
   public void withMultipleOverrides() throws Exception {
     createOwnersOverrideLabel();
 
-    when(codeOwnersPluginConfiguration.getOverrideApproval(project))
+    when(codeOwnersPluginConfigSnapshot.getOverrideApproval())
         .thenReturn(
             ImmutableSet.of(
                 RequiredApproval.create(LabelType.withDefaultValues("Owners-Override"), (short) 1),
                 RequiredApproval.create(LabelType.withDefaultValues("Code-Review"), (short) 2)));
+    when(codeOwnersPluginConfiguration.getProjectConfig(project))
+        .thenReturn(codeOwnersPluginConfigSnapshot);
 
     ImmutableList<RequiredApprovalInfo> requiredApprovalInfos =
         codeOwnerProjectConfigJson.formatOverrideApprovalInfo(project);
@@ -264,24 +271,26 @@
         .addCodeOwnerEmail(admin.email())
         .create();
 
-    when(codeOwnersPluginConfiguration.isDisabled(any(BranchNameKey.class))).thenReturn(false);
-    when(codeOwnersPluginConfiguration.getBackend(BranchNameKey.create(project, "master")))
-        .thenReturn(findOwnersBackend);
-    when(codeOwnersPluginConfiguration.getFileExtension(project)).thenReturn(Optional.of("foo"));
-    when(codeOwnersPluginConfiguration.getOverrideInfoUrl(project))
-        .thenReturn(Optional.of("http://foo.example.com"));
-    when(codeOwnersPluginConfiguration.getMergeCommitStrategy(project))
+    when(codeOwnersPluginConfigSnapshot.getFileExtension()).thenReturn(Optional.of("foo"));
+    when(codeOwnersPluginConfigSnapshot.getMergeCommitStrategy())
         .thenReturn(MergeCommitStrategy.FILES_WITH_CONFLICT_RESOLUTION);
-    when(codeOwnersPluginConfiguration.getFallbackCodeOwners(project))
+    when(codeOwnersPluginConfigSnapshot.getFallbackCodeOwners())
         .thenReturn(FallbackCodeOwners.ALL_USERS);
-    when(codeOwnersPluginConfiguration.areImplicitApprovalsEnabled(project)).thenReturn(true);
-    when(codeOwnersPluginConfiguration.getRequiredApproval(project))
+    when(codeOwnersPluginConfigSnapshot.getOverrideInfoUrl())
+        .thenReturn(Optional.of("http://foo.example.com"));
+    when(codeOwnersPluginConfigSnapshot.isDisabled(any(String.class))).thenReturn(false);
+    when(codeOwnersPluginConfigSnapshot.getBackend("refs/heads/master"))
+        .thenReturn(findOwnersBackend);
+    when(codeOwnersPluginConfigSnapshot.areImplicitApprovalsEnabled()).thenReturn(true);
+    when(codeOwnersPluginConfigSnapshot.getRequiredApproval())
         .thenReturn(RequiredApproval.create(getDefaultCodeReviewLabel(), (short) 2));
-    when(codeOwnersPluginConfiguration.getOverrideApproval(project))
+    when(codeOwnersPluginConfigSnapshot.getOverrideApproval())
         .thenReturn(
             ImmutableSet.of(
                 RequiredApproval.create(
                     LabelType.withDefaultValues("Owners-Override"), (short) 1)));
+    when(codeOwnersPluginConfiguration.getProjectConfig(project))
+        .thenReturn(codeOwnersPluginConfigSnapshot);
 
     CodeOwnerBranchConfigInfo codeOwnerBranchConfigInfo =
         codeOwnerProjectConfigJson.format(createBranchResource("refs/heads/master"));
@@ -306,7 +315,9 @@
 
   @Test
   public void formatCodeOwnerBranchConfig_disabled() throws Exception {
-    when(codeOwnersPluginConfiguration.isDisabled(any(BranchNameKey.class))).thenReturn(true);
+    when(codeOwnersPluginConfigSnapshot.isDisabled(any(String.class))).thenReturn(true);
+    when(codeOwnersPluginConfiguration.getProjectConfig(project))
+        .thenReturn(codeOwnersPluginConfigSnapshot);
 
     CodeOwnerBranchConfigInfo codeOwnerBranchConfigInfo =
         codeOwnerProjectConfigJson.format(createBranchResource("refs/heads/master"));