Merge "Add code owner scorings to CodeOwnersInfo"
diff --git a/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java b/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java
index 1283932..14257b7 100644
--- a/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java
+++ b/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java
@@ -19,6 +19,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
@@ -287,8 +288,9 @@
    *
    * @param testAccounts the accounts of the users that should be code owners
    */
-  protected void setAsDefaultCodeOwners(TestAccount... testAccounts) {
-    setAsCodeOwners(RefNames.REFS_CONFIG, "/", testAccounts);
+  @CanIgnoreReturnValue
+  protected CodeOwnerConfig.Key setAsDefaultCodeOwners(TestAccount... testAccounts) {
+    return setAsCodeOwners(RefNames.REFS_CONFIG, "/", testAccounts);
   }
 
   /**
@@ -296,8 +298,9 @@
    *
    * @param testAccounts the accounts of the users that should be code owners
    */
-  protected void setAsRootCodeOwners(TestAccount... testAccounts) {
-    setAsCodeOwners("/", testAccounts);
+  @CanIgnoreReturnValue
+  protected CodeOwnerConfig.Key setAsRootCodeOwners(TestAccount... testAccounts) {
+    return setAsCodeOwners("/", testAccounts);
   }
 
   /**
@@ -306,8 +309,9 @@
    * @param path the path of the code owner config file
    * @param testAccounts the accounts of the users that should be code owners
    */
-  protected void setAsCodeOwners(String path, TestAccount... testAccounts) {
-    setAsCodeOwners("master", path, testAccounts);
+  @CanIgnoreReturnValue
+  protected CodeOwnerConfig.Key setAsCodeOwners(String path, TestAccount... testAccounts) {
+    return setAsCodeOwners("master", path, testAccounts);
   }
 
   /**
@@ -317,7 +321,9 @@
    * @param path the path of the code owner config file
    * @param testAccounts the accounts of the users that should be code owners
    */
-  private void setAsCodeOwners(String branchName, String path, TestAccount... testAccounts) {
+  @CanIgnoreReturnValue
+  private CodeOwnerConfig.Key setAsCodeOwners(
+      String branchName, String path, TestAccount... testAccounts) {
     Builder newCodeOwnerConfigBuilder =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
@@ -327,7 +333,7 @@
     for (TestAccount testAccount : testAccounts) {
       newCodeOwnerConfigBuilder.addCodeOwnerEmail(testAccount.email());
     }
-    newCodeOwnerConfigBuilder.create();
+    return newCodeOwnerConfigBuilder.create();
   }
 
   /**
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java
index 19b02a0..15c8558 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java
@@ -46,6 +46,12 @@
   public boolean isResolvable;
 
   /**
+   * The code owner config files that are relevant for computing the code ownership, i.e. all code
+   * owner config files which have been inspected to compute the code ownership.
+   */
+  public List<CodeOwnerConfigFileInfo> codeOwnerConfigs;
+
+  /**
    * Whether the user to which the given email was resolved has read permissions on the branch.
    *
    * <p>Not set if:
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigFileInfo.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigFileInfo.java
index 41fa62b..4e9863b 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigFileInfo.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigFileInfo.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.plugins.codeowners.api;
 
+import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImportMode;
 import java.util.List;
 
@@ -43,6 +44,9 @@
   /** The path of the code owner config file. */
   public String path;
 
+  /** Links to the code owner config file in external sites. */
+  public List<WebLinkInfo> webLinks;
+
   /** Imported code owner config files. */
   public List<CodeOwnerConfigFileInfo> imports;
 
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchy.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchy.java
index 28dd5e1..dfd4fee 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchy.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchy.java
@@ -224,7 +224,7 @@
           logger.atFine().log("visit code owner config for %s", ownerConfigFolder);
           boolean visitFurtherCodeOwnerConfigs = pathCodeOwnersVisitor.visit(pathCodeOwners.get());
           boolean ignoreParentCodeOwners =
-              pathCodeOwners.get().resolveCodeOwnerConfig().get().ignoreParentCodeOwners();
+              pathCodeOwners.get().resolveCodeOwnerConfig().ignoreParentCodeOwners();
           if (ignoreParentCodeOwners) {
             parentCodeOwnersIgnoredCallback.accept(codeOwnerConfigKey);
           }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImport.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImport.java
index a75c15b..fc1b521 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImport.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImport.java
@@ -22,22 +22,30 @@
 /**
  * Information about an import of a {@link CodeOwnerConfig}.
  *
- * <p>Contains the keys of the importing and the imported code owner config, as well as the
- * reference that the importing code owner config uses to reference the imported code owner config
- * (contains the import mode).
+ * <p>Contains the importing code owner config, the key of the imported code owner config, the
+ * imported code owner config if the import could be resolved, as well as the reference that the
+ * importing code owner config uses to reference the imported code owner config (contains the import
+ * mode).
  *
  * <p>It's possible that this class represents non-resolveable imports (e.g. an import of a
- * non-existing code owner config). In this case an error message is contained that explains why the
- * import couldn't be resolved.
+ * non-existing code owner config). In this case the imported code owner config is not available and
+ * an error message is contained that explains why the import couldn't be resolved.
  */
 @AutoValue
 public abstract class CodeOwnerConfigImport {
   /** Key of the importing code owner config. */
-  public abstract CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig();
+  public abstract CodeOwnerConfig importingCodeOwnerConfig();
 
   /** Key of the imported code owner config. */
   public abstract CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig();
 
+  /**
+   * Imported code owner config.
+   *
+   * <p>Not set for unresolved imports.
+   */
+  public abstract Optional<CodeOwnerConfig> importedCodeOwnerConfig();
+
   /** The code owner config reference that references the imported code owner config. */
   public abstract CodeOwnerConfigReference codeOwnerConfigReference();
 
@@ -50,8 +58,9 @@
   @Override
   public final String toString() {
     return MoreObjects.toStringHelper(this)
-        .add("keyOfImportingCodeOwnerConfig", keyOfImportingCodeOwnerConfig())
+        .add("importingCodeOwnerConfig", importingCodeOwnerConfig())
         .add("keyOfImportedCodeOwnerConfig", keyOfImportedCodeOwnerConfig())
+        .add("importedCodeOwnerConfig", importedCodeOwnerConfig())
         .add("codeOwnerConfigReference", codeOwnerConfigReference())
         .add("errorMessage", errorMessage())
         .toString();
@@ -60,13 +69,14 @@
   /** Creates a {@link CodeOwnerConfigImport} instance for an unresolved import. */
   @VisibleForTesting
   public static CodeOwnerConfigImport createUnresolvedImport(
-      CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig,
+      CodeOwnerConfig importingCodeOwnerConfig,
       CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig,
       CodeOwnerConfigReference codeOwnerConfigReference,
       String errorMessage) {
     return new AutoValue_CodeOwnerConfigImport(
-        keyOfImportingCodeOwnerConfig,
+        importingCodeOwnerConfig,
         keyOfImportedCodeOwnerConfig,
+        Optional.empty(),
         codeOwnerConfigReference,
         Optional.of(errorMessage));
   }
@@ -74,12 +84,13 @@
   /** Creates a {@link CodeOwnerConfigImport} instance for a resolved import. */
   @VisibleForTesting
   public static CodeOwnerConfigImport createResolvedImport(
-      CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig,
-      CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig,
+      CodeOwnerConfig importingCodeOwnerConfig,
+      CodeOwnerConfig importedCodeOwnerConfig,
       CodeOwnerConfigReference codeOwnerConfigReference) {
     return new AutoValue_CodeOwnerConfigImport(
-        keyOfImportingCodeOwnerConfig,
-        keyOfImportedCodeOwnerConfig,
+        importingCodeOwnerConfig,
+        importedCodeOwnerConfig.key(),
+        Optional.of(importedCodeOwnerConfig),
         codeOwnerConfigReference,
         Optional.empty());
   }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
index 7000959..f6718eb 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
@@ -228,13 +228,12 @@
       logger.atFine().log(
           "resolve path code owners (code owner config = %s, path = %s)",
           pathCodeOwners.getCodeOwnerConfig().key(), pathCodeOwners.getPath());
-      OptionalResultWithMessages<PathCodeOwnersResult> pathCodeOwnersResult =
-          pathCodeOwners.resolveCodeOwnerConfig();
+      PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.resolveCodeOwnerConfig();
       return resolve(
-          pathCodeOwnersResult.get().getPathCodeOwners(),
-          pathCodeOwnersResult.get().getAnnotations(),
-          pathCodeOwnersResult.get().resolvedImports(),
-          pathCodeOwnersResult.get().unresolvedImports(),
+          pathCodeOwnersResult.getPathCodeOwners(),
+          pathCodeOwnersResult.getAnnotations(),
+          pathCodeOwnersResult.resolvedImports(),
+          pathCodeOwnersResult.unresolvedImports(),
           pathCodeOwnersResult.messages());
     }
   }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
index 5e7a11f..2083fc2 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
@@ -21,7 +21,6 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
@@ -36,10 +35,8 @@
 import com.google.inject.Singleton;
 import java.nio.file.Path;
 import java.util.ArrayDeque;
-import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Queue;
@@ -144,7 +141,7 @@
   private final Path path;
   private final PathExpressionMatcher pathExpressionMatcher;
 
-  private OptionalResultWithMessages<PathCodeOwnersResult> pathCodeOwnersResult;
+  private PathCodeOwnersResult pathCodeOwnersResult;
 
   private PathCodeOwners(
       CodeOwnerMetrics codeOwnerMetrics,
@@ -210,133 +207,62 @@
    * <p>Imports from other projects are always loaded from the same branch from which the importing
    * code owner config was loaded.
    *
-   * @return the resolved code owner config
+   * @return the resolved code owner config as a {@link PathCodeOwnersResult}
    */
-  public OptionalResultWithMessages<PathCodeOwnersResult> resolveCodeOwnerConfig() {
+  public PathCodeOwnersResult resolveCodeOwnerConfig() {
     if (this.pathCodeOwnersResult != null) {
       return this.pathCodeOwnersResult;
     }
 
     try (Timer0.Context ctx = codeOwnerMetrics.resolveCodeOwnerConfig.start()) {
+      Path codeOwnerConfigFilePath = codeOwners.getFilePath(codeOwnerConfig.key());
+
+      PathCodeOwnersResult.Builder pathCodeOwnersResultBuilder =
+          PathCodeOwnersResult.builder(
+              path, codeOwnerConfig.key(), codeOwnerConfig.ignoreParentCodeOwners());
+
       logger.atFine().log(
-          "resolve code owners for %s from code owner config %s", path, codeOwnerConfig.key());
+          "resolve code owners for %s from code owner config %s:%s:%s",
+          path,
+          codeOwnerConfig.key().project(),
+          codeOwnerConfig.key().shortBranchName(),
+          codeOwnerConfigFilePath);
 
-      List<String> messages = new ArrayList<>();
-      messages.add(
+      pathCodeOwnersResultBuilder.addMessage(
           String.format(
-              "resolve code owners for %s from code owner config %s", path, codeOwnerConfig.key()));
-
-      // Create a code owner config builder to create the resolved code owner config (= code owner
-      // config that is scoped to the path and which has imports resolved)
-      CodeOwnerConfig.Builder resolvedCodeOwnerConfigBuilder =
-          CodeOwnerConfig.builder(codeOwnerConfig.key(), codeOwnerConfig.revision());
+              "resolve code owners for %s from code owner config %s:%s:%s",
+              path,
+              codeOwnerConfig.key().project(),
+              codeOwnerConfig.key().shortBranchName(),
+              codeOwnerConfigFilePath));
 
       // Add all data from the original code owner config that is relevant for the path
       // (ignoreParentCodeOwners flag, global code owner sets and matching per-file code owner
       // sets). Effectively this means we are dropping all non-matching per-file rules.
-      resolvedCodeOwnerConfigBuilder.setIgnoreParentCodeOwners(
-          codeOwnerConfig.ignoreParentCodeOwners());
       getGlobalCodeOwnerSets(codeOwnerConfig)
-          .forEach(resolvedCodeOwnerConfigBuilder::addCodeOwnerSet);
-      boolean globalCodeOwnersIgnored = false;
-      for (CodeOwnerSet codeOwnerSet :
-          getMatchingPerFileCodeOwnerSets(codeOwnerConfig).collect(toImmutableSet())) {
-        messages.add(
+          .forEach(pathCodeOwnersResultBuilder::addGlobalCodeOwnerSet);
+
+      ImmutableSet<CodeOwnerSet> matchingPerFileCodeOwnerSets =
+          getMatchingPerFileCodeOwnerSets(codeOwnerConfig).collect(toImmutableSet());
+      for (CodeOwnerSet codeOwnerSet : matchingPerFileCodeOwnerSets) {
+        pathCodeOwnersResultBuilder.addMessage(
             String.format(
                 "per-file code owner set with path expressions %s matches",
                 codeOwnerSet.pathExpressions()));
-        resolvedCodeOwnerConfigBuilder.addCodeOwnerSet(codeOwnerSet);
-        if (codeOwnerSet.ignoreGlobalAndParentCodeOwners()) {
-          globalCodeOwnersIgnored = true;
-        }
+        pathCodeOwnersResultBuilder.addPerFileCodeOwnerSet(codeOwnerSet);
       }
 
       // Resolve global imports.
-      ImmutableList.Builder<CodeOwnerConfigImport> resolvedImports = ImmutableList.builder();
-      ImmutableList.Builder<CodeOwnerConfigImport> unresolvedImports = ImmutableList.builder();
       ImmutableSet<CodeOwnerImport> globalImports = getGlobalImports(0, codeOwnerConfig);
-      OptionalResultWithMessages<CodeOwnerConfigImports> globalImportedCodeOwnerConfigs;
-      if (!globalCodeOwnersIgnored) {
-        globalImportedCodeOwnerConfigs =
-            resolveImports(codeOwnerConfig.key(), globalImports, resolvedCodeOwnerConfigBuilder);
-      } else {
-        // skip global import with mode GLOBAL_CODE_OWNER_SETS_ONLY,
-        // since we already know that global code owners will be ignored, we do not need to resolve
-        // these imports
-        globalImportedCodeOwnerConfigs =
-            resolveImports(
-                codeOwnerConfig.key(),
-                globalImports.stream()
-                    .filter(
-                        codeOwnerConfigImport ->
-                            codeOwnerConfigImport.referenceToImportedCodeOwnerConfig().importMode()
-                                != CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY)
-                    .collect(toImmutableSet()),
-                resolvedCodeOwnerConfigBuilder);
-      }
-      messages.addAll(globalImportedCodeOwnerConfigs.messages());
-      resolvedImports.addAll(globalImportedCodeOwnerConfigs.get().resolved());
-      unresolvedImports.addAll(globalImportedCodeOwnerConfigs.get().unresolved());
-
-      // Remove all global code owner sets if any per-file code owner set has the
-      // ignoreGlobalAndParentCodeOwners flag set to true (as in this case they are ignored and
-      // hence not relevant).
-      // In this case also set ignoreParentCodeOwners to true, so that we do not need to inspect the
-      // ignoreGlobalAndParentCodeOwners flags on per-file code owner sets again, but can just rely
-      // on the global ignoreParentCodeOwners flag.
-      Optional<CodeOwnerSet> matchingPerFileCodeOwnerSetThatIgnoresGlobalAndParentCodeOwners =
-          getMatchingPerFileCodeOwnerSets(resolvedCodeOwnerConfigBuilder.build())
-              .filter(CodeOwnerSet::ignoreGlobalAndParentCodeOwners)
-              .findAny();
-      if (matchingPerFileCodeOwnerSetThatIgnoresGlobalAndParentCodeOwners.isPresent()) {
-        logger.atFine().log("remove folder code owner sets and set ignoreParentCodeOwners to true");
-        messages.add(
-            String.format(
-                "found matching per-file code owner set (with path expressions = %s) that ignores"
-                    + " parent code owners, hence ignoring the folder code owners",
-                matchingPerFileCodeOwnerSetThatIgnoresGlobalAndParentCodeOwners
-                    .get()
-                    .pathExpressions()));
-        // We use resolvedCodeOwnerConfigBuilder to build up a code owner config that is scoped to
-        // the path and which has imports resolved. When resolving imports the relevant code owner
-        // sets from the imported code owner configs are added to the builder.
-        // If a per-file rule ignores global and parent code owners we have to drop all global code
-        // owner sets. The problem is that AutoValue doesn't allow us to remove/override code owner
-        // sets that have previously been added to the builder (we cannot call setCodeOwnerSets(...)
-        // after addCodeOwnerSet(...) or codeOwnerSetsBuilder() has been invoked). To override the
-        // code owner sets we build the code owner config and then create a fresh builder from it.
-        // Since the builder is fresh addCodeOwnerSet(...) and codeOwnerSetsBuilder() haven't been
-        // invoked on it yet we can now call setCodeOwnerSets(...).
-        resolvedCodeOwnerConfigBuilder =
-            resolvedCodeOwnerConfigBuilder
-                .build()
-                .toBuilder()
-                .setIgnoreParentCodeOwners()
-                .setCodeOwnerSets(
-                    resolvedCodeOwnerConfigBuilder.codeOwnerSets().stream()
-                        .filter(codeOwnerSet -> !codeOwnerSet.pathExpressions().isEmpty())
-                        .collect(toImmutableSet()));
-      }
+      resolveImports(codeOwnerConfig.key(), globalImports, pathCodeOwnersResultBuilder);
 
       // Resolve per-file imports.
       ImmutableSet<CodeOwnerImport> perFileImports =
-          getPerFileImports(
-              0, codeOwnerConfig.key(), resolvedCodeOwnerConfigBuilder.codeOwnerSets());
-      OptionalResultWithMessages<CodeOwnerConfigImports> perFileImportedCodeOwnerConfigs =
-          resolveImports(codeOwnerConfig.key(), perFileImports, resolvedCodeOwnerConfigBuilder);
-      messages.addAll(perFileImportedCodeOwnerConfigs.messages());
-      resolvedImports.addAll(perFileImportedCodeOwnerConfigs.get().resolved());
-      unresolvedImports.addAll(perFileImportedCodeOwnerConfigs.get().unresolved());
+          getPerFileImports(0, codeOwnerConfig, matchingPerFileCodeOwnerSets);
+      resolveImports(codeOwnerConfig.key(), perFileImports, pathCodeOwnersResultBuilder);
 
-      this.pathCodeOwnersResult =
-          OptionalResultWithMessages.create(
-              PathCodeOwnersResult.create(
-                  path,
-                  resolvedCodeOwnerConfigBuilder.build(),
-                  resolvedImports.build(),
-                  unresolvedImports.build()),
-              messages);
-      logger.atFine().log("path code owners result = %s", pathCodeOwnersResult);
+      this.pathCodeOwnersResult = pathCodeOwnersResultBuilder.build();
+      logger.atFine().log("path code owners result = %s", this.pathCodeOwnersResult);
       return this.pathCodeOwnersResult;
     }
   }
@@ -346,16 +272,13 @@
    *
    * @param keyOfImportingCodeOwnerConfig the key of the importing code owner config
    * @param codeOwnerConfigImports the code owner configs that should be imported
-   * @param resolvedCodeOwnerConfigBuilder the builder for the resolved code owner config
-   * @return list of unresolved imports, empty list if all imports were successfully resolved
    */
-  private OptionalResultWithMessages<CodeOwnerConfigImports> resolveImports(
+  private void resolveImports(
       CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig,
       Set<CodeOwnerImport> codeOwnerConfigImports,
-      CodeOwnerConfig.Builder resolvedCodeOwnerConfigBuilder) {
-    ImmutableList.Builder<CodeOwnerConfigImport> resolvedImports = ImmutableList.builder();
-    ImmutableList.Builder<CodeOwnerConfigImport> unresolvedImports = ImmutableList.builder();
+      PathCodeOwnersResult.Builder pathCodeOwnersResultBuilder) {
     StringBuilder messageBuilder = new StringBuilder();
+
     try (Timer0.Context ctx = codeOwnerMetrics.resolveCodeOwnerConfigImports.start()) {
       logger.atFine().log("resolve imports of codeOwnerConfig %s", keyOfImportingCodeOwnerConfig);
 
@@ -384,7 +307,7 @@
             codeOwnerConfigImport.referenceToImportedCodeOwnerConfig();
         CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
             createKeyForImportedCodeOwnerConfig(
-                codeOwnerConfigImport.importingCodeOwnerConfig(), codeOwnerConfigReference);
+                codeOwnerConfigImport.importingCodeOwnerConfig().key(), codeOwnerConfigReference);
 
         try (Timer0.Context ctx2 = codeOwnerMetrics.resolveCodeOwnerConfigImport.start()) {
           logger.atFine().log(
@@ -393,7 +316,7 @@
           Optional<ProjectState> projectState =
               projectCache.get(keyOfImportedCodeOwnerConfig.project());
           if (!projectState.isPresent()) {
-            unresolvedImports.add(
+            pathCodeOwnersResultBuilder.addUnresolvedImport(
                 CodeOwnerConfigImport.createUnresolvedImport(
                     codeOwnerConfigImport.importingCodeOwnerConfig(),
                     keyOfImportedCodeOwnerConfig,
@@ -405,7 +328,7 @@
             continue;
           }
           if (!projectState.get().statePermitsRead()) {
-            unresolvedImports.add(
+            pathCodeOwnersResultBuilder.addUnresolvedImport(
                 CodeOwnerConfigImport.createUnresolvedImport(
                     codeOwnerConfigImport.importingCodeOwnerConfig(),
                     keyOfImportedCodeOwnerConfig,
@@ -431,7 +354,7 @@
                   : codeOwnerConfigLoader.getFromCurrentRevision(keyOfImportedCodeOwnerConfig);
 
           if (!mayBeImportedCodeOwnerConfig.isPresent()) {
-            unresolvedImports.add(
+            pathCodeOwnersResultBuilder.addUnresolvedImport(
                 CodeOwnerConfigImport.createUnresolvedImport(
                     codeOwnerConfigImport.importingCodeOwnerConfig(),
                     keyOfImportedCodeOwnerConfig,
@@ -447,10 +370,10 @@
 
           CodeOwnerConfig importedCodeOwnerConfig = mayBeImportedCodeOwnerConfig.get();
 
-          resolvedImports.add(
+          pathCodeOwnersResultBuilder.addResolvedImport(
               CodeOwnerConfigImport.createResolvedImport(
                   codeOwnerConfigImport.importingCodeOwnerConfig(),
-                  keyOfImportedCodeOwnerConfig,
+                  importedCodeOwnerConfig,
                   codeOwnerConfigReference));
 
           CodeOwnerConfigImportMode importMode = codeOwnerConfigReference.importMode();
@@ -462,13 +385,21 @@
           if (importMode.importIgnoreParentCodeOwners()
               && importedCodeOwnerConfig.ignoreParentCodeOwners()) {
             logger.atFine().log("import ignoreParentCodeOwners flag");
-            resolvedCodeOwnerConfigBuilder.setIgnoreParentCodeOwners();
+            pathCodeOwnersResultBuilder.ignoreParentCodeOwners(true);
           }
 
           if (importMode.importGlobalCodeOwnerSets()) {
-            logger.atFine().log("import global code owners");
-            getGlobalCodeOwnerSets(importedCodeOwnerConfig)
-                .forEach(resolvedCodeOwnerConfigBuilder::addCodeOwnerSet);
+            if (codeOwnerConfigImport.isGlobalImport()) {
+              logger.atFine().log("add possibly ignored imported global code owners");
+              getGlobalCodeOwnerSets(importedCodeOwnerConfig)
+                  .forEach(pathCodeOwnersResultBuilder::addGlobalCodeOwnerSet);
+            } else {
+              // global code owners which are being imported by a per-file rule become per-file code
+              // owners
+              logger.atFine().log("add imported global code owners as per-file code owners");
+              getGlobalCodeOwnerSets(importedCodeOwnerConfig)
+                  .forEach(pathCodeOwnersResultBuilder::addPerFileCodeOwnerSet);
+            }
           }
 
           ImmutableSet<CodeOwnerSet> matchingPerFileCodeOwnerSets =
@@ -482,7 +413,7 @@
                           String.format(
                               "per-file code owner set with path expressions %s matches\n",
                               codeOwnerSet.pathExpressions())));
-                  resolvedCodeOwnerConfigBuilder.addCodeOwnerSet(codeOwnerSet);
+                  pathCodeOwnersResultBuilder.addPerFileCodeOwnerSet(codeOwnerSet);
                 });
           }
 
@@ -495,7 +426,7 @@
             transitiveImports.addAll(
                 getPerFileImports(
                     codeOwnerConfigImport.importLevel() + 1,
-                    importedCodeOwnerConfig.key(),
+                    importedCodeOwnerConfig,
                     matchingPerFileCodeOwnerSets));
 
             if (importMode == CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY) {
@@ -529,9 +460,9 @@
     if (message.endsWith("\n")) {
       message = message.substring(0, message.length() - 1);
     }
-    return OptionalResultWithMessages.create(
-        CodeOwnerConfigImports.create(resolvedImports.build(), unresolvedImports.build()),
-        !message.isEmpty() ? ImmutableList.of(message) : ImmutableList.of());
+    if (!message.isEmpty()) {
+      pathCodeOwnersResultBuilder.addMessage(message);
+    }
   }
 
   private ImmutableSet<CodeOwnerImport> getGlobalImports(
@@ -539,15 +470,12 @@
     return codeOwnerConfig.imports().stream()
         .map(
             codeOwnerConfigReference ->
-                CodeOwnerImport.create(
-                    importLevel, codeOwnerConfig.key(), codeOwnerConfigReference))
+                CodeOwnerImport.create(importLevel, codeOwnerConfig, codeOwnerConfigReference))
         .collect(toImmutableSet());
   }
 
   private ImmutableSet<CodeOwnerImport> getPerFileImports(
-      int importLevel,
-      CodeOwnerConfig.Key importingCodeOwnerConfig,
-      Set<CodeOwnerSet> codeOwnerSets) {
+      int importLevel, CodeOwnerConfig importingCodeOwnerConfig, Set<CodeOwnerSet> codeOwnerSets) {
     ImmutableSet.Builder<CodeOwnerImport> codeOwnerConfigImports = ImmutableSet.builder();
     for (CodeOwnerSet codeOwnerSet : codeOwnerSets) {
       codeOwnerSet.imports().stream()
@@ -596,7 +524,12 @@
   }
 
   private Stream<CodeOwnerSet> getMatchingPerFileCodeOwnerSets(CodeOwnerConfig codeOwnerConfig) {
-    return codeOwnerConfig.codeOwnerSets().stream()
+    return getMatchingPerFileCodeOwnerSets(codeOwnerConfig.codeOwnerSets());
+  }
+
+  private Stream<CodeOwnerSet> getMatchingPerFileCodeOwnerSets(
+      ImmutableSet<CodeOwnerSet> codeOwnerSets) {
+    return codeOwnerSets.stream()
         .filter(codeOwnerSet -> !codeOwnerSet.pathExpressions().isEmpty())
         .filter(codeOwnerSet -> matches(codeOwnerSet, getRelativePath(), pathExpressionMatcher));
   }
@@ -646,8 +579,8 @@
      */
     public abstract int importLevel();
 
-    /** The key of the code owner config that contains the import. */
-    public abstract CodeOwnerConfig.Key importingCodeOwnerConfig();
+    /** The code owner config that contains the import. */
+    public abstract CodeOwnerConfig importingCodeOwnerConfig();
 
     /** The reference to the imported code owner config */
     public abstract CodeOwnerConfigReference referenceToImportedCodeOwnerConfig();
@@ -655,20 +588,24 @@
     /** The code owner set that specified the import, empty if it is a global import. */
     public abstract Optional<CodeOwnerSet> codeOwnerSet();
 
+    boolean isGlobalImport() {
+      return codeOwnerSet().isEmpty();
+    }
+
     public String format() {
-      if (codeOwnerSet().isPresent()) {
+      if (isGlobalImport()) {
         return getPrefix()
             + String.format(
-                "* %s (per-file import, import mode = %s, path expressions = %s)\n",
+                "* %s (global import, import mode = %s)\n",
                 referenceToImportedCodeOwnerConfig().format(),
-                referenceToImportedCodeOwnerConfig().importMode(),
-                codeOwnerSet().get().pathExpressions());
+                referenceToImportedCodeOwnerConfig().importMode());
       }
       return getPrefix()
           + String.format(
-              "* %s (global import, import mode = %s)\n",
+              "* %s (per-file import, import mode = %s, path expressions = %s)\n",
               referenceToImportedCodeOwnerConfig().format(),
-              referenceToImportedCodeOwnerConfig().importMode());
+              referenceToImportedCodeOwnerConfig().importMode(),
+              codeOwnerSet().get().pathExpressions());
     }
 
     public String formatSubItem(String message) {
@@ -699,7 +636,7 @@
 
     public static CodeOwnerImport create(
         int importLevel,
-        CodeOwnerConfig.Key importingCodeOwnerConfig,
+        CodeOwnerConfig importingCodeOwnerConfig,
         CodeOwnerConfigReference codeOwnerConfigReference) {
       return create(
           importLevel, importingCodeOwnerConfig, codeOwnerConfigReference, Optional.empty());
@@ -707,7 +644,7 @@
 
     public static CodeOwnerImport create(
         int importLevel,
-        CodeOwnerConfig.Key importingCodeOwnerConfig,
+        CodeOwnerConfig importingCodeOwnerConfig,
         CodeOwnerConfigReference codeOwnerConfigReference,
         CodeOwnerSet codeOwnerSet) {
       return create(
@@ -719,26 +656,11 @@
 
     public static CodeOwnerImport create(
         int importLevel,
-        CodeOwnerConfig.Key importingCodeOwnerConfig,
+        CodeOwnerConfig importingCodeOwnerConfig,
         CodeOwnerConfigReference codeOwnerConfigReference,
         Optional<CodeOwnerSet> codeOwnerSet) {
       return new AutoValue_PathCodeOwners_CodeOwnerImport(
           importLevel, importingCodeOwnerConfig, codeOwnerConfigReference, codeOwnerSet);
     }
   }
-
-  @AutoValue
-  abstract static class CodeOwnerConfigImports {
-    /** Imported code owner configs the could be resolved. */
-    abstract ImmutableList<CodeOwnerConfigImport> resolved();
-
-    /** Imported code owner configs the could not be resolved. */
-    abstract ImmutableList<CodeOwnerConfigImport> unresolved();
-
-    static CodeOwnerConfigImports create(
-        ImmutableList<CodeOwnerConfigImport> resolved,
-        ImmutableList<CodeOwnerConfigImport> unresolved) {
-      return new AutoValue_PathCodeOwners_CodeOwnerConfigImports(resolved, unresolved);
-    }
-  }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResult.java b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResult.java
index 5b262bf..a124e3e 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResult.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResult.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.plugins.codeowners.backend;
 
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static java.util.Objects.requireNonNull;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.MoreObjects;
@@ -22,8 +23,8 @@
 import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.nio.file.Path;
-import java.util.List;
 
 /** The result of resolving path code owners via {@link PathCodeOwners}. */
 @AutoValue
@@ -33,8 +34,20 @@
   /** Gets the path for which the code owner config was resolved. */
   abstract Path path();
 
-  /** Gets the resolved code owner config. */
-  abstract CodeOwnerConfig codeOwnerConfig();
+  /** Gets the key of the resolved code owner config. */
+  abstract CodeOwnerConfig.Key codeOwnerConfigKey();
+
+  /** Gets whether parent code owners should be ignored for the path. */
+  public abstract boolean ignoreParentCodeOwners();
+
+  /** Gets whether global code owners (aka folder code owners) should be ignored for the path. */
+  abstract boolean ignoreGlobalCodeOwners();
+
+  /** Gets code owner sets that contain global code owners (aka folder code owners). */
+  abstract ImmutableSet<CodeOwnerSet> globalCodeOwnerSets();
+
+  /** Gets code owner sets that contain per-file code owners that are matching the path. */
+  abstract ImmutableSet<CodeOwnerSet> perFileCodeOwnerSets();
 
   /** Gets a list of resolved imports. */
   public abstract ImmutableList<CodeOwnerConfigImport> resolvedImports();
@@ -47,6 +60,8 @@
     return !unresolvedImports().isEmpty();
   }
 
+  public abstract ImmutableList<String> messages();
+
   /**
    * Gets the code owners from the code owner config that apply to the path.
    *
@@ -55,10 +70,9 @@
    * @return the code owners of the path
    */
   public ImmutableSet<CodeOwnerReference> getPathCodeOwners() {
-    logger.atFine().log(
-        "retrieving path code owners for %s from %s", path(), codeOwnerConfig().key());
+    logger.atFine().log("retrieving path code owners for %s from %s", path(), codeOwnerConfigKey());
     ImmutableSet<CodeOwnerReference> pathCodeOwners =
-        codeOwnerConfig().codeOwnerSets().stream()
+        relevantCodeOwnerSets().stream()
             .flatMap(codeOwnerSet -> codeOwnerSet.codeOwners().stream())
             .collect(toImmutableSet());
     logger.atFine().log("pathCodeOwners = %s", pathCodeOwners);
@@ -73,11 +87,10 @@
    */
   public ImmutableMultimap<CodeOwnerReference, CodeOwnerAnnotation> getAnnotations() {
     logger.atFine().log(
-        "retrieving path code owner annotations for %s from %s", path(), codeOwnerConfig().key());
+        "retrieving path code owner annotations for %s from %s", path(), codeOwnerConfigKey());
     ImmutableMultimap.Builder<CodeOwnerReference, CodeOwnerAnnotation> annotationsBuilder =
         ImmutableMultimap.builder();
-    codeOwnerConfig()
-        .codeOwnerSets()
+    relevantCodeOwnerSets()
         .forEach(codeOwnerSet -> annotationsBuilder.putAll(codeOwnerSet.annotations()));
 
     ImmutableMultimap<CodeOwnerReference, CodeOwnerAnnotation> annotations =
@@ -93,35 +106,151 @@
         .collect(toImmutableSet());
   }
 
-  /**
-   * Whether parent code owners should be ignored for the path.
-   *
-   * @return whether parent code owners should be ignored for the path
-   */
-  public boolean ignoreParentCodeOwners() {
-    return codeOwnerConfig().ignoreParentCodeOwners();
+  private ImmutableSet<CodeOwnerSet> relevantCodeOwnerSets() {
+    if (ignoreGlobalCodeOwners()) {
+      return perFileCodeOwnerSets();
+    }
+
+    return ImmutableSet.<CodeOwnerSet>builder()
+        .addAll(globalCodeOwnerSets())
+        .addAll(perFileCodeOwnerSets())
+        .build();
   }
 
   @Override
   public final String toString() {
     return MoreObjects.toStringHelper(this)
         .add("path", path())
-        .add("codeOwnerConfig", codeOwnerConfig())
+        .add("codeOwnerConfigKey", codeOwnerConfigKey())
+        .add("ignoreParentCodeOwners", ignoreParentCodeOwners())
+        .add("ignoreGlobalCodeOwners", ignoreGlobalCodeOwners())
+        .add("globalCodeOwnerSets", globalCodeOwnerSets())
+        .add("perFileCodeOwnerSets", perFileCodeOwnerSets())
         .add("resolvedImports", resolvedImports())
         .add("unresolvedImports", unresolvedImports())
+        .add("messages", messages())
         .toString();
   }
 
-  /** Creates a {@link PathCodeOwnersResult} instance. */
-  public static PathCodeOwnersResult create(
-      Path path,
-      CodeOwnerConfig codeOwnerConfig,
-      List<CodeOwnerConfigImport> resolvedImports,
-      List<CodeOwnerConfigImport> unresolvedImports) {
-    return new AutoValue_PathCodeOwnersResult(
-        path,
-        codeOwnerConfig,
-        ImmutableList.copyOf(resolvedImports),
-        ImmutableList.copyOf(unresolvedImports));
+  /** Creates a builder for a {@link PathCodeOwnersResult} instance. */
+  public static Builder builder(
+      Path path, CodeOwnerConfig.Key codeOwnerConfigKey, boolean ignoreParentCodeOwners) {
+    return new AutoValue_PathCodeOwnersResult.Builder()
+        .path(path)
+        .codeOwnerConfigKey(codeOwnerConfigKey)
+        .ignoreParentCodeOwners(ignoreParentCodeOwners)
+        .ignoreGlobalCodeOwners(false);
+  }
+
+  @AutoValue.Builder
+  abstract static class Builder {
+    abstract Builder path(Path path);
+
+    abstract Builder codeOwnerConfigKey(CodeOwnerConfig.Key codeOwnerConfigKey);
+
+    abstract Builder ignoreParentCodeOwners(boolean ignoreParentCodeOwners);
+
+    abstract Builder ignoreGlobalCodeOwners(boolean ignoreGlobalCodeOwners);
+
+    abstract boolean ignoreGlobalCodeOwners();
+
+    abstract ImmutableSet.Builder<CodeOwnerSet> globalCodeOwnerSetsBuilder();
+
+    @CanIgnoreReturnValue
+    Builder addGlobalCodeOwnerSet(CodeOwnerSet globalCodeOwnerSet) {
+      requireNonNull(globalCodeOwnerSet, "globalCodeOwnerSet");
+      globalCodeOwnerSetsBuilder().add(globalCodeOwnerSet);
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    Builder addAllGlobalCodeOwnerSets(ImmutableSet<CodeOwnerSet> globalCodeOwnerSets) {
+      requireNonNull(globalCodeOwnerSets, "globalCodeOwnerSets");
+      globalCodeOwnerSetsBuilder().addAll(globalCodeOwnerSets);
+      return this;
+    }
+
+    abstract ImmutableSet.Builder<CodeOwnerSet> perFileCodeOwnerSetsBuilder();
+
+    @CanIgnoreReturnValue
+    Builder addPerFileCodeOwnerSet(CodeOwnerSet perFileCodeOwnerSet) {
+      requireNonNull(perFileCodeOwnerSet, "perFileCodeOwnerSet");
+      perFileCodeOwnerSetsBuilder().add(perFileCodeOwnerSet);
+
+      if (perFileCodeOwnerSet.ignoreGlobalAndParentCodeOwners()) {
+        ignoreParentCodeOwners(true);
+
+        if (!ignoreGlobalCodeOwners()) {
+          ignoreGlobalCodeOwners(true);
+
+          addMessage(
+              String.format(
+                  "found matching per-file code owner set (with path expressions = %s) that ignores"
+                      + " parent code owners, hence ignoring the folder code owners",
+                  perFileCodeOwnerSet.pathExpressions()));
+        }
+      }
+
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    Builder addAllPerFileCodeOwnerSets(ImmutableSet<CodeOwnerSet> perFileCodeOwnerSets) {
+      requireNonNull(perFileCodeOwnerSets, "perFileCodeOwnerSets");
+      perFileCodeOwnerSets.forEach(this::addPerFileCodeOwnerSet);
+      return this;
+    }
+
+    abstract ImmutableSet<CodeOwnerSet> perFileCodeOwnerSets();
+
+    abstract ImmutableList.Builder<CodeOwnerConfigImport> resolvedImportsBuilder();
+
+    @CanIgnoreReturnValue
+    Builder addResolvedImport(CodeOwnerConfigImport codeOwnerConfigImport) {
+      requireNonNull(codeOwnerConfigImport, "codeOwnerConfigImport");
+      resolvedImportsBuilder().add(codeOwnerConfigImport);
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    Builder addAllResolvedImports(ImmutableList<CodeOwnerConfigImport> codeOwnerConfigImports) {
+      requireNonNull(codeOwnerConfigImports, "codeOwnerConfigImports");
+      resolvedImportsBuilder().addAll(codeOwnerConfigImports);
+      return this;
+    }
+
+    abstract ImmutableList.Builder<CodeOwnerConfigImport> unresolvedImportsBuilder();
+
+    @CanIgnoreReturnValue
+    Builder addUnresolvedImport(CodeOwnerConfigImport codeOwnerConfigImport) {
+      requireNonNull(codeOwnerConfigImport, "codeOwnerConfigImport");
+      unresolvedImportsBuilder().add(codeOwnerConfigImport);
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    Builder addAllUnresolvedImports(ImmutableList<CodeOwnerConfigImport> codeOwnerConfigImports) {
+      requireNonNull(codeOwnerConfigImports, "codeOwnerConfigImports");
+      unresolvedImportsBuilder().addAll(codeOwnerConfigImports);
+      return this;
+    }
+
+    abstract ImmutableList.Builder<String> messagesBuilder();
+
+    @CanIgnoreReturnValue
+    Builder addMessage(String message) {
+      requireNonNull(message, "message");
+      messagesBuilder().add(message);
+      return this;
+    }
+
+    @CanIgnoreReturnValue
+    Builder addAllMessages(ImmutableList<String> messages) {
+      requireNonNull(messages, "messages");
+      messagesBuilder().addAll(messages);
+      return this;
+    }
+
+    abstract PathCodeOwnersResult build();
   }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportFormatter.java b/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportFormatter.java
index 7946468..9f96afc 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportFormatter.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportFormatter.java
@@ -43,9 +43,9 @@
         unresolvedImport.keyOfImportedCodeOwnerConfig().project(),
         unresolvedImport.keyOfImportedCodeOwnerConfig().shortBranchName(),
         getFilePath(unresolvedImport.keyOfImportedCodeOwnerConfig()),
-        unresolvedImport.keyOfImportingCodeOwnerConfig().project(),
-        unresolvedImport.keyOfImportingCodeOwnerConfig().shortBranchName(),
-        getFilePath(unresolvedImport.keyOfImportingCodeOwnerConfig()),
+        unresolvedImport.importingCodeOwnerConfig().key().project(),
+        unresolvedImport.importingCodeOwnerConfig().key().shortBranchName(),
+        getFilePath(unresolvedImport.importingCodeOwnerConfig().key()),
         unresolvedImport
             .errorMessage()
             .orElseThrow(
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java b/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
index d94879b..4ffc179 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
@@ -232,7 +232,7 @@
 
           codeOwnerConfigFileInfosBuilder.add(
               codeOwnerConfigFileJson.format(
-                  codeOwnerConfig.key(),
+                  codeOwnerConfig,
                   pathCodeOwners.resolvedImports(),
                   pathCodeOwners.unresolvedImports()));
 
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
index ad8b388..84e5faf 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.plugins.codeowners.api.CodeOwnerCheckInfo;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigFileInfo;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwner;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerAnnotations;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigHierarchy;
@@ -85,6 +86,7 @@
   private final AccountsCollection accountsCollection;
   private final UnresolvedImportFormatter unresolvedImportFormatter;
   private final ChangeFinder changeFinder;
+  private final CodeOwnerConfigFileJson codeOwnerConfigFileJson;
 
   private String email;
   private String path;
@@ -104,7 +106,8 @@
       CodeOwners codeOwners,
       AccountsCollection accountsCollection,
       UnresolvedImportFormatter unresolvedImportFormatter,
-      ChangeFinder changeFinder) {
+      ChangeFinder changeFinder,
+      CodeOwnerConfigFileJson codeOwnerConfigFileJson) {
     this.checkCodeOwnerCapability = checkCodeOwnerCapability;
     this.permissionBackend = permissionBackend;
     this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
@@ -115,6 +118,7 @@
     this.accountsCollection = accountsCollection;
     this.unresolvedImportFormatter = unresolvedImportFormatter;
     this.changeFinder = changeFinder;
+    this.codeOwnerConfigFileJson = codeOwnerConfigFileJson;
   }
 
   @Option(name = "--email", usage = "email for which the code ownership should be checked")
@@ -154,6 +158,8 @@
     validateInput(branchResource);
 
     Path absolutePath = JgitPath.of(path).getAsAbsolutePath();
+    ImmutableList.Builder<CodeOwnerConfigFileInfo> codeOwnerConfigFileInfosBuilder =
+        ImmutableList.builder();
     List<String> messages = new ArrayList<>();
     List<Path> codeOwnerConfigFilePaths = new ArrayList<>();
     AtomicBoolean isCodeOwnershipAssignedToEmail = new AtomicBoolean(false);
@@ -170,19 +176,25 @@
           messages.add(
               String.format(
                   "checking code owner config file %s", codeOwnerConfig.key().format(codeOwners)));
-          OptionalResultWithMessages<PathCodeOwnersResult> pathCodeOwnersResult =
+          PathCodeOwnersResult pathCodeOwnersResult =
               pathCodeOwnersFactory
                   .createWithoutCache(codeOwnerConfig, absolutePath)
                   .resolveCodeOwnerConfig();
+
+          codeOwnerConfigFileInfosBuilder.add(
+              codeOwnerConfigFileJson.format(
+                  codeOwnerConfig,
+                  pathCodeOwnersResult.resolvedImports(),
+                  pathCodeOwnersResult.unresolvedImports()));
+
           messages.addAll(pathCodeOwnersResult.messages());
           pathCodeOwnersResult
-              .get()
               .unresolvedImports()
               .forEach(
                   unresolvedImport ->
                       messages.add(unresolvedImportFormatter.format(unresolvedImport)));
           Optional<CodeOwnerReference> codeOwnerReference =
-              pathCodeOwnersResult.get().getPathCodeOwners().stream()
+              pathCodeOwnersResult.getPathCodeOwners().stream()
                   .filter(cor -> cor.email().equals(email))
                   .findAny();
           if (codeOwnerReference.isPresent()
@@ -202,8 +214,7 @@
               codeOwnerConfigFilePaths.add(codeOwnerConfigFilePath);
             }
 
-            ImmutableSet<String> localAnnotations =
-                pathCodeOwnersResult.get().getAnnotationsFor(email);
+            ImmutableSet<String> localAnnotations = pathCodeOwnersResult.getAnnotationsFor(email);
             if (!localAnnotations.isEmpty()) {
               messages.add(
                   String.format("email %s is annotated with %s", email, sort(localAnnotations)));
@@ -211,7 +222,7 @@
             }
           }
 
-          if (pathCodeOwnersResult.get().getPathCodeOwners().stream()
+          if (pathCodeOwnersResult.getPathCodeOwners().stream()
               .anyMatch(cor -> cor.email().equals(CodeOwnerResolver.ALL_USERS_WILDCARD))) {
             isCodeOwnershipAssignedToAllUsers.set(true);
 
@@ -235,7 +246,7 @@
             }
 
             ImmutableSet<String> localAnnotations =
-                pathCodeOwnersResult.get().getAnnotationsFor(CodeOwnerResolver.ALL_USERS_WILDCARD);
+                pathCodeOwnersResult.getAnnotationsFor(CodeOwnerResolver.ALL_USERS_WILDCARD);
             if (!localAnnotations.isEmpty()) {
               messages.add(
                   String.format(
@@ -252,12 +263,12 @@
             hasRevelantCodeOwnerDefinitions.set(true);
           }
 
-          if (pathCodeOwnersResult.get().ignoreParentCodeOwners()) {
+          if (pathCodeOwnersResult.ignoreParentCodeOwners()) {
             messages.add("parent code owners are ignored");
             parentCodeOwnersAreIgnored.set(true);
           }
 
-          return !pathCodeOwnersResult.get().ignoreParentCodeOwners();
+          return !pathCodeOwnersResult.ignoreParentCodeOwners();
         });
 
     boolean isGlobalCodeOwner = false;
@@ -332,6 +343,7 @@
                 || isFallbackCodeOwner)
             && isResolvable;
     codeOwnerCheckInfo.isResolvable = isResolvable;
+    codeOwnerCheckInfo.codeOwnerConfigs = codeOwnerConfigFileInfosBuilder.build();
     codeOwnerCheckInfo.canReadRef = canReadRef;
     codeOwnerCheckInfo.canSeeChange = canSeeChange;
     codeOwnerCheckInfo.canApproveChange = canApproveChange;
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerConfigFileJson.java b/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerConfigFileJson.java
index fe17a3f..ed59a8e 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerConfigFileJson.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerConfigFileJson.java
@@ -14,31 +14,35 @@
 
 package com.google.gerrit.plugins.codeowners.restapi;
 
+import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigFileInfo;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImport;
 import com.google.gerrit.plugins.codeowners.backend.UnresolvedImportFormatter;
+import com.google.gerrit.server.WebLinks;
 import com.google.inject.Inject;
 import java.util.List;
 
 /** Collection of routines to populate {@link CodeOwnerConfigFileInfo}. */
 public class CodeOwnerConfigFileJson {
+  private final WebLinks webLinks;
   private final UnresolvedImportFormatter unresolvedImportFormatter;
 
   @Inject
-  CodeOwnerConfigFileJson(UnresolvedImportFormatter unresolvedImportFormatter) {
+  CodeOwnerConfigFileJson(WebLinks webLinks, UnresolvedImportFormatter unresolvedImportFormatter) {
+    this.webLinks = webLinks;
     this.unresolvedImportFormatter = unresolvedImportFormatter;
   }
 
   /**
    * Formats the provided code owner config file information as a {@link CodeOwnerConfigFileInfo}.
    *
-   * @param codeOwnerConfigKey the key of the code owner config file as {@link
-   *     CodeOwnerConfigFileInfo}
+   * @param codeOwnerConfig the code owner config
    * @param resolvedImports code owner config files which have been successfully imported directly
    *     or indirectly
    * @param unresolvedImports code owner config files which are imported directly or indirectly but
@@ -47,6 +51,25 @@
    *     as {@link CodeOwnerConfigFileInfo}
    */
   public CodeOwnerConfigFileInfo format(
+      CodeOwnerConfig codeOwnerConfig,
+      List<CodeOwnerConfigImport> resolvedImports,
+      List<CodeOwnerConfigImport> unresolvedImports) {
+    requireNonNull(codeOwnerConfig, "codeOwnerConfig");
+    requireNonNull(resolvedImports, "resolvedImports");
+    requireNonNull(unresolvedImports, "unresolvedImports");
+
+    CodeOwnerConfigFileInfo info =
+        format(codeOwnerConfig.key(), resolvedImports, unresolvedImports);
+
+    ImmutableList<WebLinkInfo> fileLinks =
+        webLinks.getFileLinks(
+            info.project, info.branch, codeOwnerConfig.revision().getName(), info.path);
+    info.webLinks = !fileLinks.isEmpty() ? fileLinks : null;
+
+    return info;
+  }
+
+  private CodeOwnerConfigFileInfo format(
       CodeOwnerConfig.Key codeOwnerConfigKey,
       List<CodeOwnerConfigImport> resolvedImports,
       List<CodeOwnerConfigImport> unresolvedImports) {
@@ -64,7 +87,7 @@
         unresolvedImports.stream()
             .filter(
                 unresolvedImport ->
-                    unresolvedImport.keyOfImportingCodeOwnerConfig().equals(codeOwnerConfigKey))
+                    unresolvedImport.importingCodeOwnerConfig().key().equals(codeOwnerConfigKey))
             .map(
                 unresolvedImport -> {
                   CodeOwnerConfigFileInfo unresolvedCodeOwnerConfigFileInfo =
@@ -92,12 +115,15 @@
         resolvedImports.stream()
             .filter(
                 resolvedImport ->
-                    resolvedImport.keyOfImportingCodeOwnerConfig().equals(codeOwnerConfigKey))
+                    resolvedImport.importingCodeOwnerConfig().key().equals(codeOwnerConfigKey))
             .map(
                 resolvedImport -> {
+                  checkState(
+                      resolvedImport.importedCodeOwnerConfig().isPresent(),
+                      "no imported code owner config for resolved import");
                   CodeOwnerConfigFileInfo resolvedCodeOwnerConfigFileInfo =
                       format(
-                          resolvedImport.keyOfImportedCodeOwnerConfig(),
+                          resolvedImport.importedCodeOwnerConfig().get(),
                           removeImportEntriesFor(resolvedImports, codeOwnerConfigKey),
                           removeImportEntriesFor(unresolvedImports, codeOwnerConfigKey));
                   resolvedCodeOwnerConfigFileInfo.importMode =
@@ -113,7 +139,7 @@
   private ImmutableList<CodeOwnerConfigImport> removeImportEntriesFor(
       List<CodeOwnerConfigImport> imports, CodeOwnerConfig.Key codeOwnerConfigKey) {
     return imports.stream()
-        .filter(i -> !i.keyOfImportingCodeOwnerConfig().equals(codeOwnerConfigKey))
+        .filter(i -> !i.importingCodeOwnerConfig().key().equals(codeOwnerConfigKey))
         .collect(toImmutableList());
   }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerCheckInfoSubject.java b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerCheckInfoSubject.java
index 3128b7f..532fdd7 100644
--- a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerCheckInfoSubject.java
+++ b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerCheckInfoSubject.java
@@ -15,11 +15,15 @@
 package com.google.gerrit.plugins.codeowners.testing;
 
 import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerConfigFileInfoSubject.codeOwnerConfigFileInfos;
+import static com.google.gerrit.truth.ListSubject.elements;
 
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.IterableSubject;
 import com.google.common.truth.Subject;
 import com.google.gerrit.plugins.codeowners.api.CodeOwnerCheckInfo;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigFileInfo;
+import com.google.gerrit.truth.ListSubject;
 
 /** {@link Subject} for doing assertions on {@link CodeOwnerCheckInfo}s. */
 public class CodeOwnerCheckInfoSubject extends Subject {
@@ -69,6 +73,14 @@
     check("isResolvable").that(codeOwnerCheckInfo().isResolvable).isFalse();
   }
 
+  /** Returns a {@link ListSubject} for the code owner config file infos. */
+  public ListSubject<CodeOwnerConfigFileInfoSubject, CodeOwnerConfigFileInfo>
+      hasCodeOwnerConfigsThat() {
+    return check("codeOwnerConfigs")
+        .about(elements())
+        .thatCustom(codeOwnerCheckInfo().codeOwnerConfigs, codeOwnerConfigFileInfos());
+  }
+
   public void canReadRef() {
     check("canReadRef").that(codeOwnerCheckInfo().canReadRef).isTrue();
   }
diff --git a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerConfigFileInfoSubject.java b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerConfigFileInfoSubject.java
index 4d09a10..67ddce7 100644
--- a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerConfigFileInfoSubject.java
+++ b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerConfigFileInfoSubject.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.truth.ListSubject.elements;
 
 import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.IterableSubject;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
@@ -69,6 +70,16 @@
     return check("path()").that(codeOwnerConfigFileInfo().path);
   }
 
+  public IterableSubject hasWebLinksThat() {
+    return check("webLinks()").that(codeOwnerConfigFileInfo().webLinks);
+  }
+
+  @CanIgnoreReturnValue
+  public CodeOwnerConfigFileInfoSubject assertNoWebLinks() {
+    hasWebLinksThat().isNull();
+    return this;
+  }
+
   /**
    * Returns a {@link ListSubject} for the (resolved) imports of the code owner config file info.
    */
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java
index 2351c90..a9c96a8 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java
@@ -61,6 +61,7 @@
 import com.google.gerrit.plugins.codeowners.restapi.CheckCodeOwnerCapability;
 import com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnersForPathInBranch;
 import com.google.inject.Inject;
+import java.nio.file.Paths;
 import java.util.List;
 import java.util.Random;
 import org.junit.Before;
@@ -1706,7 +1707,12 @@
         .inOrder();
     assertThat(codeOwnersInfo)
         .hasDebugLogsThatContainAllOf(
-            String.format("resolve code owners for %s from code owner config %s", path, fooBarKey),
+            String.format(
+                "resolve code owners for %s from code owner config %s:%s:%s",
+                path,
+                fooBarKey.project(),
+                fooBarKey.shortBranchName(),
+                Paths.get(fooBarKey.folderPath().toString(), getCodeOwnerConfigFileName())),
             "per-file code owner set with path expressions [*.md] matches",
             String.format(
                 "The import of %s:master:/%s in %s:master:/foo/bar/%s cannot be resolved:"
@@ -1717,11 +1723,21 @@
                 getCodeOwnerConfigFileName(),
                 nonExistingProject.get()),
             String.format("resolved email %s to account %d", user.email(), user.id().get()),
-            String.format("resolve code owners for %s from code owner config %s", path, fooKey),
+            String.format(
+                "resolve code owners for %s from code owner config %s:%s:%s",
+                path,
+                fooKey.project(),
+                fooKey.shortBranchName(),
+                Paths.get(fooKey.folderPath().toString(), getCodeOwnerConfigFileName())),
             String.format(
                 "cannot resolve code owner email %s: no account with this email exists",
                 nonExistingEmail),
-            String.format("resolve code owners for %s from code owner config %s", path, rootKey),
+            String.format(
+                "resolve code owners for %s from code owner config %s:%s:%s",
+                path,
+                rootKey.project(),
+                rootKey.shortBranchName(),
+                Paths.get(rootKey.folderPath().toString(), getCodeOwnerConfigFileName())),
             String.format("resolved email %s to account %d", admin.email(), admin.id().get()),
             "resolve global code owners",
             String.format("resolved email %s to account %d", admin.email(), admin.id().get()));
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
index 8c5ce92..fe449a4 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
@@ -25,6 +25,7 @@
 import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.config.GerritConfig;
@@ -47,20 +48,25 @@
 import com.google.gerrit.plugins.codeowners.acceptance.testsuite.TestCodeOwnerConfigCreation;
 import com.google.gerrit.plugins.codeowners.acceptance.testsuite.TestPathExpressions;
 import com.google.gerrit.plugins.codeowners.api.CodeOwnerCheckInfo;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigFileInfo;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerAnnotation;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerAnnotations;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImportMode;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigReference;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerSet;
+import com.google.gerrit.plugins.codeowners.backend.config.BackendConfig;
 import com.google.gerrit.plugins.codeowners.restapi.CheckCodeOwnerCapability;
+import com.google.gerrit.plugins.codeowners.testing.CodeOwnerConfigFileInfoSubject;
 import com.google.gerrit.plugins.codeowners.util.JgitPath;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNotes;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.truth.ListSubject;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.util.Arrays;
@@ -84,10 +90,12 @@
   @Inject private ExternalIdFactory externalIdFactory;
 
   private TestPathExpressions testPathExpressions;
+  private CodeOwnerBackend backend;
 
   @Before
   public void setUpCodeOwnersPlugin() throws Exception {
     testPathExpressions = plugin.getSysInjector().getInstance(TestPathExpressions.class);
+    backend = plugin.getSysInjector().getInstance(BackendConfig.class).getDefaultBackend();
   }
 
   @Test
@@ -155,12 +163,20 @@
     TestAccount codeOwner =
         accountCreator.create(
             "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
-    setAsCodeOwners("/foo/", codeOwner);
+    CodeOwnerConfig.Key codeOwnerConfigKey = setAsCodeOwners("/foo/", codeOwner);
 
     String path = "/foo/bar/baz.md";
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, codeOwner.email());
     assertThat(checkCodeOwnerInfo).isCodeOwner();
     assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigsThat()
+        .onlyElement()
+        .assertKey(backend, codeOwnerConfigKey)
+        .assertNoWebLinks()
+        .assertNoImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
     assertThat(checkCodeOwnerInfo).canReadRef();
     assertThat(checkCodeOwnerInfo).canSeeChangeNotSet();
     assertThat(checkCodeOwnerInfo).canApproveChangeNotSet();
@@ -174,6 +190,9 @@
     assertThat(checkCodeOwnerInfo)
         .hasDebugLogsThatContainAllOf(
             String.format(
+                "resolve code owners for %s from code owner config %s:master:%s",
+                path, project, getCodeOwnerConfigFilePath("/foo/")),
+            String.format(
                 "found email %s as a code owner in %s",
                 codeOwner.email(), getCodeOwnerConfigFilePath("/foo/")),
             String.format("resolved email %s to account %s", codeOwner.email(), codeOwner.id()));
@@ -184,14 +203,41 @@
     TestAccount codeOwner =
         accountCreator.create(
             "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
-    setAsRootCodeOwners(codeOwner);
-    setAsCodeOwners("/foo/", codeOwner);
-    setAsCodeOwners("/foo/bar/", codeOwner);
+    CodeOwnerConfig.Key rootCodeOwnerConfigKey = setAsRootCodeOwners(codeOwner);
+    CodeOwnerConfig.Key fooCodeOwnerConfigKey = setAsCodeOwners("/foo/", codeOwner);
+    CodeOwnerConfig.Key fooBarCodeOwnerConfigKey = setAsCodeOwners("/foo/bar/", codeOwner);
 
     String path = "/foo/bar/baz.md";
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, codeOwner.email());
     assertThat(checkCodeOwnerInfo).isCodeOwner();
     assertThat(checkCodeOwnerInfo).isResolvable();
+
+    assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigsThat().hasSize(3);
+    assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigsThat()
+        .element(0)
+        .assertKey(backend, fooBarCodeOwnerConfigKey)
+        .assertNoWebLinks()
+        .assertNoImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
+    assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigsThat()
+        .element(1)
+        .assertKey(backend, fooCodeOwnerConfigKey)
+        .assertNoWebLinks()
+        .assertNoImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
+    assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigsThat()
+        .element(2)
+        .assertKey(backend, rootCodeOwnerConfigKey)
+        .assertNoWebLinks()
+        .assertNoImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
+
     assertThat(checkCodeOwnerInfo)
         .hasCodeOwnerConfigFilePathsThat()
         .containsExactly(
@@ -223,21 +269,41 @@
             "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
     setAsRootCodeOwners(codeOwner);
 
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/foo/")
-        .ignoreParentCodeOwners()
-        .addCodeOwnerEmail(codeOwner.email())
-        .create();
+    CodeOwnerConfig.Key fooCodeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/")
+            .ignoreParentCodeOwners()
+            .addCodeOwnerEmail(codeOwner.email())
+            .create();
 
-    setAsCodeOwners("/foo/bar/", codeOwner);
+    CodeOwnerConfig.Key fooBarCodeOwnerConfigKey = setAsCodeOwners("/foo/bar/", codeOwner);
 
     String path = "/foo/bar/baz.md";
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, codeOwner.email());
     assertThat(checkCodeOwnerInfo).isCodeOwner();
     assertThat(checkCodeOwnerInfo).isResolvable();
+
+    assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigsThat().hasSize(2);
+    assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigsThat()
+        .element(0)
+        .assertKey(backend, fooBarCodeOwnerConfigKey)
+        .assertNoWebLinks()
+        .assertNoImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
+    assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigsThat()
+        .element(1)
+        .assertKey(backend, fooCodeOwnerConfigKey)
+        .assertNoWebLinks()
+        .assertNoImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
+
     assertThat(checkCodeOwnerInfo)
         .hasCodeOwnerConfigFilePathsThat()
         .containsExactly(
@@ -270,12 +336,20 @@
         .addSecondaryEmail(secondaryEmail)
         .update();
 
-    setAsRootCodeOwners(secondaryEmail);
+    CodeOwnerConfig.Key codeOwnerConfigKey = setAsRootCodeOwners(secondaryEmail);
 
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, secondaryEmail);
     assertThat(checkCodeOwnerInfo).isCodeOwner();
     assertThat(checkCodeOwnerInfo).isResolvable();
     assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigsThat()
+        .onlyElement()
+        .assertKey(backend, codeOwnerConfigKey)
+        .assertNoWebLinks()
+        .assertNoImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
+    assertThat(checkCodeOwnerInfo)
         .hasCodeOwnerConfigFilePathsThat()
         .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
@@ -295,12 +369,21 @@
         accountCreator.create(
             "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
 
-    setAsRootCodeOwners(CodeOwnerResolver.ALL_USERS_WILDCARD);
+    CodeOwnerConfig.Key codeOwnerConfigKey =
+        setAsRootCodeOwners(CodeOwnerResolver.ALL_USERS_WILDCARD);
 
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, codeOwner.email());
     assertThat(checkCodeOwnerInfo).isCodeOwner();
     assertThat(checkCodeOwnerInfo).isResolvable();
     assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigsThat()
+        .onlyElement()
+        .assertKey(backend, codeOwnerConfigKey)
+        .assertNoWebLinks()
+        .assertNoImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
+    assertThat(checkCodeOwnerInfo)
         .hasCodeOwnerConfigFilePathsThat()
         .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
@@ -322,12 +405,21 @@
         accountCreator.create(
             "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
 
-    setAsRootCodeOwners(codeOwner.email(), CodeOwnerResolver.ALL_USERS_WILDCARD);
+    CodeOwnerConfig.Key codeOwnerConfigKey =
+        setAsRootCodeOwners(codeOwner.email(), CodeOwnerResolver.ALL_USERS_WILDCARD);
 
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, codeOwner.email());
     assertThat(checkCodeOwnerInfo).isCodeOwner();
     assertThat(checkCodeOwnerInfo).isResolvable();
     assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigsThat()
+        .onlyElement()
+        .assertKey(backend, codeOwnerConfigKey)
+        .assertNoWebLinks()
+        .assertNoImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
+    assertThat(checkCodeOwnerInfo)
         .hasCodeOwnerConfigFilePathsThat()
         .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
@@ -351,6 +443,7 @@
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, user.email());
     assertThat(checkCodeOwnerInfo).isNotCodeOwner();
     assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigsThat().isEmpty();
     assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigFilePathsThat().isEmpty();
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
@@ -364,12 +457,20 @@
   public void checkNonExistingEmail() throws Exception {
     String nonExistingEmail = "non-exiting@example.com";
 
-    setAsRootCodeOwners(nonExistingEmail);
+    CodeOwnerConfig.Key codeOwnerConfigKey = setAsRootCodeOwners(nonExistingEmail);
 
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, nonExistingEmail);
     assertThat(checkCodeOwnerInfo).isNotCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotResolvable();
     assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigsThat()
+        .onlyElement()
+        .assertKey(backend, codeOwnerConfigKey)
+        .assertNoWebLinks()
+        .assertNoImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
+    assertThat(checkCodeOwnerInfo)
         .hasCodeOwnerConfigFilePathsThat()
         .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
@@ -389,7 +490,7 @@
   public void checkAmbiguousExistingEmail() throws Exception {
     String ambiguousEmail = "ambiguous@example.com";
 
-    setAsRootCodeOwners(ambiguousEmail);
+    CodeOwnerConfig.Key codeOwnerConfigKey = setAsRootCodeOwners(ambiguousEmail);
 
     // Add the email to 2 accounts to make it ambiguous.
     addEmail(user.id(), ambiguousEmail);
@@ -399,6 +500,14 @@
     assertThat(checkCodeOwnerInfo).isNotCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotResolvable();
     assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigsThat()
+        .onlyElement()
+        .assertKey(backend, codeOwnerConfigKey)
+        .assertNoWebLinks()
+        .assertNoImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
+    assertThat(checkCodeOwnerInfo)
         .hasCodeOwnerConfigFilePathsThat()
         .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
@@ -425,12 +534,20 @@
       extIdNotes.commit(md);
     }
 
-    setAsRootCodeOwners(orphanedEmail);
+    CodeOwnerConfig.Key codeOwnerConfigKey = setAsRootCodeOwners(orphanedEmail);
 
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, orphanedEmail);
     assertThat(checkCodeOwnerInfo).isNotCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotResolvable();
     assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigsThat()
+        .onlyElement()
+        .assertKey(backend, codeOwnerConfigKey)
+        .assertNoWebLinks()
+        .assertNoImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
+    assertThat(checkCodeOwnerInfo)
         .hasCodeOwnerConfigFilePathsThat()
         .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
@@ -456,12 +573,20 @@
             "inactiveUser", "inactiveUser@example.com", "Inactive User", /* displayName= */ null);
     accountOperations.account(inactiveUser.id()).forUpdate().inactive().update();
 
-    setAsRootCodeOwners(inactiveUser);
+    CodeOwnerConfig.Key codeOwnerConfigKey = setAsRootCodeOwners(inactiveUser);
 
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, inactiveUser.email());
     assertThat(checkCodeOwnerInfo).isNotCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotResolvable();
     assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigsThat()
+        .onlyElement()
+        .assertKey(backend, codeOwnerConfigKey)
+        .assertNoWebLinks()
+        .assertNoImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
+    assertThat(checkCodeOwnerInfo)
         .hasCodeOwnerConfigFilePathsThat()
         .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
@@ -488,12 +613,20 @@
             "User with allowed emil",
             /* displayName= */ null);
 
-    setAsRootCodeOwners(userWithAllowedEmail);
+    CodeOwnerConfig.Key codeOwnerConfigKey = setAsRootCodeOwners(userWithAllowedEmail);
 
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, emailWithAllowedEmailDomain);
     assertThat(checkCodeOwnerInfo).isCodeOwner();
     assertThat(checkCodeOwnerInfo).isResolvable();
     assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigsThat()
+        .onlyElement()
+        .assertKey(backend, codeOwnerConfigKey)
+        .assertNoWebLinks()
+        .assertNoImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
+    assertThat(checkCodeOwnerInfo)
         .hasCodeOwnerConfigFilePathsThat()
         .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
@@ -514,20 +647,28 @@
   @GerritConfig(name = "plugin.code-owners.allowedEmailDomain", value = "example.net")
   public void checkEmailWithNonAllowedDomain() throws Exception {
     String emailWithNonAllowedEmailDomain = "foo@example.com";
-    TestAccount userWithAllowedEmail =
+    TestAccount userWithNonAllowedEmail =
         accountCreator.create(
             "userWithNonAllowedEmail",
             emailWithNonAllowedEmailDomain,
             "User with non-allowed emil",
             /* displayName= */ null);
 
-    setAsRootCodeOwners(userWithAllowedEmail);
+    CodeOwnerConfig.Key codeOwnerConfigKey = setAsRootCodeOwners(userWithNonAllowedEmail);
 
     CodeOwnerCheckInfo checkCodeOwnerInfo =
         checkCodeOwner(ROOT_PATH, emailWithNonAllowedEmailDomain);
     assertThat(checkCodeOwnerInfo).isNotCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotResolvable();
     assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigsThat()
+        .onlyElement()
+        .assertKey(backend, codeOwnerConfigKey)
+        .assertNoWebLinks()
+        .assertNoImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
+    assertThat(checkCodeOwnerInfo)
         .hasCodeOwnerConfigFilePathsThat()
         .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
@@ -551,6 +692,7 @@
         checkCodeOwner(ROOT_PATH, CodeOwnerResolver.ALL_USERS_WILDCARD);
     assertThat(checkCodeOwnerInfo).isNotCodeOwner();
     assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigsThat().isEmpty();
     assertThat(checkCodeOwnerInfo).canReadRefNotSet();
     assertThat(checkCodeOwnerInfo).canSeeChangeNotSet();
     assertThat(checkCodeOwnerInfo).canApproveChangeNotSet();
@@ -562,13 +704,22 @@
 
   @Test
   public void checkAllUsersWildcard_ownedByAllUsers() throws Exception {
-    setAsRootCodeOwners(CodeOwnerResolver.ALL_USERS_WILDCARD);
+    CodeOwnerConfig.Key codeOwnerConfigKey =
+        setAsRootCodeOwners(CodeOwnerResolver.ALL_USERS_WILDCARD);
 
     CodeOwnerCheckInfo checkCodeOwnerInfo =
         checkCodeOwner(ROOT_PATH, CodeOwnerResolver.ALL_USERS_WILDCARD);
     assertThat(checkCodeOwnerInfo).isCodeOwner();
     assertThat(checkCodeOwnerInfo).isResolvable();
     assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigsThat()
+        .onlyElement()
+        .assertKey(backend, codeOwnerConfigKey)
+        .assertNoWebLinks()
+        .assertNoImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
+    assertThat(checkCodeOwnerInfo)
         .hasCodeOwnerConfigFilePathsThat()
         .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
@@ -592,12 +743,20 @@
             "defaultCodeOwner@example.com",
             "Default Code Owner",
             /* displayName= */ null);
-    setAsDefaultCodeOwners(defaultCodeOwner);
+    CodeOwnerConfig.Key codeOwnerConfigKey = setAsDefaultCodeOwners(defaultCodeOwner);
 
     String path = "/foo/bar/baz.md";
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, defaultCodeOwner.email());
     assertThat(checkCodeOwnerInfo).isCodeOwner();
     assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigsThat()
+        .onlyElement()
+        .assertKey(backend, codeOwnerConfigKey)
+        .assertNoWebLinks()
+        .assertNoImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
     assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigFilePathsThat().isEmpty();
     assertThat(checkCodeOwnerInfo).isDefaultCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
@@ -620,12 +779,21 @@
             "defaultCodeOwner@example.com",
             "Default Code Owner",
             /* displayName= */ null);
-    setAsDefaultCodeOwner(CodeOwnerResolver.ALL_USERS_WILDCARD);
+    CodeOwnerConfig.Key codeOwnerConfigKey =
+        setAsDefaultCodeOwner(CodeOwnerResolver.ALL_USERS_WILDCARD);
 
     String path = "/foo/bar/baz.md";
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, defaultCodeOwner.email());
     assertThat(checkCodeOwnerInfo).isCodeOwner();
     assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigsThat()
+        .onlyElement()
+        .assertKey(backend, codeOwnerConfigKey)
+        .assertNoWebLinks()
+        .assertNoImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
     assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigFilePathsThat().isEmpty();
     assertThat(checkCodeOwnerInfo).isDefaultCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
@@ -655,6 +823,7 @@
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, globalCodeOwner.email());
     assertThat(checkCodeOwnerInfo).isCodeOwner();
     assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigsThat().isEmpty();
     assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigFilePathsThat().isEmpty();
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
     assertThat(checkCodeOwnerInfo).isGlobalCodeOwner();
@@ -682,6 +851,7 @@
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, globalCodeOwner.email());
     assertThat(checkCodeOwnerInfo).isCodeOwner();
     assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigsThat().isEmpty();
     assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigFilePathsThat().isEmpty();
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
     assertThat(checkCodeOwnerInfo).isGlobalCodeOwner();
@@ -699,13 +869,21 @@
     TestAccount codeOwner =
         accountCreator.create(
             "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
-    setAsCodeOwners("/foo/", codeOwner);
+    CodeOwnerConfig.Key codeOwnerConfigKey = setAsCodeOwners("/foo/", codeOwner);
 
     String path = "/foo/bar/baz.md";
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, codeOwner.email(), user.email());
     assertThat(checkCodeOwnerInfo).isCodeOwner();
     assertThat(checkCodeOwnerInfo).isResolvable();
     assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigsThat()
+        .onlyElement()
+        .assertKey(backend, codeOwnerConfigKey)
+        .assertNoWebLinks()
+        .assertNoImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
+    assertThat(checkCodeOwnerInfo)
         .hasCodeOwnerConfigFilePathsThat()
         .containsExactly(getCodeOwnerConfigFilePath("/foo/"));
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
@@ -738,13 +916,21 @@
         accountCreator.create(
             "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
 
-    setAsRootCodeOwners(codeOwner);
+    CodeOwnerConfig.Key codeOwnerConfigKey = setAsRootCodeOwners(codeOwner);
 
     CodeOwnerCheckInfo checkCodeOwnerInfo =
         checkCodeOwner(ROOT_PATH, codeOwner.email(), user.email());
     assertThat(checkCodeOwnerInfo).isNotCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotResolvable();
     assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigsThat()
+        .onlyElement()
+        .assertKey(backend, codeOwnerConfigKey)
+        .assertNoWebLinks()
+        .assertNoImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
+    assertThat(checkCodeOwnerInfo)
         .hasCodeOwnerConfigFilePathsThat()
         .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
@@ -772,12 +958,20 @@
         .addSecondaryEmail(secondaryEmail)
         .update();
 
-    setAsRootCodeOwners(secondaryEmail);
+    CodeOwnerConfig.Key codeOwnerConfigKey = setAsRootCodeOwners(secondaryEmail);
 
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, secondaryEmail, user.email());
     assertThat(checkCodeOwnerInfo).isNotCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotResolvable();
     assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigsThat()
+        .onlyElement()
+        .assertKey(backend, codeOwnerConfigKey)
+        .assertNoWebLinks()
+        .assertNoImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
+    assertThat(checkCodeOwnerInfo)
         .hasCodeOwnerConfigFilePathsThat()
         .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
@@ -795,16 +989,16 @@
   }
 
   @Test
-  public void debugLogsContainUnresolvedImports() throws Exception {
+  public void checkWithUnresolvedImports() throws Exception {
     skipTestIfImportsNotSupportedByCodeOwnersBackend();
 
     CodeOwnerConfigReference unresolvableCodeOwnerConfigReferenceCodeOwnerConfigNotFound =
         CodeOwnerConfigReference.create(
-            CodeOwnerConfigImportMode.ALL, "non-existing/" + getCodeOwnerConfigFileName());
+            CodeOwnerConfigImportMode.ALL, "/non-existing/" + getCodeOwnerConfigFileName());
 
     CodeOwnerConfigReference unresolvableCodeOwnerConfigReferenceProjectNotFound =
         CodeOwnerConfigReference.builder(
-                CodeOwnerConfigImportMode.ALL, getCodeOwnerConfigFileName())
+                CodeOwnerConfigImportMode.ALL, "/" + getCodeOwnerConfigFileName())
             .setProject(Project.nameKey("non-existing"))
             .build();
 
@@ -815,7 +1009,7 @@
     gApi.projects().name(nonReadableProject.get()).config(configInput);
     CodeOwnerConfigReference unresolvableCodeOwnerConfigReferenceProjectNotReadable =
         CodeOwnerConfigReference.builder(
-                CodeOwnerConfigImportMode.ALL, getCodeOwnerConfigFileName())
+                CodeOwnerConfigImportMode.ALL, "/" + getCodeOwnerConfigFileName())
             .setProject(nonReadableProject)
             .build();
 
@@ -831,6 +1025,75 @@
             .create();
 
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, user.email());
+
+    CodeOwnerConfigFileInfoSubject codeOwnerConfigFileInfoSubject =
+        assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigsThat().onlyElement();
+    codeOwnerConfigFileInfoSubject.assertKey(backend, codeOwnerConfigKey);
+    codeOwnerConfigFileInfoSubject
+        .assertNoWebLinks()
+        .assertNoResolvedImports()
+        .assertNoImportMode();
+
+    ListSubject<CodeOwnerConfigFileInfoSubject, CodeOwnerConfigFileInfo>
+        unresolvedImportsListSubject = codeOwnerConfigFileInfoSubject.hasUnresolvedImportsThat();
+    unresolvedImportsListSubject.hasSize(3);
+
+    CodeOwnerConfigFileInfoSubject unresolvedImportSubject1 =
+        unresolvedImportsListSubject.element(0);
+    unresolvedImportSubject1.hasProjectThat().isEqualTo(project.get());
+    unresolvedImportSubject1.hasBranchThat().isEqualTo("refs/heads/master");
+    unresolvedImportSubject1
+        .hasPathThat()
+        .isEqualTo(
+            unresolvableCodeOwnerConfigReferenceCodeOwnerConfigNotFound.filePath().toString());
+    unresolvedImportSubject1.assertImportMode(
+        unresolvableCodeOwnerConfigReferenceCodeOwnerConfigNotFound.importMode());
+    unresolvedImportSubject1
+        .hasUnresolvedErrorMessageThat()
+        .isEqualTo(
+            String.format(
+                "code owner config does not exist (revision = %s)",
+                projectOperations.project(project).getHead("master").name()));
+    unresolvedImportSubject1.assertNoWebLinks().assertNoImports();
+
+    CodeOwnerConfigFileInfoSubject unresolvedImportSubject2 =
+        unresolvedImportsListSubject.element(1);
+    unresolvedImportSubject2
+        .hasProjectThat()
+        .isEqualTo(unresolvableCodeOwnerConfigReferenceProjectNotFound.project().get().get());
+    unresolvedImportSubject2.hasBranchThat().isEqualTo("refs/heads/master");
+    unresolvedImportSubject2
+        .hasPathThat()
+        .isEqualTo(unresolvableCodeOwnerConfigReferenceProjectNotFound.filePath().toString());
+    unresolvedImportSubject2.assertImportMode(
+        unresolvableCodeOwnerConfigReferenceProjectNotFound.importMode());
+    unresolvedImportSubject2
+        .hasUnresolvedErrorMessageThat()
+        .isEqualTo(
+            String.format(
+                "project %s not found",
+                unresolvableCodeOwnerConfigReferenceProjectNotFound.project().get()));
+    unresolvedImportSubject2.assertNoWebLinks().assertNoImports();
+
+    CodeOwnerConfigFileInfoSubject unresolvedImportSubject3 =
+        unresolvedImportsListSubject.element(2);
+    unresolvedImportSubject3
+        .hasProjectThat()
+        .isEqualTo(unresolvableCodeOwnerConfigReferenceProjectNotReadable.project().get().get());
+    unresolvedImportSubject3.hasBranchThat().isEqualTo("refs/heads/master");
+    unresolvedImportSubject3
+        .hasPathThat()
+        .isEqualTo(unresolvableCodeOwnerConfigReferenceProjectNotReadable.filePath().toString());
+    unresolvedImportSubject3.assertImportMode(
+        unresolvableCodeOwnerConfigReferenceProjectNotReadable.importMode());
+    unresolvedImportSubject3
+        .hasUnresolvedErrorMessageThat()
+        .isEqualTo(
+            String.format(
+                "state of project %s doesn't permit read",
+                unresolvableCodeOwnerConfigReferenceProjectNotReadable.project().get()));
+    unresolvedImportSubject3.assertNoWebLinks().assertNoImports();
+
     assertThat(checkCodeOwnerInfo)
         .hasDebugLogsThatContainAllOf(
             String.format(
@@ -884,18 +1147,20 @@
   }
 
   @Test
-  public void debugLogsContainUnresolvedTransitiveImports() throws Exception {
+  public void checkWithUnresolvedTransitiveImports() throws Exception {
     skipTestIfImportsNotSupportedByCodeOwnersBackend();
 
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath(ROOT_PATH)
-        .addImport(
-            CodeOwnerConfigReference.create(
-                CodeOwnerConfigImportMode.ALL, "/foo/" + getCodeOwnerConfigFileName()))
-        .create();
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        CodeOwnerConfigReference.create(
+            CodeOwnerConfigImportMode.ALL, "/foo/" + getCodeOwnerConfigFileName());
+    CodeOwnerConfig.Key codeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath(ROOT_PATH)
+            .addImport(codeOwnerConfigReference)
+            .create();
 
     CodeOwnerConfigReference unresolvableCodeOwnerConfigReference =
         CodeOwnerConfigReference.create(
@@ -909,6 +1174,40 @@
         .create();
 
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, user.email());
+
+    CodeOwnerConfigFileInfoSubject codeOwnerConfigFileInfoSubject =
+        assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigsThat().onlyElement();
+    codeOwnerConfigFileInfoSubject.assertKey(backend, codeOwnerConfigKey);
+    codeOwnerConfigFileInfoSubject
+        .assertNoWebLinks()
+        .assertNoUnresolvedImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
+
+    CodeOwnerConfigFileInfoSubject importSubject =
+        codeOwnerConfigFileInfoSubject.hasImportsThat().onlyElement();
+    importSubject.hasProjectThat().isEqualTo(project.get());
+    importSubject.hasBranchThat().isEqualTo("refs/heads/master");
+    importSubject.hasPathThat().isEqualTo(codeOwnerConfigReference.filePath().toString());
+    importSubject.assertImportMode(codeOwnerConfigReference.importMode());
+    importSubject.assertNoWebLinks().assertNoResolvedImports();
+
+    CodeOwnerConfigFileInfoSubject transitiveImportSubject =
+        importSubject.hasUnresolvedImportsThat().onlyElement();
+    transitiveImportSubject.hasProjectThat().isEqualTo(project.get());
+    transitiveImportSubject.hasBranchThat().isEqualTo("refs/heads/master");
+    transitiveImportSubject
+        .hasPathThat()
+        .isEqualTo(unresolvableCodeOwnerConfigReference.filePath().toString());
+    transitiveImportSubject.assertImportMode(unresolvableCodeOwnerConfigReference.importMode());
+    transitiveImportSubject
+        .hasUnresolvedErrorMessageThat()
+        .isEqualTo(
+            String.format(
+                "code owner config does not exist (revision = %s)",
+                projectOperations.project(project).getHead("master").name()));
+    transitiveImportSubject.assertNoWebLinks().assertNoImports();
+
     assertThat(checkCodeOwnerInfo)
         .hasDebugLogsThatContainAllOf(
             String.format(
@@ -942,28 +1241,37 @@
         accountCreator.create(
             "mdCodeOwner", "mdCodeOwner@example.com", "Md Code Owner", /* displayName= */ null);
 
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/foo/")
-        .addCodeOwnerSet(
-            CodeOwnerSet.builder()
-                .addPathExpression(testPathExpressions.matchFileType("txt"))
-                .addCodeOwnerEmail(txtOwner.email())
-                .build())
-        .addCodeOwnerSet(
-            CodeOwnerSet.builder()
-                .addPathExpression(testPathExpressions.matchFileType("md"))
-                .addCodeOwnerEmail(mdOwner.email())
-                .build())
-        .create();
+    CodeOwnerConfig.Key codeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/")
+            .addCodeOwnerSet(
+                CodeOwnerSet.builder()
+                    .addPathExpression(testPathExpressions.matchFileType("txt"))
+                    .addCodeOwnerEmail(txtOwner.email())
+                    .build())
+            .addCodeOwnerSet(
+                CodeOwnerSet.builder()
+                    .addPathExpression(testPathExpressions.matchFileType("md"))
+                    .addCodeOwnerEmail(mdOwner.email())
+                    .build())
+            .create();
 
     String path = "/foo/bar/baz.md";
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, mdOwner.email());
     assertThat(checkCodeOwnerInfo).isCodeOwner();
     assertThat(checkCodeOwnerInfo).isResolvable();
     assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigsThat()
+        .onlyElement()
+        .assertKey(backend, codeOwnerConfigKey)
+        .assertNoWebLinks()
+        .assertNoImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
+    assertThat(checkCodeOwnerInfo)
         .hasCodeOwnerConfigFilePathsThat()
         .containsExactly(getCodeOwnerConfigFilePath("/foo/"));
     assertThat(checkCodeOwnerInfo)
@@ -998,24 +1306,33 @@
             "Folder Code Owner",
             /* displayName= */ null);
 
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/foo/")
-        .addCodeOwnerEmail(folderCodeOwner.email())
-        .addCodeOwnerSet(
-            CodeOwnerSet.builder()
-                .addPathExpression(testPathExpressions.matchFileType("md"))
-                .setIgnoreGlobalAndParentCodeOwners()
-                .addCodeOwnerEmail(fileCodeOwner.email())
-                .build())
-        .create();
+    CodeOwnerConfig.Key codeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/")
+            .addCodeOwnerEmail(folderCodeOwner.email())
+            .addCodeOwnerSet(
+                CodeOwnerSet.builder()
+                    .addPathExpression(testPathExpressions.matchFileType("md"))
+                    .setIgnoreGlobalAndParentCodeOwners()
+                    .addCodeOwnerEmail(fileCodeOwner.email())
+                    .build())
+            .create();
 
     String path = "/foo/bar/baz.md";
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, folderCodeOwner.email());
     assertThat(checkCodeOwnerInfo).isNotCodeOwner();
     assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigsThat()
+        .onlyElement()
+        .assertKey(backend, codeOwnerConfigKey)
+        .assertNoWebLinks()
+        .assertNoImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
     assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigFilePathsThat().isEmpty();
     assertThat(checkCodeOwnerInfo)
         .hasDebugLogsThatContainAllOf(
@@ -1031,6 +1348,14 @@
     assertThat(checkCodeOwnerInfo).isCodeOwner();
     assertThat(checkCodeOwnerInfo).isResolvable();
     assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigsThat()
+        .onlyElement()
+        .assertKey(backend, codeOwnerConfigKey)
+        .assertNoWebLinks()
+        .assertNoImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
+    assertThat(checkCodeOwnerInfo)
         .hasCodeOwnerConfigFilePathsThat()
         .containsExactly(getCodeOwnerConfigFilePath("/foo/"));
     assertThat(checkCodeOwnerInfo)
@@ -1057,32 +1382,58 @@
         accountCreator.create(
             "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
 
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/foo/")
-        .addImport(
-            CodeOwnerConfigReference.create(
-                CodeOwnerConfigImportMode.ALL, "/bar/" + getCodeOwnerConfigFileName()))
-        .create();
+    CodeOwnerConfigReference barCodeOwnerConfigReference =
+        CodeOwnerConfigReference.create(
+            CodeOwnerConfigImportMode.ALL, "/bar/" + getCodeOwnerConfigFileName());
+    CodeOwnerConfig.Key fooCodeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/")
+            .addImport(barCodeOwnerConfigReference)
+            .create();
 
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/bar/")
-        .addImport(
-            CodeOwnerConfigReference.create(
-                CodeOwnerConfigImportMode.ALL, "/baz/" + getCodeOwnerConfigFileName()))
-        .create();
+    CodeOwnerConfigReference bazCodeOwnerConfigReference =
+        CodeOwnerConfigReference.create(
+            CodeOwnerConfigImportMode.ALL, "/baz/" + getCodeOwnerConfigFileName());
+    CodeOwnerConfig.Key barCodeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/bar/")
+            .addImport(bazCodeOwnerConfigReference)
+            .create();
 
-    setAsCodeOwners("/baz/", codeOwner);
+    CodeOwnerConfig.Key bazCodeOwnerConfigKey = setAsCodeOwners("/baz/", codeOwner);
 
     String path = "/foo/bar/baz.md";
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, codeOwner.email());
     assertThat(checkCodeOwnerInfo).isCodeOwner();
     assertThat(checkCodeOwnerInfo).isResolvable();
+
+    CodeOwnerConfigFileInfoSubject codeOwnerConfigFileInfoSubject =
+        assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigsThat().onlyElement();
+    codeOwnerConfigFileInfoSubject.assertKey(backend, fooCodeOwnerConfigKey);
+    codeOwnerConfigFileInfoSubject
+        .assertNoWebLinks()
+        .assertNoUnresolvedImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
+
+    CodeOwnerConfigFileInfoSubject importSubject =
+        codeOwnerConfigFileInfoSubject.hasImportsThat().onlyElement();
+    importSubject.assertKey(backend, barCodeOwnerConfigKey);
+    importSubject.assertImportMode(barCodeOwnerConfigReference.importMode());
+    importSubject.assertNoWebLinks().assertNoUnresolvedImports().assertNoUnresolvedErrorMessage();
+
+    CodeOwnerConfigFileInfoSubject transitiveImportSubject =
+        importSubject.hasImportsThat().onlyElement();
+    transitiveImportSubject.assertKey(backend, bazCodeOwnerConfigKey);
+    transitiveImportSubject.assertImportMode(bazCodeOwnerConfigReference.importMode());
+    transitiveImportSubject.assertNoWebLinks().assertNoImports().assertNoUnresolvedErrorMessage();
+
     assertThat(checkCodeOwnerInfo)
         .hasCodeOwnerConfigFilePathsThat()
         .containsExactly(getCodeOwnerConfigFilePath("/foo/"));
@@ -1111,38 +1462,64 @@
         accountCreator.create(
             "mdCodeOwner", "mdCodeOwner@example.com", "Md Code Owner", /* displayName= */ null);
 
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/foo/")
-        .addImport(
-            CodeOwnerConfigReference.create(
-                CodeOwnerConfigImportMode.ALL, "/bar/" + getCodeOwnerConfigFileName()))
-        .create();
+    CodeOwnerConfigReference barCodeOwnerConfigReference =
+        CodeOwnerConfigReference.create(
+            CodeOwnerConfigImportMode.ALL, "/bar/" + getCodeOwnerConfigFileName());
+    CodeOwnerConfig.Key fooCodeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/")
+            .addImport(barCodeOwnerConfigReference)
+            .create();
 
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/bar/")
-        .addCodeOwnerSet(
-            CodeOwnerSet.builder()
-                .addPathExpression(testPathExpressions.matchFileType("md"))
-                .addImport(
-                    CodeOwnerConfigReference.create(
-                        CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
-                        "/baz/" + getCodeOwnerConfigFileName()))
-                .build())
-        .create();
+    CodeOwnerConfigReference bazCodeOwnerConfigReference =
+        CodeOwnerConfigReference.create(
+            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
+            "/baz/" + getCodeOwnerConfigFileName());
+    CodeOwnerConfig.Key barCodeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/bar/")
+            .addCodeOwnerSet(
+                CodeOwnerSet.builder()
+                    .addPathExpression(testPathExpressions.matchFileType("md"))
+                    .addImport(bazCodeOwnerConfigReference)
+                    .build())
+            .create();
 
-    setAsCodeOwners("/baz/", mdCodeOwner);
+    CodeOwnerConfig.Key bazCodeOwnerConfigKey = setAsCodeOwners("/baz/", mdCodeOwner);
 
     // 1. check for mdCodeOwner and path of an md file
     String path = "/foo/bar/baz.md";
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, mdCodeOwner.email());
     assertThat(checkCodeOwnerInfo).isCodeOwner();
     assertThat(checkCodeOwnerInfo).isResolvable();
+
+    CodeOwnerConfigFileInfoSubject codeOwnerConfigFileInfoSubject =
+        assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigsThat().onlyElement();
+    codeOwnerConfigFileInfoSubject.assertKey(backend, fooCodeOwnerConfigKey);
+    codeOwnerConfigFileInfoSubject
+        .assertNoWebLinks()
+        .assertNoUnresolvedImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
+
+    CodeOwnerConfigFileInfoSubject importSubject =
+        codeOwnerConfigFileInfoSubject.hasImportsThat().onlyElement();
+    importSubject.assertKey(backend, barCodeOwnerConfigKey);
+    importSubject.assertImportMode(barCodeOwnerConfigReference.importMode());
+    importSubject.assertNoWebLinks().assertNoUnresolvedImports().assertNoUnresolvedErrorMessage();
+
+    CodeOwnerConfigFileInfoSubject transitiveImportSubject =
+        importSubject.hasImportsThat().onlyElement();
+    transitiveImportSubject.assertKey(backend, bazCodeOwnerConfigKey);
+    transitiveImportSubject.assertImportMode(bazCodeOwnerConfigReference.importMode());
+    transitiveImportSubject.assertNoWebLinks().assertNoImports().assertNoUnresolvedErrorMessage();
+
     assertThat(checkCodeOwnerInfo)
         .hasCodeOwnerConfigFilePathsThat()
         .containsExactly(getCodeOwnerConfigFilePath("/foo/"));
@@ -1171,6 +1548,26 @@
     checkCodeOwnerInfo = checkCodeOwner(path, user.email());
     assertThat(checkCodeOwnerInfo).isNotCodeOwner();
     assertThat(checkCodeOwnerInfo).isResolvable();
+
+    codeOwnerConfigFileInfoSubject =
+        assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigsThat().onlyElement();
+    codeOwnerConfigFileInfoSubject.assertKey(backend, fooCodeOwnerConfigKey);
+    codeOwnerConfigFileInfoSubject
+        .assertNoWebLinks()
+        .assertNoUnresolvedImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
+
+    importSubject = codeOwnerConfigFileInfoSubject.hasImportsThat().onlyElement();
+    importSubject.assertKey(backend, barCodeOwnerConfigKey);
+    importSubject.assertImportMode(barCodeOwnerConfigReference.importMode());
+    importSubject.assertNoWebLinks().assertNoUnresolvedImports().assertNoUnresolvedErrorMessage();
+
+    transitiveImportSubject = importSubject.hasImportsThat().onlyElement();
+    transitiveImportSubject.assertKey(backend, bazCodeOwnerConfigKey);
+    transitiveImportSubject.assertImportMode(bazCodeOwnerConfigReference.importMode());
+    transitiveImportSubject.assertNoWebLinks().assertNoImports().assertNoUnresolvedErrorMessage();
+
     assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigFilePathsThat().isEmpty();
     assertThat(checkCodeOwnerInfo)
         .hasDebugLogsThatContainAllOf(
@@ -1196,6 +1593,21 @@
     checkCodeOwnerInfo = checkCodeOwner(path, mdCodeOwner.email());
     assertThat(checkCodeOwnerInfo).isNotCodeOwner();
     assertThat(checkCodeOwnerInfo).isResolvable();
+
+    codeOwnerConfigFileInfoSubject =
+        assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigsThat().onlyElement();
+    codeOwnerConfigFileInfoSubject.assertKey(backend, fooCodeOwnerConfigKey);
+    codeOwnerConfigFileInfoSubject
+        .assertNoWebLinks()
+        .assertNoUnresolvedImports()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoImportMode();
+
+    importSubject = codeOwnerConfigFileInfoSubject.hasImportsThat().onlyElement();
+    importSubject.assertKey(backend, barCodeOwnerConfigKey);
+    importSubject.assertImportMode(barCodeOwnerConfigReference.importMode());
+    importSubject.assertNoWebLinks().assertNoImports().assertNoUnresolvedErrorMessage();
+
     assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigFilePathsThat().isEmpty();
     assertThat(checkCodeOwnerInfo)
         .hasDebugLogsThatContainAllOf(
@@ -1568,12 +1980,17 @@
   }
 
   private String getCodeOwnerConfigFilePath(String folderPath) {
-    assertThat(folderPath).startsWith("/");
-    assertThat(folderPath).endsWith("/");
+    if (!folderPath.startsWith("/")) {
+      folderPath = "/" + folderPath;
+    }
+    if (!folderPath.endsWith("/")) {
+      folderPath = folderPath + "/";
+    }
     return folderPath + getCodeOwnerConfigFileName();
   }
 
-  private void setAsRootCodeOwners(String... emails) {
+  @CanIgnoreReturnValue
+  private CodeOwnerConfig.Key setAsRootCodeOwners(String... emails) {
     TestCodeOwnerConfigCreation.Builder builder =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
@@ -1581,11 +1998,12 @@
             .branch("master")
             .folderPath(ROOT_PATH);
     Arrays.stream(emails).forEach(builder::addCodeOwnerEmail);
-    builder.create();
+    return builder.create();
   }
 
-  private void setAsDefaultCodeOwner(String email) {
-    codeOwnerConfigOperations
+  @CanIgnoreReturnValue
+  private CodeOwnerConfig.Key setAsDefaultCodeOwner(String email) {
+    return codeOwnerConfigOperations
         .newCodeOwnerConfig()
         .project(project)
         .branch(RefNames.REFS_CONFIG)
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImportTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImportTest.java
index b6efbc6..85e7eb5 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImportTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImportTest.java
@@ -14,16 +14,24 @@
 
 package com.google.gerrit.plugins.codeowners.backend;
 
+import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
 
 /** Tests for {@link CodeOwnerConfigImport}. */
 public class CodeOwnerConfigImportTest extends AbstractAutoValueTest {
+  private static final ObjectId TEST_REVISION =
+      ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+
   @Test
   public void toStringIncludesAllData_resolvedImport() throws Exception {
     CodeOwnerConfigImport resolvedImport =
         CodeOwnerConfigImport.createResolvedImport(
-            CodeOwnerConfig.Key.create(project, "master", "/"),
-            CodeOwnerConfig.Key.create(project, "master", "/bar/"),
+            CodeOwnerConfig.builder(
+                    CodeOwnerConfig.Key.create(project, "master", "/"), TEST_REVISION)
+                .build(),
+            CodeOwnerConfig.builder(
+                    CodeOwnerConfig.Key.create(project, "master", "/bar/"), TEST_REVISION)
+                .build(),
             CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS"));
     assertThatToStringIncludesAllData(resolvedImport, CodeOwnerConfigImport.class);
   }
@@ -32,7 +40,9 @@
   public void toStringIncludesAllData_unresolvedImport() throws Exception {
     CodeOwnerConfigImport unresolvedImport =
         CodeOwnerConfigImport.createUnresolvedImport(
-            CodeOwnerConfig.Key.create(project, "master", "/"),
+            CodeOwnerConfig.builder(
+                    CodeOwnerConfig.Key.create(project, "master", "/"), TEST_REVISION)
+                .build(),
             CodeOwnerConfig.Key.create(project, "master", "/bar/"),
             CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS"),
             "test message");
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResultTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResultTest.java
index deab817..febdf6b 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResultTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResultTest.java
@@ -17,10 +17,14 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.ImmutableSet;
+import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
 
 /** Tests for {@link CodeOwnerResolverResult}. */
 public class CodeOwnerResolverResultTest extends AbstractAutoValueTest {
+  private static final ObjectId TEST_REVISION =
+      ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+
   @Test
   public void toStringIncludesAllData() throws Exception {
     CodeOwnerResolverResult codeOwnerResolverResult =
@@ -31,12 +35,18 @@
             /* hasUnresolvedCodeOwners= */ false,
             ImmutableList.of(
                 CodeOwnerConfigImport.createResolvedImport(
-                    CodeOwnerConfig.Key.create(project, "master", "/"),
-                    CodeOwnerConfig.Key.create(project, "master", "/bar/"),
+                    CodeOwnerConfig.builder(
+                            CodeOwnerConfig.Key.create(project, "master", "/"), TEST_REVISION)
+                        .build(),
+                    CodeOwnerConfig.builder(
+                            CodeOwnerConfig.Key.create(project, "master", "/bar/"), TEST_REVISION)
+                        .build(),
                     CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS"))),
             ImmutableList.of(
                 CodeOwnerConfigImport.createUnresolvedImport(
-                    CodeOwnerConfig.Key.create(project, "master", "/"),
+                    CodeOwnerConfig.builder(
+                            CodeOwnerConfig.Key.create(project, "master", "/"), TEST_REVISION)
+                        .build(),
                     CodeOwnerConfig.Key.create(project, "master", "/bar/"),
                     CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS"),
                     "test message")),
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResultTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResultTest.java
index a33835d..4d62da9 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResultTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResultTest.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.plugins.codeowners.backend;
 
-import com.google.common.collect.ImmutableList;
 import java.nio.file.Path;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
@@ -27,28 +26,32 @@
   @Test
   public void toStringIncludesAllData() throws Exception {
     CodeOwnerConfig.Key codeOwnerConfigKey = CodeOwnerConfig.Key.create(project, "master", "/");
+    CodeOwnerConfig codeOwnerConfig =
+        CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION).build();
     CodeOwnerConfigReference resolvableCodeOwnerConfigReference =
         CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS");
     CodeOwnerConfigReference unresolvableCodeOwnerConfigReference =
         CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/baz/OWNERS");
     PathCodeOwnersResult pathCodeOwnersResult =
-        PathCodeOwnersResult.create(
-            Path.of("/foo/bar/baz.md"),
-            CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
-                .addImport(resolvableCodeOwnerConfigReference)
-                .addImport(unresolvableCodeOwnerConfigReference)
-                .build(),
-            ImmutableList.of(
+        PathCodeOwnersResult.builder(
+                Path.of("/foo/bar/baz.md"),
+                codeOwnerConfig.key(),
+                codeOwnerConfig.ignoreParentCodeOwners())
+            .addAllGlobalCodeOwnerSets(codeOwnerConfig.codeOwnerSets())
+            .addResolvedImport(
                 CodeOwnerConfigImport.createResolvedImport(
-                    codeOwnerConfigKey,
-                    CodeOwnerConfig.Key.create(project, "master", "/bar/"),
-                    resolvableCodeOwnerConfigReference)),
-            ImmutableList.of(
+                    codeOwnerConfig,
+                    CodeOwnerConfig.builder(
+                            CodeOwnerConfig.Key.create(project, "master", "/bar/"), TEST_REVISION)
+                        .build(),
+                    resolvableCodeOwnerConfigReference))
+            .addUnresolvedImport(
                 CodeOwnerConfigImport.createUnresolvedImport(
-                    codeOwnerConfigKey,
+                    codeOwnerConfig,
                     CodeOwnerConfig.Key.create(project, "master", "/baz/"),
                     unresolvableCodeOwnerConfigReference,
-                    "test message")));
+                    "test message"))
+            .build();
     assertThatToStringIncludesAllData(pathCodeOwnersResult, PathCodeOwnersResult.class);
   }
 }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersTest.java
index 73d1e3d..f0f53c3 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersTest.java
@@ -44,6 +44,7 @@
 import com.google.inject.Provider;
 import com.google.inject.util.Providers;
 import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.Optional;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -234,9 +235,9 @@
     CodeOwnerConfig emptyCodeOwnerConfig = createCodeOwnerBuilder().build();
     PathCodeOwners pathCodeOwners =
         pathCodeOwnersFactory.createWithoutCache(emptyCodeOwnerConfig, Path.of("/foo/bar/baz.md"));
-    assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().getPathCodeOwners()).isEmpty();
-    assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().resolvedImports()).isEmpty();
-    assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().unresolvedImports()).isEmpty();
+    assertThat(pathCodeOwners.resolveCodeOwnerConfig().getPathCodeOwners()).isEmpty();
+    assertThat(pathCodeOwners.resolveCodeOwnerConfig().resolvedImports()).isEmpty();
+    assertThat(pathCodeOwners.resolveCodeOwnerConfig().unresolvedImports()).isEmpty();
   }
 
   @Test
@@ -247,7 +248,7 @@
             .build();
     PathCodeOwners pathCodeOwners =
         pathCodeOwnersFactory.createWithoutCache(codeOwnerConfig, Path.of("/foo/bar/baz.md"));
-    assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().getPathCodeOwners())
+    assertThat(pathCodeOwners.resolveCodeOwnerConfig().getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
   }
@@ -285,7 +286,7 @@
               .build();
       PathCodeOwners pathCodeOwners =
           pathCodeOwnersFactory.createWithoutCache(codeOwnerConfig, Path.of("/foo/bar/baz.md"));
-      assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().getPathCodeOwners())
+      assertThat(pathCodeOwners.resolveCodeOwnerConfig().getPathCodeOwners())
           .comparingElementsUsing(hasEmail())
           .containsExactly(admin.email(), user.email());
     }
@@ -306,7 +307,7 @@
               .build();
       PathCodeOwners pathCodeOwners =
           pathCodeOwnersFactory.createWithoutCache(codeOwnerConfig, Path.of("/foo/bar/baz.md"));
-      assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().getPathCodeOwners()).isEmpty();
+      assertThat(pathCodeOwners.resolveCodeOwnerConfig().getPathCodeOwners()).isEmpty();
     }
   }
 
@@ -338,7 +339,7 @@
               .build();
       PathCodeOwners pathCodeOwners =
           pathCodeOwnersFactory.createWithoutCache(codeOwnerConfig, Path.of("/foo/bar/baz.md"));
-      assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().getPathCodeOwners())
+      assertThat(pathCodeOwners.resolveCodeOwnerConfig().getPathCodeOwners())
           .comparingElementsUsing(hasEmail())
           .containsExactly(admin.email());
     }
@@ -372,7 +373,7 @@
               .build();
       PathCodeOwners pathCodeOwners =
           pathCodeOwnersFactory.createWithoutCache(codeOwnerConfig, Path.of("/foo/bar/baz.md"));
-      assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().getPathCodeOwners())
+      assertThat(pathCodeOwners.resolveCodeOwnerConfig().getPathCodeOwners())
           .comparingElementsUsing(hasEmail())
           .containsExactly(admin.email(), user.email());
     }
@@ -407,7 +408,7 @@
               .build();
       PathCodeOwners pathCodeOwners =
           pathCodeOwnersFactory.createWithoutCache(codeOwnerConfig, Path.of("/foo/bar/baz.md"));
-      assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().getPathCodeOwners())
+      assertThat(pathCodeOwners.resolveCodeOwnerConfig().getPathCodeOwners())
           .comparingElementsUsing(hasEmail())
           .containsExactly(admin.email(), user.email());
     }
@@ -420,7 +421,7 @@
         pathCodeOwnersFactory.createWithoutCache(
             createCodeOwnerBuilder().setIgnoreParentCodeOwners().build(),
             Path.of("/foo/bar/baz.md"));
-    assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().ignoreParentCodeOwners()).isTrue();
+    assertThat(pathCodeOwners.resolveCodeOwnerConfig().ignoreParentCodeOwners()).isTrue();
   }
 
   @Test
@@ -430,7 +431,7 @@
         pathCodeOwnersFactory.createWithoutCache(
             createCodeOwnerBuilder().setIgnoreParentCodeOwners(false).build(),
             Path.of("/foo/bar/baz.md"));
-    assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().ignoreParentCodeOwners()).isFalse();
+    assertThat(pathCodeOwners.resolveCodeOwnerConfig().ignoreParentCodeOwners()).isFalse();
   }
 
   @Test
@@ -446,7 +447,7 @@
                         .build())
                 .build(),
             Path.of("/foo.md"));
-    assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().ignoreParentCodeOwners()).isTrue();
+    assertThat(pathCodeOwners.resolveCodeOwnerConfig().ignoreParentCodeOwners()).isTrue();
   }
 
   @Test
@@ -463,7 +464,7 @@
                         .build())
                 .build(),
             Path.of("/foo.md"));
-    assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().ignoreParentCodeOwners()).isFalse();
+    assertThat(pathCodeOwners.resolveCodeOwnerConfig().ignoreParentCodeOwners()).isFalse();
   }
 
   @Test
@@ -474,7 +475,7 @@
         createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
 
     // create importing config with non-resolveable import
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -484,17 +485,20 @@
             .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(admin.email()).build())
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the global code owner from the importing code owner config, the
     // non-resolveable import is silently ignored
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email());
@@ -502,7 +506,7 @@
     assertThat(pathCodeOwnersResult.unresolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createUnresolvedImport(
-                importingCodeOwnerConfigKey,
+                importingCodeOwnerConfig,
                 keyOfImportedCodeOwnerConfig,
                 codeOwnerConfigReference,
                 String.format(
@@ -530,7 +534,7 @@
         createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
 
     // create config with import of non code owner config file
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -540,17 +544,20 @@
             .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(admin.email()).build())
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the global code owner from the importing code owner config, the
     // import of the non code owner config file is silently ignored
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email());
@@ -558,7 +565,7 @@
     assertThat(pathCodeOwnersResult.unresolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createUnresolvedImport(
-                importingCodeOwnerConfigKey,
+                importingCodeOwnerConfig,
                 keyOfImportedCodeOwnerConfig,
                 codeOwnerConfigReference,
                 String.format(
@@ -589,7 +596,7 @@
         createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
 
     // create the importing config
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -599,10 +606,13 @@
             .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(admin.email()).build())
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
@@ -610,7 +620,7 @@
     // Expectation: we get the global code owner from the importing code owner config, the
     // import of the code owner config file with the file extension is silently ignored since it is
     // not considered as a code owner config file
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email());
@@ -618,7 +628,7 @@
     assertThat(pathCodeOwnersResult.unresolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createUnresolvedImport(
-                importingCodeOwnerConfigKey,
+                importingCodeOwnerConfig,
                 keyOfImportedCodeOwnerConfig,
                 codeOwnerConfigReference,
                 String.format(
@@ -650,7 +660,7 @@
         createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
 
     // create the importing config
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -660,26 +670,29 @@
             .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(admin.email()).build())
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the global code owner from the importing code owner config and the global
     // code owner from the imported code owner config
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig,
-                codeOwnerConfigReference));
+                importingCodeOwnerConfig, importedCodeOwnerConfig, codeOwnerConfigReference));
     assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
@@ -708,7 +721,7 @@
         createCodeOwnerConfigReference(importMode, keyOfImportedCodeOwnerConfig);
 
     // create importing config with global code owner and import
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -718,26 +731,29 @@
             .addImport(codeOwnerConfigReference)
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the global code owners from the importing and the imported code owner
     // config
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig,
-                codeOwnerConfigReference));
+                importingCodeOwnerConfig, importedCodeOwnerConfig, codeOwnerConfigReference));
     assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
@@ -761,7 +777,7 @@
         createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
 
     // create importing config with matching per-file code owner and import
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -775,26 +791,29 @@
             .addImport(codeOwnerConfigReference)
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the matching per-file code owners from the importing and the imported
     // code owner config
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig,
-                codeOwnerConfigReference));
+                importingCodeOwnerConfig, importedCodeOwnerConfig, codeOwnerConfigReference));
     assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
@@ -818,7 +837,7 @@
         createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
 
     // create importing config with matching per-file code owner and import
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -832,10 +851,15 @@
             .addImport(codeOwnerConfigReference)
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
@@ -843,16 +867,14 @@
     // Expectation: we only get the matching per-file code owners from the importing code owner
     // config, the per-file code owners from the imported code owner config are not relevant since
     // they do not match
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email());
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig,
-                codeOwnerConfigReference));
+                importingCodeOwnerConfig, importedCodeOwnerConfig, codeOwnerConfigReference));
     assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
@@ -877,7 +899,7 @@
             CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig);
 
     // create importing config with matching per-file code owner and import
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -891,10 +913,15 @@
             .addImport(codeOwnerConfigReference)
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
@@ -902,16 +929,14 @@
     // Expectation: we only get the matching per-file code owners from the importing code owner
     // config, the matching per-file code owners from the imported code owner config are not
     // relevant with import mode GLOBAL_CODE_OWNER_SETS_ONLY
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email());
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig,
-                codeOwnerConfigReference));
+                importingCodeOwnerConfig, importedCodeOwnerConfig, codeOwnerConfigReference));
     assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
@@ -939,7 +964,7 @@
         createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
 
     // create importing config with global code owner and import
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -949,10 +974,15 @@
             .addImport(codeOwnerConfigReference)
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
@@ -962,7 +992,7 @@
     // the matching per-file code owner set in the imported code owner config has the
     // ignoreGlobalAndParentCodeOwners flag set to true which causes global code owners to be
     // ignored, in addition this flag causes parent code owners to be ignored
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(user.email());
@@ -970,9 +1000,7 @@
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig,
-                codeOwnerConfigReference));
+                importingCodeOwnerConfig, importedCodeOwnerConfig, codeOwnerConfigReference));
     assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
@@ -1000,7 +1028,7 @@
         createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
 
     // create importing config with global code owner and import
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -1010,10 +1038,15 @@
             .addImport(codeOwnerConfigReference)
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
@@ -1022,7 +1055,7 @@
     // per-file code owners from the imported code owner config and its
     // ignoreGlobalAndParentCodeOwners flag are not relevant since the per-file code owner set does
     // not match
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email());
@@ -1030,9 +1063,7 @@
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig,
-                codeOwnerConfigReference));
+                importingCodeOwnerConfig, importedCodeOwnerConfig, codeOwnerConfigReference));
     assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
@@ -1060,7 +1091,7 @@
             CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig);
 
     // create importing config with global code owner and import
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -1070,10 +1101,15 @@
             .addImport(codeOwnerConfigReference)
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
@@ -1082,7 +1118,7 @@
     // matching per-file code owners from the imported code owner config and its
     // ignoreGlobalAndParentCodeOwners flag are not relevant with import mode
     // GLOBAL_CODE_OWNER_SETS_ONLY
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email());
@@ -1090,9 +1126,7 @@
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig,
-                codeOwnerConfigReference));
+                importingCodeOwnerConfig, importedCodeOwnerConfig, codeOwnerConfigReference));
     assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
@@ -1112,7 +1146,7 @@
         createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
 
     // create importing config
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -1122,24 +1156,27 @@
             .addImport(codeOwnerConfigReference)
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: ignoreParentCodeOwners is true because the ignoreParentCodeOwners flag in the
     // imported code owner config is set to true
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.ignoreParentCodeOwners()).isTrue();
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig,
-                codeOwnerConfigReference));
+                importingCodeOwnerConfig, importedCodeOwnerConfig, codeOwnerConfigReference));
     assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
@@ -1161,7 +1198,7 @@
             CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig);
 
     // create importing config
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -1171,24 +1208,27 @@
             .addImport(codeOwnerConfigReference)
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: ignoreParentCodeOwners is false because the ignoreParentCodeOwners flag in the
     // imported code owner config is not relevant with import mode GLOBAL_CODE_OWNER_SETS_ONLY
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.ignoreParentCodeOwners()).isFalse();
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig,
-                codeOwnerConfigReference));
+                importingCodeOwnerConfig, importedCodeOwnerConfig, codeOwnerConfigReference));
     assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
@@ -1236,7 +1276,7 @@
         createCodeOwnerConfigReference(importMode, keyOfImportedCodeOwnerConfig1);
 
     // create importing config with global code owner and import
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -1246,30 +1286,33 @@
             .addImport(codeOwnerConfigReference1)
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig1 =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig1).get();
+    CodeOwnerConfig importedCodeOwnerConfig2 =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig2).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the global owners from the importing code owner config, the imported code
     // owner config and the code owner config that is imported by the imported code owner config
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email(), user2.email());
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig1,
-                codeOwnerConfigReference1),
+                importingCodeOwnerConfig, importedCodeOwnerConfig1, codeOwnerConfigReference1),
             CodeOwnerConfigImport.createResolvedImport(
-                keyOfImportedCodeOwnerConfig1,
-                keyOfImportedCodeOwnerConfig2,
-                codeOwnerConfigReference2));
+                importedCodeOwnerConfig1, importedCodeOwnerConfig2, codeOwnerConfigReference2));
     assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
@@ -1313,7 +1356,7 @@
 
     // create importing config with global code owner and import with import mode
     // GLOBAL_CODE_OWNER_SETS_ONLY
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -1323,10 +1366,17 @@
             .addImport(codeOwnerConfigReference1)
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig1 =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig1).get();
+    CodeOwnerConfig importedCodeOwnerConfig2 =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig2).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
@@ -1334,19 +1384,17 @@
     // Expectation: we get the global owners from the importing code owner config and the imported
     // code owner config but not the per file code owner from the code owner config that is imported
     // by the imported code owner config
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig1,
-                codeOwnerConfigReference1),
+                importingCodeOwnerConfig, importedCodeOwnerConfig1, codeOwnerConfigReference1),
             CodeOwnerConfigImport.createResolvedImport(
-                keyOfImportedCodeOwnerConfig1,
-                keyOfImportedCodeOwnerConfig2,
+                importedCodeOwnerConfig1,
+                importedCodeOwnerConfig2,
                 createCodeOwnerConfigReference(
                     CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
                     keyOfImportedCodeOwnerConfig2)));
@@ -1390,7 +1438,7 @@
             CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig);
 
     // create importing config with global code owner and import
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -1400,26 +1448,29 @@
             .addImport(codeOwnerConfigReference)
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the global code owners from the importing and the imported code owner
     // config
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig,
-                codeOwnerConfigReference));
+                importingCodeOwnerConfig, importedCodeOwnerConfig, codeOwnerConfigReference));
     assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
@@ -1443,7 +1494,7 @@
         createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
 
     // create importing config with global code owner and import
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -1454,29 +1505,30 @@
             .addImport(codeOwnerConfigReference1)
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the global owners from the importing and the imported code owner config
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig,
-                codeOwnerConfigReference1),
+                importingCodeOwnerConfig, importedCodeOwnerConfig, codeOwnerConfigReference1),
             CodeOwnerConfigImport.createResolvedImport(
-                keyOfImportedCodeOwnerConfig,
-                importingCodeOwnerConfigKey,
-                codeOwnerConfigReference2));
+                importedCodeOwnerConfig, importingCodeOwnerConfig, codeOwnerConfigReference2));
     assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
@@ -1526,7 +1578,7 @@
 
     // Expectation: we get the global owners from the importing and the imported code owner config
     // as they were defined at oldRevision
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
   }
@@ -1547,7 +1599,7 @@
         createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
 
     // create importing config with global code owner and import with relative path
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -1557,25 +1609,28 @@
             .addImport(codeOwnerConfigReference)
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo/bar/baz.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the global owners from the importing and the imported code owner config
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig,
-                codeOwnerConfigReference));
+                importingCodeOwnerConfig, importedCodeOwnerConfig, codeOwnerConfigReference));
     assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
@@ -1587,7 +1642,7 @@
         createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
 
     // create importing config with global code owner and import from non-existing project
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -1597,16 +1652,19 @@
             .addImport(codeOwnerConfigReference)
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo/bar/baz.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the global owners from the importing code owner config
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email());
@@ -1614,7 +1672,7 @@
     assertThat(pathCodeOwnersResult.unresolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createUnresolvedImport(
-                importingCodeOwnerConfigKey,
+                importingCodeOwnerConfig,
                 keyOfImportedCodeOwnerConfig,
                 codeOwnerConfigReference,
                 String.format("project %s not found", keyOfImportedCodeOwnerConfig.project())));
@@ -1640,7 +1698,7 @@
         createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
 
     // create importing config with global code owner and import from the hidden project
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -1650,10 +1708,13 @@
             .addImport(codeOwnerConfigReference)
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo/bar/baz.md"));
     assertThat(pathCodeOwners).isPresent();
@@ -1661,7 +1722,7 @@
     // Expectation: we get the global owners from the importing code owner config, the global code
     // owners from the imported code owner config are ignored since the project that contains the
     // code owner config is hidden
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email());
@@ -1669,7 +1730,7 @@
     assertThat(pathCodeOwnersResult.unresolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createUnresolvedImport(
-                importingCodeOwnerConfigKey,
+                importingCodeOwnerConfig,
                 keyOfImportedCodeOwnerConfig,
                 codeOwnerConfigReference,
                 String.format(
@@ -1685,7 +1746,7 @@
         createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
 
     // create importing config with global code owner and import from non-existing branch
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -1695,24 +1756,27 @@
             .addImport(codeOwnerConfigReference)
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo/bar/baz.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the global owners from the importing code owner config
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
+    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email());
     assertThat(pathCodeOwnersResult.resolvedImports()).isEmpty();
     assertThat(pathCodeOwnersResult.unresolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createUnresolvedImport(
-                importingCodeOwnerConfigKey,
+                importingCodeOwnerConfig,
                 keyOfImportedCodeOwnerConfig,
                 codeOwnerConfigReference,
                 "code owner config does not exist (revision = current)"));
@@ -1736,7 +1800,7 @@
         createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
 
     // create importing config with global code owner and import with relative path
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -1746,25 +1810,28 @@
             .addImport(codeOwnerConfigReference)
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo/bar/baz.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the global owners from the importing and the imported code owner config
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig,
-                codeOwnerConfigReference));
+                importingCodeOwnerConfig, importedCodeOwnerConfig, codeOwnerConfigReference));
     assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
@@ -1795,7 +1862,7 @@
             .build();
 
     // create importing config with global code owner and import with relative path
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -1805,25 +1872,28 @@
             .addImport(codeOwnerConfigReference)
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead(branchName),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the global owners from the importing and the imported code owner config
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig,
-                codeOwnerConfigReference));
+                importingCodeOwnerConfig, importedCodeOwnerConfig, codeOwnerConfigReference));
     assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
@@ -1847,7 +1917,7 @@
         createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
 
     // create importing config with global code owner and import with relative path
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -1857,25 +1927,28 @@
             .addImport(codeOwnerConfigReference)
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo/bar/baz.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the global owners from the importing and the imported code owner config
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig,
-                codeOwnerConfigReference));
+                importingCodeOwnerConfig, importedCodeOwnerConfig, codeOwnerConfigReference));
     assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
@@ -1901,7 +1974,7 @@
         createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
 
     // create importing config with global code owner and import with relative path
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -1911,25 +1984,28 @@
             .addImport(codeOwnerConfigReference)
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo/bar/baz.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the global owners from the importing and the imported code owner config
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig,
-                codeOwnerConfigReference));
+                importingCodeOwnerConfig, importedCodeOwnerConfig, codeOwnerConfigReference));
     assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
@@ -1942,7 +2018,7 @@
             CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig);
 
     // create importing config with non-resolveable per file import
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -1956,17 +2032,20 @@
                     .build())
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the per file code owner from the importing code owner config, the
     // non-resolveable per file import is silently ignored
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email());
@@ -1974,7 +2053,7 @@
     assertThat(pathCodeOwnersResult.unresolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createUnresolvedImport(
-                importingCodeOwnerConfigKey,
+                importingCodeOwnerConfig,
                 keyOfImportedCodeOwnerConfig,
                 codeOwnerConfigReference,
                 String.format(
@@ -2011,7 +2090,7 @@
             CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig);
 
     // create importing config with per code owner and per file import
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -2025,10 +2104,15 @@
                     .build())
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
@@ -2036,7 +2120,7 @@
     // Expectation: we get the per file code owners from the importing and the global code owner
     // from the imported code owner config, but not the per file code owner from the imported code
     // owner config
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
@@ -2047,9 +2131,7 @@
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig,
-                codeOwnerConfigReference));
+                importingCodeOwnerConfig, importedCodeOwnerConfig, codeOwnerConfigReference));
     assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
@@ -2087,7 +2169,7 @@
             CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig1);
 
     // create importing config with per file code owner and per file import
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -2101,30 +2183,33 @@
                     .build())
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig1 =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig1).get();
+    CodeOwnerConfig importedCodeOwnerConfig2 =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig2).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the global owners from the importing code owner config, the imported code
     // owner config and the code owner config that is imported by the imported code owner config
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email(), user2.email());
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig1,
-                codeOwnerConfigReference1),
+                importingCodeOwnerConfig, importedCodeOwnerConfig1, codeOwnerConfigReference1),
             CodeOwnerConfigImport.createResolvedImport(
-                keyOfImportedCodeOwnerConfig1,
-                keyOfImportedCodeOwnerConfig2,
-                codeOwnerConfigReference2));
+                importedCodeOwnerConfig1, importedCodeOwnerConfig2, codeOwnerConfigReference2));
     assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
@@ -2165,7 +2250,7 @@
             CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig1);
 
     // create importing config with per file code owner and per file import
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -2179,10 +2264,17 @@
                     .build())
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig1 =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig1).get();
+    CodeOwnerConfig importedCodeOwnerConfig2 =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig2).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
@@ -2190,19 +2282,17 @@
     // Expectation: we get the global owners from the importing code owner config and the imported
     // code owner config, but not the per file code owner from the code owner config that is
     // imported by the imported code owner config
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig1,
-                codeOwnerConfigReference1),
+                importingCodeOwnerConfig, importedCodeOwnerConfig1, codeOwnerConfigReference1),
             CodeOwnerConfigImport.createResolvedImport(
-                keyOfImportedCodeOwnerConfig1,
-                keyOfImportedCodeOwnerConfig2,
+                importedCodeOwnerConfig1,
+                importedCodeOwnerConfig2,
                 createCodeOwnerConfigReference(
                     CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
                     keyOfImportedCodeOwnerConfig2)));
@@ -2265,7 +2355,7 @@
             CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig1);
 
     // create importing config with global import
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -2274,47 +2364,50 @@
             .addImport(codeOwnerConfigReference1)
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig1 =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig1).get();
+    CodeOwnerConfig importedCodeOwnerConfig2 =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig2).get();
+    CodeOwnerConfig importedCodeOwnerConfig3 =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig3).get();
+
     // Expectation for foo.xyz file: code owners is empty since foo.xyz neither matches *.md nor
     // *.txt
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.xyz"));
     assertThat(pathCodeOwners).isPresent();
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners()).isEmpty();
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig1,
-                codeOwnerConfigReference1));
+                importingCodeOwnerConfig, importedCodeOwnerConfig1, codeOwnerConfigReference1));
     assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
 
     // Expectation for foo.md file: code owners contains only user since foo.md only matches *.md
     pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
-    pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(user.email());
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig1,
-                codeOwnerConfigReference1),
+                importingCodeOwnerConfig, importedCodeOwnerConfig1, codeOwnerConfigReference1),
             CodeOwnerConfigImport.createResolvedImport(
-                keyOfImportedCodeOwnerConfig1,
-                keyOfImportedCodeOwnerConfig2,
-                codeOwnerConfigReference2));
+                importedCodeOwnerConfig1, importedCodeOwnerConfig2, codeOwnerConfigReference2));
     assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
 
     // Expectation for foo.txt file: code owners contains only user2 since foo.txt only matches
@@ -2322,24 +2415,20 @@
     pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.txt"));
     assertThat(pathCodeOwners).isPresent();
-    pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(user2.email());
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig1,
-                codeOwnerConfigReference1),
+                importingCodeOwnerConfig, importedCodeOwnerConfig1, codeOwnerConfigReference1),
             CodeOwnerConfigImport.createResolvedImport(
-                keyOfImportedCodeOwnerConfig1,
-                keyOfImportedCodeOwnerConfig3,
-                codeOwnerConfigReference3));
+                importedCodeOwnerConfig1, importedCodeOwnerConfig3, codeOwnerConfigReference3));
   }
 
   @Test
@@ -2481,7 +2570,7 @@
     // * has a global code owner
     // * has a per-file import for md files
     // * ignores global and parent code owners for md files
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -2496,10 +2585,15 @@
                     .build())
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
@@ -2507,16 +2601,14 @@
     // Expectation: we get the global code owner from the imported code owner config (since it is
     // imported by a matching per-file rule), the global code owner from the importing code owner
     // config is ignored (since the matching per-file rule ignores parent and global code owners)
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(user.email());
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig,
-                codeOwnerConfigReference));
+                importingCodeOwnerConfig, importedCodeOwnerConfig, codeOwnerConfigReference));
     assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
@@ -2548,7 +2640,7 @@
 
     // create importing config that has a global import with mode ALL and a per-file rule for md
     // files that ignores global and parent code owners
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -2564,10 +2656,15 @@
                     .build())
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
@@ -2576,16 +2673,14 @@
     // owner config and the code owner from the matching per-file rule in the imported code owner
     // config, the global code owners are ignored since there is a matching per-file rule that
     // ignores parent and global code owners
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(user.email(), user3.email());
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig,
-                codeOwnerConfigReference));
+                importingCodeOwnerConfig, importedCodeOwnerConfig, codeOwnerConfigReference));
     assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
@@ -2625,7 +2720,7 @@
             CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig1);
 
     // create importing config
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -2635,30 +2730,33 @@
             .addImport(codeOwnerConfigReference1)
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig1 =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig1).get();
+    CodeOwnerConfig importedCodeOwnerConfig2 =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig2).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the global owners from the importing code owner config and from the
     // directly and transitively imported code owner configs in the other project
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email(), user2.email());
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig1,
-                codeOwnerConfigReference1),
+                importingCodeOwnerConfig, importedCodeOwnerConfig1, codeOwnerConfigReference1),
             CodeOwnerConfigImport.createResolvedImport(
-                keyOfImportedCodeOwnerConfig1,
-                keyOfImportedCodeOwnerConfig2,
-                codeOwnerConfigReference2));
+                importedCodeOwnerConfig1, importedCodeOwnerConfig2, codeOwnerConfigReference2));
     assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
@@ -2698,7 +2796,7 @@
             CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig1);
 
     // create importing config
-    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -2708,30 +2806,110 @@
             .addImport(codeOwnerConfigReference1)
             .create();
 
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
+    CodeOwnerConfig importedCodeOwnerConfig1 =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig1).get();
+    CodeOwnerConfig importedCodeOwnerConfig2 =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig2).get();
+
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            importingCodeOwnerConfigKey,
+            keyOfImportingCodeOwnerConfig,
             projectOperations.project(project).getHead("master"),
             Path.of("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the global owners from the importing code owner config and from the
     // directly and transitively imported code owner configs
-    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
+    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email(), user2.email());
     assertThat(pathCodeOwnersResult.resolvedImports())
         .containsExactly(
             CodeOwnerConfigImport.createResolvedImport(
-                importingCodeOwnerConfigKey,
-                keyOfImportedCodeOwnerConfig1,
-                codeOwnerConfigReference1),
+                importingCodeOwnerConfig, importedCodeOwnerConfig1, codeOwnerConfigReference1),
             CodeOwnerConfigImport.createResolvedImport(
-                keyOfImportedCodeOwnerConfig1,
-                keyOfImportedCodeOwnerConfig2,
-                codeOwnerConfigReference2));
+                importedCodeOwnerConfig1, importedCodeOwnerConfig2, codeOwnerConfigReference2));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
+  }
+
+  @Test
+  public void transitiveImportsOfPerFileCodeOwners() throws Exception {
+    TestAccount mdCodeOwner =
+        accountCreator.create(
+            "mdCodeOwner", "mdCodeOwner@example.com", "Md Code Owner", /* displayName= */ null);
+
+    CodeOwnerConfigReference barCodeOwnerConfigReference =
+        CodeOwnerConfigReference.create(
+            CodeOwnerConfigImportMode.ALL, "/bar/" + getCodeOwnerConfigFileName());
+    CodeOwnerConfig.Key fooCodeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/")
+            .fileName("OWNERS")
+            .addImport(barCodeOwnerConfigReference)
+            .create();
+
+    CodeOwnerConfigReference bazCodeOwnerConfigReference =
+        CodeOwnerConfigReference.create(
+            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
+            "/baz/" + getCodeOwnerConfigFileName());
+    CodeOwnerConfig.Key barCodeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .fileName("OWNERS")
+            .folderPath("/bar/")
+            .addCodeOwnerSet(
+                CodeOwnerSet.builder()
+                    .addPathExpression(testPathExpressions.matchFileType("md"))
+                    .addImport(bazCodeOwnerConfigReference)
+                    .build())
+            .create();
+
+    CodeOwnerConfig.Key bazCodeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/baz/")
+            .fileName("OWNERS")
+            .addCodeOwnerEmail(mdCodeOwner.email())
+            .create();
+
+    CodeOwnerConfig fooCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(fooCodeOwnerConfigKey).get();
+    CodeOwnerConfig barCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(barCodeOwnerConfigKey).get();
+    CodeOwnerConfig bazCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(bazCodeOwnerConfigKey).get();
+
+    Optional<PathCodeOwners> pathCodeOwners =
+        pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
+            fooCodeOwnerConfigKey,
+            projectOperations.project(project).getHead("master"),
+            Path.of("/foo/bar/baz.md"));
+    assertThat(pathCodeOwners).isPresent();
+
+    // Expectation: we get the per-file code owner of the code owner config that is transitively
+    // imported.
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig();
+    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().getPathCodeOwners())
+        .comparingElementsUsing(hasEmail())
+        .containsExactly(mdCodeOwner.email());
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                fooCodeOwnerConfig, barCodeOwnerConfig, barCodeOwnerConfigReference),
+            CodeOwnerConfigImport.createResolvedImport(
+                barCodeOwnerConfig, bazCodeOwnerConfig, bazCodeOwnerConfigReference));
     assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
@@ -2786,7 +2964,7 @@
 
     @Override
     public Path getFilePath(CodeOwnerConfig.Key codeOwnerConfigKey) {
-      throw new UnsupportedOperationException("not implemented");
+      return Paths.get(codeOwnerConfigKey.folderPath().toString(), "OWNERS");
     }
   }
 }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerConfigFileJsonIT.java b/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerConfigFileJsonIT.java
index ed0b301..fa1d9c8 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerConfigFileJsonIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerConfigFileJsonIT.java
@@ -19,18 +19,25 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
-import com.google.gerrit.entities.Project;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.extensions.common.WebLinkInfo;
+import com.google.gerrit.extensions.webui.FileWebLink;
 import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
 import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigFileInfo;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImport;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImportMode;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigReference;
+import com.google.inject.Inject;
 import org.junit.Before;
 import org.junit.Test;
 
 /** Tests for {@link CodeOwnerConfigFileJson}. */
 public class CodeOwnerConfigFileJsonIT extends AbstractCodeOwnersIT {
+  @Inject private ExtensionRegistry extensionRegistry;
+
   private CodeOwnerConfigFileJson CodeOwnerConfigFileJson;
 
   @Before
@@ -39,28 +46,35 @@
   }
 
   @Test
-  public void cannotFormatWithNullCodeOwnerConfigKey() throws Exception {
+  public void cannotFormatWithNullCodeOwnerConfig() throws Exception {
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
             () ->
                 CodeOwnerConfigFileJson.format(
-                    /* codeOwnerConfigKey= */ null,
+                    /* codeOwnerConfig= */ null,
                     /* resolvedImports= */ ImmutableList.of(),
                     /* unresolvedImports= */ ImmutableList.of()));
-    assertThat(npe).hasMessageThat().isEqualTo("codeOwnerConfigKey");
+    assertThat(npe).hasMessageThat().isEqualTo("codeOwnerConfig");
   }
 
   @Test
   public void cannotFormatWithNullResolvedImports() throws Exception {
     CodeOwnerConfig.Key codeOwnerConfigKey =
-        CodeOwnerConfig.Key.create(Project.nameKey("project"), "master", "/");
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .addCodeOwnerEmail(admin.email())
+            .create();
+    CodeOwnerConfig codeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).get();
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
             () ->
                 CodeOwnerConfigFileJson.format(
-                    codeOwnerConfigKey,
+                    codeOwnerConfig,
                     /* resolvedImports= */ null,
                     /* unresolvedImports= */ ImmutableList.of()));
     assertThat(npe).hasMessageThat().isEqualTo("resolvedImports");
@@ -69,13 +83,20 @@
   @Test
   public void cannotFormatWithNullUnresolvedImports() throws Exception {
     CodeOwnerConfig.Key codeOwnerConfigKey =
-        CodeOwnerConfig.Key.create(Project.nameKey("project"), "master", "/");
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .addCodeOwnerEmail(admin.email())
+            .create();
+    CodeOwnerConfig codeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).get();
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
             () ->
                 CodeOwnerConfigFileJson.format(
-                    codeOwnerConfigKey,
+                    codeOwnerConfig,
                     /* resolvedImports= */ ImmutableList.of(),
                     /* unresolvedImports= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("unresolvedImports");
@@ -83,6 +104,18 @@
 
   @Test
   public void formatWithoutImports() throws Exception {
+    testFormatWithoutImports(/* expectWebLinks= */ false);
+  }
+
+  @Test
+  public void formatWithoutImports_WithWebLinks() throws Exception {
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(new CodeOwnersConfigFileWebLink())) {
+      testFormatWithoutImports(/* expectWebLinks= */ true);
+    }
+  }
+
+  private void testFormatWithoutImports(boolean expectWebLinks) throws Exception {
     CodeOwnerConfig.Key codeOwnerConfigKey =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
@@ -91,10 +124,12 @@
             .folderPath("/foo/baz/")
             .addCodeOwnerEmail(admin.email())
             .create();
+    CodeOwnerConfig codeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).get();
 
     CodeOwnerConfigFileInfo codeOwnerConfigFileInfo =
         CodeOwnerConfigFileJson.format(
-            codeOwnerConfigKey,
+            codeOwnerConfig,
             /* resolvedImports= */ ImmutableList.of(),
             /* unresolvedImports= */ ImmutableList.of());
     assertThat(codeOwnerConfigFileInfo.project).isEqualTo(codeOwnerConfigKey.project().get());
@@ -106,11 +141,29 @@
     assertThat(codeOwnerConfigFileInfo.imports).isNull();
     assertThat(codeOwnerConfigFileInfo.unresolvedImports).isNull();
     assertThat(codeOwnerConfigFileInfo.unresolvedErrorMessage).isNull();
+
+    if (expectWebLinks) {
+      assertThat(codeOwnerConfigFileInfo.webLinks).containsExactly(createWebLink(codeOwnerConfig));
+    } else {
+      assertThat(codeOwnerConfigFileInfo.webLinks).isNull();
+    }
   }
 
   @Test
   public void formatWithUnresolvedImports() throws Exception {
-    CodeOwnerConfig.Key codeOwnerConfigKey =
+    testFormatWithUnresolvedImports(/* expectWebLinks= */ false);
+  }
+
+  @Test
+  public void formatWithUnresolvedImports_WithWebLinks() throws Exception {
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(new CodeOwnersConfigFileWebLink())) {
+      testFormatWithUnresolvedImports(/* expectWebLinks= */ true);
+    }
+  }
+
+  private void testFormatWithUnresolvedImports(boolean expectWebLinks) throws Exception {
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -118,6 +171,8 @@
             .folderPath("/foo/bar/")
             .addCodeOwnerEmail(admin.email())
             .create();
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
 
     CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
         CodeOwnerConfig.Key.create(project, "stable", "/foo/baz/");
@@ -127,19 +182,21 @@
 
     CodeOwnerConfigFileInfo codeOwnerConfigFileInfo =
         CodeOwnerConfigFileJson.format(
-            codeOwnerConfigKey,
+            importingCodeOwnerConfig,
             /* resolvedImports= */ ImmutableList.of(),
             /* unresolvedImports= */ ImmutableList.of(
                 CodeOwnerConfigImport.createUnresolvedImport(
-                    codeOwnerConfigKey,
+                    importingCodeOwnerConfig,
                     keyOfImportedCodeOwnerConfig,
                     codeOwnerConfigReference,
                     "error message")));
-    assertThat(codeOwnerConfigFileInfo.project).isEqualTo(codeOwnerConfigKey.project().get());
+    assertThat(codeOwnerConfigFileInfo.project)
+        .isEqualTo(keyOfImportingCodeOwnerConfig.project().get());
     assertThat(codeOwnerConfigFileInfo.branch)
-        .isEqualTo(codeOwnerConfigKey.branchNameKey().branch());
+        .isEqualTo(keyOfImportingCodeOwnerConfig.branchNameKey().branch());
     assertThat(codeOwnerConfigFileInfo.path)
-        .isEqualTo(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath());
+        .isEqualTo(
+            codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).getFilePath());
     assertThat(codeOwnerConfigFileInfo.importMode).isNull();
     assertThat(codeOwnerConfigFileInfo.imports).isNull();
     assertThat(codeOwnerConfigFileInfo.unresolvedErrorMessage).isNull();
@@ -158,11 +215,32 @@
     assertThat(unresolvedImportInfo.imports).isNull();
     assertThat(unresolvedImportInfo.unresolvedImports).isNull();
     assertThat(unresolvedImportInfo.unresolvedErrorMessage).isEqualTo("error message");
+
+    if (expectWebLinks) {
+      assertThat(codeOwnerConfigFileInfo.webLinks)
+          .containsExactly(createWebLink(importingCodeOwnerConfig));
+      assertThat(unresolvedImportInfo.webLinks).isNull();
+    } else {
+      assertThat(codeOwnerConfigFileInfo.webLinks).isNull();
+      assertThat(unresolvedImportInfo.webLinks).isNull();
+    }
   }
 
   @Test
   public void formatWithImports() throws Exception {
-    CodeOwnerConfig.Key codeOwnerConfigKey =
+    testFormatWithImports(/* expectWebLinks= */ false);
+  }
+
+  @Test
+  public void formatWithImports_WithWebLinks() throws Exception {
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(new CodeOwnersConfigFileWebLink())) {
+      testFormatWithImports(/* expectWebLinks= */ true);
+    }
+  }
+
+  private void testFormatWithImports(boolean expectWebLinks) throws Exception {
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -170,25 +248,38 @@
             .folderPath("/foo/bar/")
             .addCodeOwnerEmail(admin.email())
             .create();
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
 
+    createBranch(BranchNameKey.create(project, "stable"));
     CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
-        CodeOwnerConfig.Key.create(project, "stable", "/foo/baz/");
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("stable")
+            .folderPath("/foo/baz/")
+            .addCodeOwnerEmail(admin.email())
+            .create();
+    CodeOwnerConfig importedCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).get();
     CodeOwnerConfigReference codeOwnerConfigReference =
         createCodeOwnerConfigReference(
             CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig);
 
     CodeOwnerConfigFileInfo codeOwnerConfigFileInfo =
         CodeOwnerConfigFileJson.format(
-            codeOwnerConfigKey,
+            importingCodeOwnerConfig,
             /* resolvedImports= */ ImmutableList.of(
                 CodeOwnerConfigImport.createResolvedImport(
-                    codeOwnerConfigKey, keyOfImportedCodeOwnerConfig, codeOwnerConfigReference)),
+                    importingCodeOwnerConfig, importedCodeOwnerConfig, codeOwnerConfigReference)),
             /* unresolvedImports= */ ImmutableList.of());
-    assertThat(codeOwnerConfigFileInfo.project).isEqualTo(codeOwnerConfigKey.project().get());
+    assertThat(codeOwnerConfigFileInfo.project)
+        .isEqualTo(keyOfImportingCodeOwnerConfig.project().get());
     assertThat(codeOwnerConfigFileInfo.branch)
-        .isEqualTo(codeOwnerConfigKey.branchNameKey().branch());
+        .isEqualTo(keyOfImportingCodeOwnerConfig.branchNameKey().branch());
     assertThat(codeOwnerConfigFileInfo.path)
-        .isEqualTo(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath());
+        .isEqualTo(
+            codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).getFilePath());
     assertThat(codeOwnerConfigFileInfo.importMode).isNull();
     assertThat(codeOwnerConfigFileInfo.unresolvedImports).isNull();
     assertThat(codeOwnerConfigFileInfo.unresolvedErrorMessage).isNull();
@@ -206,11 +297,33 @@
     assertThat(resolvedImportInfo.imports).isNull();
     assertThat(resolvedImportInfo.unresolvedImports).isNull();
     assertThat(resolvedImportInfo.unresolvedErrorMessage).isNull();
+
+    if (expectWebLinks) {
+      assertThat(codeOwnerConfigFileInfo.webLinks)
+          .containsExactly(createWebLink(importingCodeOwnerConfig));
+      assertThat(resolvedImportInfo.webLinks)
+          .containsExactly(createWebLink(importedCodeOwnerConfig));
+    } else {
+      assertThat(codeOwnerConfigFileInfo.webLinks).isNull();
+      assertThat(resolvedImportInfo.webLinks).isNull();
+    }
   }
 
   @Test
   public void formatWithNestedImports() throws Exception {
-    CodeOwnerConfig.Key codeOwnerConfigKey =
+    testFormatWithNestedImports(/* expectWebLinks= */ false);
+  }
+
+  @Test
+  public void formatWithNestedImports_WithWebLinks() throws Exception {
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(new CodeOwnersConfigFileWebLink())) {
+      testFormatWithNestedImports(/* expectWebLinks= */ true);
+    }
+  }
+
+  private void testFormatWithNestedImports(boolean expectWebLinks) throws Exception {
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -218,6 +331,8 @@
             .folderPath("/foo/bar/")
             .addCodeOwnerEmail(admin.email())
             .create();
+    CodeOwnerConfig importingCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get();
 
     CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
         codeOwnerConfigOperations
@@ -227,12 +342,23 @@
             .folderPath("/foo/bar/baz")
             .addCodeOwnerEmail(admin.email())
             .create();
+    CodeOwnerConfig importedCodeOwnerConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).get();
     CodeOwnerConfigReference codeOwnerConfigReference =
         createCodeOwnerConfigReference(
             CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig);
 
+    createBranch(BranchNameKey.create(project, "stable"));
     CodeOwnerConfig.Key keyOfNestedImportedCodeOwnerConfig1 =
-        CodeOwnerConfig.Key.create(project, "stable", "/foo/baz1/");
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("stable")
+            .folderPath("/foo/baz1/")
+            .addCodeOwnerEmail(admin.email())
+            .create();
+    CodeOwnerConfig nestedImportedCodeOwnerConfig1 =
+        codeOwnerConfigOperations.codeOwnerConfig(keyOfNestedImportedCodeOwnerConfig1).get();
     CodeOwnerConfigReference nestedCodeOwnerConfigReference1 =
         createCodeOwnerConfigReference(
             CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
@@ -246,25 +372,27 @@
 
     CodeOwnerConfigFileInfo codeOwnerConfigFileInfo =
         CodeOwnerConfigFileJson.format(
-            codeOwnerConfigKey,
+            importingCodeOwnerConfig,
             /* resolvedImports= */ ImmutableList.of(
                 CodeOwnerConfigImport.createResolvedImport(
-                    codeOwnerConfigKey, keyOfImportedCodeOwnerConfig, codeOwnerConfigReference),
+                    importingCodeOwnerConfig, importedCodeOwnerConfig, codeOwnerConfigReference),
                 CodeOwnerConfigImport.createResolvedImport(
-                    keyOfImportedCodeOwnerConfig,
-                    keyOfNestedImportedCodeOwnerConfig1,
+                    importedCodeOwnerConfig,
+                    nestedImportedCodeOwnerConfig1,
                     nestedCodeOwnerConfigReference1)),
             /* unresolvedImports= */ ImmutableList.of(
                 CodeOwnerConfigImport.createUnresolvedImport(
-                    keyOfImportedCodeOwnerConfig,
+                    importedCodeOwnerConfig,
                     keyOfNestedImportedCodeOwnerConfig2,
                     nestedCodeOwnerConfigReference2,
                     "error message")));
-    assertThat(codeOwnerConfigFileInfo.project).isEqualTo(codeOwnerConfigKey.project().get());
+    assertThat(codeOwnerConfigFileInfo.project)
+        .isEqualTo(keyOfImportingCodeOwnerConfig.project().get());
     assertThat(codeOwnerConfigFileInfo.branch)
-        .isEqualTo(codeOwnerConfigKey.branchNameKey().branch());
+        .isEqualTo(keyOfImportingCodeOwnerConfig.branchNameKey().branch());
     assertThat(codeOwnerConfigFileInfo.path)
-        .isEqualTo(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath());
+        .isEqualTo(
+            codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).getFilePath());
     assertThat(codeOwnerConfigFileInfo.importMode).isNull();
     assertThat(codeOwnerConfigFileInfo.unresolvedImports).isNull();
     assertThat(codeOwnerConfigFileInfo.unresolvedErrorMessage).isNull();
@@ -316,10 +444,37 @@
     assertThat(nestedUnresolvedImportInfo1.imports).isNull();
     assertThat(nestedUnresolvedImportInfo1.unresolvedImports).isNull();
     assertThat(nestedUnresolvedImportInfo1.unresolvedErrorMessage).isEqualTo("error message");
+
+    if (expectWebLinks) {
+      assertThat(codeOwnerConfigFileInfo.webLinks)
+          .containsExactly(createWebLink(importingCodeOwnerConfig));
+      assertThat(resolvedImportInfo.webLinks)
+          .containsExactly(createWebLink(importedCodeOwnerConfig));
+      assertThat(nestedResolvedImportInfo.webLinks)
+          .containsExactly(createWebLink(nestedImportedCodeOwnerConfig1));
+      assertThat(nestedUnresolvedImportInfo1.webLinks).isNull();
+    } else {
+      assertThat(codeOwnerConfigFileInfo.webLinks).isNull();
+      assertThat(resolvedImportInfo.webLinks).isNull();
+      assertThat(nestedResolvedImportInfo.webLinks).isNull();
+      assertThat(nestedUnresolvedImportInfo1.webLinks).isNull();
+    }
   }
 
   @Test
   public void formatWithCyclicImports() throws Exception {
+    testFormatWithCyclicImports(/* expectWebLinks= */ false);
+  }
+
+  @Test
+  public void formatWithCyclicImports_WithWebLinks() throws Exception {
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(new CodeOwnersConfigFileWebLink())) {
+      testFormatWithCyclicImports(/* expectWebLinks= */ true);
+    }
+  }
+
+  private void testFormatWithCyclicImports(boolean expectWebLinks) throws Exception {
     CodeOwnerConfig.Key codeOwnerConfigKey1 =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
@@ -328,6 +483,8 @@
             .folderPath("/foo/bar/")
             .addCodeOwnerEmail(admin.email())
             .create();
+    CodeOwnerConfig codeOwnerConfig1 =
+        codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey1).get();
     CodeOwnerConfigReference codeOwnerConfigReference1 =
         createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, codeOwnerConfigKey1);
 
@@ -339,17 +496,19 @@
             .folderPath("/foo/bar/baz")
             .addCodeOwnerEmail(admin.email())
             .create();
+    CodeOwnerConfig codeOwnerConfig2 =
+        codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey2).get();
     CodeOwnerConfigReference codeOwnerConfigReference2 =
         createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, codeOwnerConfigKey2);
 
     CodeOwnerConfigFileInfo codeOwnerConfigFileInfo =
         CodeOwnerConfigFileJson.format(
-            codeOwnerConfigKey1,
+            codeOwnerConfig1,
             /* resolvedImports= */ ImmutableList.of(
                 CodeOwnerConfigImport.createResolvedImport(
-                    codeOwnerConfigKey1, codeOwnerConfigKey2, codeOwnerConfigReference1),
+                    codeOwnerConfig1, codeOwnerConfig2, codeOwnerConfigReference1),
                 CodeOwnerConfigImport.createResolvedImport(
-                    codeOwnerConfigKey2, codeOwnerConfigKey1, codeOwnerConfigReference2)),
+                    codeOwnerConfig2, codeOwnerConfig1, codeOwnerConfigReference2)),
             /* unresolvedImports= */ ImmutableList.of());
 
     assertThat(codeOwnerConfigFileInfo.project).isEqualTo(codeOwnerConfigKey1.project().get());
@@ -384,5 +543,40 @@
     assertThat(nestedResolvedImportInfo.imports).isNull();
     assertThat(nestedResolvedImportInfo.unresolvedImports).isNull();
     assertThat(nestedResolvedImportInfo.unresolvedErrorMessage).isNull();
+
+    if (expectWebLinks) {
+      assertThat(codeOwnerConfigFileInfo.webLinks).containsExactly(createWebLink(codeOwnerConfig1));
+      assertThat(resolvedImportInfo.webLinks).containsExactly(createWebLink(codeOwnerConfig2));
+      assertThat(nestedResolvedImportInfo.webLinks)
+          .containsExactly(createWebLink(codeOwnerConfig1));
+    } else {
+      assertThat(codeOwnerConfigFileInfo.webLinks).isNull();
+      assertThat(resolvedImportInfo.webLinks).isNull();
+      assertThat(nestedResolvedImportInfo.webLinks).isNull();
+    }
+  }
+
+  private WebLinkInfo createWebLink(CodeOwnerConfig codeOwnerConfig) {
+    return createWebLink(
+        codeOwnerConfig.key().project().get(),
+        codeOwnerConfig.key().branchNameKey().branch(),
+        codeOwnerConfig.revision().getName(),
+        codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfig.key()).getFilePath());
+  }
+
+  private WebLinkInfo createWebLink(
+      String projectName, String revision, String hash, String fileName) {
+    return new WebLinkInfo(
+        "name",
+        "imageURL",
+        "http://view/" + projectName + "/" + revision + "/" + hash + "/" + fileName);
+  }
+
+  private class CodeOwnersConfigFileWebLink implements FileWebLink {
+    @Override
+    public WebLinkInfo getFileWebLink(
+        String projectName, String revision, String hash, String fileName) {
+      return createWebLink(projectName, revision, hash, fileName);
+    }
   }
 }
diff --git a/resources/Documentation/rest-api.md b/resources/Documentation/rest-api.md
index acb1084..cd19fac 100644
--- a/resources/Documentation/rest-api.md
+++ b/resources/Documentation/rest-api.md
@@ -301,6 +301,13 @@
   {
     "is_code_owner": false,
     "is_resolvable": false,
+    "code_owner_configs": [
+      {
+        "project": "foo/bar",
+        "branch": "master",
+        "path": "/OWNERS"
+      }
+    ]
     "can_read_ref": true,
     "code_owner_config_file_paths": [
       "/OWNERS",
@@ -961,14 +968,14 @@
 | --------------- | ----------- |
 | `is_code_owner` | Whether the given email owns the specified path in the branch. True if: a) the given email is resolvable (see field `is_resolvable') and b) any code owner config file assigns codeownership to the email for the path (see field `code_owner_config_file_paths`) or the email is configured as default code owner (see field `is_default_code_owner` or the email is configured as global code owner (see field `is_global_code_owner`) or the user is a fallback code owner (see field `is_fallback_code_owner`).
 | `is_resolvable` | Whether the given email is resolvable for the specified user or the calling user if no user was specified.
+| `code_owner_configs` | The code owner config files that have been inspected to check the code owner as [CodeOwnerConfigFileInfo](#code-owner-config-file-info) entities.
 | `can_read_ref` | Whether the user to which the given email was resolved has read permissions on the branch. Not set if the given email is not resolvable or if the given email is the all users wildcard (aka '*').
 | `can_see_change`| Whether the user to which the given email was resolved can see the specified change. Not set if the given email is not resolvable, if the given email is the all users wildcard (aka '*') or if no change was specified.
 | `can_approve_change`| Whether the user to which the given email was resolved can code-owner approve the specified change. Being able to code-owner approve the change means that the user has permissions to vote on the label that is [required as code owner approval](config.html#pluginCodeOwnersRequiredApproval). Other permissions are not considered for computing this flag. In particular missing read permissions on the change don't have any effect on this flag. Whether the user misses read permissions on the change (and hence cannot apply the code owner approval) can be seen from the `can_see_change` flag. Not set if the given email is not resolvable, if the given email is the all users wildcard (aka '*') or if no change was specified.
 | `code_owner_config_file_paths` | Paths of the code owner config files that assign code ownership to the specified email and path as a list. Note that if code ownership is assigned to the email via a code owner config files, but the email is not resolvable (see field `is_resolvable` field), the user is not a code owner.
 | `is_fallback_code_owner` | Whether the given email is a fallback code owner of the specified path in the branch. True if: a) the given email is resolvable (see field `is_resolvable') and b) no code owners are defined for the specified path in the branch and c) parent code owners are not ignored and d) the user is a fallback code owner according to the [configured fallback code owner policy](config.html#pluginCodeOwnersFallbackCodeOwners)
 | `is_default_code_owner` | Whether the given email is configured as a default code owner in the code owner config file in `refs/meta/config`. Note that if the email is configured as default code owner, but the email is not resolvable (see `is_resolvable` field), the user is not a code owner.
-| `is_global_code_owner` | Whether the given email is configured as a global
-code owner. Note that if the email is configured as global code owner, but the email is not resolvable (see `is_resolvable` field), the user is not a code owner.
+| `is_global_code_owner` | Whether the given email is configured as a global code owner. Note that if the email is configured as global code owner, but the email is not resolvable (see `is_resolvable` field), the user is not a code owner.
 | `is_owned_by_all_users` | Whether the the specified path in the branch is owned by all users (aka `*`).
 | `annotation` | Annotations that were set for the user. Contains only supported annotations (unsupported annotations are reported in the `debugs_logs`). Sorted alphabetically.
 | `debug_logs` | List of debug logs that may help to understand why the user is or isn't a code owner. This information is purely for debugging and the output may be changed at any time. This means bot callers must not parse the debug logs.
@@ -979,14 +986,15 @@
 The `CodeOwnerConfigFileInfo` entity contains information about a code owner
 config file and its imports.
 
-| Field Name |          | Description |
-| ---------- | -------- | ----------- |
-| `project`  || The name of the project from which the code owner config was loaded, or for unresolved imports, from which the code owner config was supposed to be loaded.
-| `branch`   || The name of the branch from which the code owner config was loaded, or for unresolved imports, from which the code owner config was supposed to be loaded.
-| `path`     || The absolute path of the code owner config file.
-| `imports`  | optional | Imported code owner config files as [CodeOwnerConfigFileInfo](#code-owner-config-file-info) entities.
+| Field Name  |          | Description |
+| ----------- | -------- | ----------- |
+| `project`   || The name of the project from which the code owner config was loaded, or for unresolved imports, from which the code owner config was supposed to be loaded.
+| `branch`    || The name of the branch from which the code owner config was loaded, or for unresolved imports, from which the code owner config was supposed to be loaded.
+| `path`      || The absolute path of the code owner config file.
+| `web_links` | optional | Links to the code owner config file in external sites as a list of [WebLinkInfo](../../../Documentation/rest-api-changes.html#web-link-info) entities. Not set if the `CodeOwnerConfigFileInfo` represents an imported code owner config file that couldn't be resolved or if [web links](../../../Documentation/dev-plugins.html#links-to-external-tools) are not configured.
+| `imports`   | optional | Imported code owner config files as [CodeOwnerConfigFileInfo](#code-owner-config-file-info) entities.
 | `unresolved_imports` | optional | Imported code owner config files that couldn't be resolved as [CodeOwnerConfigFileInfo](#code-owner-config-file-info) entities.
-| `unresolved_error_message` | optional | Message explaining why this code owner config couldn't be resolved. Only set if the `CodeOwnerConfigFileInfo` represents an import code owner config file that couldn't be resolved.
+| `unresolved_error_message` | optional | Message explaining why this code owner config couldn't be resolved. Only set if the `CodeOwnerConfigFileInfo` represents an imported code owner config file that couldn't be resolved.
 | `import_mode` | optional | The import mode (`ALL` or `GLOBAL_CODE_OWNER_SETS_ONLY`). Only set if the `CodeOwnerConfigFileInfo` represents an imported code owner config file.
 
 ---
diff --git a/web/gr-check-code-owner.ts b/web/gr-check-code-owner.ts
new file mode 100644
index 0000000..cba304f
--- /dev/null
+++ b/web/gr-check-code-owner.ts
@@ -0,0 +1,266 @@
+/**
+ * @license
+ * Copyright (C) 2024 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.
+ */
+
+import {customElement, query, property, state} from 'lit/decorators';
+import {css, CSSResult, html, LitElement} from 'lit';
+import {PluginApi} from '@gerritcodereview/typescript-api/plugin';
+
+declare global {
+  interface Window {
+    CANONICAL_PATH?: string;
+  }
+}
+
+@customElement('gr-check-code-owner')
+export class GrCheckCodeOwner extends LitElement {
+  @query('#projectInput')
+  projectInput!: HTMLInputElement;
+
+  @query('#branchInput')
+  branchInput!: HTMLInputElement;
+
+  @query('#emailInput')
+  emailInput!: HTMLInputElement;
+
+  @query('#pathInput')
+  pathInput!: HTMLInputElement;
+
+  @query('#userInput')
+  userInput!: HTMLInputElement;
+
+  @query('#resultOutput')
+  resultOutput!: HTMLInputElement;
+
+  @property()
+  plugin!: PluginApi;
+
+  @state()
+  dataValid = false;
+
+  @state()
+  isChecking = false;
+
+  static override get styles() {
+    return [
+      window.Gerrit.styles.font as CSSResult,
+      window.Gerrit.styles.form as CSSResult,
+      css`
+        main {
+          margin: 2em auto;
+          max-width: 50em;
+        }
+        .heading {
+          font-size: x-large;
+          font-weight: 500;
+        }
+        .output {
+          min-width: 50em;
+        }
+      `,
+    ];
+  }
+
+  override render() {
+    return html`
+      <main class="gr-form-styles read-only">
+        <div>
+          <h1 class="heading">Check Code Owner</h1>
+        </div>
+        <p>
+          Checks the code ownership of a user for a path in a branch, see
+          <a href="${window.CANONICAL_PATH || ''}/plugins/code-owners/Documentation/rest-api.html#check-code-owner" target="_blank">documentation<a/>.
+        </p>
+        <p>
+          Requires that the caller has the
+          <a href="${window.CANONICAL_PATH || ''}/plugins/code-owners/Documentation/rest-api.html#checkCodeOwner" target="_blank">Check Code Owner</a>
+          or the
+          <a href="${window.CANONICAL_PATH || ''}/Documentation/access-control.html#capability_administrateServer" target="_blank">Administrate Server</a>
+          global capability.
+        </p>
+        <p>All fields, except the 'Calling User' field, are required.</p>
+        <fieldset>
+          <section>
+            <span class="title">
+              <gr-tooltip-content
+                has-tooltip
+                title="The project from which the code owner configuration should be read."
+              >
+                Project* <gr-icon icon="info"></gr-icon>:
+              </gr-tooltip-content>
+            </span>
+            <span class="value">
+              <input
+                id="projectInput"
+                type="text"
+                @input=${this.validateData}
+              />
+            </span>
+          </section>
+          <section>
+            <span class="title">
+              <gr-tooltip-content
+                has-tooltip
+                title="The branch from which the code owner configuration should be read."
+              >
+                Branch* <gr-icon icon="info"></gr-icon>:
+              </gr-tooltip-content>
+            </span>
+            <span class="value">
+              <input
+                id="branchInput"
+                type="text"
+                @input=${this.validateData}
+              />
+            </span>
+          </section>
+          <section>
+            <span class="title">
+              <gr-tooltip-content
+                has-tooltip
+                title="Email for which the code ownership should be checked."
+              >
+                Email* <gr-icon icon="info"></gr-icon>:
+              </gr-tooltip-content>
+            </span>
+            <span class="value">
+              <input
+                id="emailInput"
+                type="text"
+                @input=${this.validateData}
+              />
+            </span>
+          </section>
+          <section>
+            <span class="title">
+              <gr-tooltip-content
+                has-tooltip
+                title="Path for which the code ownership should be checked."
+              >
+                Path* <gr-icon icon="info"></gr-icon>:
+              </gr-tooltip-content>
+            </span>
+            <span class="value">
+              <input
+                id="pathInput"
+                type="text"
+                placeholder="/path/to/file.ext"
+                @input=${this.validateData}
+              />
+            </span>
+          </section>
+          <section>
+            <span class="title">
+              <gr-tooltip-content
+                has-tooltip
+                title="User for which the code owner visibility should be checked.\nCan be any account identifier (e.g. email, account ID).\nIf not specified the code owner visibility is not checked.\nCan be used to investigate why a code owner is not shown/suggested to this user."
+              >
+                Calling User (optional) <gr-icon icon="info"></gr-icon>:
+              </gr-tooltip-content>
+            </span>
+            <span class="value">
+              <input
+                id="userInput"
+                type="text"
+                @input=${this.validateData}
+              />
+            </span>
+          </section>
+        </fieldset>
+        <gr-button
+          @click=${this.handleCheckCodeOwner}
+          ?disabled="${!this.dataValid || this.isChecking}"
+        >
+          Check Code Owner
+        </gr-button>
+        <p>
+          See
+          <a href="${window.CANONICAL_PATH || ''}/plugins/code-owners/Documentation/rest-api.html#code-owner-check-info" target="_blank">CheckCodeOwnerInfo</a>
+          for an explanation of the JSON fields.
+        </p>
+        <section>
+          <span class="value">
+            <iron-autogrow-textarea
+              class="output"
+              id="resultOutput"
+              readonly
+            >
+            </iron-autogrow-textarea>
+          </span>
+        </section>
+      </main>
+    `;
+  }
+
+  private validateData() {
+    this.dataValid =
+      this.validateHasValue(this.projectInput.value) &&
+      this.validateHasValue(this.branchInput.value) &&
+      this.validateEmail(this.emailInput.value) &&
+      this.validateHasValue(this.pathInput.value);
+  }
+
+  private validateHasValue(value: string) {
+    if (value && value.trim().length > 0) {
+      return true;
+    }
+
+    return false;
+  }
+
+  private validateEmail(email: string) {
+    if (email && email.includes('@')) {
+      return true;
+    }
+
+    return false;
+  }
+
+  private async handleCheckCodeOwner() {
+    this.isChecking = true;
+
+    var project = this.projectInput.value.trim();
+    var branch = this.branchInput.value.trim();
+    var email = this.emailInput.value.trim();
+    var path = this.pathInput.value.trim();
+
+    var url = `/a/projects/${encodeURIComponent(project)}/branches/${encodeURIComponent(branch)}/code_owners.check/?email=${encodeURIComponent(email)}&path=${encodeURIComponent(path)}`;
+    if (this.userInput.value) {
+      url = url + `&user=${encodeURIComponent(this.userInput.value.trim())}`
+    }
+    url = url + "&pp=1";
+
+    try {
+      await this.plugin
+        .restApi()
+        .get<String>(url)
+        .then(response => {
+          this.resultOutput.value = JSON.stringify(response, null, 2);
+        })
+        .catch(error => {
+          this.dispatchEvent(
+            new CustomEvent('show-error', {
+              detail: {message: error},
+              bubbles: true,
+              composed: true,
+            })
+          );
+        });
+    } finally {
+      this.isChecking = false;
+    }
+  }
+}
diff --git a/web/plugin.ts b/web/plugin.ts
index 9270544..2e26843 100644
--- a/web/plugin.ts
+++ b/web/plugin.ts
@@ -28,6 +28,7 @@
   OWNER_STATUS_COLUMN_CONTENT,
 } from './owner-status-column';
 import {CodeOwnersModelMixinInterface} from './code-owners-model-mixin';
+import './gr-check-code-owner';
 
 window.Gerrit.install(plugin => {
   const restApi = plugin.restApi();
@@ -105,4 +106,5 @@
       (view as unknown as CodeOwnersModelMixinInterface).restApi = restApi;
       (view as unknown as CodeOwnersModelMixinInterface).reporting = reporting;
     });
+  plugin.screen('check-code-owner', 'gr-check-code-owner');
 });