Allow to define default code owners across branches in refs/meta/config

It is now possible to define default code owners across branches in a
code owner config file in the root directory of the refs/meta/config
branch.

The code owner config file in refs/meta/config is the parent code owner
config of the root code owner configs in all branches. This means it is
ignored if the root code owner config file in a branch has "set
noparent" set.

Being able to define code owners across branches is useful to:

* keep default code owners consistent across all branches (e.g. if a new
  default code owner should be added it can be done in the one code owner
  config file in refs/meta/config rather than needing to update the root
  code owner config files in all branches)

* initially setup code owners for a repository (e.g. just define the
  default code owners in refs/meta/config instead of needing to add a
  root code owner config file in all branches)

Default code owners are not inherited from parent projects. If code
owners should be defined for child projects, this is already possible
via global code owners.

The following changes have been done:

* CodeOwnerConfigHierarchy:
  Iterates over the code owner config files that apply for a path. This
  includes the default code owner config file in refs/meta/config now,
  which is the parent of the root code owner config file.

* CodeOwnerConfigScanner:
  Scans the code owner config files in a branch. This class has now an
  option to control if the default code owner config file in
  refs/meta/config should be included:

** We include the default code owner config file in refs/meta/config to
   decide whether we are in bootstrapping mode, as this file applies to
   the branch and we want to use the bootstrapping mode only if there is
   no applying code owner config file.

** We do not include the default code owner config file in
   refs/meta/config if the code owner config files in a branch should be
   listed or checked, as this would be unexpected.

* AbstractGetCodeOwnersForPath:
  Suggests code owners. Code owners from the default code owner config
  file in refs/meta/config are included into the suggestion. They have a
  distance that is by 1 higher than the distance of code owners that are
  defined at the root, and by 1 lower than the distance of the global
  code owners that are defined via configuration.

Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: I472a5239246970f240aa1828e31374b3d7cd086e
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java b/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java
index 101845a..adccb0f 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java
@@ -29,6 +29,7 @@
   @Override
   protected void configure() {
     factory(CodeOwnersUpdate.Factory.class);
+    factory(CodeOwnerConfigScanner.Factory.class);
 
     DynamicMap.mapOf(binder(), CodeOwnerBackend.class);
 
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
index 36d8cc9..dafbbbf 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
@@ -79,7 +79,7 @@
   private final GitRepositoryManager repoManager;
   private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
   private final ChangedFiles changedFiles;
-  private final CodeOwnerConfigScanner codeOwnerConfigScanner;
+  private final CodeOwnerConfigScanner.Factory codeOwnerConfigScannerFactory;
   private final CodeOwnerConfigHierarchy codeOwnerConfigHierarchy;
   private final Provider<CodeOwnerResolver> codeOwnerResolver;
   private final ApprovalsUtil approvalsUtil;
@@ -90,7 +90,7 @@
       GitRepositoryManager repoManager,
       CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
       ChangedFiles changedFiles,
-      CodeOwnerConfigScanner codeOwnerConfigScanner,
+      CodeOwnerConfigScanner.Factory codeOwnerConfigScannerFactory,
       CodeOwnerConfigHierarchy codeOwnerConfigHierarchy,
       Provider<CodeOwnerResolver> codeOwnerResolver,
       ApprovalsUtil approvalsUtil) {
@@ -98,7 +98,7 @@
     this.repoManager = repoManager;
     this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
     this.changedFiles = changedFiles;
-    this.codeOwnerConfigScanner = codeOwnerConfigScanner;
+    this.codeOwnerConfigScannerFactory = codeOwnerConfigScannerFactory;
     this.codeOwnerConfigHierarchy = codeOwnerConfigHierarchy;
     this.codeOwnerResolver = codeOwnerResolver;
     this.approvalsUtil = approvalsUtil;
@@ -203,7 +203,8 @@
       // (project
       // owners count as code owners) to allow bootstrapping the code owner configuration in the
       // branch.
-      boolean isBootstrapping = !codeOwnerConfigScanner.containsAnyCodeOwnerConfigFile(branch);
+      boolean isBootstrapping =
+          !codeOwnerConfigScannerFactory.create().containsAnyCodeOwnerConfigFile(branch);
       logger.atFine().log("isBootstrapping = %s", isBootstrapping);
 
       ImmutableSet<Account.Id> reviewerAccountIds = getReviewerAccountIds(changeNotes);
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfig.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfig.java
index f1ab1fe..354c10e 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfig.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfig.java
@@ -307,6 +307,13 @@
     }
 
     /**
+     * Creates a builder from this code owner config key.
+     *
+     * @return builder that was created from this code owner config key
+     */
+    public abstract Key.Builder toBuilder();
+
+    /**
      * Creates a code owner config key.
      *
      * @param project the project to which the code owner config belongs
@@ -369,6 +376,28 @@
       return builder.build();
     }
 
+    /**
+     * Create a key with the file name being set.
+     *
+     * @param codeOwnerBackend code owner backend that should be used to lookup the file name if it
+     *     is not set yet
+     * @param project the project to which the code owner config belongs
+     * @param branch the branch to which the code owner config belongs
+     * @param folderPath the path of the folder to which the code owner config belongs, must be
+     *     absolute
+     * @return the code owner config key with the file name being set
+     */
+    public static Key createWithFileName(
+        CodeOwnerBackend codeOwnerBackend,
+        Project.NameKey project,
+        String branch,
+        String folderPath) {
+      Key key = create(project, branch, folderPath);
+      return key.toBuilder()
+          .setFileName(codeOwnerBackend.getFilePath(key).getFileName().toString())
+          .build();
+    }
+
     @AutoValue.Builder
     public abstract static class Builder {
       /**
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchy.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchy.java
index 68a9a7b..e3bbb85 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchy.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchy.java
@@ -19,24 +19,42 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import java.io.IOException;
 import java.nio.file.Path;
 import java.util.Optional;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 
 /**
  * Class to visit the code owner configs in a given branch that apply for a given path by following
- * the path hierarchy from the given path up to the root folder.
+ * the path hierarchy from the given path up to the root folder and the default code owner config in
+ * {@code refs/meta/config}.
+ *
+ * <p>The default code owner config in {@code refs/meta/config} is the parent of the code owner
+ * config in the root folder of the branch. The same as any other parent it can be ignored (e.g. by
+ * using {@code set noparent} in the root code owner config if the {@code find-owners} backend is
+ * used).
  */
 @Singleton
 public class CodeOwnerConfigHierarchy {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  private final GitRepositoryManager repoManager;
   private final PathCodeOwners.Factory pathCodeOwnersFactory;
 
   @Inject
-  CodeOwnerConfigHierarchy(PathCodeOwners.Factory pathCodeOwnersFactory) {
+  CodeOwnerConfigHierarchy(
+      GitRepositoryManager repoManager, PathCodeOwners.Factory pathCodeOwnersFactory) {
+    this.repoManager = repoManager;
     this.pathCodeOwnersFactory = pathCodeOwnersFactory;
   }
 
@@ -44,7 +62,7 @@
    * Visits the code owner configs in the given branch that apply for the given path by following
    * the path hierarchy from the given path up to the root folder.
    *
-   * @param branch project and branch from which the code owner configs should be visited
+   * @param branchNameKey project and branch from which the code owner configs should be visited
    * @param revision the branch revision from which the code owner configs should be loaded
    * @param absolutePath the path for which the code owner configs should be visited; the path must
    *     be absolute; can be the path of a file or folder; the path may or may not exist
@@ -52,11 +70,11 @@
    *     configs
    */
   public void visit(
-      BranchNameKey branch,
+      BranchNameKey branchNameKey,
       ObjectId revision,
       Path absolutePath,
       CodeOwnerConfigVisitor codeOwnerConfigVisitor) {
-    requireNonNull(branch, "branch");
+    requireNonNull(branchNameKey, "branch");
     requireNonNull(revision, "revision");
     requireNonNull(absolutePath, "absolutePath");
     requireNonNull(codeOwnerConfigVisitor, "codeOwnerConfigVisitor");
@@ -64,7 +82,7 @@
 
     logger.atFine().log(
         "visiting code owner configs for '%s' in branch '%s' in project '%s' (revision = '%s')",
-        absolutePath, branch.shortName(), branch.project(), revision.name());
+        absolutePath, branchNameKey.shortName(), branchNameKey.project(), revision.name());
 
     // Next path in which we look for a code owner configuration. We start at the given path and
     // then go up the parent hierarchy.
@@ -77,7 +95,7 @@
       logger.atFine().log("inspecting code owner config for %s", ownerConfigFolder);
       Optional<PathCodeOwners> pathCodeOwners =
           pathCodeOwnersFactory.create(
-              CodeOwnerConfig.Key.create(branch, ownerConfigFolder), revision, absolutePath);
+              CodeOwnerConfig.Key.create(branchNameKey, ownerConfigFolder), revision, absolutePath);
       if (pathCodeOwners.isPresent()) {
         logger.atFine().log("visit code owner config for %s", ownerConfigFolder);
         boolean visitFurtherCodeOwnerConfigs =
@@ -87,6 +105,11 @@
             "visitFurtherCodeOwnerConfigs = %s, ignoreParentCodeOwners = %s",
             visitFurtherCodeOwnerConfigs, ignoreParentCodeOwners);
         if (!visitFurtherCodeOwnerConfigs || ignoreParentCodeOwners) {
+          // If no further code owner configs should be visited or if all parent code owner configs
+          // are ignored, we are done.
+          // No need to check further parent code owner configs (including the default code owner
+          // config in refs/meta/config which is the parent of the root code owner config), hence we
+          // can return here.
           return;
         }
       } else {
@@ -96,5 +119,54 @@
       // Continue the loop with the next parent folder.
       ownerConfigFolder = ownerConfigFolder.getParent();
     }
+
+    if (!RefNames.REFS_CONFIG.equals(branchNameKey.branch())) {
+      visitCodeOwnerConfigInRefsMetaConfig(
+          branchNameKey.project(), absolutePath, codeOwnerConfigVisitor);
+    }
+  }
+
+  /**
+   * Visits the code owner config file at the root of the {@code refs/meta/config} branch in the
+   * given project.
+   *
+   * <p>The root code owner config file in the {@code refs/meta/config} branch defines default code
+   * owners for all branches.
+   *
+   * <p>There is no inheritance of code owner config files from parent projects. If code owners
+   * should be defined for child projects, this is possible via global code owners, but not via the
+   * default code owner config file in {@code refs/meta/config}.
+   *
+   * @param project the project in which we want to visit the code owner config file at the root of
+   *     the {@code refs/meta/config} branch
+   * @param absolutePath the path for which the code owner configs should be visited; the path must
+   *     be absolute; can be the path of a file or folder; the path may or may not exist
+   * @param codeOwnerConfigVisitor visitor that should be invoked for the applying code owner
+   *     configs
+   */
+  private void visitCodeOwnerConfigInRefsMetaConfig(
+      Project.NameKey project, Path absolutePath, CodeOwnerConfigVisitor codeOwnerConfigVisitor) {
+    CodeOwnerConfig.Key metaCodeOwnerConfigKey =
+        CodeOwnerConfig.Key.create(project, RefNames.REFS_CONFIG, "/");
+    logger.atFine().log("visiting code owner config %s", metaCodeOwnerConfigKey);
+    try (Repository repository = repoManager.openRepository(project);
+        RevWalk rw = new RevWalk(repository)) {
+      Ref ref = repository.exactRef(RefNames.REFS_CONFIG);
+      if (ref == null) {
+        logger.atFine().log("%s not found", RefNames.REFS_CONFIG);
+        return;
+      }
+      RevCommit metaRevision = rw.parseCommit(ref.getObjectId());
+      Optional<PathCodeOwners> pathCodeOwners =
+          pathCodeOwnersFactory.create(metaCodeOwnerConfigKey, metaRevision, absolutePath);
+      if (pathCodeOwners.isPresent()) {
+        logger.atFine().log("visit code owner config %s", metaCodeOwnerConfigKey);
+        codeOwnerConfigVisitor.visit(pathCodeOwners.get().getCodeOwnerConfig());
+      } else {
+        logger.atFine().log("code owner config %s not found", metaCodeOwnerConfigKey);
+      }
+    } catch (IOException e) {
+      throw new StorageException(String.format("failed to read %s", metaCodeOwnerConfigKey), e);
+    }
   }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java
index f1e8a3b..e3398c5 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java
@@ -20,11 +20,11 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
-import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -32,14 +32,25 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
 
-/** Class to scan a branch for code owner config files. */
-@Singleton
+/**
+ * Class to scan a branch for code owner config files.
+ *
+ * <p>Whether the scan includes the code owner config file at the root of {@code refs/meta/config}
+ * branch that contains the default code owners for the whole repository can be controlled via
+ * {@link #includeDefaultCodeOwnerConfig(boolean)}.
+ */
 public class CodeOwnerConfigScanner {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  public interface Factory {
+    CodeOwnerConfigScanner create();
+  }
+
   private final GitRepositoryManager repoManager;
   private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
 
+  private boolean includeDefaultCodeOwnerConfig = true;
+
   @Inject
   CodeOwnerConfigScanner(
       GitRepositoryManager repoManager,
@@ -49,6 +60,16 @@
   }
 
   /**
+   * Whether the scan should include the code owner config file at the root of {@code
+   * refs/meta/config} branch that contains the default code owners for the whole repository.
+   */
+  public CodeOwnerConfigScanner includeDefaultCodeOwnerConfig(
+      boolean includeDefaultCodeOwnerConfig) {
+    this.includeDefaultCodeOwnerConfig = includeDefaultCodeOwnerConfig;
+    return this;
+  }
+
+  /**
    * Whether there is at least one code owner config file in the given project and branch.
    *
    * @param branchNameKey the project and branch for which if should be checked if it contains any
@@ -108,6 +129,26 @@
         "scanning code owner files in branch %s of project %s (path glob = %s)",
         branchNameKey.branch(), branchNameKey.project(), pathGlob);
 
+    if (includeDefaultCodeOwnerConfig && !RefNames.REFS_CONFIG.equals(branchNameKey.branch())) {
+      logger.atFine().log("Scanning code owner config file in %s", RefNames.REFS_CONFIG);
+      Optional<CodeOwnerConfig> metaCodeOwnerConfig =
+          codeOwnerBackend.getCodeOwnerConfig(
+              CodeOwnerConfig.Key.createWithFileName(
+                  codeOwnerBackend, branchNameKey.project(), RefNames.REFS_CONFIG, "/"),
+              /** revision */
+              null);
+      if (metaCodeOwnerConfig.isPresent()) {
+        boolean visitFurtherCodeOwnerConfigFiles =
+            codeOwnerConfigVisitor.visit(metaCodeOwnerConfig.get());
+        if (!visitFurtherCodeOwnerConfigFiles) {
+          // By returning false the callback told us to not visit any further code owner config
+          // files, hence we are done and do not need to search for further code owner config files
+          // in the branch.
+          return;
+        }
+      }
+    }
+
     try (Repository repository = repoManager.openRepository(branchNameKey.project());
         RevWalk rw = new RevWalk(repository);
         CodeOwnerConfigTreeWalk treeWalk =
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java b/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
index fcc5543..854ffd7 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
@@ -23,6 +23,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.ListAccountsOption;
 import com.google.gerrit.extensions.client.ListOption;
@@ -137,10 +138,9 @@
     // configuration.
     int rootDistance = rsrc.getPath().getNameCount();
 
-    // The maximal possible distance. This is the distance that applies to global code owners and is
-    // by 1 greater than the distance that applies to code owners that are defined in the root code
-    // owner configuration.
-    int maxDistance = rootDistance + 1;
+    int defaultOwnersDistance = rootDistance + 1;
+    int globalOwnersDistance = defaultOwnersDistance + 1;
+    int maxDistance = globalOwnersDistance;
 
     CodeOwnerScoring.Builder distanceScoring = CodeOwnerScore.DISTANCE.createScoring(maxDistance);
 
@@ -170,7 +170,10 @@
             return false;
           }
 
-          int distance = rootDistance - codeOwnerConfig.key().folderPath().getNameCount();
+          int distance =
+              codeOwnerConfig.key().branchNameKey().branch().equals(RefNames.REFS_CONFIG)
+                  ? defaultOwnersDistance
+                  : rootDistance - codeOwnerConfig.key().folderPath().getNameCount();
           pathCodeOwners
               .codeOwners()
               .forEach(
@@ -188,7 +191,8 @@
       CodeOwnerResolverResult globalCodeOwners = getGlobalCodeOwners(rsrc.getBranch().project());
       globalCodeOwners
           .codeOwners()
-          .forEach(codeOwner -> distanceScoring.putValueForCodeOwner(codeOwner, maxDistance));
+          .forEach(
+              codeOwner -> distanceScoring.putValueForCodeOwner(codeOwner, globalOwnersDistance));
       codeOwners.addAll(filterCodeOwners(rsrc, globalCodeOwners.codeOwners()));
 
       if (globalCodeOwners.ownedByAllUsers()) {
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java
index b193838..144f0e5 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java
@@ -74,7 +74,7 @@
   private final PermissionBackend permissionBackend;
   private final Provider<ListBranches> listBranches;
   private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
-  private final CodeOwnerConfigScanner codeOwnerConfigScanner;
+  private final CodeOwnerConfigScanner.Factory codeOwnerConfigScannerFactory;
   private final CodeOwnerConfigValidator codeOwnerConfigValidator;
 
   @Inject
@@ -83,13 +83,13 @@
       PermissionBackend permissionBackend,
       Provider<ListBranches> listBranches,
       CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
-      CodeOwnerConfigScanner codeOwnerConfigScanner,
+      CodeOwnerConfigScanner.Factory codeOwnerConfigScannerFactory,
       CodeOwnerConfigValidator codeOwnerConfigValidator) {
     this.currentUser = currentUser;
     this.permissionBackend = permissionBackend;
     this.listBranches = listBranches;
     this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
-    this.codeOwnerConfigScanner = codeOwnerConfigScanner;
+    this.codeOwnerConfigScannerFactory = codeOwnerConfigScannerFactory;
     this.codeOwnerConfigValidator = codeOwnerConfigValidator;
   }
 
@@ -150,21 +150,27 @@
       @Nullable ConsistencyProblemInfo.Status verbosity) {
     ListMultimap<String, ConsistencyProblemInfo> problemsByPath = LinkedListMultimap.create();
     CodeOwnerBackend codeOwnerBackend = codeOwnersPluginConfiguration.getBackend(branchNameKey);
-    codeOwnerConfigScanner.visit(
-        branchNameKey,
-        codeOwnerConfig -> {
-          problemsByPath.putAll(
-              codeOwnerBackend.getFilePath(codeOwnerConfig.key()).toString(),
-              checkCodeOwnerConfig(codeOwnerBackend, codeOwnerConfig, verbosity));
-          return true;
-        },
-        (codeOwnerConfigFilePath, configInvalidException) -> {
-          problemsByPath.put(
-              codeOwnerConfigFilePath.toString(),
-              new ConsistencyProblemInfo(
-                  ConsistencyProblemInfo.Status.FATAL, configInvalidException.getMessage()));
-        },
-        pathGlob);
+    codeOwnerConfigScannerFactory
+        .create()
+        // Do not check the default code owner config file in refs/meta/config, as this config is
+        // stored in another branch. If it should be checked users must check the code owner config
+        // files in refs/meta/config explicitly.
+        .includeDefaultCodeOwnerConfig(false)
+        .visit(
+            branchNameKey,
+            codeOwnerConfig -> {
+              problemsByPath.putAll(
+                  codeOwnerBackend.getFilePath(codeOwnerConfig.key()).toString(),
+                  checkCodeOwnerConfig(codeOwnerBackend, codeOwnerConfig, verbosity));
+              return true;
+            },
+            (codeOwnerConfigFilePath, configInvalidException) -> {
+              problemsByPath.put(
+                  codeOwnerConfigFilePath.toString(),
+                  new ConsistencyProblemInfo(
+                      ConsistencyProblemInfo.Status.FATAL, configInvalidException.getMessage()));
+            },
+            pathGlob);
 
     return Multimaps.asMap(problemsByPath);
   }
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJson.java b/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJson.java
index 72e6de0..65d93e8 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJson.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJson.java
@@ -50,16 +50,16 @@
 @Singleton
 public class CodeOwnerProjectConfigJson {
   private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
-  private final CodeOwnerConfigScanner codeOwnerConfigScanner;
+  private final CodeOwnerConfigScanner.Factory codeOwnerConfigScannerFactory;
   private final Provider<ListBranches> listBranches;
 
   @Inject
   CodeOwnerProjectConfigJson(
       CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
-      CodeOwnerConfigScanner codeOwnerConfigScanner,
+      CodeOwnerConfigScanner.Factory codeOwnerConfigScannerFactory,
       Provider<ListBranches> listBranches) {
     this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
-    this.codeOwnerConfigScanner = codeOwnerConfigScanner;
+    this.codeOwnerConfigScannerFactory = codeOwnerConfigScannerFactory;
     this.listBranches = listBranches;
   }
 
@@ -98,7 +98,9 @@
     info.overrideApproval = formatOverrideApprovalInfo(branchResource.getNameKey());
 
     boolean noCodeOwnersDefined =
-        !codeOwnerConfigScanner.containsAnyCodeOwnerConfigFile(branchResource.getBranchKey());
+        !codeOwnerConfigScannerFactory
+            .create()
+            .containsAnyCodeOwnerConfigFile(branchResource.getBranchKey());
     info.noCodeOwnersDefined = noCodeOwnersDefined ? noCodeOwnersDefined : null;
 
     return info;
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnerConfigFiles.java b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnerConfigFiles.java
index fe26037..0ebf942 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnerConfigFiles.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnerConfigFiles.java
@@ -46,7 +46,7 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
-  private final CodeOwnerConfigScanner codeOwnerConfigScanner;
+  private final CodeOwnerConfigScanner.Factory codeOwnerConfigScannerFactory;
 
   private String email;
 
@@ -61,9 +61,9 @@
   @Inject
   public GetCodeOwnerConfigFiles(
       CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
-      CodeOwnerConfigScanner codeOwnerConfigScanner) {
+      CodeOwnerConfigScanner.Factory codeOwnerConfigScannerFactory) {
     this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
-    this.codeOwnerConfigScanner = codeOwnerConfigScanner;
+    this.codeOwnerConfigScannerFactory = codeOwnerConfigScannerFactory;
   }
 
   @Override
@@ -78,16 +78,22 @@
           email);
     }
 
-    codeOwnerConfigScanner.visit(
-        resource.getBranchKey(),
-        codeOwnerConfig -> {
-          Path codeOwnerConfigPath = codeOwnerBackend.getFilePath(codeOwnerConfig.key());
-          if (email == null || containsEmail(codeOwnerConfig, codeOwnerConfigPath, email)) {
-            codeOwnerConfigs.add(codeOwnerConfigPath);
-          }
-          return true;
-        },
-        CodeOwnerConfigScanner.ignoreInvalidCodeOwnerConfigFiles());
+    codeOwnerConfigScannerFactory
+        .create()
+        // Do not include the default code owner config file in refs/meta/config, as this config is
+        // stored in another branch. If it should be listed users must list the code owner config
+        // files in refs/meta/config explicitly.
+        .includeDefaultCodeOwnerConfig(false)
+        .visit(
+            resource.getBranchKey(),
+            codeOwnerConfig -> {
+              Path codeOwnerConfigPath = codeOwnerBackend.getFilePath(codeOwnerConfig.key());
+              if (email == null || containsEmail(codeOwnerConfig, codeOwnerConfigPath, email)) {
+                codeOwnerConfigs.add(codeOwnerConfigPath);
+              }
+              return true;
+            },
+            CodeOwnerConfigScanner.ignoreInvalidCodeOwnerConfigFiles());
     return Response.ok(
         codeOwnerConfigs.build().stream().map(Path::toString).collect(toImmutableList()));
   }
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 4048684..b9032cb 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.Permission;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.client.ListAccountsOption;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -726,6 +727,138 @@
   }
 
   @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();
+
+    assertThat(queryCodeOwners("/foo/bar/baz.md"))
+        .comparingElementsUsing(hasAccountId())
+        .containsExactly(user.id());
+  }
+
+  @Test
+  @GerritConfig(
+      name = "plugin.code-owners.globalCodeOwner",
+      values = {"global.owner1@example.com", "global.owner2@example.com"})
+  public void getWithDefaultAndGlobalCodeOwnersAndLimit() throws Exception {
+    TestAccount globalOwner1 =
+        accountCreator.create(
+            "global_owner_1", "global.owner1@example.com", "Global Owner 1", null);
+    TestAccount globalOwner2 =
+        accountCreator.create(
+            "global_owner_2", "global.owner2@example.com", "Global Owner 2", null);
+
+    TestAccount user2 = accountCreator.user2();
+    TestAccount defaultCodeOwner1 =
+        accountCreator.create("user3", "user3@example.com", "User3", null);
+    TestAccount defaultCodeOwner2 =
+        accountCreator.create("user4", "user4@example.com", "User4", null);
+
+    // Create default code owner config file in refs/meta/config.
+    codeOwnerConfigOperations
+        .newCodeOwnerConfig()
+        .project(project)
+        .branch(RefNames.REFS_CONFIG)
+        .folderPath("/")
+        .addCodeOwnerEmail(defaultCodeOwner1.email())
+        .addCodeOwnerEmail(defaultCodeOwner2.email())
+        .create();
+
+    // create some code owner configs
+    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();
+
+    // get code owners with different limits
+    List<CodeOwnerInfo> codeOwnerInfos =
+        queryCodeOwners(getCodeOwnersApi().query().withLimit(1), "/foo/bar/baz.md");
+    assertThat(codeOwnerInfos).hasSize(1);
+    // the first 2 code owners have the same scoring, so their order is random and we don't know
+    // which of them we get when the limit is 1
+    assertThatList(codeOwnerInfos).element(0).hasAccountIdThat().isAnyOf(user.id(), user2.id());
+
+    codeOwnerInfos = queryCodeOwners(getCodeOwnersApi().query().withLimit(2), "/foo/bar/baz.md");
+    assertThat(codeOwnerInfos)
+        .comparingElementsUsing(hasAccountId())
+        .containsExactly(user.id(), user2.id());
+
+    codeOwnerInfos = getCodeOwnersApi().query().withLimit(3).get("/foo/bar/baz.md");
+    assertThat(codeOwnerInfos)
+        .comparingElementsUsing(hasAccountId())
+        .containsExactly(admin.id(), user.id(), user2.id());
+
+    codeOwnerInfos = getCodeOwnersApi().query().withLimit(4).get("/foo/bar/baz.md");
+    assertThat(codeOwnerInfos).hasSize(4);
+    // the order of the first 2 code owners is random
+    assertThatList(codeOwnerInfos).element(0).hasAccountIdThat().isAnyOf(user.id(), user2.id());
+    assertThatList(codeOwnerInfos).element(1).hasAccountIdThat().isAnyOf(user.id(), user2.id());
+    assertThatList(codeOwnerInfos).element(2).hasAccountIdThat().isEqualTo(admin.id());
+    // the order of the default code owners is random
+    assertThatList(codeOwnerInfos)
+        .element(3)
+        .hasAccountIdThat()
+        .isAnyOf(defaultCodeOwner1.id(), defaultCodeOwner2.id());
+
+    codeOwnerInfos = getCodeOwnersApi().query().withLimit(5).get("/foo/bar/baz.md");
+    assertThat(codeOwnerInfos)
+        .comparingElementsUsing(hasAccountId())
+        .containsExactly(
+            admin.id(), user.id(), user2.id(), defaultCodeOwner1.id(), defaultCodeOwner2.id());
+
+    codeOwnerInfos = getCodeOwnersApi().query().withLimit(6).get("/foo/bar/baz.md");
+    assertThat(codeOwnerInfos).hasSize(6);
+    // the order of the first 2 code owners is random
+    assertThatList(codeOwnerInfos).element(0).hasAccountIdThat().isAnyOf(user.id(), user2.id());
+    assertThatList(codeOwnerInfos).element(1).hasAccountIdThat().isAnyOf(user.id(), user2.id());
+    assertThatList(codeOwnerInfos).element(2).hasAccountIdThat().isEqualTo(admin.id());
+    // the order of the default code owners is random
+    assertThatList(codeOwnerInfos)
+        .element(3)
+        .hasAccountIdThat()
+        .isAnyOf(defaultCodeOwner1.id(), defaultCodeOwner2.id());
+    assertThatList(codeOwnerInfos)
+        .element(4)
+        .hasAccountIdThat()
+        .isAnyOf(defaultCodeOwner1.id(), defaultCodeOwner2.id());
+    // the order of the global code owners is random
+    assertThatList(codeOwnerInfos)
+        .element(5)
+        .hasAccountIdThat()
+        .isAnyOf(globalOwner1.id(), globalOwner2.id());
+
+    codeOwnerInfos = getCodeOwnersApi().query().withLimit(7).get("/foo/bar/baz.md");
+    assertThat(codeOwnerInfos)
+        .comparingElementsUsing(hasAccountId())
+        .containsExactly(
+            admin.id(),
+            user.id(),
+            user2.id(),
+            defaultCodeOwner1.id(),
+            defaultCodeOwner2.id(),
+            globalOwner1.id(),
+            globalOwner2.id());
+  }
+
+  @Test
   @GerritConfig(name = "accounts.visibility", value = "ALL")
   public void getAllUsersAsCodeOwners_allVisible() throws Exception {
     TestAccount user2 = accountCreator.user2();
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerConfigFilesIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerConfigFilesIT.java
index 16432bd..e6870c2 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerConfigFilesIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerConfigFilesIT.java
@@ -240,6 +240,34 @@
   }
 
   @Test
+  public void issuesInDefaultCodeOwnerConfigFile() throws Exception {
+    CodeOwnerConfig.Key invalidDefaultConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch(RefNames.REFS_CONFIG)
+            .folderPath("/")
+            .addCodeOwnerEmail("unknown@example.com")
+            .create();
+    String pathOfInvalidConfig =
+        codeOwnerConfigOperations.codeOwnerConfig(invalidDefaultConfig).getFilePath();
+
+    assertThat(checkCodeOwnerConfigFilesIn(project))
+        .containsExactly(
+            "refs/heads/master",
+            ImmutableMap.of(),
+            RefNames.REFS_CONFIG,
+            ImmutableMap.of(
+                pathOfInvalidConfig,
+                ImmutableList.of(
+                    error(
+                        String.format(
+                            "code owner email 'unknown@example.com' in '%s' cannot be"
+                                + " resolved for admin",
+                            pathOfInvalidConfig)))));
+  }
+
+  @Test
   public void validateSpecifiedBranches() throws Exception {
     createBranch(BranchNameKey.create(project, "stable-1.0"));
     createBranch(BranchNameKey.create(project, "stable-1.1"));
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigFilesIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigFilesIT.java
index 804fcbf..50b795b 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigFilesIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigFilesIT.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.plugins.codeowners.JgitPath;
 import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
@@ -52,6 +53,25 @@
   }
 
   @Test
+  public void defaultCodeOwnerConfigFileIsSkipped() throws Exception {
+    codeOwnerConfigOperations
+        .newCodeOwnerConfig()
+        .project(project)
+        .branch(RefNames.REFS_CONFIG)
+        .folderPath("/")
+        .addCodeOwnerEmail(user.email())
+        .create();
+
+    assertThat(
+            projectCodeOwnersApiFactory
+                .project(project)
+                .branch("master")
+                .codeOwnerConfigFiles()
+                .paths())
+        .isEmpty();
+  }
+
+  @Test
   public void getCodeOwnerConfigFiles() throws Exception {
     CodeOwnerConfig.Key codeOwnerConfigKey1 =
         codeOwnerConfigOperations
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java
index 5ea1eb4..ccbcb92 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.projects.DeleteBranchesInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -2001,6 +2002,237 @@
         .isEqualTo(CodeOwnerStatus.APPROVED);
   }
 
+  @Test
+  public void noBootstrappingIfDefaultCodeOwnerConfigExists() throws Exception {
+    TestAccount user2 = accountCreator.user2();
+
+    // Create default code owner config file in refs/meta/config.
+    codeOwnerConfigOperations
+        .newCodeOwnerConfig()
+        .project(project)
+        .branch(RefNames.REFS_CONFIG)
+        .folderPath("/")
+        .addCodeOwnerEmail(user.email())
+        .create();
+
+    // Create a change as a user that is neither a code owner nor a project owner.
+    Path path = Paths.get("/foo/bar.baz");
+    String changeId =
+        createChange(user2, "Change Adding A File", JgitPath.of(path).get(), "file content")
+            .getChangeId();
+
+    // Verify that the file is not approved yet.
+    Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses =
+        codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+    FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject =
+        assertThatStream(fileCodeOwnerStatuses).onlyElement();
+    fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+    fileCodeOwnerStatusSubject
+        .hasNewPathStatus()
+        .value()
+        .hasStatusThat()
+        .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
+
+    // Let the project owner approve the change.
+    requestScopeOperations.setApiUser(admin.id());
+    approve(changeId);
+
+    // Verify that the file is still approved yet (since we are not in bootstrapping mode, the
+    // project owner doesn't count as code owner).
+    fileCodeOwnerStatuses = codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+    fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement();
+    fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+    fileCodeOwnerStatusSubject
+        .hasNewPathStatus()
+        .value()
+        .hasStatusThat()
+        .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
+
+    // Let the code owner approve the change.
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-2, +2))
+        .update();
+    requestScopeOperations.setApiUser(user.id());
+    approve(changeId);
+
+    // Check that the file is approved now.
+    requestScopeOperations.setApiUser(admin.id());
+    fileCodeOwnerStatuses = codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+    fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement();
+    fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+    fileCodeOwnerStatusSubject
+        .hasNewPathStatus()
+        .value()
+        .hasStatusThat()
+        .isEqualTo(CodeOwnerStatus.APPROVED);
+  }
+
+  @Test
+  public void approvedByDefaultCodeOwner() throws Exception {
+    TestAccount user2 = accountCreator.user2();
+
+    // Create default code owner config file in refs/meta/config.
+    codeOwnerConfigOperations
+        .newCodeOwnerConfig()
+        .project(project)
+        .branch(RefNames.REFS_CONFIG)
+        .folderPath("/")
+        .addCodeOwnerEmail(user.email())
+        .create();
+
+    // Create a change as a user that is not a code owner.
+    Path path = Paths.get("/foo/bar.baz");
+    String changeId =
+        createChange(user2, "Change Adding A File", JgitPath.of(path).get(), "file content")
+            .getChangeId();
+
+    // Verify that the file is not approved yet.
+    Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses =
+        codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+    FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject =
+        assertThatStream(fileCodeOwnerStatuses).onlyElement();
+    fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+    fileCodeOwnerStatusSubject
+        .hasNewPathStatus()
+        .value()
+        .hasStatusThat()
+        .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
+
+    // Let the code owner approve the change.
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-2, +2))
+        .update();
+    requestScopeOperations.setApiUser(user.id());
+    approve(changeId);
+
+    // Check that the file is approved now.
+    requestScopeOperations.setApiUser(admin.id());
+    fileCodeOwnerStatuses = codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+    fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement();
+    fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+    fileCodeOwnerStatusSubject
+        .hasNewPathStatus()
+        .value()
+        .hasStatusThat()
+        .isEqualTo(CodeOwnerStatus.APPROVED);
+  }
+
+  @Test
+  public void defaultCodeOwner_noImplicitApproval() throws Exception {
+    testImplicitlyApprovedByDefaultCodeOwner(
+        /** implicitApprovalsEnabled = */
+        false);
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true")
+  public void defaultCodeOwner_withImplicitApproval() throws Exception {
+    testImplicitlyApprovedByDefaultCodeOwner(
+        /** implicitApprovalsEnabled = */
+        true);
+  }
+
+  private void testImplicitlyApprovedByDefaultCodeOwner(boolean implicitApprovalsEnabled)
+      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();
+
+    Path path = Paths.get("/foo/bar.baz");
+    String changeId =
+        createChange(user, "Change Adding A File", JgitPath.of(path).get(), "file content")
+            .getChangeId();
+
+    Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses =
+        codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+    FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject =
+        assertThatStream(fileCodeOwnerStatuses).onlyElement();
+    fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+    fileCodeOwnerStatusSubject
+        .hasNewPathStatus()
+        .value()
+        .hasStatusThat()
+        .isEqualTo(
+            implicitApprovalsEnabled
+                ? CodeOwnerStatus.APPROVED
+                : CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
+  }
+
+  @Test
+  public void defaultCodeOwnerAsReviewer() throws Exception {
+    TestAccount user2 = accountCreator.user2();
+
+    // Create default code owner config file in refs/meta/config.
+    codeOwnerConfigOperations
+        .newCodeOwnerConfig()
+        .project(project)
+        .branch(RefNames.REFS_CONFIG)
+        .folderPath("/")
+        .addCodeOwnerEmail(user.email())
+        .create();
+
+    // Create a change as a user that is not a code owner.
+    Path path = Paths.get("/foo/bar.baz");
+    String changeId =
+        createChange(user2, "Change Adding A File", JgitPath.of(path).get(), "file content")
+            .getChangeId();
+
+    // Verify that the status of the file is INSUFFICIENT_REVIEWERS.
+    Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses =
+        codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+    FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject =
+        assertThatStream(fileCodeOwnerStatuses).onlyElement();
+    fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+    fileCodeOwnerStatusSubject
+        .hasNewPathStatus()
+        .value()
+        .hasStatusThat()
+        .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
+
+    // Add the default code owner as reviewer.
+    gApi.changes().id(changeId).addReviewer(user.email());
+
+    // Check that the status of the file is PENDING now.
+    requestScopeOperations.setApiUser(admin.id());
+    fileCodeOwnerStatuses = codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+    fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement();
+    fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+    fileCodeOwnerStatusSubject
+        .hasNewPathStatus()
+        .value()
+        .hasStatusThat()
+        .isEqualTo(CodeOwnerStatus.PENDING);
+
+    // Let the default code owner approve the change.
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-2, +2))
+        .update();
+    requestScopeOperations.setApiUser(user.id());
+    approve(changeId);
+
+    // Check that the file is approved now.
+    requestScopeOperations.setApiUser(admin.id());
+    fileCodeOwnerStatuses = codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+    fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement();
+    fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+    fileCodeOwnerStatusSubject
+        .hasNewPathStatus()
+        .value()
+        .hasStatusThat()
+        .isEqualTo(CodeOwnerStatus.APPROVED);
+  }
+
   private ChangeNotes getChangeNotes(String changeId) throws Exception {
     return changeNotesFactory.create(project, Change.id(gApi.changes().id(changeId).get()._number));
   }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchyTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchyTest.java
index c1657d2..57280bb 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchyTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchyTest.java
@@ -18,12 +18,14 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest;
 import com.google.gerrit.plugins.codeowners.acceptance.testsuite.CodeOwnerConfigOperations;
 import com.google.gerrit.plugins.codeowners.acceptance.testsuite.TestPathExpressions;
@@ -530,6 +532,178 @@
     verifyNoMoreInteractions(visitor);
   }
 
+  @Test
+  public void visitorInvokedForCodeOwnerConfigInRefsMetaConfig() throws Exception {
+    CodeOwnerConfig.Key metaCodeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch(RefNames.REFS_CONFIG)
+            .folderPath("/")
+            .addCodeOwnerEmail(admin.email())
+            .create();
+
+    when(visitor.visit(any(CodeOwnerConfig.class))).thenReturn(true);
+    visit("master", "/foo/bar/baz.md");
+    verify(visitor).visit(codeOwnerConfigOperations.codeOwnerConfig(metaCodeOwnerConfigKey).get());
+    verifyNoMoreInteractions(visitor);
+  }
+
+  @Test
+  public void visitorNotInvokedForCodeOwnerConfigInRefsMetaConfigIfItDoesntApply()
+      throws Exception {
+    codeOwnerConfigOperations
+        .newCodeOwnerConfig()
+        .project(project)
+        .branch(RefNames.REFS_CONFIG)
+        .folderPath("/")
+        .addCodeOwnerSet(
+            CodeOwnerSet.builder()
+                .addPathExpression(testPathExpressions.matchAllFilesInSubfolder("other"))
+                .addCodeOwnerEmail(admin.email())
+                .build())
+        .create();
+
+    when(visitor.visit(any(CodeOwnerConfig.class))).thenReturn(true);
+    visit("master", "/foo/bar/baz.md");
+    verifyZeroInteractions(visitor);
+  }
+
+  @Test
+  public void visitorInvokedForCodeOwnerConfigInRefsMetaConfigWithMatchingPathExpression()
+      throws Exception {
+    CodeOwnerConfig.Key metaCodeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch(RefNames.REFS_CONFIG)
+            .folderPath("/")
+            .addCodeOwnerSet(
+                CodeOwnerSet.builder()
+                    .addPathExpression(testPathExpressions.matchAllFilesInSubfolder("foo"))
+                    .addCodeOwnerEmail(admin.email())
+                    .build())
+            .create();
+
+    when(visitor.visit(any(CodeOwnerConfig.class))).thenReturn(true);
+    visit("master", "/foo/bar/baz.md");
+    verify(visitor).visit(codeOwnerConfigOperations.codeOwnerConfig(metaCodeOwnerConfigKey).get());
+    verifyNoMoreInteractions(visitor);
+  }
+
+  @Test
+  public void visitorForCodeOwnerConfigInRefsMetaConfigInvokedLast() throws Exception {
+    CodeOwnerConfig.Key metaCodeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch(RefNames.REFS_CONFIG)
+            .folderPath("/")
+            .addCodeOwnerEmail(admin.email())
+            .create();
+
+    CodeOwnerConfig.Key rootCodeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/")
+            .addCodeOwnerSet(
+                CodeOwnerSet.builder()
+                    .addPathExpression(testPathExpressions.matchAllFilesInSubfolder("other"))
+                    .addCodeOwnerEmail(admin.email())
+                    .build())
+            .create();
+
+    when(visitor.visit(any(CodeOwnerConfig.class))).thenReturn(true);
+    visit("master", "/foo/bar/baz.md");
+
+    InOrder orderVerifier = Mockito.inOrder(visitor);
+    orderVerifier
+        .verify(visitor)
+        .visit(codeOwnerConfigOperations.codeOwnerConfig(rootCodeOwnerConfigKey).get());
+    orderVerifier
+        .verify(visitor)
+        .visit(codeOwnerConfigOperations.codeOwnerConfig(metaCodeOwnerConfigKey).get());
+    verifyNoMoreInteractions(visitor);
+  }
+
+  @Test
+  public void
+      visitorNotInvokedForCodeOwnerConfigInRefsMetaConfigIfRootCodeOwnerConfigIgnoresParent()
+          throws Exception {
+    codeOwnerConfigOperations
+        .newCodeOwnerConfig()
+        .project(project)
+        .branch(RefNames.REFS_CONFIG)
+        .folderPath("/")
+        .addCodeOwnerEmail(admin.email())
+        .create();
+
+    CodeOwnerConfig.Key rootCodeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/")
+            .ignoreParentCodeOwners()
+            .addCodeOwnerEmail(admin.email())
+            .create();
+
+    when(visitor.visit(any(CodeOwnerConfig.class))).thenReturn(true);
+    visit("master", "/foo/bar/baz.md");
+    verify(visitor).visit(codeOwnerConfigOperations.codeOwnerConfig(rootCodeOwnerConfigKey).get());
+    verifyNoMoreInteractions(visitor);
+  }
+
+  @Test
+  public void visitorCanStopTheIterationAtRoot() throws Exception {
+    codeOwnerConfigOperations
+        .newCodeOwnerConfig()
+        .project(project)
+        .branch(RefNames.REFS_CONFIG)
+        .folderPath("/")
+        .addCodeOwnerEmail(admin.email())
+        .create();
+
+    CodeOwnerConfig.Key rootCodeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/")
+            .ignoreParentCodeOwners()
+            .addCodeOwnerEmail(admin.email())
+            .create();
+
+    when(visitor.visit(any(CodeOwnerConfig.class))).thenReturn(false);
+    visit("master", "/foo/bar/baz.md");
+    verify(visitor).visit(codeOwnerConfigOperations.codeOwnerConfig(rootCodeOwnerConfigKey).get());
+    verifyNoMoreInteractions(visitor);
+  }
+
+  @Test
+  public void
+      visitorIsOnlyInvokedOnceForDefaultCodeOnwerConfigFileIfConfigsInRefsMetaConxfigAreIterated()
+          throws Exception {
+    CodeOwnerConfig.Key metaCodeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch(RefNames.REFS_CONFIG)
+            .folderPath("/")
+            .addCodeOwnerEmail(admin.email())
+            .create();
+
+    when(visitor.visit(any(CodeOwnerConfig.class))).thenReturn(true);
+    visit(RefNames.REFS_CONFIG, "/foo/bar/baz.md");
+
+    // Verify that we received the callback for the code owner config only once.
+    verify(visitor).visit(codeOwnerConfigOperations.codeOwnerConfig(metaCodeOwnerConfigKey).get());
+
+    verifyNoMoreInteractions(visitor);
+  }
+
   private void visit(String branchName, String path)
       throws InvalidPluginConfigurationException, IOException {
     BranchNameKey branchNameKey = BranchNameKey.create(project, branchName);
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScannerTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScannerTest.java
index 06be307..48744f6 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScannerTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScannerTest.java
@@ -24,6 +24,7 @@
 
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.plugins.codeowners.JgitPath;
 import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest;
 import com.google.gerrit.plugins.codeowners.acceptance.testsuite.CodeOwnerConfigOperations;
@@ -51,13 +52,14 @@
   @Mock private InvalidCodeOwnerConfigCallback invalidCodeOwnerConfigCallback;
 
   private CodeOwnerConfigOperations codeOwnerConfigOperations;
-  private CodeOwnerConfigScanner codeOwnerConfigScanner;
+  private CodeOwnerConfigScanner.Factory codeOwnerConfigScannerFactory;
 
   @Before
   public void setUpCodeOwnersPlugin() throws Exception {
     codeOwnerConfigOperations =
         plugin.getSysInjector().getInstance(CodeOwnerConfigOperations.class);
-    codeOwnerConfigScanner = plugin.getSysInjector().getInstance(CodeOwnerConfigScanner.class);
+    codeOwnerConfigScannerFactory =
+        plugin.getSysInjector().getInstance(CodeOwnerConfigScanner.Factory.class);
   }
 
   @Test
@@ -65,7 +67,10 @@
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () -> codeOwnerConfigScanner.visit(null, visitor, invalidCodeOwnerConfigCallback));
+            () ->
+                codeOwnerConfigScannerFactory
+                    .create()
+                    .visit(null, visitor, invalidCodeOwnerConfigCallback));
     assertThat(npe).hasMessageThat().isEqualTo("branchNameKey");
   }
 
@@ -76,7 +81,9 @@
         assertThrows(
             NullPointerException.class,
             () ->
-                codeOwnerConfigScanner.visit(branchNameKey, null, invalidCodeOwnerConfigCallback));
+                codeOwnerConfigScannerFactory
+                    .create()
+                    .visit(branchNameKey, null, invalidCodeOwnerConfigCallback));
     assertThat(npe).hasMessageThat().isEqualTo("codeOwnerConfigVisitor");
   }
 
@@ -86,7 +93,7 @@
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () -> codeOwnerConfigScanner.visit(branchNameKey, visitor, null));
+            () -> codeOwnerConfigScannerFactory.create().visit(branchNameKey, visitor, null));
     assertThat(npe).hasMessageThat().isEqualTo("invalidCodeOwnerConfigCallback");
   }
 
@@ -97,8 +104,9 @@
         assertThrows(
             IllegalStateException.class,
             () ->
-                codeOwnerConfigScanner.visit(
-                    branchNameKey, visitor, invalidCodeOwnerConfigCallback));
+                codeOwnerConfigScannerFactory
+                    .create()
+                    .visit(branchNameKey, visitor, invalidCodeOwnerConfigCallback));
     assertThat(exception)
         .hasMessageThat()
         .isEqualTo(
@@ -271,7 +279,60 @@
   }
 
   @Test
+  public void visitorInvokedForDefaultCodeOwnerConfigFileInRefsMetaConfig() throws Exception {
+    CodeOwnerConfig.Key codeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch(RefNames.REFS_CONFIG)
+            .folderPath("/")
+            .fileName("OWNERS")
+            .addCodeOwnerEmail(admin.email())
+            .create();
+
+    when(visitor.visit(any(CodeOwnerConfig.class))).thenReturn(true);
+    visit();
+
+    // Verify that we received the expected callback.
+    Mockito.verify(visitor)
+        .visit(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).get());
+    verifyNoMoreInteractions(visitor);
+
+    verifyZeroInteractions(invalidCodeOwnerConfigCallback);
+  }
+
+  @Test
+  public void visitorNotInvokedForDefaulCodeOwnerConfigFileInRefsMetaConfigIfSkipped()
+      throws Exception {
+    codeOwnerConfigOperations
+        .newCodeOwnerConfig()
+        .project(project)
+        .branch(RefNames.REFS_CONFIG)
+        .folderPath("/")
+        .addCodeOwnerEmail(admin.email())
+        .create();
+
+    visit(
+        /** includeDefaultCodeOwnerConfig */
+        false);
+
+    // Verify that we did not receive any callback.
+    verifyZeroInteractions(visitor);
+    verifyZeroInteractions(invalidCodeOwnerConfigCallback);
+  }
+
+  @Test
   public void visitorIsInvokedForAllCodeOwnerConfigFiles() throws Exception {
+    CodeOwnerConfig.Key metaCodeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch(RefNames.REFS_CONFIG)
+            .folderPath("/")
+            .fileName("OWNERS")
+            .addCodeOwnerEmail(admin.email())
+            .create();
+
     CodeOwnerConfig.Key rootCodeOwnerConfigKey =
         codeOwnerConfigOperations
             .newCodeOwnerConfig()
@@ -309,6 +370,9 @@
     InOrder orderVerifier = Mockito.inOrder(visitor);
     orderVerifier
         .verify(visitor)
+        .visit(codeOwnerConfigOperations.codeOwnerConfig(metaCodeOwnerConfigKey).get());
+    orderVerifier
+        .verify(visitor)
         .visit(codeOwnerConfigOperations.codeOwnerConfig(rootCodeOwnerConfigKey).get());
     orderVerifier
         .verify(visitor)
@@ -322,6 +386,55 @@
   }
 
   @Test
+  public void skipDefaultCodeOwnerConfigFile() throws Exception {
+    codeOwnerConfigOperations
+        .newCodeOwnerConfig()
+        .project(project)
+        .branch(RefNames.REFS_CONFIG)
+        .folderPath("/")
+        .addCodeOwnerEmail(admin.email())
+        .create();
+
+    CodeOwnerConfig.Key rootCodeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/")
+            .fileName("OWNERS")
+            .addCodeOwnerEmail(admin.email())
+            .create();
+
+    CodeOwnerConfig.Key fooCodeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/")
+            .fileName("OWNERS")
+            .addCodeOwnerEmail(admin.email())
+            .create();
+
+    when(visitor.visit(any(CodeOwnerConfig.class))).thenReturn(true);
+    visit(
+        /** includeDefaultCodeOwnerConfig */
+        false);
+
+    // Verify that we received only the expected callbacks (e.g. no callback for the default code
+    // owner config in refs/meta/config).
+    InOrder orderVerifier = Mockito.inOrder(visitor);
+    orderVerifier
+        .verify(visitor)
+        .visit(codeOwnerConfigOperations.codeOwnerConfig(rootCodeOwnerConfigKey).get());
+    orderVerifier
+        .verify(visitor)
+        .visit(codeOwnerConfigOperations.codeOwnerConfig(fooCodeOwnerConfigKey).get());
+    verifyNoMoreInteractions(visitor);
+
+    verifyZeroInteractions(invalidCodeOwnerConfigCallback);
+  }
+
+  @Test
   public void visitorCanStopTheIterationOverCodeOwnerConfigsByReturningFalse() throws Exception {
     CodeOwnerConfig.Key rootCodeOwnerConfigKey =
         codeOwnerConfigOperations
@@ -374,8 +487,9 @@
   @Test
   public void containsNoCodeOwnerConfigFile() throws Exception {
     assertThat(
-            codeOwnerConfigScanner.containsAnyCodeOwnerConfigFile(
-                BranchNameKey.create(project, "master")))
+            codeOwnerConfigScannerFactory
+                .create()
+                .containsAnyCodeOwnerConfigFile(BranchNameKey.create(project, "master")))
         .isFalse();
   }
 
@@ -391,12 +505,48 @@
         .create();
 
     assertThat(
-            codeOwnerConfigScanner.containsAnyCodeOwnerConfigFile(
-                BranchNameKey.create(project, "master")))
+            codeOwnerConfigScannerFactory
+                .create()
+                .containsAnyCodeOwnerConfigFile(BranchNameKey.create(project, "master")))
         .isTrue();
   }
 
   @Test
+  public void containsACodeOwnerConfigFile_defaultCodeOwnerConfigExists() throws Exception {
+    codeOwnerConfigOperations
+        .newCodeOwnerConfig()
+        .project(project)
+        .branch(RefNames.REFS_CONFIG)
+        .folderPath("/")
+        .addCodeOwnerEmail(admin.email())
+        .create();
+
+    assertThat(
+            codeOwnerConfigScannerFactory
+                .create()
+                .containsAnyCodeOwnerConfigFile(BranchNameKey.create(project, "master")))
+        .isTrue();
+  }
+
+  @Test
+  public void containsACodeOwnerConfigFile_defaultCodeOwnerConfigIsSkipped() throws Exception {
+    codeOwnerConfigOperations
+        .newCodeOwnerConfig()
+        .project(project)
+        .branch(RefNames.REFS_CONFIG)
+        .folderPath("/")
+        .addCodeOwnerEmail(admin.email())
+        .create();
+
+    assertThat(
+            codeOwnerConfigScannerFactory
+                .create()
+                .includeDefaultCodeOwnerConfig(false)
+                .containsAnyCodeOwnerConfigFile(BranchNameKey.create(project, "master")))
+        .isFalse();
+  }
+
+  @Test
   public void containsACodeOwnerConfigFile_invalidCodeOwnerConfigFileExists() throws Exception {
     createInvalidCodeOwnerConfig("/OWNERS");
 
@@ -410,8 +560,9 @@
         .create();
 
     assertThat(
-            codeOwnerConfigScanner.containsAnyCodeOwnerConfigFile(
-                BranchNameKey.create(project, "master")))
+            codeOwnerConfigScannerFactory
+                .create()
+                .containsAnyCodeOwnerConfigFile(BranchNameKey.create(project, "master")))
         .isTrue();
   }
 
@@ -420,14 +571,53 @@
     createInvalidCodeOwnerConfig("/OWNERS");
 
     assertThat(
-            codeOwnerConfigScanner.containsAnyCodeOwnerConfigFile(
-                BranchNameKey.create(project, "master")))
+            codeOwnerConfigScannerFactory
+                .create()
+                .containsAnyCodeOwnerConfigFile(BranchNameKey.create(project, "master")))
         .isTrue();
   }
 
+  @Test
+  public void visitorIsOnlyInvokedOnceForDefaultCodeOnwerConfigFileIfRefsMetaConfigIsScanned()
+      throws Exception {
+    CodeOwnerConfig.Key codeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch(RefNames.REFS_CONFIG)
+            .folderPath("/")
+            .fileName("OWNERS")
+            .addCodeOwnerEmail(admin.email())
+            .create();
+
+    when(visitor.visit(any(CodeOwnerConfig.class))).thenReturn(true);
+    codeOwnerConfigScannerFactory
+        .create()
+        .includeDefaultCodeOwnerConfig(true)
+        .visit(
+            BranchNameKey.create(project, RefNames.REFS_CONFIG),
+            visitor,
+            invalidCodeOwnerConfigCallback);
+
+    // Verify that we received the callback for the code owner config only once.
+    Mockito.verify(visitor)
+        .visit(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).get());
+    verifyNoMoreInteractions(visitor);
+
+    verifyZeroInteractions(invalidCodeOwnerConfigCallback);
+  }
+
   private void visit() {
-    codeOwnerConfigScanner.visit(
-        BranchNameKey.create(project, "master"), visitor, invalidCodeOwnerConfigCallback);
+    visit(
+        /** includeDefaultCodeOwnerConfig */
+        true);
+  }
+
+  private void visit(boolean includeDefaultCodeOwnerConfig) {
+    codeOwnerConfigScannerFactory
+        .create()
+        .includeDefaultCodeOwnerConfig(includeDefaultCodeOwnerConfig)
+        .visit(BranchNameKey.create(project, "master"), visitor, invalidCodeOwnerConfigCallback);
   }
 
   private void createInvalidCodeOwnerConfig(String path) throws Exception {
diff --git a/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJsonTest.java b/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJsonTest.java
index 456ebc1..cac9070 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJsonTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJsonTest.java
@@ -76,7 +76,7 @@
     codeOwnerProjectConfigJson =
         new CodeOwnerProjectConfigJson(
             codeOwnersPluginConfiguration,
-            plugin.getSysInjector().getInstance(CodeOwnerConfigScanner.class),
+            plugin.getSysInjector().getInstance(CodeOwnerConfigScanner.Factory.class),
             plugin.getSysInjector().getInstance(new Key<Provider<ListBranches>>() {}));
     findOwnersBackend = plugin.getSysInjector().getInstance(FindOwnersBackend.class);
     protoBackend = plugin.getSysInjector().getInstance(ProtoBackend.class);
diff --git a/resources/Documentation/backend-find-owners.md b/resources/Documentation/backend-find-owners.md
index 74e1170..b0c0af6 100644
--- a/resources/Documentation/backend-find-owners.md
+++ b/resources/Documentation/backend-find-owners.md
@@ -14,6 +14,17 @@
 contains an `OWNERS` file that disables the inheritance of code owners from the
 parent directories via the [set noparent](#setNoparent) keyword).
 
+<a id="defaultCodeOwnerConfiguration">
+Default code owners that apply to all branches can be defined in an `OWNERS`
+file in the root directory of the `refs/meta/config` branch. This `OWNERS` file
+is the parent of the root `OWNERS` files in all branches. This means if a root
+`OWNERS` file disables the inheritance of code owners from the parent
+directories via the [set noparent](#setNoparent) keyword the `OWNERS` file in
+the `refs/meta/config` branch is ignored. Default code owners are not inherited
+from parent projects. If code owners should be defined for child projects this
+can be done by setting [global code
+owners](config.html#codeOwnersGlobalCodeOwner).
+
 ### <a id="codeOwnerConfigFileExtension">
 **NOTE:** It's possible that projects have a [file extension for code owner
 config files](config.html#codeOwnersFileExtension) configured. In this case the
diff --git a/resources/Documentation/rest-api.md b/resources/Documentation/rest-api.md
index 22fd38a..5013366 100644
--- a/resources/Documentation/rest-api.md
+++ b/resources/Documentation/rest-api.md
@@ -287,7 +287,7 @@
 | `o`         | optional | [Account option](../../../Documentation/rest-api-accounts.html#query-options) that controls which fields in the returned accounts should be populated. Can be specified multiple times. If not given, only the `_account_id` field for the account ID is populated.
 | `O`         | optional | [Account option](../../../Documentation/rest-api-accounts.html#query-options) in hex. For the explanation see `o` parameter.
 | `limit`\|`n` | optional | Limit defining how many code owners should be returned at most. By default 10.
-| `revision` | optional | Revision from which the code owner configs should be read as commit SHA1. Can be used to read historic code owners from this branch, but imports from other branches or repositories as well as global code owners from `refs/meta/config` are still read from the current revisions. If not specified the code owner configs are read from the HEAD revision of the branch. Not supported for getting code owners for a path in a change.
+| `revision` | optional | Revision from which the code owner configs should be read as commit SHA1. Can be used to read historic code owners from this branch, but imports from other branches or repositories as well as default and global code owners from `refs/meta/config` are still read from the current revisions. If not specified the code owner configs are read from the HEAD revision of the branch. Not supported for getting code owners for a path in a change.
 
 As a response a list of [CodeOwnerInfo](#code-owner-info) entities is returned.
 The returned code owners are sorted by an internal score that expresses how good
diff --git a/resources/Documentation/user-guide.md b/resources/Documentation/user-guide.md
index 83194b2..6e31243 100644
--- a/resources/Documentation/user-guide.md
+++ b/resources/Documentation/user-guide.md
@@ -150,13 +150,14 @@
 override](#codeOwnerOverride).
 
 If the destination branch doesn't contain any [code owner config
-file](#codeOwnerConfigFiles) at all yet, the project owners are considered as
-code owners and can grant [code owner approvals](#codeOwnerApproval) for all
-files. This is to allow bootstrapping code owners and should be only a temporary
-state until the first [code owner config file](#codeOwnerConfigFiles) is added.
-Please note that the [code owner suggestion](#codeOwnerSuggestion) isn't working
-if no code owners are defined yet (project owners will not be suggested in this
-case).
+file](#codeOwnerConfigFiles) at all yet and the project also doesn't have a
+[default code owner config file](backend-find-owners.html#defaultCodeOwnerConfiguration),
+the project owners are considered as code owners and can grant [code owner
+approvals](#codeOwnerApproval) for all files. This is to allow bootstrapping
+code owners and should be only a temporary state until the first [code owner
+config file](#codeOwnerConfigFiles) is added.  Please note that the [code owner
+suggestion](#codeOwnerSuggestion) isn't working if no code owners are defined
+yet (project owners will not be suggested in this case).
 
 ## <a id="renames">Renames