Merge "Remove the bug-report link from code-owners."
diff --git a/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java b/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java
index 32c82a1..fda3e52 100644
--- a/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java
+++ b/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java
@@ -38,6 +38,9 @@
 import com.google.gerrit.plugins.codeowners.acceptance.testsuite.CodeOwnerConfigOperations;
 import com.google.gerrit.plugins.codeowners.acceptance.testsuite.TestCodeOwnerConfigCreation.Builder;
 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.config.BackendConfig;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
 import com.google.gerrit.plugins.codeowners.backend.config.StatusConfig;
@@ -341,4 +344,13 @@
     }
     throw new IllegalStateException("unknown code owner backend: " + backend.getClass().getName());
   }
+
+  protected CodeOwnerConfigReference createCodeOwnerConfigReference(
+      CodeOwnerConfigImportMode importMode, CodeOwnerConfig.Key codeOwnerConfigKey) {
+    return CodeOwnerConfigReference.builder(
+            importMode, codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath())
+        .setProject(codeOwnerConfigKey.project())
+        .setBranch(codeOwnerConfigKey.branchNameKey().branch())
+        .build();
+  }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/api/BUILD b/java/com/google/gerrit/plugins/codeowners/api/BUILD
index 9ab1591..f8d6d60 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/BUILD
+++ b/java/com/google/gerrit/plugins/codeowners/api/BUILD
@@ -6,6 +6,7 @@
     srcs = glob(["*.java"]),
     visibility = ["//visibility:public"],
     deps = PLUGIN_DEPS_NEVERLINK + [
+        "//lib/errorprone:annotations",
         "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/backend",
         "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/common",
         "//plugins/code-owners/proto:owners_metadata_java_proto",
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigFileInfo.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigFileInfo.java
new file mode 100644
index 0000000..41fa62b
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigFileInfo.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.api;
+
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImportMode;
+import java.util.List;
+
+/**
+ * Representation of a code owner config file in the REST API.
+ *
+ * <p>This class determines the JSON format of code owner config files in the REST API.
+ *
+ * <p>The JSON only contains the location of the code owner config file (project, branch, path) and
+ * information about the imported code owner config files. It's not a representation of the code
+ * owner config file content (the content of a code owner config file is represented by {@link
+ * CodeOwnerConfigInfo}).
+ */
+public class CodeOwnerConfigFileInfo {
+  /**
+   * 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.
+   */
+  public String project;
+
+  /**
+   * 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.
+   */
+  public String branch;
+
+  /** The path of the code owner config file. */
+  public String path;
+
+  /** Imported code owner config files. */
+  public List<CodeOwnerConfigFileInfo> imports;
+
+  /** Imported code owner config files that couldn't be resolved. */
+  public List<CodeOwnerConfigFileInfo> unresolvedImports;
+
+  /**
+   * Message explaining why this code owner config couldn't be resolved.
+   *
+   * <p>Only set for unresolved imports.
+   */
+  public String unresolvedErrorMessage;
+
+  /**
+   * The import mode.
+   *
+   * <p>Only set for imports.
+   */
+  public CodeOwnerConfigImportMode importMode;
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwners.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwners.java
index a2079b8..390e34c 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwners.java
@@ -17,6 +17,7 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.extensions.client.ListAccountsOption;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -78,6 +79,7 @@
      *
      * <p>Appends to the options which have been set so far.
      */
+    @CanIgnoreReturnValue
     public QueryRequest withOptions(
         ListAccountsOption option, ListAccountsOption... furtherOptions) {
       this.options.add(requireNonNull(option, "option"));
@@ -90,6 +92,7 @@
      *
      * @param limit the limit
      */
+    @CanIgnoreReturnValue
     public QueryRequest withLimit(int limit) {
       this.limit = limit;
       return this;
@@ -100,6 +103,7 @@
      *
      * @param seed seed that should be used to shuffle code owners that have the same score
      */
+    @CanIgnoreReturnValue
     public QueryRequest withSeed(long seed) {
       this.seed = seed;
       return this;
@@ -112,6 +116,7 @@
      * @param resolveAllUsers whether code ownerships that are assigned to all users should be
      *     resolved to random users
      */
+    @CanIgnoreReturnValue
     public QueryRequest setResolveAllUsers(boolean resolveAllUsers) {
       this.resolveAllUsers = resolveAllUsers;
       return this;
@@ -123,6 +128,7 @@
      * @param highestScoreOnly whether only the code owners with the highest score should be
      *     returned
      */
+    @CanIgnoreReturnValue
     public QueryRequest withHighestScoreOnly(boolean highestScoreOnly) {
       this.highestScoreOnly = highestScoreOnly;
       return this;
@@ -135,6 +141,7 @@
      *
      * @param debug whether debug logs should be included into the response
      */
+    @CanIgnoreReturnValue
     public QueryRequest withDebug(boolean debug) {
       this.debug = debug;
       return this;
@@ -147,6 +154,7 @@
      *
      * @param revision the revision from which the code owner configs should be read
      */
+    @CanIgnoreReturnValue
     public QueryRequest forRevision(String revision) {
       this.revision = revision;
       return this;
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnersInfo.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnersInfo.java
index d699029..763dff2 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnersInfo.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnersInfo.java
@@ -34,6 +34,9 @@
    */
   public Boolean ownedByAllUsers;
 
+  /** The code owner config files that have been inspected to gather the code owners. */
+  public List<CodeOwnerConfigFileInfo> codeOwnerConfigs;
+
   /**
    * Debug logs that may help to understand why a user is or isn't suggested as a code owner.
    *
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java b/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java
index a0df99d..0be6cce 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.plugins.codeowners.backend;
 
 import static com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException.newInternalServerError;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.PLUGIN;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.VERSIONED_META_DATA_CHANGE;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -30,6 +32,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.Optional;
@@ -251,7 +254,10 @@
                   codeOwnerConfigKey)
               .setCodeOwnerConfigUpdate(codeOwnerConfigUpdate);
 
-      try (MetaDataUpdate metaDataUpdate =
+      try (
+          RefUpdateContext pluginCtx = RefUpdateContext.open(PLUGIN);
+          RefUpdateContext ctx = RefUpdateContext.open(VERSIONED_META_DATA_CHANGE);
+          MetaDataUpdate metaDataUpdate =
           createMetaDataUpdate(codeOwnerConfigKey.project(), repository, currentUser)) {
         codeOwnerConfigFile.commit(metaDataUpdate);
       }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFile.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFile.java
index 90fd878..0669b68 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFile.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFile.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.plugins.codeowners.backend;
 
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.PLUGIN;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -25,6 +26,7 @@
 import com.google.gerrit.plugins.codeowners.util.JgitPath;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.Optional;
@@ -260,8 +262,11 @@
         update.getRepository().exactRef(getRefName()) != null,
         "branch %s does not exist",
         getRefName());
-
-    return super.commit(update);
+    // The commit goes to an ordinary branch (e.g. refs/heads/main). PLUGIN context is enough for
+    // such cases.
+    try(RefUpdateContext ctx = RefUpdateContext.open(PLUGIN)) {
+      return super.commit(update);
+    }
   }
 
   @Override
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java
index d0bd003..04b0827 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.plugins.codeowners.backend;
 
 import static com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException.newInternalServerError;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.PLUGIN;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Objects.requireNonNull;
 
@@ -25,6 +26,7 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -101,7 +103,9 @@
         "updating code owner files in branch %s of project %s",
         branchNameKey.branch(), branchNameKey.project());
 
-    try (Repository repository = repoManager.openRepository(branchNameKey.project());
+    try (
+        RefUpdateContext ctx = RefUpdateContext.open(PLUGIN);
+        Repository repository = repoManager.openRepository(branchNameKey.project());
         RevWalk rw = new RevWalk(repository);
         ObjectInserter oi = repository.newObjectInserter();
         CodeOwnerConfigTreeWalk treeWalk =
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImport.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImport.java
new file mode 100644
index 0000000..a75c15b
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImport.java
@@ -0,0 +1,86 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.backend;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+import java.util.Optional;
+
+/**
+ * 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>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.
+ */
+@AutoValue
+public abstract class CodeOwnerConfigImport {
+  /** Key of the importing code owner config. */
+  public abstract CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig();
+
+  /** Key of the imported code owner config. */
+  public abstract CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig();
+
+  /** The code owner config reference that references the imported code owner config. */
+  public abstract CodeOwnerConfigReference codeOwnerConfigReference();
+
+  /**
+   * If the import couldn't be resolved, a message explaining why the code owner config reference
+   * couldn't be resolved.
+   */
+  public abstract Optional<String> errorMessage();
+
+  @Override
+  public final String toString() {
+    return MoreObjects.toStringHelper(this)
+        .add("keyOfImportingCodeOwnerConfig", keyOfImportingCodeOwnerConfig())
+        .add("keyOfImportedCodeOwnerConfig", keyOfImportedCodeOwnerConfig())
+        .add("codeOwnerConfigReference", codeOwnerConfigReference())
+        .add("errorMessage", errorMessage())
+        .toString();
+  }
+
+  /** Creates a {@link CodeOwnerConfigImport} instance for an unresolved import. */
+  @VisibleForTesting
+  public static CodeOwnerConfigImport createUnresolvedImport(
+      CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig,
+      CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig,
+      CodeOwnerConfigReference codeOwnerConfigReference,
+      String errorMessage) {
+    return new AutoValue_CodeOwnerConfigImport(
+        keyOfImportingCodeOwnerConfig,
+        keyOfImportedCodeOwnerConfig,
+        codeOwnerConfigReference,
+        Optional.of(errorMessage));
+  }
+
+  /** Creates a {@link CodeOwnerConfigImport} instance for a resolved import. */
+  @VisibleForTesting
+  public static CodeOwnerConfigImport createResolvedImport(
+      CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig,
+      CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig,
+      CodeOwnerConfigReference codeOwnerConfigReference) {
+    return new AutoValue_CodeOwnerConfigImport(
+        keyOfImportingCodeOwnerConfig,
+        keyOfImportedCodeOwnerConfig,
+        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 07894ce..7db6b68 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
@@ -50,7 +50,6 @@
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
@@ -234,6 +233,7 @@
       return resolve(
           pathCodeOwnersResult.get().getPathCodeOwners(),
           pathCodeOwnersResult.get().getAnnotations(),
+          pathCodeOwnersResult.get().resolvedImports(),
           pathCodeOwnersResult.get().unresolvedImports(),
           pathCodeOwnersResult.messages());
     }
@@ -260,6 +260,7 @@
     return resolve(
         codeOwnerReferences,
         /* annotationsByCodeOwnerReference= */ ImmutableMultimap.of(),
+        /* resolvedImports= */ ImmutableList.of(),
         /* unresolvedImports= */ ImmutableList.of(),
         /* pathCodeOwnersMessages= */ ImmutableList.of());
   }
@@ -279,9 +280,11 @@
   private CodeOwnerResolverResult resolve(
       Set<CodeOwnerReference> codeOwnerReferences,
       ImmutableMultimap<CodeOwnerReference, CodeOwnerAnnotation> annotationsByCodeOwnerReference,
-      List<UnresolvedImport> unresolvedImports,
+      ImmutableList<CodeOwnerConfigImport> resolvedImports,
+      ImmutableList<CodeOwnerConfigImport> unresolvedImports,
       ImmutableList<String> pathCodeOwnersMessages) {
     requireNonNull(codeOwnerReferences, "codeOwnerReferences");
+    requireNonNull(resolvedImports, "resolvedImports");
     requireNonNull(unresolvedImports, "unresolvedImports");
     requireNonNull(pathCodeOwnersMessages, "pathCodeOwnersMessages");
 
@@ -313,7 +316,8 @@
               annotationsByCodeOwner.build(),
               ownedByAllUsers.get(),
               hasUnresolvedCodeOwners.get(),
-              !unresolvedImports.isEmpty(),
+              resolvedImports,
+              unresolvedImports,
               messageBuilder.build());
       logger.atFine().log("resolve result = %s", codeOwnerResolverResult);
       return codeOwnerResolverResult;
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java
index 7dd9e96..4db3a4c 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java
@@ -52,8 +52,16 @@
   /** Whether there are code owner references which couldn't be resolved. */
   public abstract boolean hasUnresolvedCodeOwners();
 
+  /** Imports which were successfully resolved. */
+  public abstract ImmutableList<CodeOwnerConfigImport> resolvedImports();
+
+  /** Imports which couldn't be resolved. */
+  public abstract ImmutableList<CodeOwnerConfigImport> unresolvedImports();
+
   /** Whether there are imports which couldn't be resolved. */
-  public abstract boolean hasUnresolvedImports();
+  public boolean hasUnresolvedImports() {
+    return !unresolvedImports().isEmpty();
+  }
 
   /** Gets messages that were collected while resolving the code owners. */
   public abstract ImmutableList<String> messages();
@@ -76,7 +84,8 @@
         .add("annotations", annotations())
         .add("ownedByAllUsers", ownedByAllUsers())
         .add("hasUnresolvedCodeOwners", hasUnresolvedCodeOwners())
-        .add("hasUnresolvedImports", hasUnresolvedImports())
+        .add("resolvedImports", resolvedImports())
+        .add("unresolvedImports", unresolvedImports())
         .add("messages", messages())
         .toString();
   }
@@ -87,25 +96,16 @@
       ImmutableMultimap<CodeOwner, CodeOwnerAnnotation> annotations,
       boolean ownedByAllUsers,
       boolean hasUnresolvedCodeOwners,
-      boolean hasUnresolvedImports,
+      ImmutableList<CodeOwnerConfigImport> resolvedImports,
+      ImmutableList<CodeOwnerConfigImport> unresolvedImports,
       List<String> messages) {
     return new AutoValue_CodeOwnerResolverResult(
         codeOwners,
         annotations,
         ownedByAllUsers,
         hasUnresolvedCodeOwners,
-        hasUnresolvedImports,
+        resolvedImports,
+        unresolvedImports,
         ImmutableList.copyOf(messages));
   }
-
-  /** Creates a empty {@link CodeOwnerResolverResult} instance. */
-  public static CodeOwnerResolverResult createEmpty() {
-    return new AutoValue_CodeOwnerResolverResult(
-        /* codeOwners= */ ImmutableSet.of(),
-        /* annotations= */ ImmutableMultimap.of(),
-        /* ownedByAllUsers= */ false,
-        /* hasUnresolvedCodeOwners= */ false,
-        /* hasUnresolvedImports= */ false,
-        /* messages= */ ImmutableList.of());
-  }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java
index 5052edb..78f0056 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.plugins.codeowners.backend;
 
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.PLUGIN;
 import static java.util.stream.Collectors.joining;
 
 import com.google.common.collect.ImmutableList;
@@ -36,6 +38,7 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.AccountTemplateUtil;
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.OneOffRequestContext;
@@ -157,7 +160,9 @@
               "addCodeOwnersMessageOnAddReviewer",
               updateFactory -> {
                 try (BatchUpdate batchUpdate =
-                    updateFactory.create(projectName, currentUser, when)) {
+                    updateFactory.create(projectName, currentUser, when);
+                    RefUpdateContext pluginCtx = RefUpdateContext.open(PLUGIN);
+                RefUpdateContext changeCtx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
                   batchUpdate.addOp(changeId, new Op(reviewers, maxPathsInChangeMessages));
                   batchUpdate.execute();
                 }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
index a61c913..5e7a11f 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
@@ -252,17 +252,18 @@
       }
 
       // Resolve global imports.
-      ImmutableList.Builder<UnresolvedImport> unresolvedImports = ImmutableList.builder();
-      ImmutableSet<CodeOwnerConfigImport> globalImports = getGlobalImports(0, codeOwnerConfig);
-      OptionalResultWithMessages<List<UnresolvedImport>> unresolvedGlobalImports;
+      ImmutableList.Builder<CodeOwnerConfigImport> resolvedImports = ImmutableList.builder();
+      ImmutableList.Builder<CodeOwnerConfigImport> unresolvedImports = ImmutableList.builder();
+      ImmutableSet<CodeOwnerImport> globalImports = getGlobalImports(0, codeOwnerConfig);
+      OptionalResultWithMessages<CodeOwnerConfigImports> globalImportedCodeOwnerConfigs;
       if (!globalCodeOwnersIgnored) {
-        unresolvedGlobalImports =
+        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
-        unresolvedGlobalImports =
+        globalImportedCodeOwnerConfigs =
             resolveImports(
                 codeOwnerConfig.key(),
                 globalImports.stream()
@@ -273,8 +274,9 @@
                     .collect(toImmutableSet()),
                 resolvedCodeOwnerConfigBuilder);
       }
-      messages.addAll(unresolvedGlobalImports.messages());
-      unresolvedImports.addAll(unresolvedGlobalImports.get());
+      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
@@ -317,18 +319,22 @@
       }
 
       // Resolve per-file imports.
-      ImmutableSet<CodeOwnerConfigImport> perFileImports =
+      ImmutableSet<CodeOwnerImport> perFileImports =
           getPerFileImports(
               0, codeOwnerConfig.key(), resolvedCodeOwnerConfigBuilder.codeOwnerSets());
-      OptionalResultWithMessages<List<UnresolvedImport>> unresolvedPerFileImports =
+      OptionalResultWithMessages<CodeOwnerConfigImports> perFileImportedCodeOwnerConfigs =
           resolveImports(codeOwnerConfig.key(), perFileImports, resolvedCodeOwnerConfigBuilder);
-      messages.addAll(unresolvedPerFileImports.messages());
-      unresolvedImports.addAll(unresolvedPerFileImports.get());
+      messages.addAll(perFileImportedCodeOwnerConfigs.messages());
+      resolvedImports.addAll(perFileImportedCodeOwnerConfigs.get().resolved());
+      unresolvedImports.addAll(perFileImportedCodeOwnerConfigs.get().unresolved());
 
       this.pathCodeOwnersResult =
           OptionalResultWithMessages.create(
               PathCodeOwnersResult.create(
-                  path, resolvedCodeOwnerConfigBuilder.build(), unresolvedImports.build()),
+                  path,
+                  resolvedCodeOwnerConfigBuilder.build(),
+                  resolvedImports.build(),
+                  unresolvedImports.build()),
               messages);
       logger.atFine().log("path code owners result = %s", pathCodeOwnersResult);
       return this.pathCodeOwnersResult;
@@ -343,11 +349,12 @@
    * @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<List<UnresolvedImport>> resolveImports(
+  private OptionalResultWithMessages<CodeOwnerConfigImports> resolveImports(
       CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig,
-      Set<CodeOwnerConfigImport> codeOwnerConfigImports,
+      Set<CodeOwnerImport> codeOwnerConfigImports,
       CodeOwnerConfig.Builder resolvedCodeOwnerConfigBuilder) {
-    ImmutableList.Builder<UnresolvedImport> unresolvedImports = ImmutableList.builder();
+    ImmutableList.Builder<CodeOwnerConfigImport> resolvedImports = ImmutableList.builder();
+    ImmutableList.Builder<CodeOwnerConfigImport> unresolvedImports = ImmutableList.builder();
     StringBuilder messageBuilder = new StringBuilder();
     try (Timer0.Context ctx = codeOwnerMetrics.resolveCodeOwnerConfigImports.start()) {
       logger.atFine().log("resolve imports of codeOwnerConfig %s", keyOfImportingCodeOwnerConfig);
@@ -361,7 +368,7 @@
       Map<BranchNameKey, ObjectId> revisionMap = new HashMap<>();
       revisionMap.put(codeOwnerConfig.key().branchNameKey(), codeOwnerConfig.revision());
 
-      Queue<CodeOwnerConfigImport> codeOwnerConfigsToImport = new ArrayDeque<>();
+      Queue<CodeOwnerImport> codeOwnerConfigsToImport = new ArrayDeque<>();
       codeOwnerConfigsToImport.addAll(codeOwnerConfigImports);
       if (!codeOwnerConfigsToImport.isEmpty()) {
         messageBuilder.append(
@@ -370,7 +377,7 @@
                 keyOfImportingCodeOwnerConfig.format(codeOwners)));
       }
       while (!codeOwnerConfigsToImport.isEmpty()) {
-        CodeOwnerConfigImport codeOwnerConfigImport = codeOwnerConfigsToImport.poll();
+        CodeOwnerImport codeOwnerConfigImport = codeOwnerConfigsToImport.poll();
         messageBuilder.append(codeOwnerConfigImport.format());
 
         CodeOwnerConfigReference codeOwnerConfigReference =
@@ -387,7 +394,7 @@
               projectCache.get(keyOfImportedCodeOwnerConfig.project());
           if (!projectState.isPresent()) {
             unresolvedImports.add(
-                UnresolvedImport.create(
+                CodeOwnerConfigImport.createUnresolvedImport(
                     codeOwnerConfigImport.importingCodeOwnerConfig(),
                     keyOfImportedCodeOwnerConfig,
                     codeOwnerConfigReference,
@@ -399,7 +406,7 @@
           }
           if (!projectState.get().statePermitsRead()) {
             unresolvedImports.add(
-                UnresolvedImport.create(
+                CodeOwnerConfigImport.createUnresolvedImport(
                     codeOwnerConfigImport.importingCodeOwnerConfig(),
                     keyOfImportedCodeOwnerConfig,
                     codeOwnerConfigReference,
@@ -425,7 +432,7 @@
 
           if (!mayBeImportedCodeOwnerConfig.isPresent()) {
             unresolvedImports.add(
-                UnresolvedImport.create(
+                CodeOwnerConfigImport.createUnresolvedImport(
                     codeOwnerConfigImport.importingCodeOwnerConfig(),
                     keyOfImportedCodeOwnerConfig,
                     codeOwnerConfigReference,
@@ -439,6 +446,13 @@
           }
 
           CodeOwnerConfig importedCodeOwnerConfig = mayBeImportedCodeOwnerConfig.get();
+
+          resolvedImports.add(
+              CodeOwnerConfigImport.createResolvedImport(
+                  codeOwnerConfigImport.importingCodeOwnerConfig(),
+                  keyOfImportedCodeOwnerConfig,
+                  codeOwnerConfigReference));
+
           CodeOwnerConfigImportMode importMode = codeOwnerConfigReference.importMode();
           logger.atFine().log("import mode = %s", importMode.name());
 
@@ -475,7 +489,7 @@
           if (importMode.resolveImportsOfImport()
               && seenCodeOwnerConfigs.add(keyOfImportedCodeOwnerConfig)) {
             logger.atFine().log("resolve imports of imported code owner config");
-            Set<CodeOwnerConfigImport> transitiveImports = new HashSet<>();
+            Set<CodeOwnerImport> transitiveImports = new HashSet<>();
             transitiveImports.addAll(
                 getGlobalImports(codeOwnerConfigImport.importLevel() + 1, importedCodeOwnerConfig));
             transitiveImports.addAll(
@@ -495,7 +509,7 @@
                   transitiveImports.stream()
                       .map(
                           codeOwnerCfgImport ->
-                              CodeOwnerConfigImport.create(
+                              CodeOwnerImport.create(
                                   codeOwnerCfgImport.importLevel(),
                                   codeOwnerCfgImport.importingCodeOwnerConfig(),
                                   CodeOwnerConfigReference.copyWithNewImportMode(
@@ -516,31 +530,31 @@
       message = message.substring(0, message.length() - 1);
     }
     return OptionalResultWithMessages.create(
-        unresolvedImports.build(),
+        CodeOwnerConfigImports.create(resolvedImports.build(), unresolvedImports.build()),
         !message.isEmpty() ? ImmutableList.of(message) : ImmutableList.of());
   }
 
-  private ImmutableSet<CodeOwnerConfigImport> getGlobalImports(
+  private ImmutableSet<CodeOwnerImport> getGlobalImports(
       int importLevel, CodeOwnerConfig codeOwnerConfig) {
     return codeOwnerConfig.imports().stream()
         .map(
             codeOwnerConfigReference ->
-                CodeOwnerConfigImport.create(
+                CodeOwnerImport.create(
                     importLevel, codeOwnerConfig.key(), codeOwnerConfigReference))
         .collect(toImmutableSet());
   }
 
-  private ImmutableSet<CodeOwnerConfigImport> getPerFileImports(
+  private ImmutableSet<CodeOwnerImport> getPerFileImports(
       int importLevel,
       CodeOwnerConfig.Key importingCodeOwnerConfig,
       Set<CodeOwnerSet> codeOwnerSets) {
-    ImmutableSet.Builder<CodeOwnerConfigImport> codeOwnerConfigImports = ImmutableSet.builder();
+    ImmutableSet.Builder<CodeOwnerImport> codeOwnerConfigImports = ImmutableSet.builder();
     for (CodeOwnerSet codeOwnerSet : codeOwnerSets) {
       codeOwnerSet.imports().stream()
           .forEach(
               codeOwnerConfigReference ->
                   codeOwnerConfigImports.add(
-                      CodeOwnerConfigImport.create(
+                      CodeOwnerImport.create(
                           importLevel,
                           importingCodeOwnerConfig,
                           codeOwnerConfigReference,
@@ -623,7 +637,7 @@
   }
 
   @AutoValue
-  abstract static class CodeOwnerConfigImport {
+  abstract static class CodeOwnerImport {
     /**
      * The import level.
      *
@@ -683,7 +697,7 @@
       return levels > 0 ? String.format("%" + (levels * 2) + "s", "") : "";
     }
 
-    public static CodeOwnerConfigImport create(
+    public static CodeOwnerImport create(
         int importLevel,
         CodeOwnerConfig.Key importingCodeOwnerConfig,
         CodeOwnerConfigReference codeOwnerConfigReference) {
@@ -691,7 +705,7 @@
           importLevel, importingCodeOwnerConfig, codeOwnerConfigReference, Optional.empty());
     }
 
-    public static CodeOwnerConfigImport create(
+    public static CodeOwnerImport create(
         int importLevel,
         CodeOwnerConfig.Key importingCodeOwnerConfig,
         CodeOwnerConfigReference codeOwnerConfigReference,
@@ -703,13 +717,28 @@
           Optional.of(codeOwnerSet));
     }
 
-    public static CodeOwnerConfigImport create(
+    public static CodeOwnerImport create(
         int importLevel,
         CodeOwnerConfig.Key importingCodeOwnerConfig,
         CodeOwnerConfigReference codeOwnerConfigReference,
         Optional<CodeOwnerSet> codeOwnerSet) {
-      return new AutoValue_PathCodeOwners_CodeOwnerConfigImport(
+      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 93a36be..5b262bf 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResult.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResult.java
@@ -36,8 +36,11 @@
   /** Gets the resolved code owner config. */
   abstract CodeOwnerConfig codeOwnerConfig();
 
+  /** Gets a list of resolved imports. */
+  public abstract ImmutableList<CodeOwnerConfigImport> resolvedImports();
+
   /** Gets a list of unresolved imports. */
-  public abstract ImmutableList<UnresolvedImport> unresolvedImports();
+  public abstract ImmutableList<CodeOwnerConfigImport> unresolvedImports();
 
   /** Whether there are unresolved imports. */
   public boolean hasUnresolvedImports() {
@@ -104,14 +107,21 @@
     return MoreObjects.toStringHelper(this)
         .add("path", path())
         .add("codeOwnerConfig", codeOwnerConfig())
+        .add("resolvedImports", resolvedImports())
         .add("unresolvedImports", unresolvedImports())
         .toString();
   }
 
   /** Creates a {@link PathCodeOwnersResult} instance. */
   public static PathCodeOwnersResult create(
-      Path path, CodeOwnerConfig codeOwnerConfig, List<UnresolvedImport> unresolvedImports) {
+      Path path,
+      CodeOwnerConfig codeOwnerConfig,
+      List<CodeOwnerConfigImport> resolvedImports,
+      List<CodeOwnerConfigImport> unresolvedImports) {
     return new AutoValue_PathCodeOwnersResult(
-        path, codeOwnerConfig, ImmutableList.copyOf(unresolvedImports));
+        path,
+        codeOwnerConfig,
+        ImmutableList.copyOf(resolvedImports),
+        ImmutableList.copyOf(unresolvedImports));
   }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImport.java b/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImport.java
deleted file mode 100644
index bea1e08..0000000
--- a/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImport.java
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright (C) 2020 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.plugins.codeowners.backend;
-
-import com.google.auto.value.AutoValue;
-import com.google.common.base.MoreObjects;
-
-/** Information about an unresolved import. */
-@AutoValue
-public abstract class UnresolvedImport {
-  /** Key of the importing code owner config. */
-  public abstract CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig();
-
-  /** Key of the imported code owner config. */
-  public abstract CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig();
-
-  /** The code owner config reference that was attempted to be resolved. */
-  public abstract CodeOwnerConfigReference codeOwnerConfigReference();
-
-  /** Message explaining why the code owner config reference couldn't be resolved. */
-  public abstract String message();
-
-  @Override
-  public final String toString() {
-    return MoreObjects.toStringHelper(this)
-        .add("keyOfImportingCodeOwnerConfig", keyOfImportingCodeOwnerConfig())
-        .add("keyOfImportedCodeOwnerConfig", keyOfImportedCodeOwnerConfig())
-        .add("codeOwnerConfigReference", codeOwnerConfigReference())
-        .add("message", message())
-        .toString();
-  }
-
-  /** Creates a {@link UnresolvedImport} instance. */
-  static UnresolvedImport create(
-      CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig,
-      CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig,
-      CodeOwnerConfigReference codeOwnerConfigReference,
-      String message) {
-    return new AutoValue_UnresolvedImport(
-        keyOfImportingCodeOwnerConfig,
-        keyOfImportedCodeOwnerConfig,
-        codeOwnerConfigReference,
-        message);
-  }
-}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportFormatter.java b/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportFormatter.java
index 52958d9..7946468 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportFormatter.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportFormatter.java
@@ -20,7 +20,7 @@
 import com.google.inject.Inject;
 import java.nio.file.Path;
 
-/** Class to format an {@link UnresolvedImport} as a user-readable string. */
+/** Class to format an {@link CodeOwnerConfigImport} as a user-readable string. */
 public class UnresolvedImportFormatter {
   private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
   private final ProjectCache projectCache;
@@ -37,7 +37,7 @@
   }
 
   /** Returns a user-readable string representation of the given unresolved import. */
-  public String format(UnresolvedImport unresolvedImport) {
+  public String format(CodeOwnerConfigImport unresolvedImport) {
     return String.format(
         "The import of %s:%s:%s in %s:%s:%s cannot be resolved: %s",
         unresolvedImport.keyOfImportedCodeOwnerConfig().project(),
@@ -46,10 +46,16 @@
         unresolvedImport.keyOfImportingCodeOwnerConfig().project(),
         unresolvedImport.keyOfImportingCodeOwnerConfig().shortBranchName(),
         getFilePath(unresolvedImport.keyOfImportingCodeOwnerConfig()),
-        unresolvedImport.message());
+        unresolvedImport
+            .errorMessage()
+            .orElseThrow(
+                () ->
+                    new IllegalStateException(
+                        String.format(
+                            "unresolved import %s must have an error message", unresolvedImport))));
   }
 
-  private Path getFilePath(CodeOwnerConfig.Key codeOwnerConfigKey) {
+  public Path getFilePath(CodeOwnerConfig.Key codeOwnerConfigKey) {
     return getBackend(codeOwnerConfigKey).getFilePath(codeOwnerConfigKey);
   }
 
@@ -60,6 +66,10 @@
    * returned.
    */
   private CodeOwnerBackend getBackend(CodeOwnerConfig.Key codeOwnerConfigKey) {
+    // For unresolved imports the project may not exist. Trying to get the project config for
+    // non-existing projects fails, hence check whether the project exists before trying to access
+    // the project config and fall back to the default code owner backend if the project doesn't
+    // exist.
     if (projectCache.get(codeOwnerConfigKey.project()).isPresent()) {
       return codeOwnersPluginConfiguration
           .getProjectConfig(codeOwnerConfigKey.project())
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java b/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
index 677e67c..522da89 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
@@ -38,6 +38,7 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigFileInfo;
 import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwner;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerAnnotation;
@@ -92,6 +93,7 @@
   private final CodeOwnerConfigHierarchy codeOwnerConfigHierarchy;
   private final Provider<CodeOwnerResolver> codeOwnerResolver;
   private final CodeOwnerJson.Factory codeOwnerJsonFactory;
+  private final CodeOwnerConfigFileJson codeOwnerConfigFileJson;
   private final EnumSet<ListAccountsOption> options;
   private final Set<String> hexOptions;
 
@@ -167,7 +169,8 @@
       CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
       CodeOwnerConfigHierarchy codeOwnerConfigHierarchy,
       Provider<CodeOwnerResolver> codeOwnerResolver,
-      CodeOwnerJson.Factory codeOwnerJsonFactory) {
+      CodeOwnerJson.Factory codeOwnerJsonFactory,
+      CodeOwnerConfigFileJson codeOwnerConfigFileJson) {
     this.accountVisibility = accountVisibility;
     this.accounts = accounts;
     this.accountControlFactory = accountControlFactory;
@@ -178,6 +181,7 @@
     this.codeOwnerConfigHierarchy = codeOwnerConfigHierarchy;
     this.codeOwnerResolver = codeOwnerResolver;
     this.codeOwnerJsonFactory = codeOwnerJsonFactory;
+    this.codeOwnerConfigFileJson = codeOwnerConfigFileJson;
     this.options = EnumSet.noneOf(ListAccountsOption.class);
     this.hexOptions = new HashSet<>();
   }
@@ -213,6 +217,8 @@
     ListMultimap<CodeOwner, CodeOwnerAnnotation> annotations = LinkedListMultimap.create();
     AtomicBoolean ownedByAllUsers = new AtomicBoolean(false);
     ImmutableList.Builder<String> debugLogsBuilder = ImmutableList.builder();
+    ImmutableList.Builder<CodeOwnerConfigFileInfo> codeOwnerConfigFileInfosBuilder =
+        ImmutableList.builder();
     codeOwnerConfigHierarchy.visit(
         rsrc.getBranch(),
         rsrc.getRevision(),
@@ -221,6 +227,12 @@
           CodeOwnerResolverResult pathCodeOwners =
               codeOwnerResolver.get().resolvePathCodeOwners(codeOwnerConfig, rsrc.getPath());
 
+          codeOwnerConfigFileInfosBuilder.add(
+              codeOwnerConfigFileJson.format(
+                  codeOwnerConfig.key(),
+                  pathCodeOwners.resolvedImports(),
+                  pathCodeOwners.unresolvedImports()));
+
           debugLogsBuilder.addAll(pathCodeOwners.messages());
           codeOwners.addAll(pathCodeOwners.codeOwners());
           annotations.putAll(pathCodeOwners.annotations());
@@ -308,7 +320,7 @@
     ImmutableMap<CodeOwner, Double> scoredCodeOwners =
         codeOwnerScorings.getScorings(filteredCodeOwners);
 
-    ImmutableList<CodeOwner> sortedAndLimitedCodeOwners = sortAndLimit(rsrc, scoredCodeOwners);
+    ImmutableList<CodeOwner> sortedAndLimitedCodeOwners = sortAndLimit(scoredCodeOwners);
 
     if (highestScoreOnly) {
       Optional<Double> highestScore =
@@ -325,7 +337,7 @@
     codeOwnersInfo.codeOwners =
         codeOwnerJsonFactory.create(getFillOptions()).format(sortedAndLimitedCodeOwners);
     codeOwnersInfo.ownedByAllUsers = ownedByAllUsers.get() ? true : null;
-
+    codeOwnersInfo.codeOwnerConfigs = codeOwnerConfigFileInfosBuilder.build();
     ImmutableList<String> debugLogs = debugLogsBuilder.build();
     codeOwnersInfo.debugLogs = debug ? debugLogs : null;
     logger.atFine().log("debug logs: %s", debugLogs);
@@ -480,9 +492,8 @@
     return fillOptions;
   }
 
-  private ImmutableList<CodeOwner> sortAndLimit(
-      R rsrc, ImmutableMap<CodeOwner, Double> scoredCodeOwners) {
-    return sortCodeOwners(rsrc, seed, scoredCodeOwners).limit(limit).collect(toImmutableList());
+  private ImmutableList<CodeOwner> sortAndLimit(ImmutableMap<CodeOwner, Double> scoredCodeOwners) {
+    return sortCodeOwners(seed, scoredCodeOwners).limit(limit).collect(toImmutableList());
   }
 
   /**
@@ -492,13 +503,12 @@
    *
    * <p>The order of code owners with the same score is random.
    *
-   * @param rsrc resource on which this REST endpoint is invoked
    * @param seed seed that should be used to randomize the order
    * @param scoredCodeOwners the code owners with their scores
    * @return the sorted code owners
    */
   private Stream<CodeOwner> sortCodeOwners(
-      R rsrc, Optional<Long> seed, ImmutableMap<CodeOwner, Double> scoredCodeOwners) {
+      Optional<Long> seed, ImmutableMap<CodeOwner, Double> scoredCodeOwners) {
     return randomizeOrder(seed, scoredCodeOwners.keySet())
         .sorted(Comparator.comparingDouble(scoredCodeOwners::get).reversed());
   }
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerConfigFileJson.java b/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerConfigFileJson.java
new file mode 100644
index 0000000..fe17a3f
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerConfigFileJson.java
@@ -0,0 +1,119 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.restapi;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.ImmutableList;
+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.inject.Inject;
+import java.util.List;
+
+/** Collection of routines to populate {@link CodeOwnerConfigFileInfo}. */
+public class CodeOwnerConfigFileJson {
+  private final UnresolvedImportFormatter unresolvedImportFormatter;
+
+  @Inject
+  CodeOwnerConfigFileJson(UnresolvedImportFormatter unresolvedImportFormatter) {
+    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 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
+   *     couldn't be resolved
+   * @return the provided {@link com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig.Key}
+   *     as {@link CodeOwnerConfigFileInfo}
+   */
+  public CodeOwnerConfigFileInfo format(
+      CodeOwnerConfig.Key codeOwnerConfigKey,
+      List<CodeOwnerConfigImport> resolvedImports,
+      List<CodeOwnerConfigImport> unresolvedImports) {
+    requireNonNull(codeOwnerConfigKey, "codeOwnerConfigKey");
+    requireNonNull(resolvedImports, "resolvedImports");
+    requireNonNull(unresolvedImports, "unresolvedImports");
+
+    CodeOwnerConfigFileInfo info = new CodeOwnerConfigFileInfo();
+
+    info.project = codeOwnerConfigKey.project().get();
+    info.branch = codeOwnerConfigKey.branchNameKey().branch();
+    info.path = unresolvedImportFormatter.getFilePath(codeOwnerConfigKey).toString();
+
+    ImmutableList<CodeOwnerConfigFileInfo> unresolvedImportInfos =
+        unresolvedImports.stream()
+            .filter(
+                unresolvedImport ->
+                    unresolvedImport.keyOfImportingCodeOwnerConfig().equals(codeOwnerConfigKey))
+            .map(
+                unresolvedImport -> {
+                  CodeOwnerConfigFileInfo unresolvedCodeOwnerConfigFileInfo =
+                      format(
+                          unresolvedImport.keyOfImportedCodeOwnerConfig(),
+                          /* resolvedImports= */ ImmutableList.of(),
+                          /* unresolvedImports= */ ImmutableList.of());
+                  unresolvedCodeOwnerConfigFileInfo.importMode =
+                      unresolvedImport.codeOwnerConfigReference().importMode();
+                  unresolvedCodeOwnerConfigFileInfo.unresolvedErrorMessage =
+                      unresolvedImport
+                          .errorMessage()
+                          .orElseThrow(
+                              () ->
+                                  new IllegalStateException(
+                                      String.format(
+                                          "unresolved import %s must have an error message",
+                                          unresolvedImport)));
+                  return unresolvedCodeOwnerConfigFileInfo;
+                })
+            .collect(toImmutableList());
+    info.unresolvedImports = !unresolvedImportInfos.isEmpty() ? unresolvedImportInfos : null;
+
+    ImmutableList<CodeOwnerConfigFileInfo> importInfos =
+        resolvedImports.stream()
+            .filter(
+                resolvedImport ->
+                    resolvedImport.keyOfImportingCodeOwnerConfig().equals(codeOwnerConfigKey))
+            .map(
+                resolvedImport -> {
+                  CodeOwnerConfigFileInfo resolvedCodeOwnerConfigFileInfo =
+                      format(
+                          resolvedImport.keyOfImportedCodeOwnerConfig(),
+                          removeImportEntriesFor(resolvedImports, codeOwnerConfigKey),
+                          removeImportEntriesFor(unresolvedImports, codeOwnerConfigKey));
+                  resolvedCodeOwnerConfigFileInfo.importMode =
+                      resolvedImport.codeOwnerConfigReference().importMode();
+                  return resolvedCodeOwnerConfigFileInfo;
+                })
+            .collect(toImmutableList());
+    info.imports = !importInfos.isEmpty() ? importInfos : null;
+
+    return info;
+  }
+
+  private ImmutableList<CodeOwnerConfigImport> removeImportEntriesFor(
+      List<CodeOwnerConfigImport> imports, CodeOwnerConfig.Key codeOwnerConfigKey) {
+    return imports.stream()
+        .filter(i -> !i.keyOfImportingCodeOwnerConfig().equals(codeOwnerConfigKey))
+        .collect(toImmutableList());
+  }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInBranch.java b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInBranch.java
index e0a9145..85410e9 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInBranch.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInBranch.java
@@ -82,6 +82,7 @@
       CodeOwnerConfigHierarchy codeOwnerConfigHierarchy,
       Provider<CodeOwnerResolver> codeOwnerResolver,
       CodeOwnerJson.Factory codeOwnerJsonFactory,
+      CodeOwnerConfigFileJson codeOwnerConfigFileJson,
       GitRepositoryManager repoManager) {
     super(
         accountVisibility,
@@ -93,7 +94,8 @@
         codeOwnersPluginConfiguration,
         codeOwnerConfigHierarchy,
         codeOwnerResolver,
-        codeOwnerJsonFactory);
+        codeOwnerJsonFactory,
+        codeOwnerConfigFileJson);
     this.repoManager = repoManager;
   }
 
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java
index 3ed9a51..b7149c5 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java
@@ -73,7 +73,8 @@
       CodeOwnerConfigHierarchy codeOwnerConfigHierarchy,
       Provider<CodeOwnerResolver> codeOwnerResolver,
       ServiceUserClassifier serviceUserClassifier,
-      CodeOwnerJson.Factory codeOwnerJsonFactory) {
+      CodeOwnerJson.Factory codeOwnerJsonFactory,
+      CodeOwnerConfigFileJson codeOwnerConfigFileJson) {
     super(
         accountVisibility,
         accounts,
@@ -84,7 +85,8 @@
         codeOwnersPluginConfiguration,
         codeOwnerConfigHierarchy,
         codeOwnerResolver,
-        codeOwnerJsonFactory);
+        codeOwnerJsonFactory,
+        codeOwnerConfigFileJson);
     this.serviceUserClassifier = serviceUserClassifier;
   }
 
diff --git a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerConfigFileInfoSubject.java b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerConfigFileInfoSubject.java
new file mode 100644
index 0000000..4d09a10
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerConfigFileInfoSubject.java
@@ -0,0 +1,188 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.testing;
+
+import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.truth.ListSubject.elements;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.StringSubject;
+import com.google.common.truth.Subject;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigFileInfo;
+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.truth.ListSubject;
+
+/** {@link Subject} for doing assertions on {@link CodeOwnerConfigFileInfo}s. */
+public class CodeOwnerConfigFileInfoSubject extends Subject {
+  /**
+   * Starts fluent chain to do assertions on a {@link CodeOwnerConfigFileInfo}.
+   *
+   * @param codeOwnerConfigFileInfo the code owner config file info on which assertions should be
+   *     done
+   * @return the created {@link CodeOwnerConfigFileInfoSubject}
+   */
+  public static CodeOwnerConfigFileInfoSubject assertThat(
+      CodeOwnerConfigFileInfo codeOwnerConfigFileInfo) {
+    return assertAbout(codeOwnerConfigFileInfos()).that(codeOwnerConfigFileInfo);
+  }
+
+  public static Factory<CodeOwnerConfigFileInfoSubject, CodeOwnerConfigFileInfo>
+      codeOwnerConfigFileInfos() {
+    return CodeOwnerConfigFileInfoSubject::new;
+  }
+
+  private final CodeOwnerConfigFileInfo codeOwnerConfigFileInfo;
+
+  private CodeOwnerConfigFileInfoSubject(
+      FailureMetadata metadata, CodeOwnerConfigFileInfo codeOwnerConfigFileInfo) {
+    super(metadata, codeOwnerConfigFileInfo);
+    this.codeOwnerConfigFileInfo = codeOwnerConfigFileInfo;
+  }
+
+  /** Returns a subject for the project of the code owner config file info. */
+  public StringSubject hasProjectThat() {
+    return check("project()").that(codeOwnerConfigFileInfo().project);
+  }
+
+  /** Returns a subject for the branch of the code owner config file info. */
+  public StringSubject hasBranchThat() {
+    return check("branch()").that(codeOwnerConfigFileInfo().branch);
+  }
+
+  /** Returns a subject for the path of the code owner config file info. */
+  public StringSubject hasPathThat() {
+    return check("path()").that(codeOwnerConfigFileInfo().path);
+  }
+
+  /**
+   * Returns a {@link ListSubject} for the (resolved) imports of the code owner config file info.
+   */
+  public ListSubject<CodeOwnerConfigFileInfoSubject, CodeOwnerConfigFileInfo> hasImportsThat() {
+    return check("imports()")
+        .about(elements())
+        .thatCustom(codeOwnerConfigFileInfo().imports, codeOwnerConfigFileInfos());
+  }
+
+  /**
+   * Returns a {@link ListSubject} for the unresolved imports of the code owner config file info.
+   */
+  public ListSubject<CodeOwnerConfigFileInfoSubject, CodeOwnerConfigFileInfo>
+      hasUnresolvedImportsThat() {
+    return check("unresolvedimports()")
+        .about(elements())
+        .thatCustom(codeOwnerConfigFileInfo().unresolvedImports, codeOwnerConfigFileInfos());
+  }
+
+  /** Returns a subject for the import mode of the code owner config file info. */
+  public Subject hasImportModeThat() {
+    return check("importMode()").that(codeOwnerConfigFileInfo().importMode);
+  }
+
+  /** Returns a subject for the unresolved error message of the code owner config file info. */
+  public Subject hasUnresolvedErrorMessageThat() {
+    return check("unresolvedErrorMessage()").that(codeOwnerConfigFileInfo().unresolvedErrorMessage);
+  }
+
+  @CanIgnoreReturnValue
+  public CodeOwnerConfigFileInfoSubject assertKey(
+      CodeOwnerBackend codeOwnerBackend, CodeOwnerConfig.Key codeOwnerConfigKey) {
+    hasProjectThat().isEqualTo(codeOwnerConfigKey.project().get());
+    hasBranchThat().isEqualTo(codeOwnerConfigKey.branchNameKey().branch());
+    hasPathThat().isEqualTo(codeOwnerBackend.getFilePath(codeOwnerConfigKey).toString());
+    return this;
+  }
+
+  @CanIgnoreReturnValue
+  public CodeOwnerConfigFileInfoSubject assertNoResolvedImports() {
+    hasImportsThat().isNull();
+    return this;
+  }
+
+  @CanIgnoreReturnValue
+  public CodeOwnerConfigFileInfoSubject assertResolvedImport(
+      CodeOwnerBackend codeOwnerBackend,
+      CodeOwnerConfig.Key codeOwnerConfigKey,
+      CodeOwnerConfigImportMode importMode) {
+    hasImportsThat().hasSize(1);
+    CodeOwnerConfigFileInfoSubject subjectForResolvedImport = hasImportsThat().element(0);
+    subjectForResolvedImport
+        .assertKey(codeOwnerBackend, codeOwnerConfigKey)
+        .assertImportMode(importMode)
+        .assertNoUnresolvedErrorMessage();
+    return this;
+  }
+
+  @CanIgnoreReturnValue
+  public CodeOwnerConfigFileInfoSubject assertNoUnresolvedImports() {
+    hasUnresolvedImportsThat().isNull();
+    return this;
+  }
+
+  @CanIgnoreReturnValue
+  public CodeOwnerConfigFileInfoSubject assertUnresolvedImport(
+      CodeOwnerBackend codeOwnerBackend,
+      CodeOwnerConfig.Key codeOwnerConfigKey,
+      CodeOwnerConfigImportMode importMode,
+      String unresolvedErrorMessage) {
+    hasUnresolvedImportsThat().hasSize(1);
+    CodeOwnerConfigFileInfoSubject subjectForUnresolvedImport =
+        hasUnresolvedImportsThat().element(0);
+    subjectForUnresolvedImport
+        .assertKey(codeOwnerBackend, codeOwnerConfigKey)
+        .assertImportMode(importMode)
+        .assertUnresolvedErrorMessage(unresolvedErrorMessage);
+    return this;
+  }
+
+  @CanIgnoreReturnValue
+  public CodeOwnerConfigFileInfoSubject assertNoImports() {
+    assertNoResolvedImports();
+    assertNoUnresolvedImports();
+    return this;
+  }
+
+  @CanIgnoreReturnValue
+  public CodeOwnerConfigFileInfoSubject assertImportMode(CodeOwnerConfigImportMode importMode) {
+    hasImportModeThat().isEqualTo(importMode);
+    return this;
+  }
+
+  @CanIgnoreReturnValue
+  public CodeOwnerConfigFileInfoSubject assertNoImportMode() {
+    hasImportModeThat().isNull();
+    return this;
+  }
+
+  @CanIgnoreReturnValue
+  public CodeOwnerConfigFileInfoSubject assertUnresolvedErrorMessage(
+      String unresolvedErrorMessage) {
+    hasUnresolvedErrorMessageThat().isEqualTo(unresolvedErrorMessage);
+    return this;
+  }
+
+  @CanIgnoreReturnValue
+  public CodeOwnerConfigFileInfoSubject assertNoUnresolvedErrorMessage() {
+    hasUnresolvedErrorMessageThat().isNull();
+    return this;
+  }
+
+  private CodeOwnerConfigFileInfo codeOwnerConfigFileInfo() {
+    isNotNull();
+    return codeOwnerConfigFileInfo;
+  }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnersInfoSubject.java b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnersInfoSubject.java
index 11433e2..9e5fb44 100644
--- a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnersInfoSubject.java
+++ b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnersInfoSubject.java
@@ -15,6 +15,7 @@
 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.plugins.codeowners.testing.CodeOwnerInfoSubject.codeOwnerInfos;
 import static com.google.gerrit.truth.ListSubject.elements;
 
@@ -22,6 +23,7 @@
 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.CodeOwnerConfigFileInfo;
 import com.google.gerrit.plugins.codeowners.api.CodeOwnerInfo;
 import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo;
 import com.google.gerrit.truth.ListSubject;
@@ -65,6 +67,13 @@
     }
   }
 
+  public ListSubject<CodeOwnerConfigFileInfoSubject, CodeOwnerConfigFileInfo>
+      hasCodeOwnerConfigsThat() {
+    return check("codeOwnerConfigs()")
+        .about(elements())
+        .thatCustom(codeOwnersInfo().codeOwnerConfigs, codeOwnerConfigFileInfos());
+  }
+
   public IterableSubject hasDebugLogsThat() {
     return check("debugLogs").that(codeOwnersInfo().debugLogs);
   }
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 74c9eb2..cd6f9a2 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerConfigFileInfoSubject.assertThat;
 import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerInfoSubject.hasAccountId;
 import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerInfoSubject.hasAccountName;
 import static com.google.gerrit.plugins.codeowners.testing.CodeOwnersInfoSubject.assertThat;
@@ -25,6 +26,7 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
@@ -44,13 +46,16 @@
 import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
 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.CodeOwnerConfigFileInfo;
 import com.google.gerrit.plugins.codeowners.api.CodeOwners;
 import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo;
+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.restapi.GetCodeOwnersForPathInBranch;
 import com.google.inject.Inject;
@@ -77,11 +82,14 @@
   @Inject private GroupOperations groupOperations;
   @Inject private ProjectOperations projectOperations;
 
+  private CodeOwnerBackend backend;
+
   protected TestPathExpressions testPathExpressions;
 
   @Before
   public void setup() throws Exception {
     testPathExpressions = plugin.getSysInjector().getInstance(TestPathExpressions.class);
+    backend = plugin.getSysInjector().getInstance(BackendConfig.class).getDefaultBackend();
   }
 
   /** Must return the {@link CodeOwners} API against which the tests should be run. */
@@ -98,7 +106,10 @@
 
   @Test
   public void getCodeOwnersWhenNoCodeOwnerConfigsExist() throws Exception {
-    assertThat(queryCodeOwners("/foo/bar/baz.md")).hasCodeOwnersThat().isEmpty();
+    CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md");
+    assertThat(codeOwnersInfo).hasCodeOwnersThat().isEmpty();
+    assertThat(codeOwnersInfo).hasOwnedByAllUsersThat().isNull();
+    assertThat(codeOwnersInfo).hasCodeOwnerConfigsThat().isEmpty();
   }
 
   @Test
@@ -115,6 +126,7 @@
     CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md");
     assertThat(codeOwnersInfo).hasCodeOwnersThat().isEmpty();
     assertThat(codeOwnersInfo).hasOwnedByAllUsersThat().isNull();
+    assertThat(codeOwnersInfo).hasCodeOwnerConfigsThat().isEmpty();
   }
 
   @Test
@@ -136,29 +148,32 @@
   private void testGetCodeOwners(boolean useAbsolutePath) throws Exception {
     TestAccount user2 = accountCreator.user2();
 
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/")
-        .addCodeOwnerEmail(admin.email())
-        .create();
+    CodeOwnerConfig.Key codeOwnerConfigKey1 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/")
+            .addCodeOwnerEmail(admin.email())
+            .create();
 
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/foo/")
-        .addCodeOwnerEmail(user.email())
-        .create();
+    CodeOwnerConfig.Key codeOwnerConfigKey2 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/")
+            .addCodeOwnerEmail(user.email())
+            .create();
 
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/foo/bar/")
-        .addCodeOwnerEmail(user2.email())
-        .create();
+    CodeOwnerConfig.Key codeOwnerConfigKey3 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/bar/")
+            .addCodeOwnerEmail(user2.email())
+            .create();
 
     CodeOwnersInfo codeOwnersInfo =
         queryCodeOwners(useAbsolutePath ? "/foo/bar/baz.md" : "foo/bar/baz.md");
@@ -172,6 +187,27 @@
         .comparingElementsUsing(hasAccountName())
         .containsExactly(null, null, null);
     assertThat(codeOwnersInfo).hasOwnedByAllUsersThat().isNull();
+
+    assertThat(codeOwnersInfo.codeOwnerConfigs).hasSize(3);
+    CodeOwnerConfigFileInfo codeOwnerConfigFileInfo3 = codeOwnersInfo.codeOwnerConfigs.get(0);
+    CodeOwnerConfigFileInfo codeOwnerConfigFileInfo2 = codeOwnersInfo.codeOwnerConfigs.get(1);
+    CodeOwnerConfigFileInfo codeOwnerConfigFileInfo1 = codeOwnersInfo.codeOwnerConfigs.get(2);
+    assertThat(codeOwnerConfigFileInfo3)
+        .assertKey(backend, codeOwnerConfigKey3)
+        .assertNoImports()
+        .assertNoImportMode()
+        .assertNoUnresolvedErrorMessage();
+    assertThat(codeOwnerConfigFileInfo2)
+        .assertKey(backend, codeOwnerConfigKey2)
+        .assertNoImports()
+        .assertNoImportMode()
+        .assertNoUnresolvedErrorMessage();
+    assertThat(codeOwnerConfigFileInfo1)
+        .assertKey(backend, codeOwnerConfigKey1)
+        .assertNoImports()
+        .assertNoImportMode()
+        .assertNoUnresolvedErrorMessage();
+
     assertThat(codeOwnersInfo).hasDebugLogsThat().isNull();
   }
 
@@ -184,24 +220,26 @@
 
     // 1. code owner config that makes "user2" a code owner, inheriting code owners from parent code
     // owner configs is enabled by default
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/foo/bar/")
-        .addCodeOwnerEmail(user2.email())
-        .create();
+    CodeOwnerConfig.Key codeOwnerConfigKey1 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/bar/")
+            .addCodeOwnerEmail(user2.email())
+            .create();
 
     // 2. code owner config that makes "user" a code owner, code owners from parent code owner
     // configs are ignored
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/foo/")
-        .ignoreParentCodeOwners()
-        .addCodeOwnerEmail(user.email())
-        .create();
+    CodeOwnerConfig.Key codeOwnerConfigKey2 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/")
+            .ignoreParentCodeOwners()
+            .addCodeOwnerEmail(user.email())
+            .create();
 
     // 3. code owner config that makes "admin" a code owner and assigns code ownership to all users,
     // but for this test this code owner config is ignored, since the 2. code owner config ignores
@@ -227,36 +265,71 @@
         .containsExactly(user2.id(), user.id())
         .inOrder();
     assertThat(codeOwnersInfo).hasOwnedByAllUsersThat().isNull();
+
+    assertThat(codeOwnersInfo.codeOwnerConfigs).hasSize(2);
+    CodeOwnerConfigFileInfo codeOwnerConfigFileInfo1 = codeOwnersInfo.codeOwnerConfigs.get(0);
+    CodeOwnerConfigFileInfo codeOwnerConfigFileInfo2 = codeOwnersInfo.codeOwnerConfigs.get(1);
+    assertThat(codeOwnerConfigFileInfo1)
+        .assertKey(backend, codeOwnerConfigKey1)
+        .assertNoImports()
+        .assertNoImportMode()
+        .assertNoUnresolvedErrorMessage();
+    assertThat(codeOwnerConfigFileInfo2)
+        .assertKey(backend, codeOwnerConfigKey2)
+        .assertNoImports()
+        .assertNoImportMode()
+        .assertNoUnresolvedErrorMessage();
   }
 
   @Test
   public void getPerFileCodeOwners() throws Exception {
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/foo/bar/")
-        .addCodeOwnerSet(
-            CodeOwnerSet.builder()
-                .addPathExpression(testPathExpressions.matchFileType("txt"))
-                .addCodeOwnerEmail(admin.email())
-                .build())
-        .addCodeOwnerSet(
-            CodeOwnerSet.builder()
-                .addPathExpression(testPathExpressions.matchFileType("md"))
-                .addCodeOwnerEmail(user.email())
-                .build())
-        .create();
+    CodeOwnerConfig.Key codeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/bar/")
+            .addCodeOwnerSet(
+                CodeOwnerSet.builder()
+                    .addPathExpression(testPathExpressions.matchFileType("txt"))
+                    .addCodeOwnerEmail(admin.email())
+                    .build())
+            .addCodeOwnerSet(
+                CodeOwnerSet.builder()
+                    .addPathExpression(testPathExpressions.matchFileType("md"))
+                    .addCodeOwnerEmail(user.email())
+                    .build())
+            .create();
 
-    assertThat(queryCodeOwners("/foo/bar/config.txt"))
+    CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar/config.txt");
+    assertThat(codeOwnersInfo)
         .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(admin.id());
-    assertThat(queryCodeOwners("/foo/bar/baz.md"))
+    assertThat(Iterables.getOnlyElement(codeOwnersInfo.codeOwnerConfigs))
+        .assertKey(backend, codeOwnerConfigKey)
+        .assertNoImports()
+        .assertNoImportMode()
+        .assertNoUnresolvedErrorMessage();
+
+    codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md");
+    assertThat(codeOwnersInfo)
         .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(user.id());
-    assertThat(queryCodeOwners("/foo/bar/main.config")).hasCodeOwnersThat().isEmpty();
+    assertThat(Iterables.getOnlyElement(codeOwnersInfo.codeOwnerConfigs))
+        .assertKey(backend, codeOwnerConfigKey)
+        .assertNoImports()
+        .assertNoImportMode()
+        .assertNoUnresolvedErrorMessage();
+
+    codeOwnersInfo = queryCodeOwners("/foo/bar/main.config");
+    assertThat(codeOwnersInfo).hasCodeOwnersThat().isEmpty();
+    assertThat(Iterables.getOnlyElement(codeOwnersInfo.codeOwnerConfigs))
+        .assertKey(backend, codeOwnerConfigKey)
+        .assertNoImports()
+        .assertNoImportMode()
+        .assertNoUnresolvedErrorMessage();
   }
 
   @Test
@@ -548,22 +621,24 @@
     TestAccount user2 = accountCreator.user2();
 
     // create some code owner configs
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/")
-        .addCodeOwnerEmail(admin.email())
-        .create();
+    CodeOwnerConfig.Key codeOwnerConfigKey1 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/")
+            .addCodeOwnerEmail(admin.email())
+            .create();
 
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/foo/bar/")
-        .addCodeOwnerEmail(user.email())
-        .addCodeOwnerEmail(user2.email())
-        .create();
+    CodeOwnerConfig.Key codeOwnerConfigKey2 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/bar/")
+            .addCodeOwnerEmail(user.email())
+            .addCodeOwnerEmail(user2.email())
+            .create();
 
     // get code owners with different limits
     CodeOwnersInfo codeOwnersInfo =
@@ -576,18 +651,51 @@
         .element(0)
         .hasAccountIdThat()
         .isAnyOf(user.id(), user2.id());
+    assertThat(codeOwnersInfo.codeOwnerConfigs).hasSize(2);
+    assertThat(codeOwnersInfo.codeOwnerConfigs.get(0))
+        .assertKey(backend, codeOwnerConfigKey2)
+        .assertNoImports()
+        .assertNoImportMode()
+        .assertNoUnresolvedErrorMessage();
+    assertThat(codeOwnersInfo.codeOwnerConfigs.get(1))
+        .assertKey(backend, codeOwnerConfigKey1)
+        .assertNoImports()
+        .assertNoImportMode()
+        .assertNoUnresolvedErrorMessage();
 
     codeOwnersInfo = queryCodeOwners(getCodeOwnersApi().query().withLimit(2), "/foo/bar/baz.md");
     assertThat(codeOwnersInfo)
         .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(user.id(), user2.id());
+    assertThat(codeOwnersInfo.codeOwnerConfigs).hasSize(2);
+    assertThat(codeOwnersInfo.codeOwnerConfigs.get(0))
+        .assertKey(backend, codeOwnerConfigKey2)
+        .assertNoImports()
+        .assertNoImportMode()
+        .assertNoUnresolvedErrorMessage();
+    assertThat(codeOwnersInfo.codeOwnerConfigs.get(1))
+        .assertKey(backend, codeOwnerConfigKey1)
+        .assertNoImports()
+        .assertNoImportMode()
+        .assertNoUnresolvedErrorMessage();
 
     codeOwnersInfo = getCodeOwnersApi().query().withLimit(3).get("/foo/bar/baz.md");
     assertThat(codeOwnersInfo)
         .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(admin.id(), user.id(), user2.id());
+    assertThat(codeOwnersInfo.codeOwnerConfigs).hasSize(2);
+    assertThat(codeOwnersInfo.codeOwnerConfigs.get(0))
+        .assertKey(backend, codeOwnerConfigKey2)
+        .assertNoImports()
+        .assertNoImportMode()
+        .assertNoUnresolvedErrorMessage();
+    assertThat(codeOwnersInfo.codeOwnerConfigs.get(1))
+        .assertKey(backend, codeOwnerConfigKey1)
+        .assertNoImports()
+        .assertNoImportMode()
+        .assertNoUnresolvedErrorMessage();
   }
 
   @Test
@@ -632,10 +740,12 @@
   public void getGlobalCodeOwners() throws Exception {
     TestAccount globalOwner =
         accountCreator.create("global_owner", "global.owner@example.com", "Global Owner", null);
-    assertThat(queryCodeOwners("/foo/bar/baz.md"))
+    CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md");
+    assertThat(codeOwnersInfo)
         .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(globalOwner.id());
+    assertThat(codeOwnersInfo).hasCodeOwnerConfigsThat().isEmpty();
   }
 
   @Test
@@ -767,18 +877,25 @@
   @Test
   public void getDefaultCodeOwners() throws Exception {
     // Create default code owner config file in refs/meta/config.
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch(RefNames.REFS_CONFIG)
-        .folderPath("/")
-        .addCodeOwnerEmail(user.email())
-        .create();
+    CodeOwnerConfig.Key codeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch(RefNames.REFS_CONFIG)
+            .folderPath("/")
+            .addCodeOwnerEmail(user.email())
+            .create();
 
-    assertThat(queryCodeOwners("/foo/bar/baz.md"))
+    CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md");
+    assertThat(codeOwnersInfo)
         .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(user.id());
+    assertThat(Iterables.getOnlyElement(codeOwnersInfo.codeOwnerConfigs))
+        .assertKey(backend, codeOwnerConfigKey)
+        .assertNoImports()
+        .assertNoImportMode()
+        .assertNoUnresolvedErrorMessage();
   }
 
   @Test
@@ -1643,4 +1760,168 @@
     }
     assertThat(foundDifferentOrder).isTrue();
   }
+
+  @Test
+  public void getCodeOwnersWithUnresolvedImport() throws Exception {
+    skipTestIfImportsNotSupportedByCodeOwnersBackend();
+
+    Project.NameKey nonExistingProject = Project.nameKey("non-existing");
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        CodeOwnerConfig.Key.create(nonExistingProject, "master", "/", "OWNERS");
+    CodeOwnerConfigReference nonResolvableCodeOwnerConfigReference =
+        createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
+
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/bar/")
+            .addImport(nonResolvableCodeOwnerConfigReference)
+            .addCodeOwnerEmail(admin.email())
+            .addCodeOwnerEmail(user.email())
+            .create();
+
+    String path = "/foo/bar/baz.md";
+    CodeOwnersInfo codeOwnersInfo = queryCodeOwners(getCodeOwnersApi().query(), path);
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .comparingElementsUsing(hasAccountId())
+        .containsExactly(user.id(), admin.id());
+    assertThat(Iterables.getOnlyElement(codeOwnersInfo.codeOwnerConfigs))
+        .assertKey(backend, keyOfImportingCodeOwnerConfig)
+        .assertNoResolvedImports()
+        .assertUnresolvedImport(
+            backend,
+            keyOfImportedCodeOwnerConfig,
+            nonResolvableCodeOwnerConfigReference.importMode(),
+            String.format("project %s not found", nonExistingProject));
+  }
+
+  @Test
+  public void getCodeOwnersWithResolvedImport() throws Exception {
+    skipTestIfImportsNotSupportedByCodeOwnersBackend();
+
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/baz/")
+            .addCodeOwnerEmail(admin.email())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
+
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/bar/")
+            .addImport(codeOwnerConfigReference)
+            .addCodeOwnerEmail(user.email())
+            .create();
+
+    String path = "/foo/bar/baz.md";
+    CodeOwnersInfo codeOwnersInfo = queryCodeOwners(getCodeOwnersApi().query(), path);
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .comparingElementsUsing(hasAccountId())
+        .containsExactly(user.id(), admin.id());
+    assertThat(Iterables.getOnlyElement(codeOwnersInfo.codeOwnerConfigs))
+        .assertKey(backend, keyOfImportingCodeOwnerConfig)
+        .assertNoUnresolvedImports()
+        .assertResolvedImport(
+            backend, keyOfImportedCodeOwnerConfig, codeOwnerConfigReference.importMode());
+  }
+
+  @Test
+  public void getCodeOwnersWithNestedImport() throws Exception {
+    skipTestIfImportsNotSupportedByCodeOwnersBackend();
+
+    TestAccount user2 = accountCreator.user2();
+
+    Project.NameKey nonExistingProject = Project.nameKey("non-existing");
+    CodeOwnerConfig.Key keyOfUnresolvableCodeOwnerConfig =
+        CodeOwnerConfig.Key.create(nonExistingProject, "master", "/", "OWNERS");
+    CodeOwnerConfigReference unresolvableCodeOwnerConfigReference =
+        createCodeOwnerConfigReference(
+            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
+            keyOfUnresolvableCodeOwnerConfig);
+
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig2 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/bar/")
+            .addImport(unresolvableCodeOwnerConfigReference)
+            .addCodeOwnerEmail(admin.email())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference2 =
+        createCodeOwnerConfigReference(
+            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig2);
+
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig1 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/baz/")
+            .addImport(codeOwnerConfigReference2)
+            .addCodeOwnerEmail(user2.email())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference1 =
+        createCodeOwnerConfigReference(
+            CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig1);
+
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/bar/")
+            .addImport(codeOwnerConfigReference1)
+            .addCodeOwnerEmail(user.email())
+            .create();
+
+    String path = "/foo/bar/baz.md";
+    CodeOwnersInfo codeOwnersInfo = queryCodeOwners(getCodeOwnersApi().query(), path);
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .comparingElementsUsing(hasAccountId())
+        .containsExactly(user.id(), user2.id(), admin.id());
+
+    CodeOwnerConfigFileInfo codeOwnerConfigFileInfo =
+        Iterables.getOnlyElement(codeOwnersInfo.codeOwnerConfigs);
+    assertThat(codeOwnerConfigFileInfo)
+        .assertKey(backend, keyOfImportingCodeOwnerConfig)
+        .assertNoImportMode()
+        .assertNoUnresolvedErrorMessage()
+        .assertNoUnresolvedImports()
+        .assertResolvedImport(
+            backend, keyOfImportedCodeOwnerConfig1, codeOwnerConfigReference1.importMode());
+
+    codeOwnerConfigFileInfo = Iterables.getOnlyElement(codeOwnerConfigFileInfo.imports);
+    assertThat(codeOwnerConfigFileInfo)
+        .assertKey(backend, keyOfImportedCodeOwnerConfig1)
+        .assertImportMode(codeOwnerConfigReference1.importMode())
+        .assertNoUnresolvedErrorMessage()
+        .assertNoUnresolvedImports()
+        .assertResolvedImport(
+            backend, keyOfImportedCodeOwnerConfig2, codeOwnerConfigReference2.importMode());
+
+    codeOwnerConfigFileInfo = Iterables.getOnlyElement(codeOwnerConfigFileInfo.imports);
+    assertThat(codeOwnerConfigFileInfo)
+        .assertKey(backend, keyOfImportedCodeOwnerConfig2)
+        .assertImportMode(codeOwnerConfigReference2.importMode())
+        .assertNoUnresolvedErrorMessage()
+        .assertNoResolvedImports()
+        .assertUnresolvedImport(
+            backend,
+            keyOfUnresolvableCodeOwnerConfig,
+            unresolvableCodeOwnerConfigReference.importMode(),
+            String.format("project %s not found", nonExistingProject));
+  }
 }
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 83eb13f..66928ef 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.plugins.codeowners.acceptance.api;
 
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
@@ -100,7 +101,7 @@
   @Test
   public void checkCodeOwnerForSymbolicRefPointingToAnUnbornBranch() throws Exception {
     try (Repository repo = repoManager.openRepository(project)) {
-      repo.updateRef(Constants.HEAD, true).link("refs/heads/non-existing");
+      testRefAction(() -> repo.updateRef(Constants.HEAD, true).link("refs/heads/non-existing"));
     }
     RestResponse response =
         adminRestSession.get(
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerSubmitRuleIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerSubmitRuleIT.java
index f3e7d1b..2d2c8c7 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerSubmitRuleIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerSubmitRuleIT.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.plugins.codeowners.acceptance.api;
 
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
@@ -716,7 +717,7 @@
     try (Repository repo = repoManager.openRepository(project)) {
       RefUpdate ru = repo.updateRef(RefNames.refsCacheAutomerge(mergeCommit.name()));
       ru.setForceUpdate(true);
-      assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
+      assertThat(testRefAction(() -> ru.delete())).isEqualTo(RefUpdate.Result.FORCED);
       assertThat(repo.exactRef(RefNames.refsCacheAutomerge(mergeCommit.name()))).isNull();
     }
   }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigForPathInBranchIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigForPathInBranchIT.java
index 5ddd85f..843b656 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigForPathInBranchIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigForPathInBranchIT.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.plugins.codeowners.acceptance.api;
 
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerConfigInfoSubject.assertThatOptional;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
@@ -54,7 +55,7 @@
   @GerritConfig(name = "plugin.code-owners.enableExperimentalRestEndpoints", value = "true")
   public void getCodeOwnerConfigFromSymbolicRefPointingToAnUnbornBranch() throws Exception {
     try (Repository repo = repoManager.openRepository(project)) {
-      repo.updateRef(Constants.HEAD, true).link("refs/heads/non-existing");
+      testRefAction(() -> repo.updateRef(Constants.HEAD, true).link("refs/heads/non-existing"));
     }
     RestResponse response =
         adminRestSession.get(
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInBranchIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInBranchIT.java
index a78d420..34d2271 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInBranchIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInBranchIT.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.plugins.codeowners.acceptance.api;
 
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerInfoSubject.hasAccountId;
 import static com.google.gerrit.plugins.codeowners.testing.CodeOwnersInfoSubject.assertThat;
@@ -73,7 +74,7 @@
   @Test
   public void getCodeOwnersFromSymbolicRefPointingToAnUnbornBranch() throws Exception {
     try (Repository repo = repoManager.openRepository(project)) {
-      repo.updateRef(Constants.HEAD, true).link("refs/heads/non-existing");
+      testRefAction(() -> repo.updateRef(Constants.HEAD, true).link("refs/heads/non-existing"));
     }
     RestResponse response =
         adminRestSession.get(
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImportTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImportTest.java
new file mode 100644
index 0000000..b6efbc6
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImportTest.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.backend;
+
+import org.junit.Test;
+
+/** Tests for {@link CodeOwnerConfigImport}. */
+public class CodeOwnerConfigImportTest extends AbstractAutoValueTest {
+  @Test
+  public void toStringIncludesAllData_resolvedImport() throws Exception {
+    CodeOwnerConfigImport resolvedImport =
+        CodeOwnerConfigImport.createResolvedImport(
+            CodeOwnerConfig.Key.create(project, "master", "/"),
+            CodeOwnerConfig.Key.create(project, "master", "/bar/"),
+            CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS"));
+    assertThatToStringIncludesAllData(resolvedImport, CodeOwnerConfigImport.class);
+  }
+
+  @Test
+  public void toStringIncludesAllData_unresolvedImport() throws Exception {
+    CodeOwnerConfigImport unresolvedImport =
+        CodeOwnerConfigImport.createUnresolvedImport(
+            CodeOwnerConfig.Key.create(project, "master", "/"),
+            CodeOwnerConfig.Key.create(project, "master", "/bar/"),
+            CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS"),
+            "test message");
+    assertThatToStringIncludesAllData(unresolvedImport, CodeOwnerConfigImport.class);
+  }
+}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResultTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResultTest.java
index 18ea017..deab817 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResultTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResultTest.java
@@ -29,7 +29,17 @@
             /* annotations= */ ImmutableMultimap.of(),
             /* ownedByAllUsers= */ false,
             /* hasUnresolvedCodeOwners= */ false,
-            /* hasUnresolvedImports= */ false,
+            ImmutableList.of(
+                CodeOwnerConfigImport.createResolvedImport(
+                    CodeOwnerConfig.Key.create(project, "master", "/"),
+                    CodeOwnerConfig.Key.create(project, "master", "/bar/"),
+                    CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS"))),
+            ImmutableList.of(
+                CodeOwnerConfigImport.createUnresolvedImport(
+                    CodeOwnerConfig.Key.create(project, "master", "/"),
+                    CodeOwnerConfig.Key.create(project, "master", "/bar/"),
+                    CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS"),
+                    "test message")),
             ImmutableList.of("test message"));
     assertThatToStringIncludesAllData(codeOwnerResolverResult, CodeOwnerResolverResult.class);
   }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResultTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResultTest.java
index d270fdc..d9b3a71 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResultTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResultTest.java
@@ -27,19 +27,27 @@
   @Test
   public void toStringIncludesAllData() throws Exception {
     CodeOwnerConfig.Key codeOwnerConfigKey = CodeOwnerConfig.Key.create(project, "master", "/");
-    CodeOwnerConfigReference codeOwnerConfigReference =
+    CodeOwnerConfigReference resolvableCodeOwnerConfigReference =
         CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS");
+    CodeOwnerConfigReference unresolvableCodeOwnerConfigReference =
+        CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/baz/OWNERS");
     PathCodeOwnersResult pathCodeOwnersResult =
         PathCodeOwnersResult.create(
             Paths.get("/foo/bar/baz.md"),
             CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
-                .addImport(codeOwnerConfigReference)
+                .addImport(resolvableCodeOwnerConfigReference)
+                .addImport(unresolvableCodeOwnerConfigReference)
                 .build(),
             ImmutableList.of(
-                UnresolvedImport.create(
+                CodeOwnerConfigImport.createResolvedImport(
                     codeOwnerConfigKey,
                     CodeOwnerConfig.Key.create(project, "master", "/bar/"),
-                    codeOwnerConfigReference,
+                    resolvableCodeOwnerConfigReference)),
+            ImmutableList.of(
+                CodeOwnerConfigImport.createUnresolvedImport(
+                    codeOwnerConfigKey,
+                    CodeOwnerConfig.Key.create(project, "master", "/baz/"),
+                    unresolvableCodeOwnerConfigReference,
                     "test message")));
     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 9563ade..04df442 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersTest.java
@@ -239,7 +239,8 @@
         pathCodeOwnersFactory.createWithoutCache(
             emptyCodeOwnerConfig, Paths.get("/foo/bar/baz.md"));
     assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().getPathCodeOwners()).isEmpty();
-    assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().hasUnresolvedImports()).isFalse();
+    assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().resolvedImports()).isEmpty();
+    assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().unresolvedImports()).isEmpty();
   }
 
   @Test
@@ -471,6 +472,11 @@
 
   @Test
   public void nonResolveableImportIsIgnored() throws Exception {
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        CodeOwnerConfig.Key.create(project, "master", "/non-existing/", "OWNERS");
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
+
     // create importing config with non-resolveable import
     CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
@@ -478,9 +484,7 @@
             .project(project)
             .branch("master")
             .folderPath("/")
-            .addImport(
-                CodeOwnerConfigReference.create(
-                    CodeOwnerConfigImportMode.ALL, "/non-existing/OWNERS"))
+            .addImport(codeOwnerConfigReference)
             .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(admin.email()).build())
             .create();
 
@@ -494,24 +498,40 @@
 
     // Expectation: we get the global code owner from the importing code owner config, the
     // non-resolveable import is silently ignored
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email());
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()).isTrue();
+    assertThat(pathCodeOwnersResult.resolvedImports()).isEmpty();
+    assertThat(pathCodeOwnersResult.unresolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createUnresolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference,
+                String.format(
+                    "code owner config does not exist (revision = %s)",
+                    projectOperations
+                        .project(keyOfImportedCodeOwnerConfig.project())
+                        .getHead(keyOfImportedCodeOwnerConfig.branchNameKey().branch())
+                        .name())));
   }
 
   @Test
   public void importOfNonCodeOwnerConfigFileIsIgnored() throws Exception {
     // create a file that looks like a code owner config file, but which has a name that is not
     // allowed as code owner config file
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/")
-        .fileName("FOO")
-        .addCodeOwnerEmail(user.email())
-        .create();
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/")
+            .fileName("FOO")
+            .addCodeOwnerEmail(user.email())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
 
     // create config with import of non code owner config file
     CodeOwnerConfig.Key importingCodeOwnerConfigKey =
@@ -520,7 +540,7 @@
             .project(project)
             .branch("master")
             .folderPath("/")
-            .addImport(CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/FOO"))
+            .addImport(codeOwnerConfigReference)
             .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(admin.email()).build())
             .create();
 
@@ -538,7 +558,19 @@
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email());
-    assertThat(pathCodeOwnersResult.hasUnresolvedImports()).isTrue();
+    assertThat(pathCodeOwnersResult.resolvedImports()).isEmpty();
+    assertThat(pathCodeOwnersResult.unresolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createUnresolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference,
+                String.format(
+                    "code owner config does not exist (revision = %s)",
+                    projectOperations
+                        .project(keyOfImportedCodeOwnerConfig.project())
+                        .getHead(keyOfImportedCodeOwnerConfig.branchNameKey().branch())
+                        .name())));
   }
 
   @Test
@@ -548,14 +580,17 @@
     // parameter fileExtension) or file extensions are enabled for code owner config files (config
     // paramater enableCodeOwnerConfigFilesWithFileExtensions). Both is not the case here, hence any
     // import of this file in another code owner config file should get ignored.
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/")
-        .fileName("OWNERS.foo")
-        .addCodeOwnerEmail(user.email())
-        .create();
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/")
+            .fileName("OWNERS.foo")
+            .addCodeOwnerEmail(user.email())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
 
     // create the importing config
     CodeOwnerConfig.Key importingCodeOwnerConfigKey =
@@ -564,8 +599,7 @@
             .project(project)
             .branch("master")
             .folderPath("/")
-            .addImport(
-                CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/OWNERS.FOO"))
+            .addImport(codeOwnerConfigReference)
             .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(admin.email()).build())
             .create();
 
@@ -584,7 +618,19 @@
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email());
-    assertThat(pathCodeOwnersResult.hasUnresolvedImports()).isTrue();
+    assertThat(pathCodeOwnersResult.resolvedImports()).isEmpty();
+    assertThat(pathCodeOwnersResult.unresolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createUnresolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference,
+                String.format(
+                    "code owner config does not exist (revision = %s)",
+                    projectOperations
+                        .project(keyOfImportedCodeOwnerConfig.project())
+                        .getHead(keyOfImportedCodeOwnerConfig.branchNameKey().branch())
+                        .name())));
   }
 
   @Test
@@ -595,14 +641,17 @@
     // Create a code owner config file with a file extension. This file is considered as a code
     // owner config file since file extensions for code owner config files are enabled (paramater
     // enableCodeOwnerConfigFilesWithFileExtensions).
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/")
-        .fileName("OWNERS.FOO")
-        .addCodeOwnerEmail(user.email())
-        .create();
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/")
+            .fileName("OWNERS.FOO")
+            .addCodeOwnerEmail(user.email())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
 
     // create the importing config
     CodeOwnerConfig.Key importingCodeOwnerConfigKey =
@@ -611,8 +660,7 @@
             .project(project)
             .branch("master")
             .folderPath("/")
-            .addImport(
-                CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/OWNERS.FOO"))
+            .addImport(codeOwnerConfigReference)
             .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(admin.email()).build())
             .create();
 
@@ -630,7 +678,13 @@
     assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
-    assertThat(pathCodeOwnersResult.hasUnresolvedImports()).isFalse();
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
   @Test
@@ -644,6 +698,19 @@
   }
 
   private void testImportGlobalCodeOwners(CodeOwnerConfigImportMode importMode) throws Exception {
+    // create imported config with global code owner
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/bar/")
+            .fileName("OWNERS")
+            .addCodeOwnerEmail(user.email())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(importMode, keyOfImportedCodeOwnerConfig);
+
     // create importing config with global code owner and import
     CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
@@ -652,18 +719,9 @@
             .branch("master")
             .folderPath("/")
             .addCodeOwnerEmail(admin.email())
-            .addImport(CodeOwnerConfigReference.create(importMode, "/bar/OWNERS"))
+            .addImport(codeOwnerConfigReference)
             .create();
 
-    // create imported config with global code owner
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/bar/")
-        .addCodeOwnerEmail(user.email())
-        .create();
-
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
@@ -674,17 +732,40 @@
 
     // Expectation: we get the global code owners from the importing and the imported code owner
     // config
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports())
-        .isFalse();
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
   @Test
   public void importPerFileCodeOwners_importModeAll() throws Exception {
+    // create imported config with matching per-file code owner
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/bar/")
+            .fileName("OWNERS")
+            .addCodeOwnerSet(
+                CodeOwnerSet.builder()
+                    .addPathExpression("*.md")
+                    .addCodeOwnerEmail(user.email())
+                    .build())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
+
     // create importing config with matching per-file code owner and import
-    CodeOwnerConfig.Key rootCodeOwnerConfigKey =
+    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -695,44 +776,53 @@
                     .addPathExpression("*.md")
                     .addCodeOwnerEmail(admin.email())
                     .build())
-            .addImport(
-                CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS"))
+            .addImport(codeOwnerConfigReference)
             .create();
 
-    // create imported config with matching per-file code owner
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/bar/")
-        .addCodeOwnerSet(
-            CodeOwnerSet.builder()
-                .addPathExpression("*.md")
-                .addCodeOwnerEmail(user.email())
-                .build())
-        .create();
-
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            rootCodeOwnerConfigKey,
+            importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the matching per-file code owners from the importing and the imported
     // code owner config
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports())
-        .isFalse();
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
   @Test
   public void nonMatchingPerFileCodeOwnersAreNotImported_importModeAll() throws Exception {
+    // create imported config with non-matching per-file code owner
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/bar/")
+            .fileName("OWNERS")
+            .addCodeOwnerSet(
+                CodeOwnerSet.builder()
+                    .addPathExpression("*.txt")
+                    .addCodeOwnerEmail(user.email())
+                    .build())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
+
     // create importing config with matching per-file code owner and import
-    CodeOwnerConfig.Key rootCodeOwnerConfigKey =
+    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -743,27 +833,13 @@
                     .addPathExpression("*.md")
                     .addCodeOwnerEmail(admin.email())
                     .build())
-            .addImport(
-                CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS"))
+            .addImport(codeOwnerConfigReference)
             .create();
 
-    // create imported config with non-matching per-file code owner
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/bar/")
-        .addCodeOwnerSet(
-            CodeOwnerSet.builder()
-                .addPathExpression("*.txt")
-                .addCodeOwnerEmail(user.email())
-                .build())
-        .create();
-
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            rootCodeOwnerConfigKey,
+            importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
@@ -771,17 +847,41 @@
     // 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
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email());
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports())
-        .isFalse();
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
   @Test
   public void perFileCodeOwnersAreNotImported_importModeGlobalCodeOwnerSetsOnly() throws Exception {
+    // create imported config with matching per-file code owner
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/bar/")
+            .fileName("OWNERS")
+            .addCodeOwnerSet(
+                CodeOwnerSet.builder()
+                    .addPathExpression("*.md")
+                    .addCodeOwnerEmail(user.email())
+                    .build())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(
+            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig);
+
     // create importing config with matching per-file code owner and import
-    CodeOwnerConfig.Key rootCodeOwnerConfigKey =
+    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -792,28 +892,13 @@
                     .addPathExpression("*.md")
                     .addCodeOwnerEmail(admin.email())
                     .build())
-            .addImport(
-                CodeOwnerConfigReference.create(
-                    CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS"))
+            .addImport(codeOwnerConfigReference)
             .create();
 
-    // create imported config with matching per-file code owner
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/bar/")
-        .addCodeOwnerSet(
-            CodeOwnerSet.builder()
-                .addPathExpression("*.md")
-                .addCodeOwnerEmail(user.email())
-                .build())
-        .create();
-
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            rootCodeOwnerConfigKey,
+            importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
@@ -821,48 +906,57 @@
     // 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
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email());
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports())
-        .isFalse();
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
   @Test
   public void
       importIgnoreGlobalAndParentCodeOwnersFlagFromMatchingPerFileCodeOwnerSet_importModeAll()
           throws Exception {
+    // create imported config with matching per-file code owner that has the
+    // ignoreGlobalAndParentCodeOwners flag set to true
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/bar/")
+            .fileName("OWNERS")
+            .addCodeOwnerSet(
+                CodeOwnerSet.builder()
+                    .setIgnoreGlobalAndParentCodeOwners()
+                    .addPathExpression("*.md")
+                    .addCodeOwnerEmail(user.email())
+                    .build())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
+
     // create importing config with global code owner and import
-    CodeOwnerConfig.Key rootCodeOwnerConfigKey =
+    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
             .branch("master")
             .folderPath("/")
             .addCodeOwnerEmail(admin.email())
-            .addImport(
-                CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS"))
+            .addImport(codeOwnerConfigReference)
             .create();
 
-    // create imported config with matching per-file code owner that has the
-    // ignoreGlobalAndParentCodeOwners flag set to true
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/bar/")
-        .addCodeOwnerSet(
-            CodeOwnerSet.builder()
-                .setIgnoreGlobalAndParentCodeOwners()
-                .addPathExpression("*.md")
-                .addCodeOwnerEmail(user.email())
-                .build())
-        .create();
-
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            rootCodeOwnerConfigKey,
+            importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
@@ -872,50 +966,58 @@
     // 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
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(user.email());
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().ignoreParentCodeOwners())
-        .isTrue();
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports())
-        .isFalse();
+    assertThat(pathCodeOwnersResult.ignoreParentCodeOwners()).isTrue();
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
   @Test
   public void
       ignoreGlobalAndParentCodeOwnersFlagIsNotImportedFromNonMatchingPerFileCodeOwnerSet_importModeAll()
           throws Exception {
+    // create imported config with non-matching per-file code owner that has the
+    // ignoreGlobalAndParentCodeOwners flag set to true
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/bar/")
+            .fileName("OWNERS")
+            .addCodeOwnerSet(
+                CodeOwnerSet.builder()
+                    .setIgnoreGlobalAndParentCodeOwners()
+                    .addPathExpression("*.txt")
+                    .addCodeOwnerEmail(user.email())
+                    .build())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
+
     // create importing config with global code owner and import
-    CodeOwnerConfig.Key rootCodeOwnerConfigKey =
+    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
             .branch("master")
             .folderPath("/")
             .addCodeOwnerEmail(admin.email())
-            .addImport(
-                CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS"))
+            .addImport(codeOwnerConfigReference)
             .create();
 
-    // create imported config with non-matching per-file code owner that has the
-    // ignoreGlobalAndParentCodeOwners flag set to true
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/bar/")
-        .addCodeOwnerSet(
-            CodeOwnerSet.builder()
-                .setIgnoreGlobalAndParentCodeOwners()
-                .addPathExpression("*.txt")
-                .addCodeOwnerEmail(user.email())
-                .build())
-        .create();
-
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            rootCodeOwnerConfigKey,
+            importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
@@ -924,50 +1026,58 @@
     // 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
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email());
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().ignoreParentCodeOwners())
-        .isFalse();
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports())
-        .isFalse();
+    assertThat(pathCodeOwnersResult.ignoreParentCodeOwners()).isFalse();
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
   @Test
   public void ignoreGlobalAndParentCodeOwnersFlagIsNotImported_importModeGlobalCodeOwnerSetsOnly()
       throws Exception {
+    // create imported config with matching per-file code owner that has the
+    // ignoreGlobalAndParentCodeOwners flag set to true
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/bar/")
+            .fileName("OWNERS")
+            .addCodeOwnerSet(
+                CodeOwnerSet.builder()
+                    .setIgnoreGlobalAndParentCodeOwners()
+                    .addPathExpression("*.md")
+                    .addCodeOwnerEmail(user.email())
+                    .build())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(
+            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig);
+
     // create importing config with global code owner and import
-    CodeOwnerConfig.Key rootCodeOwnerConfigKey =
+    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
             .branch("master")
             .folderPath("/")
             .addCodeOwnerEmail(admin.email())
-            .addImport(
-                CodeOwnerConfigReference.create(
-                    CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS"))
+            .addImport(codeOwnerConfigReference)
             .create();
 
-    // create imported config with matching per-file code owner that has the
-    // ignoreGlobalAndParentCodeOwners flag set to true
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/bar/")
-        .addCodeOwnerSet(
-            CodeOwnerSet.builder()
-                .setIgnoreGlobalAndParentCodeOwners()
-                .addPathExpression("*.md")
-                .addCodeOwnerEmail(user.email())
-                .build())
-        .create();
-
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            rootCodeOwnerConfigKey,
+            importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
@@ -976,89 +1086,114 @@
     // 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
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email());
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().ignoreParentCodeOwners())
-        .isFalse();
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports())
-        .isFalse();
+    assertThat(pathCodeOwnersResult.ignoreParentCodeOwners()).isFalse();
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
   @Test
   public void importIgnoreParentCodeOwnersFlag_importModeAll() throws Exception {
+    // create imported config with the ignoreParentCodeOnwers flag set to true
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/bar/")
+            .fileName("OWNERS")
+            .ignoreParentCodeOwners()
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
+
     // create importing config
-    CodeOwnerConfig.Key rootCodeOwnerConfigKey =
+    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
             .branch("master")
             .folderPath("/")
             .addCodeOwnerEmail(admin.email())
-            .addImport(
-                CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS"))
+            .addImport(codeOwnerConfigReference)
             .create();
 
-    // create imported config with the ignoreParentCodeOnwers flag set to true
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/bar/")
-        .ignoreParentCodeOwners()
-        .create();
-
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            rootCodeOwnerConfigKey,
+            importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: ignoreParentCodeOwners is true because the ignoreParentCodeOwners flag in the
     // imported code owner config is set to true
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().ignoreParentCodeOwners())
-        .isTrue();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.ignoreParentCodeOwners()).isTrue();
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
   @Test
   public void ignoreParentCodeOwnersFlagNotImported_importModeGlobalCodeOwnerSetsOnly()
       throws Exception {
+    // create imported config with the ignoreParentCodeOnwers flag set to true
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/bar/")
+            .fileName("OWNERS")
+            .ignoreParentCodeOwners()
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(
+            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig);
+
     // create importing config
-    CodeOwnerConfig.Key rootCodeOwnerConfigKey =
+    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
             .branch("master")
             .folderPath("/")
             .addCodeOwnerEmail(admin.email())
-            .addImport(
-                CodeOwnerConfigReference.create(
-                    CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS"))
+            .addImport(codeOwnerConfigReference)
             .create();
 
-    // create imported config with the ignoreParentCodeOnwers flag set to true
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/bar/")
-        .ignoreParentCodeOwners()
-        .create();
-
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            rootCodeOwnerConfigKey,
+            importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/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
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().ignoreParentCodeOwners())
-        .isFalse();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.ignoreParentCodeOwners()).isFalse();
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
   @Test
@@ -1077,51 +1212,69 @@
       throws Exception {
     TestAccount user2 = accountCreator.user2();
 
+    // create config with global code owner that is imported by the imported config
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig2 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/baz/")
+            .fileName("OWNERS")
+            .addCodeOwnerEmail(user2.email())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference2 =
+        createCodeOwnerConfigReference(importMode, keyOfImportedCodeOwnerConfig2);
+
+    // create imported config with global code owner and import
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig1 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/bar/")
+            .fileName("OWNERS")
+            .addCodeOwnerEmail(user.email())
+            .addImport(codeOwnerConfigReference2)
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference1 =
+        createCodeOwnerConfigReference(importMode, keyOfImportedCodeOwnerConfig1);
+
     // create importing config with global code owner and import
-    CodeOwnerConfig.Key rootCodeOwnerConfigKey =
+    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
             .branch("master")
             .folderPath("/")
             .addCodeOwnerEmail(admin.email())
-            .addImport(CodeOwnerConfigReference.create(importMode, "/bar/OWNERS"))
+            .addImport(codeOwnerConfigReference1)
             .create();
 
-    // create imported config with global code owner and import
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/bar/")
-        .addCodeOwnerEmail(user.email())
-        .addImport(CodeOwnerConfigReference.create(importMode, "/baz/OWNERS"))
-        .create();
-
-    // create config with global code owner that is imported by the imported config
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/baz/")
-        .addCodeOwnerEmail(user2.email())
-        .create();
-
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            rootCodeOwnerConfigKey,
+            importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/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
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email(), user2.email());
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports())
-        .isFalse();
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig1,
+                codeOwnerConfigReference1),
+            CodeOwnerConfigImport.createResolvedImport(
+                keyOfImportedCodeOwnerConfig1,
+                keyOfImportedCodeOwnerConfig2,
+                codeOwnerConfigReference2));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
   @Test
@@ -1130,47 +1283,54 @@
           throws Exception {
     TestAccount user2 = accountCreator.user2();
 
+    // create config with per file code owner that is imported by the imported config
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig2 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/baz/")
+            .fileName("OWNERS")
+            .addCodeOwnerSet(
+                CodeOwnerSet.builder()
+                    .addPathExpression("foo.md")
+                    .addCodeOwnerEmail(user2.email())
+                    .build())
+            .create();
+
+    // create imported config with global code owner and import with import mode ALL
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig1 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/bar/")
+            .fileName("OWNERS")
+            .addCodeOwnerEmail(user.email())
+            .addImport(
+                createCodeOwnerConfigReference(
+                    CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig2))
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference1 =
+        createCodeOwnerConfigReference(
+            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig1);
+
     // create importing config with global code owner and import with import mode
     // GLOBAL_CODE_OWNER_SETS_ONLY
-    CodeOwnerConfig.Key rootCodeOwnerConfigKey =
+    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
             .branch("master")
             .folderPath("/")
             .addCodeOwnerEmail(admin.email())
-            .addImport(
-                CodeOwnerConfigReference.create(
-                    CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS"))
+            .addImport(codeOwnerConfigReference1)
             .create();
 
-    // create imported config with global code owner and import with import mode ALL
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/bar/")
-        .addCodeOwnerEmail(user.email())
-        .addImport(CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/baz/OWNERS"))
-        .create();
-
-    // create config with per file code owner that is imported by the imported config
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/baz/")
-        .addCodeOwnerSet(
-            CodeOwnerSet.builder()
-                .addPathExpression("foo.md")
-                .addCodeOwnerEmail(user2.email())
-                .build())
-        .create();
-
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            rootCodeOwnerConfigKey,
+            importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
@@ -1178,11 +1338,23 @@
     // 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
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports())
-        .isFalse();
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig1,
+                codeOwnerConfigReference1),
+            CodeOwnerConfigImport.createResolvedImport(
+                keyOfImportedCodeOwnerConfig1,
+                keyOfImportedCodeOwnerConfig2,
+                createCodeOwnerConfigReference(
+                    CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
+                    keyOfImportedCodeOwnerConfig2)));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
   @Test
@@ -1197,6 +1369,21 @@
 
   private void testImportCodeOwnerConfigWithNameExtension(String nameOfImportedCodeOwnerConfig)
       throws Exception {
+    // create imported config with global code owner
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/bar/")
+            .fileName("OWNERS")
+            .fileName(nameOfImportedCodeOwnerConfig)
+            .addCodeOwnerEmail(user.email())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(
+            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig);
+
     // create importing config with global code owner and import
     CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
@@ -1205,22 +1392,9 @@
             .branch("master")
             .folderPath("/")
             .addCodeOwnerEmail(admin.email())
-            .addImport(
-                CodeOwnerConfigReference.create(
-                    CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
-                    "/bar/" + nameOfImportedCodeOwnerConfig))
+            .addImport(codeOwnerConfigReference)
             .create();
 
-    // create imported config with global code owner
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/bar/")
-        .fileName(nameOfImportedCodeOwnerConfig)
-        .addCodeOwnerEmail(user.email())
-        .create();
-
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
@@ -1231,47 +1405,74 @@
 
     // Expectation: we get the global code owners from the importing and the imported code owner
     // config
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
   @Test
   public void cyclicImports() throws Exception {
+    CodeOwnerConfigReference codeOwnerConfigReference2 =
+        CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/OWNERS");
+
+    // create imported config with global code owner and that imports the importing config
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/bar/")
+            .fileName("OWNERS")
+            .addCodeOwnerEmail(user.email())
+            .addImport(codeOwnerConfigReference2)
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference1 =
+        createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
+
     // create importing config with global code owner and import
-    CodeOwnerConfig.Key rootCodeOwnerConfigKey =
+    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
             .branch("master")
             .folderPath("/")
+            .fileName("OWNERS")
             .addCodeOwnerEmail(admin.email())
-            .addImport(
-                CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS"))
+            .addImport(codeOwnerConfigReference1)
             .create();
 
-    // create imported config with global code owner and that imports the importing config
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/bar/")
-        .addCodeOwnerEmail(user.email())
-        .addImport(CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/OWNERS"))
-        .create();
-
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            rootCodeOwnerConfigKey,
+            importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the global owners from the importing and the imported code owner config
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference1),
+            CodeOwnerConfigImport.createResolvedImport(
+                keyOfImportedCodeOwnerConfig,
+                importingCodeOwnerConfigKey,
+                codeOwnerConfigReference2));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
   @Test
@@ -1327,72 +1528,91 @@
 
   @Test
   public void importWithRelativePath() throws Exception {
+    // create imported config with global code owner
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/baz/")
+            .fileName("OWNERS")
+            .addCodeOwnerEmail(user.email())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
+
     // create importing config with global code owner and import with relative path
-    CodeOwnerConfig.Key rootCodeOwnerConfigKey =
+    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
             .branch("master")
             .folderPath("/foo/bar/")
             .addCodeOwnerEmail(admin.email())
-            .addImport(
-                CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "../baz/OWNERS"))
+            .addImport(codeOwnerConfigReference)
             .create();
 
-    // create imported config with global code owner
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/foo/baz/")
-        .addCodeOwnerEmail(user.email())
-        .create();
-
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            rootCodeOwnerConfigKey,
+            importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo/bar/baz.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the global owners from the importing and the imported code owner config
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports())
-        .isFalse();
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
   @Test
   public void importFromNonExistingProjectIsIgnored() throws Exception {
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        CodeOwnerConfig.Key.create(Project.nameKey("non-existing"), "master", "/bar/", "OWNERS");
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
+
     // create importing config with global code owner and import from non-existing project
-    CodeOwnerConfig.Key rootCodeOwnerConfigKey =
+    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
             .branch("master")
             .folderPath("/")
             .addCodeOwnerEmail(admin.email())
-            .addImport(
-                CodeOwnerConfigReference.builder(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS")
-                    .setProject(Project.nameKey("non-existing"))
-                    .build())
+            .addImport(codeOwnerConfigReference)
             .create();
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            rootCodeOwnerConfigKey,
+            importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo/bar/baz.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the global owners from the importing code owner config
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email());
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()).isTrue();
+    assertThat(pathCodeOwnersResult.resolvedImports()).isEmpty();
+    assertThat(pathCodeOwnersResult.unresolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createUnresolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference,
+                String.format("project %s not found", keyOfImportedCodeOwnerConfig.project())));
   }
 
   @Test
@@ -1402,32 +1622,33 @@
     ConfigInput configInput = new ConfigInput();
     configInput.state = ProjectState.HIDDEN;
     gApi.projects().name(hiddenProject.get()).config(configInput);
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(hiddenProject)
-        .branch("master")
-        .folderPath("/")
-        .addCodeOwnerEmail(user.email())
-        .create();
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(hiddenProject)
+            .branch("master")
+            .folderPath("/")
+            .fileName("OWNERS")
+            .addCodeOwnerEmail(user.email())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
 
     // create importing config with global code owner and import from the hidden project
-    CodeOwnerConfig.Key rootCodeOwnerConfigKey =
+    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
             .branch("master")
             .folderPath("/")
             .addCodeOwnerEmail(admin.email())
-            .addImport(
-                CodeOwnerConfigReference.builder(CodeOwnerConfigImportMode.ALL, "/OWNERS")
-                    .setProject(hiddenProject)
-                    .build())
+            .addImport(codeOwnerConfigReference)
             .create();
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            rootCodeOwnerConfigKey,
+            importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo/bar/baz.md"));
     assertThat(pathCodeOwners).isPresent();
@@ -1435,85 +1656,111 @@
     // 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
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email());
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()).isTrue();
+    assertThat(pathCodeOwnersResult.resolvedImports()).isEmpty();
+    assertThat(pathCodeOwnersResult.unresolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createUnresolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference,
+                String.format(
+                    "state of project %s doesn't permit read",
+                    keyOfImportedCodeOwnerConfig.project())));
   }
 
   @Test
   public void importFromNonExistingBranchIsIgnored() throws Exception {
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        CodeOwnerConfig.Key.create(project, "non-existing", "/bar/", "OWNERS");
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
+
     // create importing config with global code owner and import from non-existing branch
-    CodeOwnerConfig.Key rootCodeOwnerConfigKey =
+    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
             .branch("master")
             .folderPath("/")
             .addCodeOwnerEmail(admin.email())
-            .addImport(
-                CodeOwnerConfigReference.builder(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS")
-                    .setProject(project)
-                    .setBranch("non-existing")
-                    .build())
+            .addImport(codeOwnerConfigReference)
             .create();
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            rootCodeOwnerConfigKey,
+            importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/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())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email());
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()).isTrue();
+    assertThat(pathCodeOwnersResult.resolvedImports()).isEmpty();
+    assertThat(pathCodeOwnersResult.unresolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createUnresolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference,
+                "code owner config does not exist (revision = current)"));
   }
 
   @Test
   public void importFromOtherProject() throws Exception {
     Project.NameKey otherProject = projectOperations.newProject().create();
 
+    // create imported config with global code owner
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(otherProject)
+            .branch("master")
+            .folderPath("/bar/")
+            .fileName("OWNERS")
+            .addCodeOwnerEmail(user.email())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
+
     // create importing config with global code owner and import with relative path
-    CodeOwnerConfig.Key rootCodeOwnerConfigKey =
+    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
             .branch("master")
             .folderPath("/")
             .addCodeOwnerEmail(admin.email())
-            .addImport(
-                CodeOwnerConfigReference.builder(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS")
-                    .setProject(otherProject)
-                    .build())
+            .addImport(codeOwnerConfigReference)
             .create();
 
-    // create imported config with global code owner
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(otherProject)
-        .branch("master")
-        .folderPath("/bar/")
-        .addCodeOwnerEmail(user.email())
-        .create();
-
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            rootCodeOwnerConfigKey,
+            importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo/bar/baz.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the global owners from the importing and the imported code owner config
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports())
-        .isFalse();
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
   @Test
@@ -1527,43 +1774,52 @@
     // Create other branches in other project.
     createBranch(BranchNameKey.create(otherProject, branchName));
 
+    // create imported config with global code owner
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(otherProject)
+            .branch(branchName)
+            .folderPath("/bar/")
+            .fileName("OWNERS")
+            .addCodeOwnerEmail(user.email())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        CodeOwnerConfigReference.builder(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS")
+            .setProject(otherProject)
+            .build();
+
     // create importing config with global code owner and import with relative path
-    CodeOwnerConfig.Key rootCodeOwnerConfigKey =
+    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
             .branch(branchName)
             .folderPath("/")
             .addCodeOwnerEmail(admin.email())
-            .addImport(
-                CodeOwnerConfigReference.builder(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS")
-                    .setProject(otherProject)
-                    .build())
+            .addImport(codeOwnerConfigReference)
             .create();
 
-    // create imported config with global code owner
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(otherProject)
-        .branch(branchName)
-        .folderPath("/bar/")
-        .addCodeOwnerEmail(user.email())
-        .create();
-
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            rootCodeOwnerConfigKey,
+            importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead(branchName),
             Paths.get("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the global owners from the importing and the imported code owner config
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports())
-        .isFalse();
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
   @Test
@@ -1572,44 +1828,50 @@
     String otherBranch = "foo";
     createBranch(BranchNameKey.create(project, otherBranch));
 
+    // create imported config with global code owner
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch(otherBranch)
+            .folderPath("/bar/")
+            .fileName("OWNERS")
+            .addCodeOwnerEmail(user.email())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
+
     // create importing config with global code owner and import with relative path
-    CodeOwnerConfig.Key rootCodeOwnerConfigKey =
+    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
             .branch("master")
             .folderPath("/")
             .addCodeOwnerEmail(admin.email())
-            .addImport(
-                CodeOwnerConfigReference.builder(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS")
-                    .setProject(project)
-                    .setBranch(otherBranch)
-                    .build())
+            .addImport(codeOwnerConfigReference)
             .create();
 
-    // create imported config with global code owner
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch(otherBranch)
-        .folderPath("/bar/")
-        .addCodeOwnerEmail(user.email())
-        .create();
-
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            rootCodeOwnerConfigKey,
+            importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo/bar/baz.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the global owners from the importing and the imported code owner config
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports())
-        .isFalse();
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
   @Test
@@ -1620,48 +1882,60 @@
     String otherBranch = "foo";
     createBranch(BranchNameKey.create(otherProject, otherBranch));
 
+    // create imported config with global code owner
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(otherProject)
+            .branch(otherBranch)
+            .folderPath("/bar/")
+            .fileName("OWNERS")
+            .addCodeOwnerEmail(user.email())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
+
     // create importing config with global code owner and import with relative path
-    CodeOwnerConfig.Key rootCodeOwnerConfigKey =
+    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
             .branch("master")
             .folderPath("/")
             .addCodeOwnerEmail(admin.email())
-            .addImport(
-                CodeOwnerConfigReference.builder(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS")
-                    .setProject(otherProject)
-                    .setBranch(otherBranch)
-                    .build())
+            .addImport(codeOwnerConfigReference)
             .create();
 
-    // create imported config with global code owner
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(otherProject)
-        .branch(otherBranch)
-        .folderPath("/bar/")
-        .addCodeOwnerEmail(user.email())
-        .create();
-
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            rootCodeOwnerConfigKey,
+            importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo/bar/baz.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the global owners from the importing and the imported code owner config
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports())
-        .isFalse();
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
   @Test
   public void nonResolveablePerFileImportIsIgnored() throws Exception {
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        CodeOwnerConfig.Key.create(project, "master", "/non-existing/", "OWNERS");
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(
+            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig);
+
     // create importing config with non-resolveable per file import
     CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
@@ -1673,10 +1947,7 @@
                 CodeOwnerSet.builder()
                     .addCodeOwnerEmail(admin.email())
                     .addPathExpression("foo.md")
-                    .addImport(
-                        CodeOwnerConfigReference.create(
-                            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
-                            "/non-existing/OWNERS"))
+                    .addImport(codeOwnerConfigReference)
                     .build())
             .create();
 
@@ -1690,16 +1961,50 @@
 
     // Expectation: we get the per file code owner from the importing code owner config, the
     // non-resolveable per file import is silently ignored
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email());
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()).isTrue();
+    assertThat(pathCodeOwnersResult.resolvedImports()).isEmpty();
+    assertThat(pathCodeOwnersResult.unresolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createUnresolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference,
+                String.format(
+                    "code owner config does not exist (revision = %s)",
+                    projectOperations
+                        .project(keyOfImportedCodeOwnerConfig.project())
+                        .getHead(keyOfImportedCodeOwnerConfig.branchNameKey().branch())
+                        .name())));
   }
 
   @Test
   public void perFileImport() throws Exception {
     TestAccount user2 = accountCreator.user2();
 
+    // create imported config with ignoreParentCodeOwners = true, a global code owner and a per file
+    // code owner
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/bar/")
+            .fileName("OWNERS")
+            .ignoreParentCodeOwners()
+            .addCodeOwnerEmail(user.email())
+            .addCodeOwnerSet(
+                CodeOwnerSet.builder()
+                    .addPathExpression("foo.md")
+                    .addCodeOwnerEmail(user2.email())
+                    .build())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(
+            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig);
+
     // create importing config with per code owner and per file import
     CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
@@ -1711,28 +2016,10 @@
                 CodeOwnerSet.builder()
                     .addPathExpression("foo.md")
                     .addCodeOwnerEmail(admin.email())
-                    .addImport(
-                        CodeOwnerConfigReference.create(
-                            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS"))
+                    .addImport(codeOwnerConfigReference)
                     .build())
             .create();
 
-    // create imported config with ignoreParentCodeOwners = true, a global code owner and a per file
-    // code owner
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/bar/")
-        .ignoreParentCodeOwners()
-        .addCodeOwnerEmail(user.email())
-        .addCodeOwnerSet(
-            CodeOwnerSet.builder()
-                .addPathExpression("foo.md")
-                .addCodeOwnerEmail(user2.email())
-                .build())
-        .create();
-
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
@@ -1744,23 +2031,58 @@
     // 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
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
 
     // Expectation: the ignoreParentCodeOwners flag from the imported code owner config is ignored
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().ignoreParentCodeOwners())
-        .isFalse();
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports())
-        .isFalse();
+    assertThat(pathCodeOwnersResult.ignoreParentCodeOwners()).isFalse();
+
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
   @Test
   public void importsOfPerFileImportedCodeOwnerConfigAreResolved() throws Exception {
     TestAccount user2 = accountCreator.user2();
 
+    // create config with global code owner that is imported by the imported config
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig2 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/baz/")
+            .fileName("OWNERS")
+            .addCodeOwnerEmail(user2.email())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference2 =
+        createCodeOwnerConfigReference(
+            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig2);
+
+    // create imported config with global code owner and global import
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig1 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/bar/")
+            .fileName("OWNERS")
+            .addCodeOwnerEmail(user.email())
+            .addImport(codeOwnerConfigReference2)
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference1 =
+        createCodeOwnerConfigReference(
+            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig1);
+
     // create importing config with per file code owner and per file import
-    CodeOwnerConfig.Key rootCodeOwnerConfigKey =
+    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -1770,56 +2092,75 @@
                 CodeOwnerSet.builder()
                     .addPathExpression("foo.md")
                     .addCodeOwnerEmail(admin.email())
-                    .addImport(
-                        CodeOwnerConfigReference.create(
-                            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS"))
+                    .addImport(codeOwnerConfigReference1)
                     .build())
             .create();
 
-    // create imported config with global code owner and global import
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/bar/")
-        .addCodeOwnerEmail(user.email())
-        .addImport(
-            CodeOwnerConfigReference.create(
-                CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/baz/OWNERS"))
-        .create();
-
-    // create config with global code owner that is imported by the imported config
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/baz/")
-        .addCodeOwnerEmail(user2.email())
-        .create();
-
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            rootCodeOwnerConfigKey,
+            importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/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
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email(), user2.email());
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports())
-        .isFalse();
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig1,
+                codeOwnerConfigReference1),
+            CodeOwnerConfigImport.createResolvedImport(
+                keyOfImportedCodeOwnerConfig1,
+                keyOfImportedCodeOwnerConfig2,
+                codeOwnerConfigReference2));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
   @Test
   public void onlyGlobalCodeOwnersAreImportedForTransitivePerFileImports() throws Exception {
     TestAccount user2 = accountCreator.user2();
 
+    // create config with per file code owner that is imported by the imported config
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig2 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/baz/")
+            .fileName("OWNERS")
+            .addCodeOwnerSet(
+                CodeOwnerSet.builder()
+                    .addPathExpression("foo.md")
+                    .addCodeOwnerEmail(user2.email())
+                    .build())
+            .create();
+
+    // create imported config with per global owner and global import with mode ALL
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig1 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/bar/")
+            .fileName("OWNERS")
+            .addCodeOwnerEmail(user.email())
+            .addImport(
+                createCodeOwnerConfigReference(
+                    CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig2))
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference1 =
+        createCodeOwnerConfigReference(
+            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig1);
+
     // create importing config with per file code owner and per file import
-    CodeOwnerConfig.Key rootCodeOwnerConfigKey =
+    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
@@ -1829,39 +2170,14 @@
                 CodeOwnerSet.builder()
                     .addPathExpression("foo.md")
                     .addCodeOwnerEmail(admin.email())
-                    .addImport(
-                        CodeOwnerConfigReference.create(
-                            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS"))
+                    .addImport(codeOwnerConfigReference1)
                     .build())
             .create();
 
-    // create imported config with per global owner and global import with mode ALL
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/bar/")
-        .addCodeOwnerEmail(user.email())
-        .addImport(CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/baz/OWNERS"))
-        .create();
-
-    // create config with per file code owner that is imported by the imported config
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/baz/")
-        .addCodeOwnerSet(
-            CodeOwnerSet.builder()
-                .addPathExpression("foo.md")
-                .addCodeOwnerEmail(user2.email())
-                .build())
-        .create();
-
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            rootCodeOwnerConfigKey,
+            importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
@@ -1869,104 +2185,156 @@
     // 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
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports())
-        .isFalse();
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig1,
+                codeOwnerConfigReference1),
+            CodeOwnerConfigImport.createResolvedImport(
+                keyOfImportedCodeOwnerConfig1,
+                keyOfImportedCodeOwnerConfig2,
+                createCodeOwnerConfigReference(
+                    CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
+                    keyOfImportedCodeOwnerConfig2)));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
   @Test
   public void onlyMatchingTransitivePerFileImportsAreImported() throws Exception {
     TestAccount user2 = accountCreator.user2();
 
+    // create config with global code owner that is imported by the imported config for *.md files
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig2 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/md/")
+            .fileName("OWNERS")
+            .addCodeOwnerEmail(user.email())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference2 =
+        createCodeOwnerConfigReference(
+            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig2);
+
+    // create config with global code owner that is imported by the imported config for *.txt files
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig3 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/txt/")
+            .fileName("OWNERS")
+            .addCodeOwnerEmail(user2.email())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference3 =
+        createCodeOwnerConfigReference(
+            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig3);
+
+    // create imported config with 2 per file imports, one for *.md files and one for *.txt
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig1 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/bar/")
+            .fileName("OWNERS")
+            .addCodeOwnerSet(
+                CodeOwnerSet.builder()
+                    .addPathExpression("*.md")
+                    .addImport(codeOwnerConfigReference2)
+                    .build())
+            .addCodeOwnerSet(
+                CodeOwnerSet.builder()
+                    .addPathExpression("*.txt")
+                    .addImport(codeOwnerConfigReference3)
+                    .build())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference1 =
+        createCodeOwnerConfigReference(
+            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig1);
+
     // create importing config with global import
-    CodeOwnerConfig.Key rootCodeOwnerConfigKey =
+    CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
             .project(project)
             .branch("master")
             .folderPath("/")
-            .addImport(
-                CodeOwnerConfigReference.create(
-                    CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS"))
+            .addImport(codeOwnerConfigReference1)
             .create();
 
-    // create imported config with 2 per file imports, one for *.md files and one for *.txt
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/bar/")
-        .addCodeOwnerSet(
-            CodeOwnerSet.builder()
-                .addPathExpression("*.md")
-                .addImport(
-                    CodeOwnerConfigReference.create(
-                        CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/md/OWNERS"))
-                .build())
-        .addCodeOwnerSet(
-            CodeOwnerSet.builder()
-                .addPathExpression("*.txt")
-                .addImport(
-                    CodeOwnerConfigReference.create(
-                        CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/txt/OWNERS"))
-                .build())
-        .create();
-
-    // create config with global code owner that is imported by the imported config for *.md files
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/md/")
-        .addCodeOwnerEmail(user.email())
-        .create();
-
-    // create config with global code owner that is imported by the imported config for *.txt files
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/txt/")
-        .addCodeOwnerEmail(user2.email())
-        .create();
-
     // Expectation for foo.xyz file: code owners is empty since foo.xyz neither matches *.md nor
     // *.txt
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            rootCodeOwnerConfigKey,
+            importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.xyz"));
     assertThat(pathCodeOwners).isPresent();
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()).isEmpty();
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners()).isEmpty();
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig1,
+                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(),
-            rootCodeOwnerConfigKey,
+            importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(user.email());
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig1,
+                codeOwnerConfigReference1),
+            CodeOwnerConfigImport.createResolvedImport(
+                keyOfImportedCodeOwnerConfig1,
+                keyOfImportedCodeOwnerConfig2,
+                codeOwnerConfigReference2));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
 
     // Expectation for foo.txt file: code owners contains only user2 since foo.txt only matches
     // *.txt
     pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
-            rootCodeOwnerConfigKey,
+            importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.txt"));
     assertThat(pathCodeOwners).isPresent();
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(user2.email());
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig1,
+                codeOwnerConfigReference1),
+            CodeOwnerConfigImport.createResolvedImport(
+                keyOfImportedCodeOwnerConfig1,
+                keyOfImportedCodeOwnerConfig3,
+                codeOwnerConfigReference3));
   }
 
   @Test
@@ -2090,6 +2458,20 @@
   @Test
   public void perFileRuleThatIgnoresGlobalCodeOwnersCanImportGlobalCodeOwnersFromOtherFile()
       throws Exception {
+    // create imported config with a global code owner
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/bar/")
+            .fileName("OWNERS")
+            .addCodeOwnerEmail(user.email())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(
+            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig);
+
     // create importing config that:
     // * has a global code owner
     // * has a per-file import for md files
@@ -2105,21 +2487,10 @@
                 CodeOwnerSet.builder()
                     .addPathExpression(testPathExpressions.matchFileType("md"))
                     .setIgnoreGlobalAndParentCodeOwners()
-                    .addImport(
-                        CodeOwnerConfigReference.create(
-                            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS"))
+                    .addImport(codeOwnerConfigReference)
                     .build())
             .create();
 
-    // create imported config with a global code owner
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/bar/")
-        .addCodeOwnerEmail(user.email())
-        .create();
-
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
@@ -2131,11 +2502,17 @@
     // 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)
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(user.email());
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports())
-        .isFalse();
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
   @Test
@@ -2146,6 +2523,24 @@
     TestAccount user3 =
         accountCreator.create("user3", "user3@example.com", "User3", /* displayName= */ null);
 
+    // create imported config that has a matching per-file rule
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/bar/")
+            .fileName("OWNERS")
+            .addCodeOwnerEmail(user2.email())
+            .addCodeOwnerSet(
+                CodeOwnerSet.builder()
+                    .addPathExpression(testPathExpressions.matchFileType("md"))
+                    .addCodeOwnerEmail(user3.email())
+                    .build())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig);
+
     // 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 =
@@ -2155,8 +2550,7 @@
             .branch("master")
             .folderPath("/")
             .addCodeOwnerEmail(admin.email())
-            .addImport(
-                CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS"))
+            .addImport(codeOwnerConfigReference)
             .addCodeOwnerSet(
                 CodeOwnerSet.builder()
                     .addPathExpression(testPathExpressions.matchFileType("md"))
@@ -2165,20 +2559,6 @@
                     .build())
             .create();
 
-    // create imported config that has a matching per-file rule
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(project)
-        .branch("master")
-        .folderPath("/bar/")
-        .addCodeOwnerEmail(user2.email())
-        .addCodeOwnerSet(
-            CodeOwnerSet.builder()
-                .addPathExpression(testPathExpressions.matchFileType("md"))
-                .addCodeOwnerEmail(user3.email())
-                .build())
-        .create();
-
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
@@ -2191,11 +2571,17 @@
     // 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
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(user.email(), user3.email());
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports())
-        .isFalse();
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig,
+                codeOwnerConfigReference));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
   @Test
@@ -2204,6 +2590,35 @@
 
     Project.NameKey otherProject = projectOperations.newProject().create();
 
+    // create transitively imported config in other project
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig2 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(otherProject)
+            .branch("master")
+            .folderPath("/baz/")
+            .fileName("OWNERS")
+            .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(user2.email()).build())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference2 =
+        createCodeOwnerConfigReference(
+            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig2);
+
+    // create imported config in other project
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig1 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(otherProject)
+            .branch("master")
+            .folderPath("/bar/")
+            .fileName("OWNERS")
+            .addCodeOwnerEmail(user.email())
+            .addImport(codeOwnerConfigReference2)
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference1 =
+        createCodeOwnerConfigReference(
+            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig1);
+
     // create importing config
     CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
@@ -2212,32 +2627,9 @@
             .branch("master")
             .folderPath("/")
             .addCodeOwnerEmail(admin.email())
-            .addImport(
-                CodeOwnerConfigReference.builder(
-                        CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS")
-                    .setProject(otherProject)
-                    .build())
+            .addImport(codeOwnerConfigReference1)
             .create();
 
-    // create imported config in other project
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(otherProject)
-        .branch("master")
-        .folderPath("/bar/")
-        .addCodeOwnerEmail(user.email())
-        .addImport(CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/baz/OWNERS"))
-        .create();
-
-    // create transitively imported config in other project
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(otherProject)
-        .branch("master")
-        .folderPath("/baz/")
-        .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(user2.email()).build())
-        .create();
-
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
@@ -2248,11 +2640,21 @@
 
     // 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
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+    PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get();
+    assertThat(pathCodeOwnersResult.getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email(), user2.email());
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports())
-        .isFalse();
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig1,
+                codeOwnerConfigReference1),
+            CodeOwnerConfigImport.createResolvedImport(
+                keyOfImportedCodeOwnerConfig1,
+                keyOfImportedCodeOwnerConfig2,
+                codeOwnerConfigReference2));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
   @Test
@@ -2261,6 +2663,35 @@
 
     Project.NameKey otherProject = projectOperations.newProject().create();
 
+    // create transitively imported config
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig2 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(otherProject)
+            .branch("master")
+            .folderPath("/bar/baz/")
+            .fileName("OWNERS")
+            .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(user2.email()).build())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference2 =
+        createCodeOwnerConfigReference(
+            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig2);
+
+    // create imported config
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig1 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(otherProject)
+            .branch("master")
+            .folderPath("/bar/")
+            .fileName("OWNERS")
+            .addCodeOwnerEmail(user.email())
+            .addImport(codeOwnerConfigReference2)
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference1 =
+        createCodeOwnerConfigReference(
+            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig1);
+
     // create importing config
     CodeOwnerConfig.Key importingCodeOwnerConfigKey =
         codeOwnerConfigOperations
@@ -2269,32 +2700,9 @@
             .branch("master")
             .folderPath("/")
             .addCodeOwnerEmail(admin.email())
-            .addImport(
-                CodeOwnerConfigReference.builder(
-                        CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "bar/OWNERS")
-                    .setProject(otherProject)
-                    .build())
+            .addImport(codeOwnerConfigReference1)
             .create();
 
-    // create imported config
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(otherProject)
-        .branch("master")
-        .folderPath("/bar/")
-        .addCodeOwnerEmail(user.email())
-        .addImport(CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "baz/OWNERS"))
-        .create();
-
-    // create transitively imported config
-    codeOwnerConfigOperations
-        .newCodeOwnerConfig()
-        .project(otherProject)
-        .branch("master")
-        .folderPath("/bar/baz/")
-        .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(user2.email()).build())
-        .create();
-
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
             transientCodeOwnerConfigCacheProvider.get(),
@@ -2305,11 +2713,21 @@
 
     // 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())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email(), user2.email());
-    assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports())
-        .isFalse();
+    assertThat(pathCodeOwnersResult.resolvedImports())
+        .containsExactly(
+            CodeOwnerConfigImport.createResolvedImport(
+                importingCodeOwnerConfigKey,
+                keyOfImportedCodeOwnerConfig1,
+                codeOwnerConfigReference1),
+            CodeOwnerConfigImport.createResolvedImport(
+                keyOfImportedCodeOwnerConfig1,
+                keyOfImportedCodeOwnerConfig2,
+                codeOwnerConfigReference2));
+    assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty();
   }
 
   private CodeOwnerConfig.Builder createCodeOwnerBuilder() {
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportTest.java
deleted file mode 100644
index ce0494f..0000000
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportTest.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (C) 2021 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.plugins.codeowners.backend;
-
-import org.junit.Test;
-
-/** Tests for {@link UnresolvedImport}. */
-public class UnresolvedImportTest extends AbstractAutoValueTest {
-  @Test
-  public void toStringIncludesAllData() throws Exception {
-    UnresolvedImport unresolvedImport =
-        UnresolvedImport.create(
-            CodeOwnerConfig.Key.create(project, "master", "/"),
-            CodeOwnerConfig.Key.create(project, "master", "/bar/"),
-            CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS"),
-            "test message");
-    assertThatToStringIncludesAllData(unresolvedImport, UnresolvedImport.class);
-  }
-}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerConfigFileJsonIT.java b/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerConfigFileJsonIT.java
new file mode 100644
index 0000000..ed0b301
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerConfigFileJsonIT.java
@@ -0,0 +1,388 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.restapi;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.entities.Project;
+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 org.junit.Before;
+import org.junit.Test;
+
+/** Tests for {@link CodeOwnerConfigFileJson}. */
+public class CodeOwnerConfigFileJsonIT extends AbstractCodeOwnersIT {
+  private CodeOwnerConfigFileJson CodeOwnerConfigFileJson;
+
+  @Before
+  public void setUpCodeOwnersPlugin() throws Exception {
+    CodeOwnerConfigFileJson = plugin.getSysInjector().getInstance(CodeOwnerConfigFileJson.class);
+  }
+
+  @Test
+  public void cannotFormatWithNullCodeOwnerConfigKey() throws Exception {
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class,
+            () ->
+                CodeOwnerConfigFileJson.format(
+                    /* codeOwnerConfigKey= */ null,
+                    /* resolvedImports= */ ImmutableList.of(),
+                    /* unresolvedImports= */ ImmutableList.of()));
+    assertThat(npe).hasMessageThat().isEqualTo("codeOwnerConfigKey");
+  }
+
+  @Test
+  public void cannotFormatWithNullResolvedImports() throws Exception {
+    CodeOwnerConfig.Key codeOwnerConfigKey =
+        CodeOwnerConfig.Key.create(Project.nameKey("project"), "master", "/");
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class,
+            () ->
+                CodeOwnerConfigFileJson.format(
+                    codeOwnerConfigKey,
+                    /* resolvedImports= */ null,
+                    /* unresolvedImports= */ ImmutableList.of()));
+    assertThat(npe).hasMessageThat().isEqualTo("resolvedImports");
+  }
+
+  @Test
+  public void cannotFormatWithNullUnresolvedImports() throws Exception {
+    CodeOwnerConfig.Key codeOwnerConfigKey =
+        CodeOwnerConfig.Key.create(Project.nameKey("project"), "master", "/");
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class,
+            () ->
+                CodeOwnerConfigFileJson.format(
+                    codeOwnerConfigKey,
+                    /* resolvedImports= */ ImmutableList.of(),
+                    /* unresolvedImports= */ null));
+    assertThat(npe).hasMessageThat().isEqualTo("unresolvedImports");
+  }
+
+  @Test
+  public void formatWithoutImports() throws Exception {
+    CodeOwnerConfig.Key codeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/baz/")
+            .addCodeOwnerEmail(admin.email())
+            .create();
+
+    CodeOwnerConfigFileInfo codeOwnerConfigFileInfo =
+        CodeOwnerConfigFileJson.format(
+            codeOwnerConfigKey,
+            /* resolvedImports= */ ImmutableList.of(),
+            /* unresolvedImports= */ ImmutableList.of());
+    assertThat(codeOwnerConfigFileInfo.project).isEqualTo(codeOwnerConfigKey.project().get());
+    assertThat(codeOwnerConfigFileInfo.branch)
+        .isEqualTo(codeOwnerConfigKey.branchNameKey().branch());
+    assertThat(codeOwnerConfigFileInfo.path)
+        .isEqualTo(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath());
+    assertThat(codeOwnerConfigFileInfo.importMode).isNull();
+    assertThat(codeOwnerConfigFileInfo.imports).isNull();
+    assertThat(codeOwnerConfigFileInfo.unresolvedImports).isNull();
+    assertThat(codeOwnerConfigFileInfo.unresolvedErrorMessage).isNull();
+  }
+
+  @Test
+  public void formatWithUnresolvedImports() throws Exception {
+    CodeOwnerConfig.Key codeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/bar/")
+            .addCodeOwnerEmail(admin.email())
+            .create();
+
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        CodeOwnerConfig.Key.create(project, "stable", "/foo/baz/");
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(
+            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig);
+
+    CodeOwnerConfigFileInfo codeOwnerConfigFileInfo =
+        CodeOwnerConfigFileJson.format(
+            codeOwnerConfigKey,
+            /* resolvedImports= */ ImmutableList.of(),
+            /* unresolvedImports= */ ImmutableList.of(
+                CodeOwnerConfigImport.createUnresolvedImport(
+                    codeOwnerConfigKey,
+                    keyOfImportedCodeOwnerConfig,
+                    codeOwnerConfigReference,
+                    "error message")));
+    assertThat(codeOwnerConfigFileInfo.project).isEqualTo(codeOwnerConfigKey.project().get());
+    assertThat(codeOwnerConfigFileInfo.branch)
+        .isEqualTo(codeOwnerConfigKey.branchNameKey().branch());
+    assertThat(codeOwnerConfigFileInfo.path)
+        .isEqualTo(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath());
+    assertThat(codeOwnerConfigFileInfo.importMode).isNull();
+    assertThat(codeOwnerConfigFileInfo.imports).isNull();
+    assertThat(codeOwnerConfigFileInfo.unresolvedErrorMessage).isNull();
+
+    assertThat(codeOwnerConfigFileInfo.unresolvedImports).hasSize(1);
+    CodeOwnerConfigFileInfo unresolvedImportInfo =
+        Iterables.getOnlyElement(codeOwnerConfigFileInfo.unresolvedImports);
+    assertThat(unresolvedImportInfo.project)
+        .isEqualTo(keyOfImportedCodeOwnerConfig.project().get());
+    assertThat(unresolvedImportInfo.branch)
+        .isEqualTo(keyOfImportedCodeOwnerConfig.branchNameKey().branch());
+    assertThat(unresolvedImportInfo.path)
+        .isEqualTo(
+            codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).getFilePath());
+    assertThat(unresolvedImportInfo.importMode).isEqualTo(codeOwnerConfigReference.importMode());
+    assertThat(unresolvedImportInfo.imports).isNull();
+    assertThat(unresolvedImportInfo.unresolvedImports).isNull();
+    assertThat(unresolvedImportInfo.unresolvedErrorMessage).isEqualTo("error message");
+  }
+
+  @Test
+  public void formatWithImports() throws Exception {
+    CodeOwnerConfig.Key codeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/bar/")
+            .addCodeOwnerEmail(admin.email())
+            .create();
+
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        CodeOwnerConfig.Key.create(project, "stable", "/foo/baz/");
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(
+            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig);
+
+    CodeOwnerConfigFileInfo codeOwnerConfigFileInfo =
+        CodeOwnerConfigFileJson.format(
+            codeOwnerConfigKey,
+            /* resolvedImports= */ ImmutableList.of(
+                CodeOwnerConfigImport.createResolvedImport(
+                    codeOwnerConfigKey, keyOfImportedCodeOwnerConfig, codeOwnerConfigReference)),
+            /* unresolvedImports= */ ImmutableList.of());
+    assertThat(codeOwnerConfigFileInfo.project).isEqualTo(codeOwnerConfigKey.project().get());
+    assertThat(codeOwnerConfigFileInfo.branch)
+        .isEqualTo(codeOwnerConfigKey.branchNameKey().branch());
+    assertThat(codeOwnerConfigFileInfo.path)
+        .isEqualTo(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath());
+    assertThat(codeOwnerConfigFileInfo.importMode).isNull();
+    assertThat(codeOwnerConfigFileInfo.unresolvedImports).isNull();
+    assertThat(codeOwnerConfigFileInfo.unresolvedErrorMessage).isNull();
+
+    assertThat(codeOwnerConfigFileInfo.imports).hasSize(1);
+    CodeOwnerConfigFileInfo resolvedImportInfo =
+        Iterables.getOnlyElement(codeOwnerConfigFileInfo.imports);
+    assertThat(resolvedImportInfo.project).isEqualTo(keyOfImportedCodeOwnerConfig.project().get());
+    assertThat(resolvedImportInfo.branch)
+        .isEqualTo(keyOfImportedCodeOwnerConfig.branchNameKey().branch());
+    assertThat(resolvedImportInfo.path)
+        .isEqualTo(
+            codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).getFilePath());
+    assertThat(resolvedImportInfo.importMode).isEqualTo(codeOwnerConfigReference.importMode());
+    assertThat(resolvedImportInfo.imports).isNull();
+    assertThat(resolvedImportInfo.unresolvedImports).isNull();
+    assertThat(resolvedImportInfo.unresolvedErrorMessage).isNull();
+  }
+
+  @Test
+  public void formatWithNestedImports() throws Exception {
+    CodeOwnerConfig.Key codeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/bar/")
+            .addCodeOwnerEmail(admin.email())
+            .create();
+
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/bar/baz")
+            .addCodeOwnerEmail(admin.email())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        createCodeOwnerConfigReference(
+            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig);
+
+    CodeOwnerConfig.Key keyOfNestedImportedCodeOwnerConfig1 =
+        CodeOwnerConfig.Key.create(project, "stable", "/foo/baz1/");
+    CodeOwnerConfigReference nestedCodeOwnerConfigReference1 =
+        createCodeOwnerConfigReference(
+            CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
+            keyOfNestedImportedCodeOwnerConfig1);
+
+    CodeOwnerConfig.Key keyOfNestedImportedCodeOwnerConfig2 =
+        CodeOwnerConfig.Key.create(project, "stable", "/foo/baz2/");
+    CodeOwnerConfigReference nestedCodeOwnerConfigReference2 =
+        createCodeOwnerConfigReference(
+            CodeOwnerConfigImportMode.ALL, keyOfNestedImportedCodeOwnerConfig2);
+
+    CodeOwnerConfigFileInfo codeOwnerConfigFileInfo =
+        CodeOwnerConfigFileJson.format(
+            codeOwnerConfigKey,
+            /* resolvedImports= */ ImmutableList.of(
+                CodeOwnerConfigImport.createResolvedImport(
+                    codeOwnerConfigKey, keyOfImportedCodeOwnerConfig, codeOwnerConfigReference),
+                CodeOwnerConfigImport.createResolvedImport(
+                    keyOfImportedCodeOwnerConfig,
+                    keyOfNestedImportedCodeOwnerConfig1,
+                    nestedCodeOwnerConfigReference1)),
+            /* unresolvedImports= */ ImmutableList.of(
+                CodeOwnerConfigImport.createUnresolvedImport(
+                    keyOfImportedCodeOwnerConfig,
+                    keyOfNestedImportedCodeOwnerConfig2,
+                    nestedCodeOwnerConfigReference2,
+                    "error message")));
+    assertThat(codeOwnerConfigFileInfo.project).isEqualTo(codeOwnerConfigKey.project().get());
+    assertThat(codeOwnerConfigFileInfo.branch)
+        .isEqualTo(codeOwnerConfigKey.branchNameKey().branch());
+    assertThat(codeOwnerConfigFileInfo.path)
+        .isEqualTo(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath());
+    assertThat(codeOwnerConfigFileInfo.importMode).isNull();
+    assertThat(codeOwnerConfigFileInfo.unresolvedImports).isNull();
+    assertThat(codeOwnerConfigFileInfo.unresolvedErrorMessage).isNull();
+
+    assertThat(codeOwnerConfigFileInfo.imports).hasSize(1);
+    CodeOwnerConfigFileInfo resolvedImportInfo =
+        Iterables.getOnlyElement(codeOwnerConfigFileInfo.imports);
+    assertThat(resolvedImportInfo.project).isEqualTo(keyOfImportedCodeOwnerConfig.project().get());
+    assertThat(resolvedImportInfo.branch)
+        .isEqualTo(keyOfImportedCodeOwnerConfig.branchNameKey().branch());
+    assertThat(resolvedImportInfo.path)
+        .isEqualTo(
+            codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).getFilePath());
+    assertThat(resolvedImportInfo.importMode).isEqualTo(codeOwnerConfigReference.importMode());
+    assertThat(resolvedImportInfo.unresolvedErrorMessage).isNull();
+
+    assertThat(resolvedImportInfo.imports).hasSize(1);
+    CodeOwnerConfigFileInfo nestedResolvedImportInfo =
+        Iterables.getOnlyElement(resolvedImportInfo.imports);
+    assertThat(nestedResolvedImportInfo.project)
+        .isEqualTo(keyOfNestedImportedCodeOwnerConfig1.project().get());
+    assertThat(nestedResolvedImportInfo.branch)
+        .isEqualTo(keyOfNestedImportedCodeOwnerConfig1.branchNameKey().branch());
+    assertThat(nestedResolvedImportInfo.path)
+        .isEqualTo(
+            codeOwnerConfigOperations
+                .codeOwnerConfig(keyOfNestedImportedCodeOwnerConfig1)
+                .getFilePath());
+    assertThat(nestedResolvedImportInfo.importMode)
+        .isEqualTo(nestedCodeOwnerConfigReference1.importMode());
+    assertThat(nestedResolvedImportInfo.imports).isNull();
+    assertThat(nestedResolvedImportInfo.unresolvedImports).isNull();
+    assertThat(nestedResolvedImportInfo.unresolvedErrorMessage).isNull();
+
+    assertThat(resolvedImportInfo.unresolvedImports).hasSize(1);
+    CodeOwnerConfigFileInfo nestedUnresolvedImportInfo1 =
+        Iterables.getOnlyElement(resolvedImportInfo.unresolvedImports);
+    assertThat(nestedUnresolvedImportInfo1.project)
+        .isEqualTo(keyOfNestedImportedCodeOwnerConfig2.project().get());
+    assertThat(nestedUnresolvedImportInfo1.branch)
+        .isEqualTo(keyOfNestedImportedCodeOwnerConfig2.branchNameKey().branch());
+    assertThat(nestedUnresolvedImportInfo1.path)
+        .isEqualTo(
+            codeOwnerConfigOperations
+                .codeOwnerConfig(keyOfNestedImportedCodeOwnerConfig2)
+                .getFilePath());
+    assertThat(nestedUnresolvedImportInfo1.importMode)
+        .isEqualTo(nestedCodeOwnerConfigReference2.importMode());
+    assertThat(nestedUnresolvedImportInfo1.imports).isNull();
+    assertThat(nestedUnresolvedImportInfo1.unresolvedImports).isNull();
+    assertThat(nestedUnresolvedImportInfo1.unresolvedErrorMessage).isEqualTo("error message");
+  }
+
+  @Test
+  public void formatWithCyclicImports() throws Exception {
+    CodeOwnerConfig.Key codeOwnerConfigKey1 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/bar/")
+            .addCodeOwnerEmail(admin.email())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference1 =
+        createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, codeOwnerConfigKey1);
+
+    CodeOwnerConfig.Key codeOwnerConfigKey2 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/bar/baz")
+            .addCodeOwnerEmail(admin.email())
+            .create();
+    CodeOwnerConfigReference codeOwnerConfigReference2 =
+        createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, codeOwnerConfigKey2);
+
+    CodeOwnerConfigFileInfo codeOwnerConfigFileInfo =
+        CodeOwnerConfigFileJson.format(
+            codeOwnerConfigKey1,
+            /* resolvedImports= */ ImmutableList.of(
+                CodeOwnerConfigImport.createResolvedImport(
+                    codeOwnerConfigKey1, codeOwnerConfigKey2, codeOwnerConfigReference1),
+                CodeOwnerConfigImport.createResolvedImport(
+                    codeOwnerConfigKey2, codeOwnerConfigKey1, codeOwnerConfigReference2)),
+            /* unresolvedImports= */ ImmutableList.of());
+
+    assertThat(codeOwnerConfigFileInfo.project).isEqualTo(codeOwnerConfigKey1.project().get());
+    assertThat(codeOwnerConfigFileInfo.branch)
+        .isEqualTo(codeOwnerConfigKey1.branchNameKey().branch());
+    assertThat(codeOwnerConfigFileInfo.path)
+        .isEqualTo(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey1).getFilePath());
+    assertThat(codeOwnerConfigFileInfo.importMode).isNull();
+    assertThat(codeOwnerConfigFileInfo.unresolvedImports).isNull();
+    assertThat(codeOwnerConfigFileInfo.unresolvedErrorMessage).isNull();
+
+    assertThat(codeOwnerConfigFileInfo.imports).hasSize(1);
+    CodeOwnerConfigFileInfo resolvedImportInfo =
+        Iterables.getOnlyElement(codeOwnerConfigFileInfo.imports);
+    assertThat(resolvedImportInfo.project).isEqualTo(codeOwnerConfigKey2.project().get());
+    assertThat(resolvedImportInfo.branch).isEqualTo(codeOwnerConfigKey2.branchNameKey().branch());
+    assertThat(resolvedImportInfo.path)
+        .isEqualTo(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey2).getFilePath());
+    assertThat(resolvedImportInfo.importMode).isEqualTo(codeOwnerConfigReference1.importMode());
+    assertThat(resolvedImportInfo.unresolvedErrorMessage).isNull();
+
+    assertThat(resolvedImportInfo.imports).hasSize(1);
+    CodeOwnerConfigFileInfo nestedResolvedImportInfo =
+        Iterables.getOnlyElement(resolvedImportInfo.imports);
+    assertThat(nestedResolvedImportInfo.project).isEqualTo(codeOwnerConfigKey1.project().get());
+    assertThat(nestedResolvedImportInfo.branch)
+        .isEqualTo(codeOwnerConfigKey1.branchNameKey().branch());
+    assertThat(nestedResolvedImportInfo.path)
+        .isEqualTo(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey1).getFilePath());
+    assertThat(nestedResolvedImportInfo.importMode)
+        .isEqualTo(codeOwnerConfigReference1.importMode());
+    assertThat(nestedResolvedImportInfo.imports).isNull();
+    assertThat(nestedResolvedImportInfo.unresolvedImports).isNull();
+    assertThat(nestedResolvedImportInfo.unresolvedErrorMessage).isNull();
+  }
+}
diff --git a/resources/Documentation/backend-find-owners.md b/resources/Documentation/backend-find-owners.md
index 846e8d9..1ea48ab 100644
--- a/resources/Documentation/backend-find-owners.md
+++ b/resources/Documentation/backend-find-owners.md
@@ -371,10 +371,12 @@
 annotation, this annotation applies to all these users. E.g. if an annotation is
 set for the all users wildcard (aka `*`) it applies to all users.
 
-**NOTE:** Only [email lines](#userEmails) and [per-file lines](#perFile) support
-annotations, for other lines (e.g. [file lines](#fileKeyword) and [include
-lines](#includeKeyword)) annotations are interpreted as [comments](#comments)
-and are silently ignored.
+**NOTE:** Only [email lines](#userEmails) and [per-file lines](#perFile) that
+assign code ownership directly to users support annotations, for other lines
+(e.g.  [file lines](#fileKeyword), [include lines](#includeKeyword) and
+[per-file lines](#perFile) that reference other `OWNERS` files via the
+[file](#fileKeyword) keyword) annotations are interpreted as
+[comments](#comments) and are silently ignored.
 
 ### <a id="comments">Comments
 
diff --git a/resources/Documentation/rest-api.md b/resources/Documentation/rest-api.md
index 0185851..c97ba2c 100644
--- a/resources/Documentation/rest-api.md
+++ b/resources/Documentation/rest-api.md
@@ -475,6 +475,71 @@
 (visible) users is returned, as many as are needed to fill up the requested
 limit.
 
+#### Request
+
+```
+  GET /projects/foo%2Fbar/branches/master/code_owners/docs%2Findex.md HTTP/1.0
+```
+
+#### Response
+
+```
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "code_owners": [
+      {
+        "account": {
+          "_account_id": 1000096
+        }
+      },
+      {
+        "account": {
+          "_account_id": 1001439
+        }
+      },
+      {
+        "account": {
+          "_account_id": 1007265
+        }
+      },
+      {
+        "account": {
+          "_account_id": 1009877
+        }
+      },
+      {
+        "account": {
+          "_account_id": 1002930
+        }
+      }
+    ],
+    "code_owner_configs": [
+      {
+        "project": "foo/bar",
+        "branch": "master",
+        "path": "/docs/OWNERS"
+      },
+      {
+        "project": "foo/bar",
+        "branch": "master",
+        "path": "/OWNERS",
+        "imports": [
+          {
+            "project": "foo",
+            "branch": "master",
+            "path": "/OWNERS",
+            "import_mode": "ALL",
+          }
+        ]
+      }
+    ]
+  }
+```
+
 #### <a id="scoringFactors">Scoring Factors
 
 The following factors are taken into account for computing the scores of the
@@ -550,36 +615,6 @@
 
 The same applies for [default code owners](config-guide.html#codeOwners).
 
-#### Request
-
-```
-  GET /projects/foo%2Fbar/branches/master/code_owners/docs%2Findex.md HTTP/1.0
-```
-
-#### Response
-
-```
-  HTTP/1.1 200 OK
-  Content-Disposition: attachment
-  Content-Type: application/json; charset=UTF-8
-
-  )]}'
-  {
-    "code_owners": [
-      {
-        "account": {
-          "_account_id": 1000096
-        }
-      },
-      {
-        "account": {
-          "_account_id": 1001439
-        },
-      }
-    ]
-  }
-```
-
 #### <a id="batch-list-code-owners"> Batch Request
 
 There is no REST endpoint that allows to retrieve code owners for multiple
@@ -918,6 +953,22 @@
 
 ---
 
+### <a id="code-owner-config-file-info"> CodeOwnerConfigFileInfo
+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.
+| `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.
+| `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.
+
+---
+
 ### <a id="code-owner-config-info"> CodeOwnerConfigInfo
 The `CodeOwnerConfigInfo` entity contains information about a code owner config
 for a path.
@@ -1042,6 +1093,7 @@
 | ------------- | -------- | ----------- |
 | `code_owners` |          | List of code owners as [CodeOwnerInfo](#code-owner-info) entities. The code owners are sorted by a score that is computed from mutliple [scoring factors](#scoringFactors).
 | `owned_by_all_users` | optional | Whether the path is owned by all users. Not set if `false`.
+| `code_owner_configs` || The code owner config files that have been inspected to gather the code owners as [CodeOwnerConfigFileInfo](#code-owner-config-file-info) entities.
 | `debug_logs`  | optional | Debug logs that may help to understand why a user is or isn't suggested as a code owner. Only set if requested via `--debug`. 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.
 
 ### <a id="file-code-owner-status-info"> FileCodeOwnerStatusInfo