Merge changes Id70060a4,Ic092c3db

* changes:
  CodeOwnerApprovalCheck: Do not read config params per file
  CodeOwnerApprovalCheck: Avoid double instantiation of PathCodeOwners
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
index 6c36cd1..a582589 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
@@ -282,6 +282,9 @@
           getApproverAccountIds(requiredApproval, changeNotes, patchSetUploader);
       logger.atFine().log("reviewers = %s, approvers = %s", reviewerAccountIds, approverAccountIds);
 
+      FallbackCodeOwners fallbackCodeOwners =
+          codeOwnersPluginConfiguration.getFallbackCodeOwners(branch.project());
+
       return changedFiles
           .compute(changeNotes.getProjectName(), changeNotes.getCurrentPatchSet().commitId())
           .stream()
@@ -295,6 +298,7 @@
                       patchSetUploader,
                       reviewerAccountIds,
                       approverAccountIds,
+                      fallbackCodeOwners,
                       hasOverride,
                       changedFile));
     }
@@ -362,6 +366,7 @@
                       /* reviewerAccountIds= */ ImmutableSet.of(),
                       // Assume an explicit approval of the given account.
                       /* approverAccountIds= */ ImmutableSet.of(accountId),
+                      fallbackCodeOwners,
                       /* hasOverride= */ false,
                       changedFile));
     }
@@ -408,6 +413,7 @@
       @Nullable Account.Id patchSetUploader,
       ImmutableSet<Account.Id> reviewerAccountIds,
       ImmutableSet<Account.Id> approverAccountIds,
+      FallbackCodeOwners fallbackCodeOwners,
       boolean hasOverride,
       ChangedFile changedFile) {
     try (Timer0.Context ctx = codeOwnerMetrics.computeFileStatus.start()) {
@@ -427,6 +433,7 @@
                           patchSetUploader,
                           reviewerAccountIds,
                           approverAccountIds,
+                          fallbackCodeOwners,
                           hasOverride,
                           newPath));
 
@@ -448,6 +455,7 @@
                     patchSetUploader,
                     reviewerAccountIds,
                     approverAccountIds,
+                    fallbackCodeOwners,
                     hasOverride,
                     changedFile.oldPath().get()));
       }
@@ -467,6 +475,7 @@
       @Nullable Account.Id patchSetUploader,
       ImmutableSet<Account.Id> reviewerAccountIds,
       ImmutableSet<Account.Id> approverAccountIds,
+      FallbackCodeOwners fallbackCodeOwners,
       boolean hasOverride,
       Path absolutePath) {
     logger.atFine().log("computing path status for %s", absolutePath);
@@ -503,38 +512,39 @@
           branch,
           revision,
           absolutePath,
-          codeOwnerConfig -> {
-            CodeOwnerResolverResult codeOwners = getCodeOwners(codeOwnerConfig, absolutePath);
-            logger.atFine().log(
-                "code owners = %s (code owner config folder path = %s, file name = %s)",
-                codeOwners,
-                codeOwnerConfig.key().folderPath(),
-                codeOwnerConfig.key().fileName().orElse("<default>"));
+          (PathCodeOwnersVisitor)
+              pathCodeOwners -> {
+                CodeOwnerResolverResult codeOwners = resolveCodeOwners(pathCodeOwners);
+                logger.atFine().log(
+                    "code owners = %s (code owner config folder path = %s, file name = %s)",
+                    codeOwners,
+                    pathCodeOwners.getCodeOwnerConfig().key().folderPath(),
+                    pathCodeOwners.getCodeOwnerConfig().key().fileName().orElse("<default>"));
 
-            if (codeOwners.hasRevelantCodeOwnerDefinitions()) {
-              hasRevelantCodeOwnerDefinitions.set(true);
-            }
+                if (codeOwners.hasRevelantCodeOwnerDefinitions()) {
+                  hasRevelantCodeOwnerDefinitions.set(true);
+                }
 
-            if (isApproved(
-                absolutePath,
-                codeOwners,
-                approverAccountIds,
-                enableImplicitApprovalFromUploader,
-                patchSetUploader)) {
-              codeOwnerStatus.set(CodeOwnerStatus.APPROVED);
-              return false;
-            } else if (isPending(absolutePath, codeOwners, reviewerAccountIds)) {
-              codeOwnerStatus.set(CodeOwnerStatus.PENDING);
+                if (isApproved(
+                    absolutePath,
+                    codeOwners,
+                    approverAccountIds,
+                    enableImplicitApprovalFromUploader,
+                    patchSetUploader)) {
+                  codeOwnerStatus.set(CodeOwnerStatus.APPROVED);
+                  return false;
+                } else if (isPending(absolutePath, codeOwners, reviewerAccountIds)) {
+                  codeOwnerStatus.set(CodeOwnerStatus.PENDING);
 
-              // We need to continue to check if any of the higher-level code owners approved the
-              // change.
-              return true;
-            }
+                  // We need to continue to check if any of the higher-level code owners approved
+                  // the change.
+                  return true;
+                }
 
-            // We need to continue to check if any of the higher-level code owners approved the
-            // change or is a reviewer.
-            return true;
-          },
+                // We need to continue to check if any of the higher-level code owners approved the
+                // change or is a reviewer.
+                return true;
+              },
           codeOwnerConfigKey -> {
             logger.atFine().log(
                 "code owner config %s ignores parent code owners for %s",
@@ -557,6 +567,7 @@
                 patchSetUploader,
                 reviewerAccountIds,
                 approverAccountIds,
+                fallbackCodeOwners,
                 absolutePath));
       }
     }
@@ -699,9 +710,8 @@
       @Nullable Account.Id patchSetUploader,
       ImmutableSet<Account.Id> reviewerAccountIds,
       ImmutableSet<Account.Id> approverAccountIds,
+      FallbackCodeOwners fallbackCodeOwners,
       Path absolutePath) {
-    FallbackCodeOwners fallbackCodeOwners =
-        codeOwnersPluginConfiguration.getFallbackCodeOwners(branch.project());
     logger.atFine().log(
         "getting code owner status for fallback code owners (fallback code owners = %s)",
         fallbackCodeOwners);
@@ -822,17 +832,12 @@
   }
 
   /**
-   * Gets the code owners that own the given path according to the given code owner config.
+   * Resolves the given path code owners.
    *
-   * @param codeOwnerConfig the code owner config from which the code owners should be retrieved
-   * @param absolutePath the path for which the code owners should be retrieved
+   * @param pathCodeOwners the path code owners that should be resolved
    */
-  private CodeOwnerResolverResult getCodeOwners(
-      CodeOwnerConfig codeOwnerConfig, Path absolutePath) {
-    return codeOwnerResolver
-        .get()
-        .enforceVisibility(false)
-        .resolvePathCodeOwners(codeOwnerConfig, absolutePath);
+  private CodeOwnerResolverResult resolveCodeOwners(PathCodeOwners pathCodeOwners) {
+    return codeOwnerResolver.get().enforceVisibility(false).resolvePathCodeOwners(pathCodeOwners);
   }
 
   /**
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchy.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchy.java
index 473cdc5..cecd46d 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchy.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchy.java
@@ -101,10 +101,39 @@
       Path absolutePath,
       CodeOwnerConfigVisitor codeOwnerConfigVisitor,
       Consumer<CodeOwnerConfig.Key> parentCodeOwnersIgnoredCallback) {
+    requireNonNull(codeOwnerConfigVisitor, "codeOwnerConfigVisitor");
+    PathCodeOwnersVisitor pathCodeOwnersVisitor =
+        pathCodeOwners -> codeOwnerConfigVisitor.visit(pathCodeOwners.getCodeOwnerConfig());
+    visit(
+        branchNameKey,
+        revision,
+        absolutePath,
+        pathCodeOwnersVisitor,
+        parentCodeOwnersIgnoredCallback);
+  }
+
+  /**
+   * Visits the path code owners in the given branch that apply for the given path by following the
+   * path hierarchy from the given path up to the root folder.
+   *
+   * @param branchNameKey project and branch from which the code owner configs should be visited
+   * @param revision the branch revision from which the code owner configs should be loaded
+   * @param absolutePath the path for which the code owner configs should be visited; the path must
+   *     be absolute; can be the path of a file or folder; the path may or may not exist
+   * @param pathCodeOwnersVisitor visitor that should be invoked for the applying path code owners
+   * @param parentCodeOwnersIgnoredCallback callback that is invoked for the first visited code
+   *     owner config that ignores parent code owners
+   */
+  public void visit(
+      BranchNameKey branchNameKey,
+      ObjectId revision,
+      Path absolutePath,
+      PathCodeOwnersVisitor pathCodeOwnersVisitor,
+      Consumer<CodeOwnerConfig.Key> parentCodeOwnersIgnoredCallback) {
     requireNonNull(branchNameKey, "branch");
     requireNonNull(revision, "revision");
     requireNonNull(absolutePath, "absolutePath");
-    requireNonNull(codeOwnerConfigVisitor, "codeOwnerConfigVisitor");
+    requireNonNull(pathCodeOwnersVisitor, "pathCodeOwnersVisitor");
     requireNonNull(parentCodeOwnersIgnoredCallback, "parentCodeOwnersIgnoredCallback");
     checkState(absolutePath.isAbsolute(), "path %s must be absolute", absolutePath);
 
@@ -127,8 +156,7 @@
           pathCodeOwnersFactory.create(codeOwnerConfigKey, revision, absolutePath);
       if (pathCodeOwners.isPresent()) {
         logger.atFine().log("visit code owner config for %s", ownerConfigFolder);
-        boolean visitFurtherCodeOwnerConfigs =
-            codeOwnerConfigVisitor.visit(pathCodeOwners.get().getCodeOwnerConfig());
+        boolean visitFurtherCodeOwnerConfigs = pathCodeOwnersVisitor.visit(pathCodeOwners.get());
         boolean ignoreParentCodeOwners =
             pathCodeOwners.get().resolveCodeOwnerConfig().get().ignoreParentCodeOwners();
         if (ignoreParentCodeOwners) {
@@ -155,7 +183,7 @@
 
     if (!RefNames.REFS_CONFIG.equals(branchNameKey.branch())) {
       visitCodeOwnerConfigInRefsMetaConfig(
-          branchNameKey.project(), absolutePath, codeOwnerConfigVisitor);
+          branchNameKey.project(), absolutePath, pathCodeOwnersVisitor);
     }
   }
 
@@ -174,11 +202,10 @@
    *     the {@code refs/meta/config} branch
    * @param absolutePath the path for which the code owner configs should be visited; the path must
    *     be absolute; can be the path of a file or folder; the path may or may not exist
-   * @param codeOwnerConfigVisitor visitor that should be invoked for the applying code owner
-   *     configs
+   * @param pathCodeOwnersVisitor visitor that should be invoked
    */
   private void visitCodeOwnerConfigInRefsMetaConfig(
-      Project.NameKey project, Path absolutePath, CodeOwnerConfigVisitor codeOwnerConfigVisitor) {
+      Project.NameKey project, Path absolutePath, PathCodeOwnersVisitor pathCodeOwnersVisitor) {
     CodeOwnerConfig.Key metaCodeOwnerConfigKey =
         CodeOwnerConfig.Key.create(project, RefNames.REFS_CONFIG, "/");
     logger.atFine().log("visiting code owner config %s", metaCodeOwnerConfigKey);
@@ -194,7 +221,7 @@
           pathCodeOwnersFactory.create(metaCodeOwnerConfigKey, metaRevision, absolutePath);
       if (pathCodeOwners.isPresent()) {
         logger.atFine().log("visit code owner config %s", metaCodeOwnerConfigKey);
-        codeOwnerConfigVisitor.visit(pathCodeOwners.get().getCodeOwnerConfig());
+        pathCodeOwnersVisitor.visit(pathCodeOwners.get());
       } else {
         logger.atFine().log("code owner config %s not found", metaCodeOwnerConfigKey);
       }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
index cd874ee..a16714c 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
@@ -138,6 +138,10 @@
    * Resolves the code owners from the given code owner config for the given path from {@link
    * CodeOwnerReference}s to a {@link CodeOwner}s.
    *
+   * <p>If the code owner config has already been resolved to {@link PathCodeOwners}, prefer calling
+   * {@link #resolvePathCodeOwners(PathCodeOwners)} instead, so that {@link PathCodeOwners} do not
+   * need to be created again.
+   *
    * <p>Non-resolvable code owners are filtered out.
    *
    * @param codeOwnerConfig the code owner config for which the local owners for the given path
@@ -151,13 +155,27 @@
     requireNonNull(codeOwnerConfig, "codeOwnerConfig");
     requireNonNull(absolutePath, "absolutePath");
     checkState(absolutePath.isAbsolute(), "path %s must be absolute", absolutePath);
+    return resolvePathCodeOwners(pathCodeOwnersFactory.create(codeOwnerConfig, absolutePath));
+  }
+
+  /**
+   * Resolves the the given path code owners from {@link CodeOwnerReference}s to a {@link
+   * CodeOwner}s.
+   *
+   * <p>Non-resolvable code owners are filtered out.
+   *
+   * @param pathCodeOwners the path code owners that should be resolved
+   * @return the resolved code owners
+   */
+  public CodeOwnerResolverResult resolvePathCodeOwners(PathCodeOwners pathCodeOwners) {
+    requireNonNull(pathCodeOwners, "pathCodeOwners");
 
     try (Timer0.Context ctx = codeOwnerMetrics.resolvePathCodeOwners.start()) {
       logger.atFine().log(
           "resolve path code owners (code owner config = %s, path = %s)",
-          codeOwnerConfig.key(), absolutePath);
+          pathCodeOwners.getCodeOwnerConfig().key(), pathCodeOwners.getPath());
       OptionalResultWithMessages<PathCodeOwnersResult> pathCodeOwnersResult =
-          pathCodeOwnersFactory.create(codeOwnerConfig, absolutePath).resolveCodeOwnerConfig();
+          pathCodeOwners.resolveCodeOwnerConfig();
       return resolve(
           pathCodeOwnersResult.get().getPathCodeOwners(),
           pathCodeOwnersResult.get().unresolvedImports(),
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
index cdf0f84..78e315a 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
@@ -156,6 +156,11 @@
     return codeOwnerConfig;
   }
 
+  /** Returns the absolute path for which code owners were computed. */
+  public Path getPath() {
+    return path;
+  }
+
   /**
    * Resolves the {@link #codeOwnerConfig}.
    *
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersVisitor.java b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersVisitor.java
new file mode 100644
index 0000000..2d69ca2
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersVisitor.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.backend;
+
+/** Callback interface to visit {@link PathCodeOwners}. */
+@FunctionalInterface
+public interface PathCodeOwnersVisitor {
+  /**
+   * Callback for {@link PathCodeOwners}.
+   *
+   * @param pathCodeOwners the path code owners
+   * @return whether further path code owner configs should be visited
+   */
+  boolean visit(PathCodeOwners pathCodeOwners);
+}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java
index 6a4ea64..0787e9a 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java
@@ -422,6 +422,15 @@
   }
 
   @Test
+  public void cannotResolvePathCodeOwnersOfNullPathCodeOwners() throws Exception {
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class,
+            () -> codeOwnerResolver.get().resolvePathCodeOwners(/* pathCodeOwners= */ null));
+    assertThat(npe).hasMessageThat().isEqualTo("pathCodeOwners");
+  }
+
+  @Test
   public void cannotResolvePathCodeOwnersForRelativePath() throws Exception {
     String relativePath = "foo/bar.md";
     CodeOwnerConfig codeOwnerConfig =