Merge changes Ie703498f,I04530aed,I489c26ac,I99349b7d,I02a1e3cb, ...

* changes:
  Make all callers of ChangedFiles go through getOrCompute
  ChangeFiles: Allow using getFromDiffCache for initial commits
  Drop possibility to pass in RevWalk into CodeOwnerBackend and ChangedFiles
  BackendConfig: Remove @VisibleForTesting from getDefaultBackend()
  config.md: Remove line-break that appears in the middle of a sentence
  Allow to configure the path expression syntax that should be used
diff --git a/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/TestPathExpressions.java b/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/TestPathExpressions.java
index d2acaed..2637eda 100644
--- a/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/TestPathExpressions.java
+++ b/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/TestPathExpressions.java
@@ -14,10 +14,12 @@
 
 package com.google.gerrit.plugins.codeowners.acceptance.testsuite;
 
+import com.google.gerrit.plugins.codeowners.backend.AbstractFileBasedCodeOwnerBackend;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
 import com.google.gerrit.plugins.codeowners.backend.FindOwnersGlobMatcher;
 import com.google.gerrit.plugins.codeowners.backend.GlobMatcher;
 import com.google.gerrit.plugins.codeowners.backend.PathExpressionMatcher;
+import com.google.gerrit.plugins.codeowners.backend.PathExpressions;
 import com.google.gerrit.plugins.codeowners.backend.SimplePathExpressionMatcher;
 import com.google.gerrit.plugins.codeowners.backend.config.BackendConfig;
 import com.google.inject.Inject;
@@ -83,8 +85,20 @@
 
   private PathExpressionMatcher getPathExpressionMatcher() {
     CodeOwnerBackend defaultBackend = backendConfig.getDefaultBackend();
+    if (defaultBackend instanceof AbstractFileBasedCodeOwnerBackend) {
+      return ((AbstractFileBasedCodeOwnerBackend) defaultBackend)
+          .getDefaultPathExpressions()
+          .map(PathExpressions::getMatcher)
+          .orElseThrow(
+              () ->
+                  new IllegalStateException(
+                      String.format(
+                          "code owner backend %s doesn't support path expressions",
+                          defaultBackend.getClass().getName())));
+    }
+
     return defaultBackend
-        .getPathExpressionMatcher()
+        .getPathExpressionMatcher(/* branchNameKey= */ null)
         .orElseThrow(
             () ->
                 new IllegalStateException(
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java b/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java
index 15aed2d..a6fff53 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java
@@ -16,9 +16,11 @@
 
 import static java.util.Objects.requireNonNull;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Throwables;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginProjectConfigSnapshot;
@@ -77,9 +79,7 @@
 
   @Override
   public final Optional<CodeOwnerConfig> getCodeOwnerConfig(
-      CodeOwnerConfig.Key codeOwnerConfigKey,
-      @Nullable RevWalk revWalk,
-      @Nullable ObjectId revision) {
+      CodeOwnerConfig.Key codeOwnerConfigKey, @Nullable ObjectId revision) {
     String fileName =
         codeOwnerConfigKey.fileName().orElse(getFileName(codeOwnerConfigKey.project()));
 
@@ -96,33 +96,21 @@
       return Optional.empty();
     }
 
-    return loadCodeOwnerConfigFile(codeOwnerConfigKey, fileName, revWalk, revision)
+    return loadCodeOwnerConfigFile(codeOwnerConfigKey, fileName, revision)
         .getLoadedCodeOwnerConfig();
   }
 
   private CodeOwnerConfigFile loadCodeOwnerConfigFile(
-      CodeOwnerConfig.Key codeOwnerConfigKey,
-      String fileName,
-      @Nullable RevWalk revWalk,
-      @Nullable ObjectId revision) {
+      CodeOwnerConfig.Key codeOwnerConfigKey, String fileName, @Nullable ObjectId revision) {
     try (Repository repository = repoManager.openRepository(codeOwnerConfigKey.project())) {
       if (revision == null) {
         return codeOwnerConfigFileFactory.loadCurrent(
             fileName, codeOwnerConfigParser, repository, codeOwnerConfigKey);
       }
 
-      boolean closeRevWalk = false;
-      if (revWalk == null) {
-        closeRevWalk = true;
-        revWalk = new RevWalk(repository);
-      }
-      try {
+      try (RevWalk revWalk = new RevWalk(repository)) {
         return codeOwnerConfigFileFactory.load(
             fileName, codeOwnerConfigParser, revWalk, revision, codeOwnerConfigKey);
-      } finally {
-        if (closeRevWalk) {
-          revWalk.close();
-        }
       }
     } catch (IOException e) {
       throw new CodeOwnersInternalServerErrorException(
@@ -226,6 +214,28 @@
     }
   }
 
+  @Override
+  public final Optional<PathExpressionMatcher> getPathExpressionMatcher(
+      BranchNameKey branchNameKey) {
+    Optional<PathExpressions> pathExpressions =
+        codeOwnersPluginConfiguration
+            .getProjectConfig(branchNameKey.project())
+            .getPathExpressions(branchNameKey.branch());
+    boolean hasConfiguredPathExpressions = pathExpressions.isPresent();
+    if (!hasConfiguredPathExpressions) {
+      pathExpressions = getDefaultPathExpressions();
+    }
+    logger.atFine().log(
+        "using %s path expression syntax %s for project/branch %s",
+        (hasConfiguredPathExpressions ? "configured" : "default"),
+        pathExpressions.map(PathExpressions::name).orElse("<none>"),
+        branchNameKey);
+    return pathExpressions.map(PathExpressions::getMatcher);
+  }
+
+  @VisibleForTesting
+  public abstract Optional<PathExpressions> getDefaultPathExpressions();
+
   private Optional<CodeOwnerConfig> upsertCodeOwnerConfigInSourceBranch(
       @Nullable IdentifiedUser currentUser,
       CodeOwnerConfig.Key codeOwnerConfigKey,
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/ChangedFiles.java b/java/com/google/gerrit/plugins/codeowners/backend/ChangedFiles.java
index 36a9751..fb72365 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/ChangedFiles.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/ChangedFiles.java
@@ -39,7 +39,6 @@
 import com.google.gerrit.server.patch.AutoMerger;
 import com.google.gerrit.server.patch.DiffNotAvailableException;
 import com.google.gerrit.server.patch.DiffOperations;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.patch.filediff.FileDiffOutput;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -63,16 +62,16 @@
 /**
  * Class to get/compute the files that have been changed in a revision.
  *
- * <p>The {@link #getFromDiffCache(Project.NameKey, ObjectId)} method is retrieving the file diff
- * from the diff cache and has rename detection enabled.
+ * <p>The {@link #getFromDiffCache(Project.NameKey, ObjectId, MergeCommitStrategy)} method is
+ * retrieving the file diff from the diff cache and has rename detection enabled.
  *
- * <p>In contrast to this, for the {@code compute} methods the file diff is newly computed on each
- * access and rename detection is disabled (as it's too expensive to do it on each access).
+ * <p>In contrast to this, for the {@link #compute(Project.NameKey, ObjectId, MergeCommitStrategy)}
+ * method the file diff is newly computed on each access and rename detection is disabled (as it's
+ * too expensive to do it on each access).
  *
- * <p>If possible, using {@link #getFromDiffCache(Project.NameKey, ObjectId)} is preferred, however
- * {@link #getFromDiffCache(Project.NameKey, ObjectId)} cannot be used for newly created commits
- * that are only available from a specific {@link RevWalk} instance since the {@link RevWalk}
- * instance cannot be passed in.
+ * <p>Which of these methods is invoked when calling any of {@code getOrCompute} methods is
+ * controlled by the {@link CodeOwnersExperimentFeaturesConstants#USE_DIFF_CACHE} experiment feature
+ * flag.
  *
  * <p>The {@link com.google.gerrit.server.patch.PatchListCache} is deprecated, and hence it not
  * being used here.
@@ -113,104 +112,100 @@
    * Returns the changed files for the given revision.
    *
    * <p>By default the changed files are computed on access (see {@link #compute(Project.NameKey,
-   * ObjectId)}).
+   * ObjectId, MergeCommitStrategy)}).
    *
    * <p>Only if enabled via the {@link CodeOwnersExperimentFeaturesConstants#USE_DIFF_CACHE}
    * experiment feature flag the changed files are retrieved from the diff cache (see {@link
-   * #getFromDiffCache(Project.NameKey, ObjectId)}).
+   * #getFromDiffCache(Project.NameKey, ObjectId, MergeCommitStrategy)}).
    *
    * @param project the project
    * @param revision the revision for which the changed files should be computed
    * @return the files that have been changed in the given revision, sorted alphabetically by path
    */
-  public ImmutableList<ChangedFile> getOrCompute(Project.NameKey project, ObjectId revision)
-      throws IOException, PatchListNotAvailableException, DiffNotAvailableException {
+  public ImmutableList<ChangedFile> getOrCompute(
+      Project.NameKey project, ObjectId revision, MergeCommitStrategy mergeCommitStrategy)
+      throws IOException, DiffNotAvailableException {
+    requireNonNull(project, "project");
+    requireNonNull(revision, "revision");
+    requireNonNull(mergeCommitStrategy, "mergeCommitStrategy");
+
     if (experimentFeatures.isFeatureEnabled(CodeOwnersExperimentFeaturesConstants.USE_DIFF_CACHE)) {
-      return getFromDiffCache(project, revision);
+      return getFromDiffCache(project, revision, mergeCommitStrategy);
     }
-    return compute(project, revision);
+    return compute(project, revision, mergeCommitStrategy);
   }
 
   /**
-   * Computes the files that have been changed in the given revision.
+   * Returns the changed files for the given revision.
    *
-   * <p>The diff is computed against the parent commit.
+   * <p>Uses the configured merge commit strategy.
    *
-   * <p>Rename detection is disabled.
+   * @param project the project
+   * @param revision the revision for which the changed files should be computed
+   * @throws IOException thrown if the computation fails due to an I/O error
+   * @see #getOrCompute(Project.NameKey, ObjectId, MergeCommitStrategy)
+   */
+  public ImmutableList<ChangedFile> getOrCompute(Project.NameKey project, ObjectId revision)
+      throws IOException, DiffNotAvailableException {
+    requireNonNull(project, "project");
+    requireNonNull(revision, "revision");
+
+    return getOrCompute(
+        project,
+        revision,
+        codeOwnersPluginConfiguration.getProjectConfig(project).getMergeCommitStrategy());
+  }
+
+  /**
+   * Returns the changed files for the given revision.
+   *
+   * <p>Uses the configured merge commit strategy.
    *
    * @param revisionResource the revision resource for which the changed files should be computed
    * @return the files that have been changed in the given revision, sorted alphabetically by path
    * @throws IOException thrown if the computation fails due to an I/O error
-   * @throws PatchListNotAvailableException thrown if getting the patch list for a merge commit
-   *     against the auto merge failed
+   * @see #getOrCompute(Project.NameKey, ObjectId, MergeCommitStrategy)
    */
-  public ImmutableList<ChangedFile> compute(RevisionResource revisionResource)
-      throws IOException, PatchListNotAvailableException {
+  public ImmutableList<ChangedFile> getOrCompute(RevisionResource revisionResource)
+      throws IOException, DiffNotAvailableException {
     requireNonNull(revisionResource, "revisionResource");
-    return compute(revisionResource.getProject(), revisionResource.getPatchSet().commitId());
+    return getOrCompute(revisionResource.getProject(), revisionResource.getPatchSet().commitId());
   }
 
   /**
-   * Computes the files that have been changed in the given revision.
+   * Computed the changed files for the given revision.
    *
-   * <p>The diff is computed against the parent commit.
-   *
-   * <p>Rename detection is disabled.
+   * <p>The changed files are newly computed on each access. Rename detection is disabled (as it's
+   * too expensive to do it on each access).
    *
    * @param project the project
    * @param revision the revision for which the changed files should be computed
-   * @return the files that have been changed in the given revision, sorted alphabetically by path
-   * @throws IOException thrown if the computation fails due to an I/O error
-   * @throws PatchListNotAvailableException thrown if getting the patch list for a merge commit
-   *     against the auto merge failed
+   * @param mergeCommitStrategy the merge commit strategy that should be used to compute the changed
+   *     files for merge commits
+   * @return the changed files
    */
-  public ImmutableList<ChangedFile> compute(Project.NameKey project, ObjectId revision)
-      throws IOException, PatchListNotAvailableException {
+  private ImmutableList<ChangedFile> compute(
+      Project.NameKey project, ObjectId revision, MergeCommitStrategy mergeCommitStrategy)
+      throws IOException {
     requireNonNull(project, "project");
     requireNonNull(revision, "revision");
-
-    try (Repository repository = repoManager.openRepository(project);
-        RevWalk revWalk = new RevWalk(repository)) {
-      RevCommit revCommit = revWalk.parseCommit(revision);
-      return compute(project, repository.getConfig(), revWalk, revCommit);
-    }
-  }
-
-  public ImmutableList<ChangedFile> compute(
-      Project.NameKey project, Config repoConfig, RevWalk revWalk, RevCommit revCommit)
-      throws IOException {
-    return compute(
-        project,
-        repoConfig,
-        revWalk,
-        revCommit,
-        codeOwnersPluginConfiguration.getProjectConfig(project).getMergeCommitStrategy());
-  }
-
-  public ImmutableList<ChangedFile> compute(
-      Project.NameKey project,
-      Config repoConfig,
-      RevWalk revWalk,
-      RevCommit revCommit,
-      MergeCommitStrategy mergeCommitStrategy)
-      throws IOException {
-    requireNonNull(project, "project");
-    requireNonNull(repoConfig, "repoConfig");
-    requireNonNull(revWalk, "revWalk");
-    requireNonNull(revCommit, "revCommit");
     requireNonNull(mergeCommitStrategy, "mergeCommitStrategy");
 
     logger.atFine().log(
-        "computing changed files for revision %s in project %s", revCommit.name(), project);
+        "computing changed files for revision %s in project %s", revision.name(), project);
 
-    if (revCommit.getParentCount() > 1
-        && MergeCommitStrategy.FILES_WITH_CONFLICT_RESOLUTION.equals(mergeCommitStrategy)) {
-      RevCommit autoMergeCommit = getAutoMergeCommit(project, revCommit);
-      return compute(repoConfig, revWalk, revCommit, autoMergeCommit);
+    try (Repository repo = repoManager.openRepository(project);
+        RevWalk revWalk = new RevWalk(repo)) {
+      RevCommit revCommit = revWalk.parseCommit(revision);
+      if (revCommit.getParentCount() > 1
+          && MergeCommitStrategy.FILES_WITH_CONFLICT_RESOLUTION.equals(mergeCommitStrategy)) {
+        RevCommit autoMergeCommit = getAutoMergeCommit(project, revCommit);
+        return compute(repo.getConfig(), revWalk, revCommit, autoMergeCommit);
+      }
+
+      RevCommit baseCommit = revCommit.getParentCount() > 0 ? revCommit.getParent(0) : null;
+      return compute(repo.getConfig(), revWalk, revCommit, baseCommit);
     }
-
-    RevCommit baseCommit = revCommit.getParentCount() > 0 ? revCommit.getParent(0) : null;
-    return compute(repoConfig, revWalk, revCommit, baseCommit);
   }
 
   private RevCommit getAutoMergeCommit(Project.NameKey project, RevCommit mergeCommit)
@@ -273,19 +268,14 @@
    * Gets the changed files from the diff cache.
    *
    * <p>Rename detection is enabled.
-   *
-   * @throws IllegalStateException thrown if invoked for an initial revision
    */
   @VisibleForTesting
-  ImmutableList<ChangedFile> getFromDiffCache(Project.NameKey project, ObjectId revision)
+  ImmutableList<ChangedFile> getFromDiffCache(
+      Project.NameKey project, ObjectId revision, MergeCommitStrategy mergeCommitStrategy)
       throws IOException, DiffNotAvailableException {
     requireNonNull(project, "project");
     requireNonNull(revision, "revision");
-
-    checkState(!isInitialCommit(project, revision), "diff cache doesn't support initial commits");
-
-    MergeCommitStrategy mergeCommitStrategy =
-        codeOwnersPluginConfiguration.getProjectConfig(project).getMergeCommitStrategy();
+    requireNonNull(mergeCommitStrategy, "mergeCommitStrategy");
 
     try (Timer0.Context ctx = codeOwnerMetrics.getChangedFiles.start()) {
       Map<String, FileDiffOutput> fileDiffOutputs;
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
index 8266681..d11767a 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
@@ -48,7 +48,6 @@
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.patch.DiffNotAvailableException;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
@@ -162,7 +161,7 @@
         ownedPaths = ownedPaths.limit(limit);
       }
       return ownedPaths.collect(toImmutableList());
-    } catch (IOException | PatchListNotAvailableException | DiffNotAvailableException e) {
+    } catch (IOException | DiffNotAvailableException e) {
       throw new CodeOwnersInternalServerErrorException(
           String.format(
               "failed to compute owned paths of patch set %s for account %d",
@@ -178,8 +177,7 @@
    * @return whether the given change has sufficient code owner approvals to be submittable
    */
   public boolean isSubmittable(ChangeNotes changeNotes)
-      throws ResourceConflictException, IOException, PatchListNotAvailableException,
-          DiffNotAvailableException {
+      throws ResourceConflictException, IOException, DiffNotAvailableException {
     requireNonNull(changeNotes, "changeNotes");
     logger.atFine().log(
         "checking if change %d in project %s is submittable",
@@ -225,8 +223,7 @@
    */
   public ImmutableSet<FileCodeOwnerStatus> getFileStatusesAsSet(
       ChangeNotes changeNotes, int start, int limit)
-      throws ResourceConflictException, IOException, PatchListNotAvailableException,
-          DiffNotAvailableException {
+      throws ResourceConflictException, IOException, DiffNotAvailableException {
     requireNonNull(changeNotes, "changeNotes");
     try (Timer0.Context ctx = codeOwnerMetrics.computeFileStatuses.start()) {
       logger.atFine().log(
@@ -279,8 +276,7 @@
       CodeOwnerConfigHierarchy codeOwnerConfigHierarchy,
       CodeOwnerResolver codeOwnerResolver,
       ChangeNotes changeNotes)
-      throws ResourceConflictException, IOException, PatchListNotAvailableException,
-          DiffNotAvailableException {
+      throws ResourceConflictException, IOException, DiffNotAvailableException {
     requireNonNull(changeNotes, "changeNotes");
     try (Timer0.Context ctx = codeOwnerMetrics.prepareFileStatusComputation.start()) {
       logger.atFine().log(
@@ -400,8 +396,7 @@
   @VisibleForTesting
   public Stream<FileCodeOwnerStatus> getFileStatusesForAccount(
       ChangeNotes changeNotes, PatchSet patchSet, Account.Id accountId)
-      throws ResourceConflictException, IOException, PatchListNotAvailableException,
-          DiffNotAvailableException {
+      throws ResourceConflictException, IOException, DiffNotAvailableException {
     requireNonNull(changeNotes, "changeNotes");
     requireNonNull(patchSet, "patchSet");
     requireNonNull(accountId, "accountId");
@@ -470,7 +465,7 @@
 
   private Stream<FileCodeOwnerStatus> getAllPathsAsApproved(
       ChangeNotes changeNotes, PatchSet patchSet, String reason)
-      throws IOException, PatchListNotAvailableException, DiffNotAvailableException {
+      throws IOException, DiffNotAvailableException {
     logger.atFine().log("all paths are approved (reason = %s)", reason);
     return changedFiles.getOrCompute(changeNotes.getProjectName(), patchSet.commitId()).stream()
         .map(
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerBackend.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerBackend.java
index 3775f89..22d91bc 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerBackend.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerBackend.java
@@ -15,13 +15,13 @@
 package com.google.gerrit.plugins.codeowners.backend;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.server.IdentifiedUser;
 import java.nio.file.Path;
 import java.util.Optional;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.revwalk.RevWalk;
 
 /**
  * Interface for code owner backends.
@@ -51,25 +51,8 @@
    *     {@code null} the code owner config is loaded from the current revision of the branch
    * @return code owner config for the given key if it exists, otherwise {@link Optional#empty()}
    */
-  default Optional<CodeOwnerConfig> getCodeOwnerConfig(
-      CodeOwnerConfig.Key codeOwnerConfigKey, @Nullable ObjectId revision) {
-    return getCodeOwnerConfig(codeOwnerConfigKey, /* revWalk= */ null, revision);
-  }
-
-  /**
-   * Gets the code owner config for the given key if it exists.
-   *
-   * @param codeOwnerConfigKey the code owner config key for which the code owner config should be
-   *     returned
-   * @param revWalk optional rev walk, if given this rev walk is used to load the given revision
-   * @param revision the branch revision from which the code owner config should be loaded, if
-   *     {@code null} the code owner config is loaded from the current revision of the branch
-   * @return code owner config for the given key if it exists, otherwise {@link Optional#empty()}
-   */
   Optional<CodeOwnerConfig> getCodeOwnerConfig(
-      CodeOwnerConfig.Key codeOwnerConfigKey,
-      @Nullable RevWalk revWalk,
-      @Nullable ObjectId revision);
+      CodeOwnerConfig.Key codeOwnerConfigKey, @Nullable ObjectId revision);
 
   /**
    * Returns the absolute file path of the specified code owner config.
@@ -111,8 +94,11 @@
    * <p>May return {@link Optional#empty()} if path expressions are not supported by the code owner
    * backend. It this case all {@link CodeOwnerSet}s that have path expressions are ignored and will
    * not have any effect.
+   *
+   * @param branchNameKey project and branch for which the path expression matcher should be
+   *     returned
    */
-  Optional<PathExpressionMatcher> getPathExpressionMatcher();
+  Optional<PathExpressionMatcher> getPathExpressionMatcher(BranchNameKey branchNameKey);
 
   /**
    * Replaces the old email in the given code owner config file content with the new email.
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExperimentFeaturesConstants.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExperimentFeaturesConstants.java
index a0e82ad..01bf7d6 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExperimentFeaturesConstants.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExperimentFeaturesConstants.java
@@ -24,7 +24,8 @@
    * be used to get changed files, instead of computing the changed files on our own.
    *
    * @see ChangedFiles#getOrCompute(com.google.gerrit.entities.Project.NameKey,
-   *     org.eclipse.jgit.lib.ObjectId)
+   *     org.eclipse.jgit.lib.ObjectId,
+   *     com.google.gerrit.plugins.codeowners.common.MergeCommitStrategy)
    */
   public static final String USE_DIFF_CACHE =
       "GerritBackendRequestFeature__code_owners_use_diff_cache";
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
index 8272bb6..fcd207a 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
@@ -131,7 +131,7 @@
               .getProjectConfig(codeOwnerConfigKey.project())
               .getBackend(codeOwnerConfigKey.branchNameKey().branch());
       return codeOwnerBackend
-          .getPathExpressionMatcher()
+          .getPathExpressionMatcher(codeOwnerConfigKey.branchNameKey())
           .orElse((pathExpression, relativePath) -> false);
     }
   }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/PathExpressions.java b/java/com/google/gerrit/plugins/codeowners/backend/PathExpressions.java
new file mode 100644
index 0000000..d378a7e
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/PathExpressions.java
@@ -0,0 +1,62 @@
+// 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 com.google.gerrit.common.Nullable;
+import java.util.Locale;
+import java.util.Optional;
+
+/** Enum listing the supported options for path expressions syntaxes. */
+public enum PathExpressions {
+  /** Simple path expressions as implemented by {@link SimplePathExpressionMatcher}. */
+  SIMPLE(SimplePathExpressionMatcher.INSTANCE),
+
+  /** Plain glob path expressions as implemented by {@link GlobMatcher}. */
+  GLOB(GlobMatcher.INSTANCE),
+
+  /**
+   * Find-owners compatible glob path expressions as implemented by {@link FindOwnersGlobMatcher}.
+   */
+  FIND_OWNERS_GLOB(FindOwnersGlobMatcher.INSTANCE);
+
+  private final PathExpressionMatcher matcher;
+
+  private PathExpressions(PathExpressionMatcher matcher) {
+    this.matcher = matcher;
+  }
+
+  /** Gets the path expression matcher. */
+  public PathExpressionMatcher getMatcher() {
+    return matcher;
+  }
+
+  /**
+   * Tries to parse a string as a {@link PathExpressions} enum.
+   *
+   * @param value the string value to be parsed
+   * @return the parsed {@link PathExpressions} enum, {@link Optional#empty()} if the given value
+   *     couldn't be parsed as {@link PathExpressions} enum
+   */
+  public static Optional<PathExpressions> tryParse(@Nullable String value) {
+    if (value == null) {
+      return Optional.empty();
+    }
+    try {
+      return Optional.of(PathExpressions.valueOf(value.toUpperCase(Locale.US)));
+    } catch (IllegalArgumentException e) {
+      return Optional.empty();
+    }
+  }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/BackendConfig.java b/java/com/google/gerrit/plugins/codeowners/backend/config/BackendConfig.java
index cbeb83d..05c9f0e 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/config/BackendConfig.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/BackendConfig.java
@@ -18,6 +18,7 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.BranchNameKey;
@@ -26,6 +27,7 @@
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackendId;
+import com.google.gerrit.plugins.codeowners.backend.PathExpressions;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
 import com.google.gerrit.server.git.validators.ValidationMessage;
@@ -57,6 +59,7 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @VisibleForTesting public static final String KEY_BACKEND = "backend";
+  @VisibleForTesting public static final String KEY_PATH_EXPRESSIONS = "pathExpressions";
 
   private final String pluginName;
   private final DynamicMap<CodeOwnerBackend> codeOwnerBackends;
@@ -64,6 +67,9 @@
   /** The name of the configured code owners default backend. */
   private final String defaultBackendName;
 
+  /** The configured default path expressions. */
+  private final Optional<PathExpressions> defaultPathExpressions;
+
   @Inject
   BackendConfig(
       @PluginName String pluginName,
@@ -76,6 +82,19 @@
         pluginConfigFactory
             .getFromGerritConfig(pluginName)
             .getString(KEY_BACKEND, CodeOwnerBackendId.FIND_OWNERS.getBackendId());
+
+    String defaultPathExpressionsName =
+        pluginConfigFactory
+            .getFromGerritConfig(pluginName)
+            .getString(KEY_PATH_EXPRESSIONS, /* defaultValue= */ null);
+    this.defaultPathExpressions = PathExpressions.tryParse(defaultPathExpressionsName);
+    if (!Strings.isNullOrEmpty(defaultPathExpressionsName)
+        && !this.defaultPathExpressions.isPresent()) {
+      logger.atWarning().log(
+          "Path expressions '%s' that are configured in gerrit.config"
+              + " (parameter plugin.%s.%s) not found.",
+          defaultPathExpressionsName, pluginName, KEY_PATH_EXPRESSIONS);
+    }
   }
 
   /**
@@ -105,6 +124,18 @@
       }
     }
 
+    String pathExpressionsName =
+        projectLevelConfig.getString(SECTION_CODE_OWNERS, null, KEY_PATH_EXPRESSIONS);
+    if (!Strings.isNullOrEmpty(pathExpressionsName)
+        && !PathExpressions.tryParse(pathExpressionsName).isPresent()) {
+      validationMessages.add(
+          new CommitValidationMessage(
+              String.format(
+                  "Path expressions '%s' that are configured in %s (parameter %s.%s) not found.",
+                  pathExpressionsName, fileName, SECTION_CODE_OWNERS, KEY_PATH_EXPRESSIONS),
+              ValidationMessage.Type.ERROR));
+    }
+
     for (String subsection : projectLevelConfig.getSubsections(SECTION_CODE_OWNERS)) {
       backendName = projectLevelConfig.getString(SECTION_CODE_OWNERS, subsection, KEY_BACKEND);
       if (backendName != null) {
@@ -117,6 +148,22 @@
                   ValidationMessage.Type.ERROR));
         }
       }
+
+      pathExpressionsName =
+          projectLevelConfig.getString(SECTION_CODE_OWNERS, subsection, KEY_PATH_EXPRESSIONS);
+      if (!Strings.isNullOrEmpty(pathExpressionsName)
+          && !PathExpressions.tryParse(pathExpressionsName).isPresent()) {
+        validationMessages.add(
+            new CommitValidationMessage(
+                String.format(
+                    "Path expressions '%s' that are configured in %s (parameter %s.%s.%s) not found.",
+                    pathExpressionsName,
+                    fileName,
+                    SECTION_CODE_OWNERS,
+                    subsection,
+                    KEY_PATH_EXPRESSIONS),
+                ValidationMessage.Type.ERROR));
+      }
     }
 
     return ImmutableList.copyOf(validationMessages);
@@ -211,7 +258,6 @@
   }
 
   /** Gets the default code owner backend. */
-  @VisibleForTesting
   public CodeOwnerBackend getDefaultBackend() {
     return lookupBackend(defaultBackendName)
         .orElseThrow(
@@ -233,4 +279,93 @@
     // plugin name.
     return Optional.ofNullable(codeOwnerBackends.get("gerrit", backendName));
   }
+
+  /**
+   * Gets the path expressions that are configured for the given branch.
+   *
+   * <p>The path expressions configuration is evaluated in the following order:
+   *
+   * <ul>
+   *   <li>path expressions for branch by full name (with inheritance)
+   *   <li>path expressions for branch by short name (with inheritance)
+   * </ul>
+   *
+   * @param pluginConfig the plugin config from which the path expressions should be read.
+   * @param branch the project and branch for which the configured path expressions should be read
+   * @return the path expressions that are configured for the given branch, {@link Optional#empty()}
+   *     if there is no branch-specific path expressions configuration
+   */
+  Optional<PathExpressions> getPathExpressionsForBranch(Config pluginConfig, BranchNameKey branch) {
+    requireNonNull(pluginConfig, "pluginConfig");
+    requireNonNull(branch, "branch");
+
+    // check for branch specific path expressions by full branch name
+    Optional<PathExpressions> pathExpressions =
+        getPathExpressionsForBranch(pluginConfig, branch.project(), branch.branch());
+    if (!pathExpressions.isPresent()) {
+      // check for branch specific path expressions by short branch name
+      pathExpressions =
+          getPathExpressionsForBranch(pluginConfig, branch.project(), branch.shortName());
+    }
+    return pathExpressions;
+  }
+
+  private Optional<PathExpressions> getPathExpressionsForBranch(
+      Config pluginConfig, Project.NameKey project, String branch) {
+    String pathExpressionsName =
+        pluginConfig.getString(SECTION_CODE_OWNERS, branch, KEY_PATH_EXPRESSIONS);
+    if (Strings.isNullOrEmpty(pathExpressionsName)) {
+      return Optional.empty();
+    }
+
+    Optional<PathExpressions> pathExpressions = PathExpressions.tryParse(pathExpressionsName);
+    if (!pathExpressions.isPresent()) {
+      logger.atWarning().log(
+          "Path expressions '%s' that are configured for project %s in"
+              + " %s.config (parameter %s.%s.%s) not found. Falling back to default path"
+              + " expressions.",
+          pathExpressionsName,
+          project,
+          pluginName,
+          SECTION_CODE_OWNERS,
+          branch,
+          KEY_PATH_EXPRESSIONS);
+    }
+    return pathExpressions;
+  }
+
+  /**
+   * Gets the path expressions that are configured for the given project.
+   *
+   * @param pluginConfig the plugin config from which the path expressions should be read.
+   * @param project the project for which the configured path expressions should be read
+   * @return the path expressions that are configured for the given project, {@link
+   *     Optional#empty()} if there is no project-specific path expression configuration
+   */
+  Optional<PathExpressions> getPathExpressionsForProject(
+      Config pluginConfig, Project.NameKey project) {
+    requireNonNull(pluginConfig, "pluginConfig");
+    requireNonNull(project, "project");
+
+    String pathExpressionsName =
+        pluginConfig.getString(SECTION_CODE_OWNERS, null, KEY_PATH_EXPRESSIONS);
+    if (Strings.isNullOrEmpty(pathExpressionsName)) {
+      return Optional.empty();
+    }
+
+    Optional<PathExpressions> pathExpressions = PathExpressions.tryParse(pathExpressionsName);
+    if (!pathExpressions.isPresent()) {
+      logger.atWarning().log(
+          "Path expressions '%s' that are configured for project %s in"
+              + " %s.config (parameter %s.%s) not found. Falling back to default path"
+              + " expressions.",
+          pathExpressionsName, project, pluginName, SECTION_CODE_OWNERS, KEY_PATH_EXPRESSIONS);
+    }
+    return pathExpressions;
+  }
+
+  /** Gets the default path expressions. */
+  public Optional<PathExpressions> getDefaultPathExpressions() {
+    return defaultPathExpressions;
+  }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigValidator.java b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigValidator.java
index c6181b5..a92e0eb 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigValidator.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigValidator.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.server.git.validators.CommitValidationListener;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
 import com.google.gerrit.server.git.validators.ValidationMessage;
+import com.google.gerrit.server.patch.DiffNotAvailableException;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.ProjectLevelConfig;
 import com.google.gerrit.server.project.ProjectState;
@@ -97,7 +98,7 @@
             exceptionMessage(fileName, cfg.getRevision()), validationMessages);
       }
       return ImmutableList.of();
-    } catch (IOException | ConfigInvalidException e) {
+    } catch (IOException | DiffNotAvailableException | ConfigInvalidException e) {
       String errorMessage =
           String.format(
               "failed to validate file %s for revision %s in ref %s of project %s",
@@ -124,12 +125,10 @@
    * @param fileName the name of the file
    */
   private boolean isFileChanged(CommitReceivedEvent receiveEvent, String fileName)
-      throws IOException {
+      throws IOException, DiffNotAvailableException {
     return changedFiles
-        .compute(
+        .getOrCompute(
             receiveEvent.project.getNameKey(),
-            receiveEvent.repoConfig,
-            receiveEvent.revWalk,
             receiveEvent.commit,
             MergeCommitStrategy.ALL_CHANGED_FILES)
         .stream()
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginProjectConfigSnapshot.java b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginProjectConfigSnapshot.java
index 0500d7c..dbf69c1 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginProjectConfigSnapshot.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginProjectConfigSnapshot.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException;
 import com.google.gerrit.plugins.codeowners.backend.EnableImplicitApprovals;
 import com.google.gerrit.plugins.codeowners.backend.FallbackCodeOwners;
+import com.google.gerrit.plugins.codeowners.backend.PathExpressions;
 import com.google.gerrit.plugins.codeowners.common.CodeOwnerConfigValidationPolicy;
 import com.google.gerrit.plugins.codeowners.common.MergeCommitStrategy;
 import com.google.gerrit.server.account.Emails;
@@ -87,6 +88,8 @@
   @Nullable private Boolean isDisabled;
   private Map<String, CodeOwnerBackend> backendByBranch = new HashMap<>();
   @Nullable private CodeOwnerBackend backend;
+  private Map<String, Optional<PathExpressions>> pathExpressionsByBranch = new HashMap<>();
+  @Nullable private Optional<PathExpressions> pathExpressions;
   @Nullable private Boolean implicitApprovalsEnabled;
   @Nullable private RequiredApproval requiredApproval;
   @Nullable private ImmutableSortedSet<RequiredApproval> overrideApprovals;
@@ -461,6 +464,74 @@
   }
 
   /**
+   * Returns the configured {@link PathExpressions} for the given branch.
+   *
+   * <p>The path expression configuration is evaluated in the following order:
+   *
+   * <ul>
+   *   <li>path expression configuration for branch (with inheritance, first by full branch name,
+   *       then by short branch name)
+   *   <li>path expressions configuration for project (with inheritance)
+   * </ul>
+   *
+   * <p>The first path expressions configuration that exists counts and the evaluation is stopped.
+   *
+   * @param branchName the branch for which the configured path expressions should be returned
+   * @return the {@link PathExpressions} that should be used for the branch, {@link
+   *     Optional#empty()} if no path expressions are configured for the branch
+   */
+  public Optional<PathExpressions> getPathExpressions(String branchName) {
+    requireNonNull(branchName, "branchName");
+
+    BranchNameKey branchNameKey = BranchNameKey.create(projectName, branchName);
+    return pathExpressionsByBranch.computeIfAbsent(
+        branchNameKey.branch(),
+        b -> {
+          Optional<PathExpressions> pathExpressions =
+              backendConfig.getPathExpressionsForBranch(
+                  pluginConfig, BranchNameKey.create(projectName, branchName));
+          if (pathExpressions.isPresent()) {
+            return pathExpressions;
+          }
+          return getPathExpressions();
+        });
+  }
+
+  /**
+   * Returns the configured {@link PathExpressions}.
+   *
+   * <p>The path expression configuration is evaluated in the following order:
+   *
+   * <ul>
+   *   <li>path expression configuration for project (with inheritance)
+   *   <li>default path expressions (globally configured path expressions)
+   * </ul>
+   *
+   * <p>The first path expression configuration that exists counts and the evaluation is stopped.
+   *
+   * @return the {@link PathExpressions} that should be used, {@link Optional#empty()} if no path
+   *     expressions are configured
+   */
+  public Optional<PathExpressions> getPathExpressions() {
+    if (pathExpressions == null) {
+      pathExpressions = readPathExpressions();
+    }
+    return pathExpressions;
+  }
+
+  private Optional<PathExpressions> readPathExpressions() {
+    // check if project specific path expressions are configured
+    Optional<PathExpressions> pathExpressions =
+        backendConfig.getPathExpressionsForProject(pluginConfig, projectName);
+    if (pathExpressions.isPresent()) {
+      return pathExpressions;
+    }
+
+    // fall back to the default path expressions
+    return backendConfig.getDefaultPathExpressions();
+  }
+
+  /**
    * Checks whether implicit code owner approvals are enabled.
    *
    * <p>If enabled, an implict code owner approval from the change owner is assumed if the last
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersBackend.java b/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersBackend.java
index 0e923d7..513bd12 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersBackend.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersBackend.java
@@ -18,8 +18,7 @@
 import com.google.gerrit.plugins.codeowners.backend.AbstractFileBasedCodeOwnerBackend;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigFile;
-import com.google.gerrit.plugins.codeowners.backend.FindOwnersGlobMatcher;
-import com.google.gerrit.plugins.codeowners.backend.PathExpressionMatcher;
+import com.google.gerrit.plugins.codeowners.backend.PathExpressions;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -63,8 +62,8 @@
   }
 
   @Override
-  public Optional<PathExpressionMatcher> getPathExpressionMatcher() {
-    return Optional.of(FindOwnersGlobMatcher.INSTANCE);
+  public Optional<PathExpressions> getDefaultPathExpressions() {
+    return Optional.of(PathExpressions.FIND_OWNERS_GLOB);
   }
 
   @Override
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/proto/ProtoBackend.java b/java/com/google/gerrit/plugins/codeowners/backend/proto/ProtoBackend.java
index b273ab2..ccb59df 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/proto/ProtoBackend.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/proto/ProtoBackend.java
@@ -18,8 +18,7 @@
 import com.google.gerrit.plugins.codeowners.backend.AbstractFileBasedCodeOwnerBackend;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigFile;
-import com.google.gerrit.plugins.codeowners.backend.PathExpressionMatcher;
-import com.google.gerrit.plugins.codeowners.backend.SimplePathExpressionMatcher;
+import com.google.gerrit.plugins.codeowners.backend.PathExpressions;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -67,7 +66,7 @@
   }
 
   @Override
-  public Optional<PathExpressionMatcher> getPathExpressionMatcher() {
-    return Optional.of(SimplePathExpressionMatcher.INSTANCE);
+  public Optional<PathExpressions> getDefaultPathExpressions() {
+    return Optional.of(PathExpressions.SIMPLE);
   }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java
index 1eaaed6..1b931ba 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java
@@ -41,7 +41,6 @@
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
 import com.google.gerrit.plugins.codeowners.validation.CodeOwnerConfigValidator;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -55,8 +54,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
 
 /**
  * REST endpoint that checks/validates the code owner config files in a project.
@@ -75,7 +72,6 @@
 
   private final Provider<CurrentUser> currentUser;
   private final PermissionBackend permissionBackend;
-  private final GitRepositoryManager repoManager;
   private final Provider<ListBranches> listBranches;
   private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
   private final CodeOwnerConfigScanner.Factory codeOwnerConfigScannerFactory;
@@ -85,14 +81,12 @@
   public CheckCodeOwnerConfigFiles(
       Provider<CurrentUser> currentUser,
       PermissionBackend permissionBackend,
-      GitRepositoryManager repoManager,
       Provider<ListBranches> listBranches,
       CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
       CodeOwnerConfigScanner.Factory codeOwnerConfigScannerFactory,
       CodeOwnerConfigValidator codeOwnerConfigValidator) {
     this.currentUser = currentUser;
     this.permissionBackend = permissionBackend;
-    this.repoManager = repoManager;
     this.listBranches = listBranches;
     this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
     this.codeOwnerConfigScannerFactory = codeOwnerConfigScannerFactory;
@@ -126,25 +120,22 @@
 
     validateInput(projectResource.getNameKey(), branches, input);
 
-    try (Repository repo = repoManager.openRepository(projectResource.getNameKey());
-        RevWalk revWalk = new RevWalk(repo)) {
-      ImmutableMap.Builder<String, Map<String, List<ConsistencyProblemInfo>>>
-          resultsByBranchBuilder = ImmutableMap.builder();
-      branches.stream()
-          .filter(branchNameKey -> shouldValidateBranch(input, branchNameKey))
-          .filter(
-              branchNameKey ->
-                  validateDisabledBranches(input)
-                      || !codeOwnersPluginConfiguration
-                          .getProjectConfig(branchNameKey.project())
-                          .isDisabled(branchNameKey.branch()))
-          .forEach(
-              branchNameKey ->
-                  resultsByBranchBuilder.put(
-                      branchNameKey.branch(),
-                      checkBranch(revWalk, input.path, branchNameKey, input.verbosity)));
-      return Response.ok(resultsByBranchBuilder.build());
-    }
+    ImmutableMap.Builder<String, Map<String, List<ConsistencyProblemInfo>>> resultsByBranchBuilder =
+        ImmutableMap.builder();
+    branches.stream()
+        .filter(branchNameKey -> shouldValidateBranch(input, branchNameKey))
+        .filter(
+            branchNameKey ->
+                validateDisabledBranches(input)
+                    || !codeOwnersPluginConfiguration
+                        .getProjectConfig(branchNameKey.project())
+                        .isDisabled(branchNameKey.branch()))
+        .forEach(
+            branchNameKey ->
+                resultsByBranchBuilder.put(
+                    branchNameKey.branch(),
+                    checkBranch(input.path, branchNameKey, input.verbosity)));
+    return Response.ok(resultsByBranchBuilder.build());
   }
 
   private ImmutableSet<BranchNameKey> branches(ProjectResource projectResource)
@@ -156,7 +147,6 @@
   }
 
   private Map<String, List<ConsistencyProblemInfo>> checkBranch(
-      RevWalk revWalk,
       String pathGlob,
       BranchNameKey branchNameKey,
       @Nullable ConsistencyProblemInfo.Status verbosity) {
@@ -177,7 +167,7 @@
               problemsByPath.putAll(
                   codeOwnerBackend.getFilePath(codeOwnerConfig.key()).toString(),
                   checkCodeOwnerConfig(
-                      branchNameKey, revWalk, codeOwnerBackend, codeOwnerConfig, verbosity));
+                      branchNameKey, codeOwnerBackend, codeOwnerConfig, verbosity));
               return true;
             },
             (codeOwnerConfigFilePath, configInvalidException) -> {
@@ -193,17 +183,12 @@
 
   private ImmutableList<ConsistencyProblemInfo> checkCodeOwnerConfig(
       BranchNameKey branchNameKey,
-      RevWalk revWalk,
       CodeOwnerBackend codeOwnerBackend,
       CodeOwnerConfig codeOwnerConfig,
       @Nullable ConsistencyProblemInfo.Status verbosity) {
     return codeOwnerConfigValidator
         .validateCodeOwnerConfig(
-            branchNameKey,
-            revWalk,
-            currentUser.get().asIdentifiedUser(),
-            codeOwnerBackend,
-            codeOwnerConfig)
+            branchNameKey, currentUser.get().asIdentifiedUser(), codeOwnerBackend, codeOwnerConfig)
         .map(
             commitValidationMessage ->
                 createConsistencyProblemInfo(commitValidationMessage, verbosity))
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFilesInRevision.java b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFilesInRevision.java
index db939c4..a53b19b 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFilesInRevision.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFilesInRevision.java
@@ -29,7 +29,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.patch.DiffNotAvailableException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -76,7 +76,7 @@
   @Override
   public Response<Map<String, List<ConsistencyProblemInfo>>> apply(
       RevisionResource revisionResource, CheckCodeOwnerConfigFilesInRevisionInput input)
-      throws IOException, PatchListNotAvailableException {
+      throws IOException, DiffNotAvailableException {
     logger.atFine().log(
         "checking code owner config files for revision %d of change %d (path = %s)",
         revisionResource.getPatchSet().number(),
@@ -95,7 +95,7 @@
         RevWalk rw = new RevWalk(repository)) {
       RevCommit commit = rw.parseCommit(revisionResource.getPatchSet().commitId());
       return Response.ok(
-          changedFiles.compute(revisionResource.getProject(), commit).stream()
+          changedFiles.getOrCompute(revisionResource.getProject(), commit).stream()
               // filter out deletions (files without new path)
               .filter(changedFile -> changedFile.newPath().isPresent())
               // filter out non code owner config files
@@ -122,7 +122,6 @@
                                   codeOwnerBackend,
                                   revisionResource.getChange().getDest(),
                                   changedFile,
-                                  rw,
                                   commit)
                               .map(
                                   commitValidationMessage ->
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnersInChangeCollection.java b/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnersInChangeCollection.java
index ad0b1ad..e19192b 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnersInChangeCollection.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnersInChangeCollection.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.plugins.codeowners.restapi.CodeOwnersInChangeCollection.PathResource;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.patch.DiffNotAvailableException;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -68,7 +69,8 @@
 
   @Override
   public PathResource parse(RevisionResource revisionResource, IdString id)
-      throws RestApiException, IOException, PatchListNotAvailableException {
+      throws RestApiException, IOException, PatchListNotAvailableException,
+          DiffNotAvailableException {
     // Check if the file exists in the revision only after creating the path resource. This way we
     // get a more specific error response for invalid paths ('400 Bad Request' instead of a '404 Not
     // Found').
@@ -100,8 +102,9 @@
 
   private void checkThatFileExists(
       RevisionResource revisionResource, PathResource pathResource, IdString id)
-      throws RestApiException, IOException, PatchListNotAvailableException {
-    if (!changedFiles.compute(revisionResource).stream()
+      throws RestApiException, IOException, PatchListNotAvailableException,
+          DiffNotAvailableException {
+    if (!changedFiles.getOrCompute(revisionResource).stream()
         .anyMatch(
             changedFile ->
                 // Check whether the path matches any file in the change.
diff --git a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
index 6292194..1666568 100644
--- a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
+++ b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
@@ -66,6 +66,7 @@
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.patch.DiffNotAvailableException;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
@@ -82,7 +83,6 @@
 import java.util.Optional;
 import java.util.stream.Stream;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -204,8 +204,6 @@
           validationResult =
               validateCodeOwnerConfig(
                   receiveEvent.getBranchNameKey(),
-                  receiveEvent.repoConfig,
-                  receiveEvent.revWalk,
                   receiveEvent.commit,
                   receiveEvent.user,
                   codeOwnerConfigValidationPolicy.isForced(),
@@ -294,8 +292,6 @@
           validationResult =
               validateCodeOwnerConfig(
                   branchNameKey,
-                  repository.getConfig(),
-                  revWalk,
                   commit,
                   patchSetUploader,
                   codeOwnerConfigValidationPolicy.isForced(),
@@ -337,7 +333,6 @@
    *
    * @param branchNameKey the project and branch that contains the provided commit or for which the
    *     commit is being pushed
-   * @param revWalk the rev walk that should be used to load revCommit
    * @param revCommit the commit for which newly added and modified code owner configs should be
    *     validated
    * @param user user for which the code owner visibility checks should be performed
@@ -348,8 +343,6 @@
    */
   private Optional<ValidationResult> validateCodeOwnerConfig(
       BranchNameKey branchNameKey,
-      Config repoConfig,
-      RevWalk revWalk,
       RevCommit revCommit,
       IdentifiedUser user,
       boolean force,
@@ -417,12 +410,8 @@
       // MergeCommitStrategy.FILES_WITH_CONFLICT_RESOLUTION is configured.
       ImmutableList<ChangedFile> modifiedCodeOwnerConfigFiles =
           changedFiles
-              .compute(
-                  branchNameKey.project(),
-                  repoConfig,
-                  revWalk,
-                  revCommit,
-                  MergeCommitStrategy.ALL_CHANGED_FILES)
+              .getOrCompute(
+                  branchNameKey.project(), revCommit, MergeCommitStrategy.ALL_CHANGED_FILES)
               .stream()
               // filter out deletions (files without new path)
               .filter(changedFile -> changedFile.newPath().isPresent())
@@ -448,12 +437,7 @@
                   .flatMap(
                       changedFile ->
                           validateCodeOwnerConfig(
-                              user,
-                              codeOwnerBackend,
-                              branchNameKey,
-                              changedFile,
-                              revWalk,
-                              revCommit))));
+                              user, codeOwnerBackend, branchNameKey, changedFile, revCommit))));
     } catch (InvalidPluginConfigurationException e) {
       // If the code-owners plugin configuration is invalid we cannot get the code owners backend
       // and hence we are not able to detect and validate code owner config files. Instead of
@@ -473,7 +457,7 @@
                   "code-owners plugin configuration is invalid,"
                       + " cannot validate code owner config files",
                   ValidationMessage.Type.WARNING)));
-    } catch (IOException e) {
+    } catch (IOException | DiffNotAvailableException e) {
       String errorMessage =
           String.format(
               "failed to validate code owner config files in revision %s"
@@ -491,7 +475,6 @@
    * @param codeOwnerBackend the code owner backend from which the code owner config can be loaded
    * @param branchNameKey the project and branch of the code owner config
    * @param changedFile the changed file that represents the code owner config
-   * @param revWalk the rev walk that should be used to load revCommit
    * @param revCommit the commit from which the code owner config should be loaded
    * @return a stream of validation messages that describe issues with the code owner config, an
    *     empty stream if there are no issues
@@ -501,13 +484,11 @@
       CodeOwnerBackend codeOwnerBackend,
       BranchNameKey branchNameKey,
       ChangedFile changedFile,
-      RevWalk revWalk,
       RevCommit revCommit) {
     requireNonNull(user, "user");
     requireNonNull(codeOwnerBackend, "codeOwnerBackend");
     requireNonNull(branchNameKey, "branchNameKey");
     requireNonNull(changedFile, "changedFile");
-    requireNonNull(revWalk, "revWalk");
     requireNonNull(revCommit, "revCommit");
 
     if (!changedFile.newPath().isPresent()) {
@@ -524,7 +505,7 @@
           createCodeOwnerConfigKey(branchNameKey, changedFile.newPath().get());
       codeOwnerConfig =
           codeOwnerBackend
-              .getCodeOwnerConfig(codeOwnerConfigKey, revWalk, revCommit)
+              .getCodeOwnerConfig(codeOwnerConfigKey, revCommit)
               // We already know that the path exists, so either the code owner config is
               // successfully loaded (this case) or the loading fails with an exception because the
               // code owner config is not parseable (catch block below), but it cannot happen that
@@ -553,7 +534,7 @@
           new CommitValidationMessage(
               invalidCodeOwnerConfigException.get().getMessage(),
               getValidationMessageTypeForParsingError(
-                  codeOwnerBackend, branchNameKey, changedFile, revWalk, revCommit)));
+                  codeOwnerBackend, branchNameKey, changedFile, revCommit)));
     }
 
     // The code owner config was successfully loaded and parsed.
@@ -564,15 +545,14 @@
     Optional<CodeOwnerConfig> baseCodeOwnerConfig;
     try {
       baseCodeOwnerConfig =
-          getBaseCodeOwnerConfig(codeOwnerBackend, branchNameKey, changedFile, revWalk, revCommit);
+          getBaseCodeOwnerConfig(codeOwnerBackend, branchNameKey, changedFile, revCommit);
     } catch (CodeOwnersInternalServerErrorException codeOwnersInternalServerErrorException) {
       if (getInvalidCodeOwnerConfigCause(codeOwnersInternalServerErrorException).isPresent()) {
         // The base code owner config is non-parseable. Since the update makes the code owner
         // config parseable, it is a good update even if the code owner config still contains
         // issues. Hence in this case we downgrade all validation errors in the new version to
         // warnings so that the update is not blocked.
-        return validateCodeOwnerConfig(
-                branchNameKey, revWalk, user, codeOwnerBackend, codeOwnerConfig)
+        return validateCodeOwnerConfig(branchNameKey, user, codeOwnerBackend, codeOwnerConfig)
             .map(CodeOwnerConfigValidator::downgradeErrorToWarning);
       }
 
@@ -583,14 +563,9 @@
     // Validate the parsed code owner config.
     if (baseCodeOwnerConfig.isPresent()) {
       return validateCodeOwnerConfig(
-          branchNameKey,
-          revWalk,
-          user,
-          codeOwnerBackend,
-          codeOwnerConfig,
-          baseCodeOwnerConfig.get());
+          branchNameKey, user, codeOwnerBackend, codeOwnerConfig, baseCodeOwnerConfig.get());
     }
-    return validateCodeOwnerConfig(branchNameKey, revWalk, user, codeOwnerBackend, codeOwnerConfig);
+    return validateCodeOwnerConfig(branchNameKey, user, codeOwnerBackend, codeOwnerConfig);
   }
 
   /**
@@ -621,7 +596,6 @@
    * @param branchNameKey the project and branch of the base code owner config
    * @param changedFile the changed file of the code owner config that contains the path of the base
    *     code owner config as old path
-   * @param revWalk rev walk that should be used to load the base code owner config
    * @param revCommit the commit of the code owner config for which the base code owner config
    *     should be loaded
    * @return the loaded base code owner config, {@link Optional#empty()} if no base code owner
@@ -631,15 +605,13 @@
       CodeOwnerBackend codeOwnerBackend,
       BranchNameKey branchNameKey,
       ChangedFile changedFile,
-      RevWalk revWalk,
       RevCommit revCommit) {
     if (changedFile.oldPath().isPresent()) {
       Optional<ObjectId> parentRevision = getParentRevision(branchNameKey.project(), revCommit);
       if (parentRevision.isPresent()) {
         CodeOwnerConfig.Key baseCodeOwnerConfigKey =
             createCodeOwnerConfigKey(branchNameKey, changedFile.oldPath().get());
-        return codeOwnerBackend.getCodeOwnerConfig(
-            baseCodeOwnerConfigKey, revWalk, parentRevision.get());
+        return codeOwnerBackend.getCodeOwnerConfig(baseCodeOwnerConfigKey, parentRevision.get());
       }
     }
     return Optional.empty();
@@ -665,7 +637,6 @@
    * @param codeOwnerBackend the code owner backend from which the code owner config can be loaded
    * @param branchNameKey the project and branch of the code owner config
    * @param changedFile the changed file that represents the code owner config
-   * @param revWalk rev walk that should be used to load the code owner config
    * @param revCommit the commit from which the code owner config should be loaded
    * @return the {@link com.google.gerrit.server.git.validators.ValidationMessage.Type} (ERROR or
    *     WARNING) that should be used for parsing error of a code owner config file
@@ -674,7 +645,6 @@
       CodeOwnerBackend codeOwnerBackend,
       BranchNameKey branchNameKey,
       ChangedFile changedFile,
-      RevWalk revWalk,
       RevCommit revCommit) {
     //
     if (changedFile.oldPath().isPresent()) {
@@ -693,7 +663,7 @@
         // there.
         CodeOwnerConfig.Key baseCodeOwnerConfigKey =
             createCodeOwnerConfigKey(branchNameKey, changedFile.oldPath().get());
-        codeOwnerBackend.getCodeOwnerConfig(baseCodeOwnerConfigKey, revWalk, parentRevision);
+        codeOwnerBackend.getCodeOwnerConfig(baseCodeOwnerConfigKey, parentRevision);
         // The code owner config at the parent revision is parseable. This means the parsing error
         // is introduced by the new commit and we should block uploading it, which we achieve by
         // setting the validation message type to fatal.
@@ -757,7 +727,6 @@
    * they are not newly introduced by the given code owner config).
    *
    * @param branchNameKey the branch and the project
-   * @param revWalk rev walk that should be used to load the code owner configs
    * @param user user for which the code owner visibility checks should be performed
    * @param codeOwnerBackend the code owner backend from which the code owner configs were loaded
    * @param codeOwnerConfig the code owner config that should be validated
@@ -767,7 +736,6 @@
    */
   private Stream<CommitValidationMessage> validateCodeOwnerConfig(
       BranchNameKey branchNameKey,
-      RevWalk revWalk,
       IdentifiedUser user,
       CodeOwnerBackend codeOwnerBackend,
       CodeOwnerConfig codeOwnerConfig,
@@ -776,9 +744,9 @@
     requireNonNull(baseCodeOwnerConfig, "baseCodeOwnerConfig");
 
     ImmutableSet<CommitValidationMessage> issuesInBaseVersion =
-        validateCodeOwnerConfig(branchNameKey, revWalk, user, codeOwnerBackend, baseCodeOwnerConfig)
+        validateCodeOwnerConfig(branchNameKey, user, codeOwnerBackend, baseCodeOwnerConfig)
             .collect(toImmutableSet());
-    return validateCodeOwnerConfig(branchNameKey, revWalk, user, codeOwnerBackend, codeOwnerConfig)
+    return validateCodeOwnerConfig(branchNameKey, user, codeOwnerBackend, codeOwnerConfig)
         .map(
             commitValidationMessage ->
                 issuesInBaseVersion.contains(commitValidationMessage)
@@ -790,7 +758,6 @@
    * Validates the given code owner config and returns validation issues as stream.
    *
    * @param branchNameKey the branch and the project
-   * @param revWalk rev walk that should be used to load the code owner configs from {@code project}
    * @param user user for which the code owner visibility checks should be performed
    * @param codeOwnerBackend the code owner backend from which the code owner config was loaded
    * @param codeOwnerConfig the code owner config that should be validated
@@ -799,7 +766,6 @@
    */
   public Stream<CommitValidationMessage> validateCodeOwnerConfig(
       BranchNameKey branchNameKey,
-      RevWalk revWalk,
       IdentifiedUser user,
       CodeOwnerBackend codeOwnerBackend,
       CodeOwnerConfig codeOwnerConfig) {
@@ -811,10 +777,7 @@
             codeOwnerBackend.getFilePath(codeOwnerConfig.key()),
             codeOwnerConfig),
         validateImports(
-            branchNameKey,
-            revWalk,
-            codeOwnerBackend.getFilePath(codeOwnerConfig.key()),
-            codeOwnerConfig));
+            branchNameKey, codeOwnerBackend.getFilePath(codeOwnerConfig.key()), codeOwnerConfig));
   }
 
   /**
@@ -891,7 +854,6 @@
    * Validates the imports of the given code owner config.
    *
    * @param branchNameKey the branch and the project
-   * @param revWalk rev walk that should be used to load the code owner configs from {@code project}
    * @param codeOwnerConfigFilePath the path of the code owner config file which contains the code
    *     owner config
    * @param codeOwnerConfig the code owner config for which the imports should be validated
@@ -899,19 +861,19 @@
    *     if there are no issues
    */
   private Stream<CommitValidationMessage> validateImports(
-      BranchNameKey branchNameKey,
-      RevWalk revWalk,
-      Path codeOwnerConfigFilePath,
-      CodeOwnerConfig codeOwnerConfig) {
+      BranchNameKey branchNameKey, Path codeOwnerConfigFilePath, CodeOwnerConfig codeOwnerConfig) {
     try {
-      RevCommit codeOwnerConfigRevision = revWalk.parseCommit(codeOwnerConfig.revision());
+      RevCommit codeOwnerConfigRevision;
+      try (Repository repo = repoManager.openRepository(branchNameKey.project());
+          RevWalk revWalk = new RevWalk(repo)) {
+        codeOwnerConfigRevision = revWalk.parseCommit(codeOwnerConfig.revision());
+      }
       return Streams.concat(
               codeOwnerConfig.imports().stream()
                   .map(
                       codeOwnerConfigReference ->
                           validateCodeOwnerConfigReference(
                               branchNameKey,
-                              revWalk,
                               codeOwnerConfigFilePath,
                               codeOwnerConfig.key(),
                               codeOwnerConfigRevision,
@@ -923,7 +885,6 @@
                       codeOwnerConfigReference ->
                           validateCodeOwnerConfigReference(
                               branchNameKey,
-                              revWalk,
                               codeOwnerConfigFilePath,
                               codeOwnerConfig.key(),
                               codeOwnerConfigRevision,
@@ -941,7 +902,6 @@
    * Validates a code owner config reference.
    *
    * @param branchNameKey the branch and the project
-   * @param revWalk rev walk that should be used to load the code owner configs from {@code project}
    * @param codeOwnerConfigFilePath the path of the code owner config file which contains the code
    *     owner config reference
    * @param keyOfImportingCodeOwnerConfig key of the importing code owner config
@@ -954,7 +914,6 @@
    */
   private Optional<CommitValidationMessage> validateCodeOwnerConfigReference(
       BranchNameKey branchNameKey,
-      RevWalk revWalk,
       Path codeOwnerConfigFilePath,
       CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig,
       RevCommit codeOwnerConfigRevision,
@@ -1040,8 +999,7 @@
       // from it could fail with MissingObjectException.
       Optional<CodeOwnerConfig> importedCodeOwnerConfig =
           keyOfImportedCodeOwnerConfig.project().equals(branchNameKey.project())
-              ? codeOwnerBackend.getCodeOwnerConfig(
-                  keyOfImportedCodeOwnerConfig, revWalk, revision.get())
+              ? codeOwnerBackend.getCodeOwnerConfig(keyOfImportedCodeOwnerConfig, revision.get())
               : codeOwnerBackend.getCodeOwnerConfig(keyOfImportedCodeOwnerConfig, revision.get());
       if (!importedCodeOwnerConfig.isPresent()) {
         return nonResolvableImport(
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorErrorHandlingIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorErrorHandlingIT.java
index e1936b1..a1c34ba 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorErrorHandlingIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorErrorHandlingIT.java
@@ -19,6 +19,7 @@
 
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Project.NameKey;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -35,7 +36,6 @@
 import java.nio.file.Path;
 import java.util.Optional;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -124,7 +124,7 @@
 
     @Override
     public Optional<CodeOwnerConfig> getCodeOwnerConfig(
-        CodeOwnerConfig.Key codeOwnerConfigKey, RevWalk revWalk, ObjectId revision) {
+        CodeOwnerConfig.Key codeOwnerConfigKey, ObjectId revision) {
       return Optional.empty();
     }
 
@@ -142,7 +142,7 @@
     }
 
     @Override
-    public Optional<PathExpressionMatcher> getPathExpressionMatcher() {
+    public Optional<PathExpressionMatcher> getPathExpressionMatcher(BranchNameKey branchNameKey) {
       return Optional.empty();
     }
   }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java
index 4724629..432057b 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java
@@ -2079,9 +2079,7 @@
         .create();
 
     // Create a change that merges the other branch into master. The code owner config files in the
-    // created merge commit will be validated. This only works if CodeOwnerConfigValidator uses the
-    // same RevWalk instance that inserted the new merge commit. If it doesn't, the create change
-    // call below would fail with a MissingObjectException.
+    // created merge commit will be validated.
     ChangeInput changeInput = new ChangeInput();
     changeInput.project = project.get();
     changeInput.branch = "master";
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersPluginConfigValidatorIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersPluginConfigValidatorIT.java
index 3b1258e..17ba5c4 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersPluginConfigValidatorIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersPluginConfigValidatorIT.java
@@ -20,6 +20,7 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
 import static com.google.gerrit.plugins.codeowners.testing.RequiredApprovalSubject.assertThat;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.truth.OptionalSubject.assertThat;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -35,6 +36,7 @@
 import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackendId;
 import com.google.gerrit.plugins.codeowners.backend.FallbackCodeOwners;
+import com.google.gerrit.plugins.codeowners.backend.PathExpressions;
 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.GeneralConfig;
@@ -242,6 +244,86 @@
   }
 
   @Test
+  public void configurePathExpressionsForProject() throws Exception {
+    fetchRefsMetaConfig();
+
+    Config cfg = new Config();
+    cfg.setString(
+        CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS,
+        /* subsection= */ null,
+        BackendConfig.KEY_PATH_EXPRESSIONS,
+        PathExpressions.GLOB.name());
+    setCodeOwnersConfig(cfg);
+
+    PushResult r = pushRefsMetaConfig();
+    assertThat(r.getRemoteUpdate(RefNames.REFS_CONFIG).getStatus()).isEqualTo(Status.OK);
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getPathExpressions("master"))
+        .value()
+        .isEqualTo(PathExpressions.GLOB);
+  }
+
+  @Test
+  public void configurePathExpressionsForBranch() throws Exception {
+    fetchRefsMetaConfig();
+
+    Config cfg = new Config();
+    cfg.setString(
+        CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS,
+        "master",
+        BackendConfig.KEY_PATH_EXPRESSIONS,
+        PathExpressions.GLOB.name());
+    setCodeOwnersConfig(cfg);
+
+    PushResult r = pushRefsMetaConfig();
+    assertThat(r.getRemoteUpdate(RefNames.REFS_CONFIG).getStatus()).isEqualTo(Status.OK);
+    assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getPathExpressions("master"))
+        .value()
+        .isEqualTo(PathExpressions.GLOB);
+  }
+
+  @Test
+  public void cannotConfigureInvalidPathExpressionsForProject() throws Exception {
+    fetchRefsMetaConfig();
+
+    Config cfg = new Config();
+    cfg.setString(
+        CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS,
+        /* subsection= */ null,
+        BackendConfig.KEY_PATH_EXPRESSIONS,
+        "INVALID");
+    setCodeOwnersConfig(cfg);
+
+    PushResult r = pushRefsMetaConfig();
+    assertThat(r.getRemoteUpdate(RefNames.REFS_CONFIG).getStatus())
+        .isEqualTo(Status.REJECTED_OTHER_REASON);
+    assertThat(r.getMessages())
+        .contains(
+            "Path expressions 'INVALID' that are configured in code-owners.config"
+                + " (parameter codeOwners.pathExpressions) not found.");
+  }
+
+  @Test
+  public void cannotConfigureInvalidPathExpressionsForBranch() throws Exception {
+    fetchRefsMetaConfig();
+
+    Config cfg = new Config();
+    cfg.setString(
+        CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS,
+        "master",
+        BackendConfig.KEY_PATH_EXPRESSIONS,
+        "INVALID");
+    setCodeOwnersConfig(cfg);
+
+    PushResult r = pushRefsMetaConfig();
+    assertThat(r.getRemoteUpdate(RefNames.REFS_CONFIG).getStatus())
+        .isEqualTo(Status.REJECTED_OTHER_REASON);
+    assertThat(r.getMessages())
+        .contains(
+            "Path expressions 'INVALID' that are configured in code-owners.config"
+                + " (parameter codeOwners.master.pathExpressions) not found.");
+  }
+
+  @Test
   public void configureRequiredApproval() throws Exception {
     fetchRefsMetaConfig();
 
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/testsuite/TestPathExpressionsTest.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/testsuite/TestPathExpressionsTest.java
index cd155ba..b3d15d6 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/testsuite/TestPathExpressionsTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/testsuite/TestPathExpressionsTest.java
@@ -19,6 +19,7 @@
 
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Project.NameKey;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.PrivateInternals_DynamicMapImpl;
@@ -35,7 +36,6 @@
 import java.nio.file.Path;
 import java.util.Optional;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -200,7 +200,7 @@
 
     @Override
     public Optional<CodeOwnerConfig> getCodeOwnerConfig(
-        CodeOwnerConfig.Key codeOwnerConfigKey, RevWalk revWalk, ObjectId revision) {
+        CodeOwnerConfig.Key codeOwnerConfigKey, ObjectId revision) {
       throw new UnsupportedOperationException();
     }
 
@@ -218,7 +218,7 @@
     }
 
     @Override
-    public Optional<PathExpressionMatcher> getPathExpressionMatcher() {
+    public Optional<PathExpressionMatcher> getPathExpressionMatcher(BranchNameKey branchNameKey) {
       return Optional.ofNullable(pathExpressionMatcher);
     }
   }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackendTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackendTest.java
index 1bbce45..7ec9295 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackendTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackendTest.java
@@ -21,6 +21,7 @@
 
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest;
 import com.google.gerrit.plugins.codeowners.testing.backend.TestCodeOwnerConfigStorage;
 import com.google.gerrit.plugins.codeowners.util.JgitPath;
@@ -487,4 +488,13 @@
         .isEqualTo(
             Paths.get(codeOwnerConfigKey.folderPath() + codeOwnerConfigKey.fileName().get()));
   }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.pathExpressions", value = "GLOB")
+  public void getConfiguredPathExpressionMatcher() throws Exception {
+    com.google.gerrit.truth.OptionalSubject.assertThat(
+            codeOwnerBackend.getPathExpressionMatcher(BranchNameKey.create(project, "master")))
+        .value()
+        .isInstanceOf(GlobMatcher.class);
+  }
 }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/BUILD b/javatests/com/google/gerrit/plugins/codeowners/backend/BUILD
index f21c5c1..5db2a85 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/BUILD
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/BUILD
@@ -36,6 +36,7 @@
         "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/com/google/gerrit/truth",
         "//lib:guava",
         "//lib:jgit",
         "//lib:jgit-junit",
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/ChangedFilesTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/ChangedFilesTest.java
index 4534df2..fc8f176 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/ChangedFilesTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/ChangedFilesTest.java
@@ -68,24 +68,26 @@
   public void cannotComputeForNullRevisionResource() throws Exception {
     NullPointerException npe =
         assertThrows(
-            NullPointerException.class, () -> changedFiles.compute(/* revisionResource= */ null));
+            NullPointerException.class,
+            () -> changedFiles.getOrCompute(/* revisionResource= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("revisionResource");
   }
 
   @Test
-  public void cannotComputeForNullProject() throws Exception {
+  public void cannotGetOrComputeForNullProject() throws Exception {
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () -> changedFiles.compute(/* project= */ null, ObjectId.zeroId()));
+            () -> changedFiles.getOrCompute(/* project= */ null, ObjectId.zeroId()));
     assertThat(npe).hasMessageThat().isEqualTo("project");
   }
 
   @Test
-  public void cannotComputeForNullRevision() throws Exception {
+  public void cannotGetOrComputeForNullRevision() throws Exception {
     NullPointerException npe =
         assertThrows(
-            NullPointerException.class, () -> changedFiles.compute(project, /* revision= */ null));
+            NullPointerException.class,
+            () -> changedFiles.getOrCompute(project, /* revision= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("revision");
   }
 
@@ -96,7 +98,7 @@
         createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId();
 
     ImmutableList<ChangedFile> changedFilesSet =
-        changedFiles.compute(getRevisionResource(changeId));
+        changedFiles.getOrCompute(getRevisionResource(changeId));
     assertThat(changedFilesSet).hasSize(1);
     ChangedFile changedFile = Iterables.getOnlyElement(changedFilesSet);
     assertThat(changedFile).hasNewPath().value().isEqualTo(Paths.get(path));
@@ -115,7 +117,7 @@
             .getChangeId();
 
     ImmutableList<ChangedFile> changedFilesSet =
-        changedFiles.compute(getRevisionResource(changeId));
+        changedFiles.getOrCompute(getRevisionResource(changeId));
     assertThat(changedFilesSet).hasSize(1);
     ChangedFile changedFile = Iterables.getOnlyElement(changedFilesSet);
     assertThat(changedFile).hasNewPath().value().isEqualTo(Paths.get(path));
@@ -130,7 +132,7 @@
     String changeId = createChangeWithFileDeletion(path);
 
     ImmutableList<ChangedFile> changedFilesSet =
-        changedFiles.compute(getRevisionResource(changeId));
+        changedFiles.getOrCompute(getRevisionResource(changeId));
     assertThat(changedFilesSet).hasSize(1);
     ChangedFile changedFile = Iterables.getOnlyElement(changedFilesSet);
     assertThat(changedFile).hasNewPath().isEmpty();
@@ -151,7 +153,7 @@
     // ChangedFiles uses a DiffFormatter without rename detection (because rename detection requires
     // loading the file contents which is too expensive).
     ImmutableList<ChangedFile> changedFilesSet =
-        changedFiles.compute(getRevisionResource(changeId));
+        changedFiles.getOrCompute(getRevisionResource(changeId));
     assertThat(changedFilesSet).hasSize(2);
     ChangedFileSubject changedFile1 = assertThatCollection(changedFilesSet).element(0);
     changedFile1.hasNewPath().value().isEqualTo(Paths.get(newPath));
@@ -174,7 +176,7 @@
     String changeId = r.getChangeId();
 
     ImmutableList<ChangedFile> changedFilesSet =
-        changedFiles.compute(getRevisionResource(changeId));
+        changedFiles.getOrCompute(getRevisionResource(changeId));
     assertThat(changedFilesSet).hasSize(1);
     ChangedFile changedFile = Iterables.getOnlyElement(changedFilesSet);
     assertThat(changedFile).hasNewPath().value().isEqualTo(Paths.get(path));
@@ -254,7 +256,7 @@
             .create();
 
     ImmutableList<ChangedFile> changedFilesSet =
-        changedFiles.compute(getRevisionResource(Integer.toString(mergeChange.get())));
+        changedFiles.getOrCompute(getRevisionResource(Integer.toString(mergeChange.get())));
 
     if (MergeCommitStrategy.ALL_CHANGED_FILES.equals(mergeCommitStrategy)) {
       assertThat(changedFilesSet).comparingElementsUsing(hasPath()).containsExactly(file1, file2);
@@ -327,7 +329,7 @@
             .create();
 
     ImmutableList<ChangedFile> changedFilesSet =
-        changedFiles.compute(getRevisionResource(Integer.toString(mergeChange.get())));
+        changedFiles.getOrCompute(getRevisionResource(Integer.toString(mergeChange.get())));
     ImmutableSet<String> oldPaths =
         changedFilesSet.stream()
             .map(changedFile -> JgitPath.of(changedFile.oldPath().get()).get())
@@ -359,7 +361,7 @@
             .getChangeId();
 
     ImmutableList<ChangedFile> changedFilesSet =
-        changedFiles.compute(getRevisionResource(changeId));
+        changedFiles.getOrCompute(getRevisionResource(changeId));
     assertThat(changedFilesSet)
         .comparingElementsUsing(hasPath())
         .containsExactly(file4, file3, file5, file1, file2)
@@ -470,7 +472,7 @@
             .create();
 
     ImmutableList<ChangedFile> changedFilesSet =
-        changedFiles.compute(getRevisionResource(Integer.toString(mergeChange.get())));
+        changedFiles.getOrCompute(getRevisionResource(Integer.toString(mergeChange.get())));
 
     if (MergeCommitStrategy.ALL_CHANGED_FILES.equals(mergeCommitStrategy)) {
       assertThat(changedFilesSet)
@@ -491,7 +493,9 @@
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () -> changedFiles.getFromDiffCache(/* project= */ null, ObjectId.zeroId()));
+            () ->
+                changedFiles.getFromDiffCache(
+                    /* project= */ null, ObjectId.zeroId(), MergeCommitStrategy.ALL_CHANGED_FILES));
     assertThat(npe).hasMessageThat().isEqualTo("project");
   }
 
@@ -500,17 +504,31 @@
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () -> changedFiles.getFromDiffCache(project, /* revision= */ null));
+            () ->
+                changedFiles.getFromDiffCache(
+                    project, /* revision= */ null, MergeCommitStrategy.ALL_CHANGED_FILES));
     assertThat(npe).hasMessageThat().isEqualTo("revision");
   }
 
   @Test
+  public void cannotGetFromDiffCacheForNullMergeCommitStrategy() throws Exception {
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class,
+            () ->
+                changedFiles.getFromDiffCache(
+                    project, ObjectId.zeroId(), /* mergeCommitStrategy= */ null));
+    assertThat(npe).hasMessageThat().isEqualTo("mergeCommitStrategy");
+  }
+
+  @Test
   public void getFromDiffCacheForChangeThatAddedAFile() throws Exception {
     String path = "/foo/bar/baz.txt";
     RevCommit commit =
         createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getCommit();
 
-    ImmutableList<ChangedFile> changedFilesSet = changedFiles.getFromDiffCache(project, commit);
+    ImmutableList<ChangedFile> changedFilesSet =
+        changedFiles.getFromDiffCache(project, commit, MergeCommitStrategy.ALL_CHANGED_FILES);
     assertThat(changedFilesSet).hasSize(1);
     ChangedFile changedFile = Iterables.getOnlyElement(changedFilesSet);
     assertThat(changedFile).hasNewPath().value().isEqualTo(Paths.get(path));
@@ -528,7 +546,8 @@
         createChange("Change Modifying A File", JgitPath.of(path).get(), "new file content")
             .getCommit();
 
-    ImmutableList<ChangedFile> changedFilesSet = changedFiles.getFromDiffCache(project, commit);
+    ImmutableList<ChangedFile> changedFilesSet =
+        changedFiles.getFromDiffCache(project, commit, MergeCommitStrategy.ALL_CHANGED_FILES);
     assertThat(changedFilesSet).hasSize(1);
     ChangedFile changedFile = Iterables.getOnlyElement(changedFilesSet);
     assertThat(changedFile).hasNewPath().value().isEqualTo(Paths.get(path));
@@ -544,7 +563,9 @@
 
     ImmutableList<ChangedFile> changedFilesSet =
         changedFiles.getFromDiffCache(
-            project, getRevisionResource(changeId).getPatchSet().commitId());
+            project,
+            getRevisionResource(changeId).getPatchSet().commitId(),
+            MergeCommitStrategy.ALL_CHANGED_FILES);
     assertThat(changedFilesSet).hasSize(1);
     ChangedFile changedFile = Iterables.getOnlyElement(changedFilesSet);
     assertThat(changedFile).hasNewPath().isEmpty();
@@ -563,7 +584,9 @@
 
     ImmutableList<ChangedFile> changedFilesSet =
         changedFiles.getFromDiffCache(
-            project, getRevisionResource(changeId).getPatchSet().commitId());
+            project,
+            getRevisionResource(changeId).getPatchSet().commitId(),
+            MergeCommitStrategy.ALL_CHANGED_FILES);
     ChangedFileSubject changedFile = assertThatCollection(changedFilesSet).onlyElement();
     changedFile.hasNewPath().value().isEqualTo(Paths.get(newPath));
     changedFile.hasOldPath().value().isEqualTo(Paths.get(oldPath));
@@ -579,10 +602,14 @@
         createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getCommit();
     assertThat(commit.getParents()).isEmpty();
 
-    IllegalStateException exception =
-        assertThrows(
-            IllegalStateException.class, () -> changedFiles.getFromDiffCache(project, commit));
-    assertThat(exception).hasMessageThat().isEqualTo("diff cache doesn't support initial commits");
+    ImmutableList<ChangedFile> changedFilesSet =
+        changedFiles.getFromDiffCache(project, commit, MergeCommitStrategy.ALL_CHANGED_FILES);
+    assertThat(changedFilesSet).hasSize(1);
+    ChangedFile changedFile = Iterables.getOnlyElement(changedFilesSet);
+    assertThat(changedFile).hasNewPath().value().isEqualTo(Paths.get(path));
+    assertThat(changedFile).hasOldPath().isEmpty();
+    assertThat(changedFile).isNoRename();
+    assertThat(changedFile).isNoDeletion();
   }
 
   @Test
@@ -659,7 +686,8 @@
     ImmutableList<ChangedFile> changedFilesSet =
         changedFiles.getFromDiffCache(
             project,
-            getRevisionResource(Integer.toString(mergeChange.get())).getPatchSet().commitId());
+            getRevisionResource(Integer.toString(mergeChange.get())).getPatchSet().commitId(),
+            mergeCommitStrategy);
 
     if (MergeCommitStrategy.ALL_CHANGED_FILES.equals(mergeCommitStrategy)) {
       assertThat(changedFilesSet).comparingElementsUsing(hasPath()).containsExactly(file1, file2);
@@ -675,7 +703,8 @@
   public void
       getFromFileDiffCacheForMergeChangeThatContainsADeletedFileAsConflictResolution_allChangedFiles()
           throws Exception {
-    testGetFromFileDiffCacheForMergeChangeThatContainsADeletedFileAsConflictResolution();
+    testGetFromFileDiffCacheForMergeChangeThatContainsADeletedFileAsConflictResolution(
+        MergeCommitStrategy.ALL_CHANGED_FILES);
   }
 
   @Test
@@ -685,11 +714,12 @@
   public void
       getFromFileDiffCacheForMergeChangeThatContainsADeletedFileAsConflictResolution_filesWithConflictResolution()
           throws Exception {
-    testGetFromFileDiffCacheForMergeChangeThatContainsADeletedFileAsConflictResolution();
+    testGetFromFileDiffCacheForMergeChangeThatContainsADeletedFileAsConflictResolution(
+        MergeCommitStrategy.FILES_WITH_CONFLICT_RESOLUTION);
   }
 
-  private void testGetFromFileDiffCacheForMergeChangeThatContainsADeletedFileAsConflictResolution()
-      throws Exception {
+  private void testGetFromFileDiffCacheForMergeChangeThatContainsADeletedFileAsConflictResolution(
+      MergeCommitStrategy mergeCommitStrategy) throws Exception {
     setAsRootCodeOwners(admin);
 
     String file = "foo/a.txt";
@@ -735,7 +765,8 @@
     ImmutableList<ChangedFile> changedFilesSet =
         changedFiles.getFromDiffCache(
             project,
-            getRevisionResource(Integer.toString(mergeChange.get())).getPatchSet().commitId());
+            getRevisionResource(Integer.toString(mergeChange.get())).getPatchSet().commitId(),
+            mergeCommitStrategy);
     ImmutableSet<String> oldPaths =
         changedFilesSet.stream()
             .map(changedFile -> JgitPath.of(changedFile.oldPath().get()).get())
@@ -766,7 +797,8 @@
                     "file content"))
             .getCommit();
 
-    ImmutableList<ChangedFile> changedFilesSet = changedFiles.getFromDiffCache(project, commit);
+    ImmutableList<ChangedFile> changedFilesSet =
+        changedFiles.getFromDiffCache(project, commit, MergeCommitStrategy.ALL_CHANGED_FILES);
     assertThat(changedFilesSet)
         .comparingElementsUsing(hasPath())
         .containsExactly(file4, file3, file5, file1, file2)
@@ -880,7 +912,8 @@
     ImmutableList<ChangedFile> changedFilesSet =
         changedFiles.getFromDiffCache(
             project,
-            getRevisionResource(Integer.toString(mergeChange.get())).getPatchSet().commitId());
+            getRevisionResource(Integer.toString(mergeChange.get())).getPatchSet().commitId(),
+            mergeCommitStrategy);
 
     if (MergeCommitStrategy.ALL_CHANGED_FILES.equals(mergeCommitStrategy)) {
       assertThat(changedFilesSet)
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerBackendIdTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerBackendIdTest.java
index 3f5ee09..a5e56f2 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerBackendIdTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerBackendIdTest.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Project.NameKey;
 import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest;
 import com.google.gerrit.plugins.codeowners.backend.findowners.FindOwnersBackend;
@@ -26,7 +27,6 @@
 import java.nio.file.Path;
 import java.util.Optional;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.Test;
 
 /** Tests for {@link com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackendId}. */
@@ -50,9 +50,7 @@
   private static class TestCodeOwnerBackend implements CodeOwnerBackend {
     @Override
     public Optional<CodeOwnerConfig> getCodeOwnerConfig(
-        CodeOwnerConfig.Key codeOwnerConfigKey,
-        @Nullable RevWalk revWalk,
-        @Nullable ObjectId revision) {
+        CodeOwnerConfig.Key codeOwnerConfigKey, @Nullable ObjectId revision) {
       throw new UnsupportedOperationException("not implemented");
     }
 
@@ -75,7 +73,7 @@
     }
 
     @Override
-    public Optional<PathExpressionMatcher> getPathExpressionMatcher() {
+    public Optional<PathExpressionMatcher> getPathExpressionMatcher(BranchNameKey branchNameKey) {
       return Optional.empty();
     }
   }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersTest.java
index 92f7f5b..5deb1d6 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersTest.java
@@ -49,7 +49,6 @@
 import java.util.Optional;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -2226,9 +2225,7 @@
 
     @Override
     public Optional<CodeOwnerConfig> getCodeOwnerConfig(
-        CodeOwnerConfig.Key codeOwnerConfigKey,
-        @Nullable RevWalk revWalk,
-        @Nullable ObjectId revision) {
+        CodeOwnerConfig.Key codeOwnerConfigKey, @Nullable ObjectId revision) {
       throw new UnsupportedOperationException("not implemented");
     }
 
@@ -2241,7 +2238,7 @@
     }
 
     @Override
-    public Optional<PathExpressionMatcher> getPathExpressionMatcher() {
+    public Optional<PathExpressionMatcher> getPathExpressionMatcher(BranchNameKey branchNameKey) {
       return Optional.ofNullable(pathExpressionMatcher);
     }
 
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/config/BackendConfigTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/config/BackendConfigTest.java
index 10ef51c..dcc9cb3 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/config/BackendConfigTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/config/BackendConfigTest.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.plugins.codeowners.backend.config.BackendConfig.KEY_BACKEND;
+import static com.google.gerrit.plugins.codeowners.backend.config.BackendConfig.KEY_PATH_EXPRESSIONS;
 import static com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static com.google.gerrit.truth.OptionalSubject.assertThat;
@@ -26,6 +27,7 @@
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackendId;
+import com.google.gerrit.plugins.codeowners.backend.PathExpressions;
 import com.google.gerrit.plugins.codeowners.backend.findowners.FindOwnersBackend;
 import com.google.gerrit.plugins.codeowners.backend.proto.ProtoBackend;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
@@ -186,6 +188,106 @@
   }
 
   @Test
+  public void cannotGetPathExpressionsForBranchWithNullPluginConfig() throws Exception {
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class,
+            () ->
+                backendConfig.getPathExpressionsForBranch(
+                    null, BranchNameKey.create(project, "master")));
+    assertThat(npe).hasMessageThat().isEqualTo("pluginConfig");
+  }
+
+  @Test
+  public void cannotGetPathExpressionsForBranchForNullBranch() throws Exception {
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class,
+            () -> backendConfig.getPathExpressionsForBranch(new Config(), null));
+    assertThat(npe).hasMessageThat().isEqualTo("branch");
+  }
+
+  @Test
+  public void getPathExpressionsForBranchWhenPathExpressionsAreNotSet() throws Exception {
+    assertThat(
+            backendConfig.getPathExpressionsForBranch(
+                new Config(), BranchNameKey.create(project, "master")))
+        .isEmpty();
+  }
+
+  @Test
+  public void getPathExpressionsForBranch() throws Exception {
+    Config cfg = new Config();
+    cfg.setString(
+        SECTION_CODE_OWNERS,
+        "refs/heads/master",
+        KEY_PATH_EXPRESSIONS,
+        PathExpressions.GLOB.name());
+    assertThat(
+            backendConfig.getPathExpressionsForBranch(cfg, BranchNameKey.create(project, "master")))
+        .value()
+        .isEqualTo(PathExpressions.GLOB);
+  }
+
+  @Test
+  public void getPathExpressionsForBranchShortName() throws Exception {
+    Config cfg = new Config();
+    cfg.setString(SECTION_CODE_OWNERS, "master", KEY_PATH_EXPRESSIONS, PathExpressions.GLOB.name());
+    assertThat(
+            backendConfig.getPathExpressionsForBranch(cfg, BranchNameKey.create(project, "master")))
+        .value()
+        .isEqualTo(PathExpressions.GLOB);
+  }
+
+  @Test
+  public void getPathExpressionsForBranchIfConfigIsInvalid() throws Exception {
+    Config cfg = new Config();
+    cfg.setString(SECTION_CODE_OWNERS, "master", KEY_PATH_EXPRESSIONS, "INVALID");
+    assertThat(
+            backendConfig.getPathExpressionsForBranch(cfg, BranchNameKey.create(project, "master")))
+        .isEmpty();
+  }
+
+  @Test
+  public void cannotGetPathExpressionsForProjectWithNullPluginConfig() throws Exception {
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class,
+            () -> backendConfig.getPathExpressionsForProject(null, project));
+    assertThat(npe).hasMessageThat().isEqualTo("pluginConfig");
+  }
+
+  @Test
+  public void cannotGetPathExpressionsForProjectForNullProject() throws Exception {
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class,
+            () -> backendConfig.getPathExpressionsForProject(new Config(), null));
+    assertThat(npe).hasMessageThat().isEqualTo("project");
+  }
+
+  @Test
+  public void getPathExpressionsForProjectWhenBackendIsNotSet() throws Exception {
+    assertThat(backendConfig.getPathExpressionsForProject(new Config(), project)).isEmpty();
+  }
+
+  @Test
+  public void getPathExpressionsForProject() throws Exception {
+    Config cfg = new Config();
+    cfg.setString(SECTION_CODE_OWNERS, null, KEY_PATH_EXPRESSIONS, PathExpressions.GLOB.name());
+    assertThat(backendConfig.getPathExpressionsForProject(cfg, project))
+        .value()
+        .isEqualTo(PathExpressions.GLOB);
+  }
+
+  @Test
+  public void getPathExpressionsForProjectIfConfigIsInvalid() throws Exception {
+    Config cfg = new Config();
+    cfg.setString(SECTION_CODE_OWNERS, null, KEY_PATH_EXPRESSIONS, "INVALID");
+    assertThat(backendConfig.getPathExpressionsForProject(cfg, project)).isEmpty();
+  }
+
+  @Test
   public void cannotValidateProjectLevelConfigWithNullFileName() throws Exception {
     NullPointerException npe =
         assertThrows(
@@ -204,6 +306,23 @@
   }
 
   @Test
+  public void getDefaultPathExpressions() throws Exception {
+    assertThat(backendConfig.getDefaultPathExpressions()).isEmpty();
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.pathExpressions", value = "GLOB")
+  public void getConfiguredDefaultPathExpressions() throws Exception {
+    assertThat(backendConfig.getDefaultPathExpressions()).value().isEqualTo(PathExpressions.GLOB);
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.pathExpressions", value = "INVALID")
+  public void getDefaultPathExpressionsIfConfigIsInvalid() throws Exception {
+    assertThat(backendConfig.getDefaultPathExpressions()).isEmpty();
+  }
+
+  @Test
   public void validateEmptyProjectLevelConfig() throws Exception {
     ImmutableList<CommitValidationMessage> commitValidationMessage =
         backendConfig.validateProjectLevelConfig("code-owners.config", new Config());
@@ -215,13 +334,15 @@
     Config cfg = new Config();
     cfg.setString(
         SECTION_CODE_OWNERS, null, KEY_BACKEND, CodeOwnerBackendId.FIND_OWNERS.getBackendId());
+    cfg.setString(SECTION_CODE_OWNERS, null, KEY_PATH_EXPRESSIONS, PathExpressions.GLOB.name());
     ImmutableList<CommitValidationMessage> commitValidationMessage =
         backendConfig.validateProjectLevelConfig("code-owners.config", cfg);
     assertThat(commitValidationMessage).isEmpty();
   }
 
   @Test
-  public void validateInvalidProjectLevelConfig_invalidProjectConfiguration() throws Exception {
+  public void validateInvalidProjectLevelConfig_invalidProjectLevelBackendConfiguration()
+      throws Exception {
     Config cfg = new Config();
     cfg.setString(SECTION_CODE_OWNERS, null, KEY_BACKEND, "INVALID");
     ImmutableList<CommitValidationMessage> commitValidationMessages =
@@ -237,7 +358,25 @@
   }
 
   @Test
-  public void validateInvalidProjectLevelConfig_invalidBranchConfiguration() throws Exception {
+  public void validateInvalidProjectLevelConfig_invalidProjectLevelPathExpressionsConfiguration()
+      throws Exception {
+    Config cfg = new Config();
+    cfg.setString(SECTION_CODE_OWNERS, null, KEY_PATH_EXPRESSIONS, "INVALID");
+    ImmutableList<CommitValidationMessage> commitValidationMessages =
+        backendConfig.validateProjectLevelConfig("code-owners.config", cfg);
+    assertThat(commitValidationMessages).hasSize(1);
+    CommitValidationMessage commitValidationMessage =
+        Iterables.getOnlyElement(commitValidationMessages);
+    assertThat(commitValidationMessage.getType()).isEqualTo(ValidationMessage.Type.ERROR);
+    assertThat(commitValidationMessage.getMessage())
+        .isEqualTo(
+            "Path expressions 'INVALID' that are configured in code-owners.config (parameter"
+                + " codeOwners.pathExpressions) not found.");
+  }
+
+  @Test
+  public void validateInvalidProjectLevelConfig_invalidBranchLevelBackendConfiguration()
+      throws Exception {
     Config cfg = new Config();
     cfg.setString(SECTION_CODE_OWNERS, "someBranch", KEY_BACKEND, "INVALID");
     ImmutableList<CommitValidationMessage> commitValidationMessages =
@@ -251,4 +390,21 @@
             "Code owner backend 'INVALID' that is configured in code-owners.config (parameter"
                 + " codeOwners.someBranch.backend) not found.");
   }
+
+  @Test
+  public void validateInvalidProjectLevelConfig_invalidBranchLevelPathExpressionsConfiguration()
+      throws Exception {
+    Config cfg = new Config();
+    cfg.setString(SECTION_CODE_OWNERS, "someBranch", KEY_PATH_EXPRESSIONS, "INVALID");
+    ImmutableList<CommitValidationMessage> commitValidationMessages =
+        backendConfig.validateProjectLevelConfig("code-owners.config", cfg);
+    assertThat(commitValidationMessages).hasSize(1);
+    CommitValidationMessage commitValidationMessage =
+        Iterables.getOnlyElement(commitValidationMessages);
+    assertThat(commitValidationMessage.getType()).isEqualTo(ValidationMessage.Type.ERROR);
+    assertThat(commitValidationMessage.getMessage())
+        .isEqualTo(
+            "Path expressions 'INVALID' that are configured in code-owners.config (parameter"
+                + " codeOwners.someBranch.pathExpressions) not found.");
+  }
 }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigValidatorTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigValidatorTest.java
index e05323c..d712ba4 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigValidatorTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigValidatorTest.java
@@ -24,7 +24,9 @@
 import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Before;
 import org.junit.Test;
@@ -52,29 +54,33 @@
         /* subsection= */ null,
         GeneralConfig.KEY_FALLBACK_CODE_OWNERS,
         FallbackCodeOwners.ALL_USERS);
-    RevCommit commit =
-        testRepo
-            .commit()
-            .add("code-owners.config", cfg.toText())
-            .add("project.config", "INVALID")
-            .create();
-    CommitReceivedEvent receiveEvent = new CommitReceivedEvent();
-    receiveEvent.project =
-        projectCache.get(project).orElseThrow(illegalState(project)).getProject();
-    receiveEvent.refName = RefNames.REFS_CONFIG;
-    receiveEvent.commit = commit;
-    receiveEvent.revWalk = testRepo.getRevWalk();
-    receiveEvent.repoConfig = new Config();
-    CommitValidationException exception =
-        assertThrows(
-            CommitValidationException.class,
-            () -> codeOwnersPluginConfigValidator.onCommitReceived(receiveEvent));
-    assertThat(exception)
-        .hasMessageThat()
-        .isEqualTo(
-            String.format(
-                "failed to validate file code-owners.config for revision %s in ref %s of project %s",
-                commit.getName(), RefNames.REFS_CONFIG, project));
-    assertThat(exception).hasCauseThat().isInstanceOf(ConfigInvalidException.class);
+    try (TestRepository<Repository> testRepo =
+        new TestRepository<>(repoManager.openRepository(project))) {
+      RevCommit commit =
+          testRepo
+              .commit()
+              .add("code-owners.config", cfg.toText())
+              .add("project.config", "INVALID")
+              .create();
+
+      CommitReceivedEvent receiveEvent = new CommitReceivedEvent();
+      receiveEvent.project =
+          projectCache.get(project).orElseThrow(illegalState(project)).getProject();
+      receiveEvent.refName = RefNames.REFS_CONFIG;
+      receiveEvent.commit = commit;
+      receiveEvent.revWalk = testRepo.getRevWalk();
+      receiveEvent.repoConfig = new Config();
+      CommitValidationException exception =
+          assertThrows(
+              CommitValidationException.class,
+              () -> codeOwnersPluginConfigValidator.onCommitReceived(receiveEvent));
+      assertThat(exception)
+          .hasMessageThat()
+          .isEqualTo(
+              String.format(
+                  "failed to validate file code-owners.config for revision %s in ref %s of project %s",
+                  commit.getName(), RefNames.REFS_CONFIG, project));
+      assertThat(exception).hasCauseThat().isInstanceOf(ConfigInvalidException.class);
+    }
   }
 }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginProjectConfigSnapshotTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginProjectConfigSnapshotTest.java
index f155572..1dbe420 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginProjectConfigSnapshotTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginProjectConfigSnapshotTest.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.Project.NameKey;
 import com.google.gerrit.entities.RefNames;
@@ -42,6 +43,7 @@
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigUpdate;
 import com.google.gerrit.plugins.codeowners.backend.FallbackCodeOwners;
 import com.google.gerrit.plugins.codeowners.backend.PathExpressionMatcher;
+import com.google.gerrit.plugins.codeowners.backend.PathExpressions;
 import com.google.gerrit.plugins.codeowners.backend.findowners.FindOwnersBackend;
 import com.google.gerrit.plugins.codeowners.common.CodeOwnerConfigValidationPolicy;
 import com.google.gerrit.plugins.codeowners.common.MergeCommitStrategy;
@@ -52,7 +54,6 @@
 import java.nio.file.Path;
 import java.util.Optional;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -1032,6 +1033,154 @@
   }
 
   @Test
+  public void cannotGetPathExpressionsForNullBranch() throws Exception {
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class,
+            () -> cfgSnapshot().getPathExpressions(/* branchName= */ null));
+    assertThat(npe).hasMessageThat().isEqualTo("branchName");
+  }
+
+  @Test
+  public void getPathExpressionsForNonExistingBranch() throws Exception {
+    assertThat(cfgSnapshot().getPathExpressions("non-existing")).isEmpty();
+  }
+
+  @Test
+  public void getPathExpressionsWhenNoPathExpressionsAreConfigured() throws Exception {
+    assertThat(cfgSnapshot().getPathExpressions("master")).isEmpty();
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.pathExpressions", value = "GLOB")
+  public void getConfiguredPathExpressions() throws Exception {
+    assertThat(cfgSnapshot().getPathExpressions("master")).value().isEqualTo(PathExpressions.GLOB);
+  }
+
+  @Test
+  @GerritConfig(
+      name = "plugin.code-owners.pathExpressions",
+      value = "non-existing-path-expressions")
+  public void getPathExpressionsIfNonExistingPathExpressionsAreConfigured() throws Exception {
+    assertThat(cfgSnapshot().getPathExpressions("master")).isEmpty();
+  }
+
+  @Test
+  public void getPathExpressionsConfiguredOnProjectLevel() throws Exception {
+    configurePathExpressions(project, PathExpressions.GLOB.name());
+    assertThat(cfgSnapshot().getPathExpressions("master")).value().isEqualTo(PathExpressions.GLOB);
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.backend", value = "GLOB")
+  public void pathExpressionsConfiguredOnProjectLevelOverrideDefaultPathExpressions()
+      throws Exception {
+    configurePathExpressions(project, PathExpressions.SIMPLE.name());
+    assertThat(cfgSnapshot().getPathExpressions("master"))
+        .value()
+        .isEqualTo(PathExpressions.SIMPLE);
+  }
+
+  @Test
+  public void pathExpressionsAreInheritedFromParentProject() throws Exception {
+    configurePathExpressions(allProjects, PathExpressions.GLOB.name());
+    assertThat(cfgSnapshot().getPathExpressions("master")).value().isEqualTo(PathExpressions.GLOB);
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.pathExpressions", value = "GLOB")
+  public void inheritedPathExpressionsOverrideDefaultPathExpressions() throws Exception {
+    configurePathExpressions(allProjects, PathExpressions.SIMPLE.name());
+    assertThat(cfgSnapshot().getPathExpressions("master"))
+        .value()
+        .isEqualTo(PathExpressions.SIMPLE);
+  }
+
+  @Test
+  public void projectLevelPathExpressionsOverrideInheritedPathExpressions() throws Exception {
+    configurePathExpressions(allProjects, PathExpressions.GLOB.name());
+    configurePathExpressions(project, PathExpressions.SIMPLE.name());
+    assertThat(cfgSnapshot().getPathExpressions("master"))
+        .value()
+        .isEqualTo(PathExpressions.SIMPLE);
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.pathExpressions", value = "GLOB")
+  public void
+      pathExpressionsAreReadFromGlobalConfigIfNonExistingPathExpressionsAreConfiguredOnProjectLevel()
+          throws Exception {
+    configurePathExpressions(project, "non-existing-path-expressions");
+    assertThat(cfgSnapshot().getPathExpressions("master")).value().isEqualTo(PathExpressions.GLOB);
+  }
+
+  @Test
+  public void projectLevelPathExpressionsForOtherProjectHasNoEffect() throws Exception {
+    Project.NameKey otherProject = projectOperations.newProject().create();
+    configurePathExpressions(otherProject, PathExpressions.GLOB.name());
+    assertThat(cfgSnapshot().getPathExpressions("master")).isEmpty();
+  }
+
+  @Test
+  public void getPathExpressionsConfiguredOnBranchLevel() throws Exception {
+    configurePathExpressions(project, "refs/heads/master", PathExpressions.GLOB.name());
+    assertThat(cfgSnapshot().getPathExpressions("master")).value().isEqualTo(PathExpressions.GLOB);
+  }
+
+  @Test
+  public void getPathExpressionsConfiguredOnBranchLevelShortName() throws Exception {
+    configurePathExpressions(project, "master", PathExpressions.GLOB.name());
+    assertThat(cfgSnapshot().getPathExpressions("master")).value().isEqualTo(PathExpressions.GLOB);
+  }
+
+  @Test
+  public void
+      branchLevelPathExpressionsOnFullNameTakePrecedenceOverBranchLevelPathExpressionsOnShortName()
+          throws Exception {
+    configurePathExpressions(project, "master", PathExpressions.GLOB.name());
+    configurePathExpressions(project, "refs/heads/master", PathExpressions.SIMPLE.name());
+    assertThat(cfgSnapshot().getPathExpressions("master"))
+        .value()
+        .isEqualTo(PathExpressions.SIMPLE);
+  }
+
+  @Test
+  public void branchLevelPathExpressionsOverridesProjectLevelPathExpressions() throws Exception {
+    configurePathExpressions(project, PathExpressions.GLOB.name());
+    configurePathExpressions(project, "master", PathExpressions.SIMPLE.name());
+    assertThat(cfgSnapshot().getPathExpressions("master"))
+        .value()
+        .isEqualTo(PathExpressions.SIMPLE);
+  }
+
+  @Test
+  public void
+      pathExpressionsAreReadFromProjectIfNonExistingPathExpressionsAreConfiguredOnBranchLevel()
+          throws Exception {
+    updateCodeOwnersConfig(
+        project,
+        codeOwnersConfig -> {
+          codeOwnersConfig.setString(
+              CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS,
+              /* subsection= */ null,
+              BackendConfig.KEY_PATH_EXPRESSIONS,
+              PathExpressions.GLOB.name());
+          codeOwnersConfig.setString(
+              CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS,
+              "master",
+              BackendConfig.KEY_PATH_EXPRESSIONS,
+              "non-existing-path-expressions");
+        });
+    assertThat(cfgSnapshot().getPathExpressions("master")).value().isEqualTo(PathExpressions.GLOB);
+  }
+
+  @Test
+  public void branchLevelPathExpressionsForOtherBranchHaveNoEffect() throws Exception {
+    configurePathExpressions(project, "foo", PathExpressions.GLOB.name());
+    assertThat(cfgSnapshot().getPathExpressions("master")).isEmpty();
+  }
+
+  @Test
   public void getDefaultRequiredApprovalWhenNoRequiredApprovalIsConfigured() throws Exception {
     RequiredApproval requiredApproval = cfgSnapshot().getRequiredApproval();
     assertThat(requiredApproval).hasLabelNameThat().isEqualTo(RequiredApprovalConfig.DEFAULT_LABEL);
@@ -1858,6 +2007,17 @@
     setCodeOwnersConfig(project, branch, BackendConfig.KEY_BACKEND, backendName);
   }
 
+  private void configurePathExpressions(Project.NameKey project, String pathExpressionsName)
+      throws Exception {
+    configurePathExpressions(project, /* branch= */ null, pathExpressionsName);
+  }
+
+  private void configurePathExpressions(
+      Project.NameKey project, @Nullable String branch, String pathExpressionsName)
+      throws Exception {
+    setCodeOwnersConfig(project, branch, BackendConfig.KEY_PATH_EXPRESSIONS, pathExpressionsName);
+  }
+
   private void configureRequiredApproval(Project.NameKey project, String requiredApproval)
       throws Exception {
     setCodeOwnersConfig(
@@ -1980,9 +2140,7 @@
 
     @Override
     public Optional<CodeOwnerConfig> getCodeOwnerConfig(
-        CodeOwnerConfig.Key codeOwnerConfigKey,
-        @Nullable RevWalk revWalk,
-        @Nullable ObjectId revision) {
+        CodeOwnerConfig.Key codeOwnerConfigKey, @Nullable ObjectId revision) {
       throw new UnsupportedOperationException("not implemented");
     }
 
@@ -2005,7 +2163,7 @@
     }
 
     @Override
-    public Optional<PathExpressionMatcher> getPathExpressionMatcher() {
+    public Optional<PathExpressionMatcher> getPathExpressionMatcher(BranchNameKey branchNameKey) {
       return Optional.empty();
     }
   }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersBackendTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersBackendTest.java
index ff6f184..4b26b73 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersBackendTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersBackendTest.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.truth.OptionalSubject.assertThat;
 
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.plugins.codeowners.backend.AbstractFileBasedCodeOwnerBackend;
 import com.google.gerrit.plugins.codeowners.backend.AbstractFileBasedCodeOwnerBackendTest;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigParser;
@@ -41,7 +42,7 @@
 
   @Test
   public void getPathExpressionMatcher() throws Exception {
-    assertThat(codeOwnerBackend.getPathExpressionMatcher())
+    assertThat(codeOwnerBackend.getPathExpressionMatcher(BranchNameKey.create(project, "master")))
         .value()
         .isInstanceOf(FindOwnersGlobMatcher.class);
   }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/proto/ProtoBackendTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/proto/ProtoBackendTest.java
index d231cdf..01d7091 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/proto/ProtoBackendTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/proto/ProtoBackendTest.java
@@ -2,6 +2,7 @@
 
 import static com.google.gerrit.truth.OptionalSubject.assertThat;
 
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.plugins.codeowners.backend.AbstractFileBasedCodeOwnerBackend;
 import com.google.gerrit.plugins.codeowners.backend.AbstractFileBasedCodeOwnerBackendTest;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigParser;
@@ -27,7 +28,7 @@
 
   @Test
   public void getPathExpressionMatcher() throws Exception {
-    assertThat(codeOwnerBackend.getPathExpressionMatcher())
+    assertThat(codeOwnerBackend.getPathExpressionMatcher(BranchNameKey.create(project, "master")))
         .value()
         .isInstanceOf(SimplePathExpressionMatcher.class);
   }
diff --git a/resources/Documentation/config.md b/resources/Documentation/config.md
index 8ac1897..099645b 100644
--- a/resources/Documentation/config.md
+++ b/resources/Documentation/config.md
@@ -148,6 +148,25 @@
         syntax so that the existing code owner config files can no longer be
         parsed.
 
+<a id="pluginCodeOwnersPathExpressions">plugin.@PLUGIN@.pathExpressions</a>
+:       The path expression syntax that is used in
+        [code owner config files](user-guide.html#codeOwnerConfigFiles).\
+        Can be overridden per project by setting
+        [codeOwners.pathExpressions](#codeOwnersPathExpressions) in
+        `@PLUGIN@.config`.\
+        The supported path expression syntaxes are listed and explained at the
+        [Path Expressions](path-expressions.html) page.\
+        By default unset which means that the path expression syntax is derived
+        from the configured [code owner backend](#pluginCodeOwnersBackend).\
+        \
+        **NOTE:** Be careful with changing this parameter as it affects how path
+        expressions in existing
+        [code owner config files](user-guide.html#codeOwnerConfigFiles) are
+        interpreted. E.g. by changing the path expression syntax existing path
+        expressions may now match different files, or existing path expressions
+        may no longer be valid and fail to parse, in which case they would not
+        match anything any more.
+
 <a id="pluginCodeOwnersFileExtension">plugin.@PLUGIN@.fileExtension</a>
 :       The file extension that should be used for code owner config files.\
         Allows to use a different code owner configuration in a fork. E.g. if
@@ -621,7 +640,7 @@
         The supported code owner backends are listed at the
         [Backends](backends.html) page.\
         If not set, the global setting
-        [plugin.@PLUGIN@.backend](#pluginCodeOwnersBackend) in `gerrit.config`\
+        [plugin.@PLUGIN@.backend](#pluginCodeOwnersBackend) in `gerrit.config`
         is used.\
         \
         **NOTE:** Be careful with changing this parameter as it invalidates all
@@ -650,6 +669,51 @@
         syntax so that the existing code owner config files can no longer be
         parsed.
 
+<a id="codeOwnersPathExpressions">codeOwners.pathExpressions</a>
+:       The path expression syntax that is used in`
+        [code owner config files](user-guide.html#codeOwnerConfigFiles).\
+        Overrides the global setting
+        [plugin.@PLUGIN@.pathExpressions](#pluginCodeOwnersPathExpressions) in
+        `gerrit.config` and the `codeOwners.pathExpressions` setting from parent
+        projects.\
+        Can be overridden per branch by setting
+        [codeOwners.\<branch\>.pathExpressions](#codeOwnersBranchPathExpressions).\
+        The supported path expression syntaxes are listed and explained at the
+        [Path Expressions](path-expressions.html) page.\
+        If not set, the global setting
+        [plugin.@PLUGIN@.pathExpressions](#pluginCodeOwnersPathExpressions) in
+        `gerrit.config` is used.\
+        \
+        **NOTE:** Be careful with changing this parameter as it affects how path
+        expressions in existing
+        [code owner config files](user-guide.html#codeOwnerConfigFiles) are
+        interpreted. E.g. by changing the path expression syntax existing path
+        expressions may now match different files, or existing path expressions
+        may no longer be valid and fail to parse, in which case they would not
+        match anything any more.
+
+<a id="codeOwnersBranchPathExpressions">codeOwners.\<branch\>.pathExpressions</a>
+:       The path expression syntax that is used in
+        [code owner config files](user-guide.html#codeOwnerConfigFiles) that are
+        contained in this branch.\
+        The branch can be the short or full name. If both configurations exist
+        the one for the full name takes precedence.\
+        Overrides the per repository setting
+        [codeOwners.pathExpressions](#codeOwnersPathExpressions) and the
+        `codeOwners.<branch>.pathExpressions` setting from parent projects.\
+        The path expression syntax that is used in
+        [code owner config files](user-guide.html#codeOwnerConfigFiles).\
+        If not set, the project level configuration
+        [codeOwners.pathExpressions](#codeOwnersPathExpressions) is used.\
+        \
+        **NOTE:** Be careful with changing this parameter as it affects how path
+        expressions in existing
+        [code owner config files](user-guide.html#codeOwnerConfigFiles) are
+        interpreted. E.g. by changing the path expression syntax existing path
+        expressions may now match different files, or existing path expressions
+        may no longer be valid and fail to parse, in which case they would not
+        match anything any more.
+
 <a id="codeOwnersFileExtension">codeOwners.fileExtension</a>
 :       The file extension that should be used for the code owner config files
         in this project.\
diff --git a/resources/Documentation/path-expressions.md b/resources/Documentation/path-expressions.md
index a8fc387..ea306cf 100644
--- a/resources/Documentation/path-expressions.md
+++ b/resources/Documentation/path-expressions.md
@@ -5,15 +5,29 @@
 [per-file](backend-find-owners.html#perFile) rule for the
 [find-owners](backend-find-owners.html) backend).
 
-Which syntax is used depends on the used code owner backend:
+The following path expression syntaxes are supported:
+
+* `GLOB`:
+  Uses [globs](#globs) to match paths.
+* `FIND_OWNERS_GLOB`:
+  Uses [globs](#globs) to match paths, but each glob is automatically prefixed
+  with `{**/,}` so that subfolders are always matched, e.g. `*.md` matches all
+  md files in all subfolders, rather then only md files in the current folder
+  (also see the [caveat](#findOwnersCaveat) section below).
+* `SIMPLE`:
+  Uses [simple path expressions](#simplePathExpressions) to match paths.
+
+Which syntax is used by default depends on the used code owner backend:
 
 * [find-owners](backend-find-owners.html) backend:
-  uses [globs](#globs), but each glob is automatically prefixed with `{**/,}`
-  so that subfolders are always matched, e.g. `*.md` matches all md files in all
-  subfolders, rather then only md files in the current folder (also see the
-  [caveat](#findOwnersCaveat) section below)
+  Uses `FIND_OWNERS_GLOB` as path expression syntax.
 * [proto](backend-proto.html) backend:
-  uses [simple path expressions](#simplePathExpressions)
+  Uses `SIMPLE` as path expression syntax.
+
+The default path expression syntax that is derived from the backend can be
+overriden by [global configuration](config.html#pluginCodeOwnersPathExpressions),
+on [project-level](config.html#codeOwnersPathExpressions) or on
+[branch-level](config.html#codeOwnersBranchPathExpressions).
 
 ## <a id="globs">Globs