Merge "CodeOwnerApprovalCheck: Factor out computation of input data"
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java b/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java
index 1309f8f..89f493f 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java
@@ -36,6 +36,7 @@
 public class BackendModule extends FactoryModule {
   @Override
   protected void configure() {
+    factory(CodeOwnerApprovalCheckInput.Loader.Factory.class);
     factory(CodeOwnersUpdate.Factory.class);
     factory(CodeOwnerConfigScanner.Factory.class);
     factory(CodeOwnersPluginGlobalConfigSnapshot.Factory.class);
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
index 9045c90..792f641 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
@@ -41,14 +41,12 @@
 import com.google.gerrit.plugins.codeowners.common.ChangedFile;
 import com.google.gerrit.plugins.codeowners.common.CodeOwnerStatus;
 import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
-import com.google.gerrit.server.approval.ApprovalsUtil;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.PureRevertCache;
 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.util.AccountTemplateUtil;
-import com.google.gerrit.server.util.LabelVote;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -89,7 +87,7 @@
   private final PureRevertCache pureRevertCache;
   private final Provider<CodeOwnerConfigHierarchy> codeOwnerConfigHierarchyProvider;
   private final Provider<CodeOwnerResolver> codeOwnerResolverProvider;
-  private final ApprovalsUtil approvalsUtil;
+  private final CodeOwnerApprovalCheckInput.Loader.Factory inputLoaderFactory;
   private final CodeOwnerMetrics codeOwnerMetrics;
 
   @Inject
@@ -100,7 +98,7 @@
       PureRevertCache pureRevertCache,
       Provider<CodeOwnerConfigHierarchy> codeOwnerConfigHierarchyProvider,
       Provider<CodeOwnerResolver> codeOwnerResolverProvider,
-      ApprovalsUtil approvalsUtil,
+      CodeOwnerApprovalCheckInput.Loader.Factory codeOwnerApprovalCheckInputLoaderFactory,
       CodeOwnerMetrics codeOwnerMetrics) {
     this.repoManager = repoManager;
     this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
@@ -108,7 +106,7 @@
     this.pureRevertCache = pureRevertCache;
     this.codeOwnerConfigHierarchyProvider = codeOwnerConfigHierarchyProvider;
     this.codeOwnerResolverProvider = codeOwnerResolverProvider;
-    this.approvalsUtil = approvalsUtil;
+    this.inputLoaderFactory = codeOwnerApprovalCheckInputLoaderFactory;
     this.codeOwnerMetrics = codeOwnerMetrics;
   }
 
@@ -315,7 +313,6 @@
       CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig =
           codeOwnersPluginConfiguration.getProjectConfig(changeNotes.getProjectName());
 
-      Account.Id changeOwner = changeNotes.getChange().getOwner();
       Account.Id patchSetUploader = changeNotes.getCurrentPatchSet().uploader();
       ImmutableSet<Account.Id> exemptedAccounts = codeOwnersConfig.getExemptedAccounts();
       logger.atFine().log("exemptedAccounts = %s", exemptedAccounts);
@@ -342,38 +339,6 @@
             "change is a pure revert and is exempted from requiring code owner approvals");
       }
 
-      boolean implicitApprovalConfig = codeOwnersConfig.areImplicitApprovalsEnabled();
-      boolean enableImplicitApproval =
-          implicitApprovalConfig && changeOwner.equals(patchSetUploader);
-      logger.atFine().log(
-          "changeOwner = %d, patchSetUploader = %d, implict approval config = %s\n"
-              + "=> implicit approval is %s",
-          changeOwner.get(),
-          patchSetUploader.get(),
-          implicitApprovalConfig,
-          enableImplicitApproval ? "enabled" : "disabled");
-
-      ImmutableList<PatchSetApproval> currentPatchSetApprovals =
-          getCurrentPatchSetApprovals(changeNotes);
-
-      RequiredApproval requiredApproval = codeOwnersConfig.getRequiredApproval();
-      logger.atFine().log("requiredApproval = %s", requiredApproval);
-
-      ImmutableSet<RequiredApproval> overrideApprovals = codeOwnersConfig.getOverrideApprovals();
-      ImmutableSet<PatchSetApproval> overrides =
-          getOverride(currentPatchSetApprovals, overrideApprovals, patchSetUploader);
-      logger.atFine().log(
-          "hasOverride = %s (overrideApprovals = %s, overrides = %s)",
-          !overrides.isEmpty(),
-          overrideApprovals.stream()
-              .map(
-                  overrideApproval ->
-                      String.format(
-                          "%s (ignoreSelfApproval = %s)",
-                          overrideApproval, overrideApproval.labelType().isIgnoreSelfApproval()))
-              .collect(toImmutableList()),
-          overrides);
-
       BranchNameKey branch = changeNotes.getChange().getDest();
       Optional<ObjectId> revision = getDestBranchRevision(changeNotes.getChange());
       if (revision.isPresent()) {
@@ -383,17 +348,8 @@
         logger.atFine().log("dest branch %s does not exist", branch.branch());
       }
 
-      CodeOwnerResolverResult globalCodeOwners =
-          codeOwnerResolver.resolveGlobalCodeOwners(changeNotes.getProjectName());
-      logger.atFine().log("global code owners = %s", globalCodeOwners);
-
-      ImmutableSet<Account.Id> reviewerAccountIds =
-          getReviewerAccountIds(requiredApproval, changeNotes, patchSetUploader);
-      ImmutableSet<Account.Id> approverAccountIds =
-          getApproverAccountIds(currentPatchSetApprovals, requiredApproval, patchSetUploader);
-      logger.atFine().log("reviewers = %s, approvers = %s", reviewerAccountIds, approverAccountIds);
-
-      FallbackCodeOwners fallbackCodeOwners = codeOwnersConfig.getFallbackCodeOwners();
+      CodeOwnerApprovalCheckInput input =
+          inputLoaderFactory.create(codeOwnersConfig, codeOwnerResolver, changeNotes).load();
 
       return changedFiles
           .getFromDiffCache(
@@ -406,14 +362,8 @@
                       codeOwnerResolver,
                       branch,
                       revision.orElse(null),
-                      globalCodeOwners,
-                      enableImplicitApproval ? changeOwner : null,
-                      reviewerAccountIds,
-                      approverAccountIds,
-                      fallbackCodeOwners,
-                      overrides,
                       changedFile,
-                      /* checkAllOwners= */ false));
+                      input));
     }
   }
 
@@ -470,8 +420,10 @@
       CodeOwnerConfigHierarchy codeOwnerConfigHierarchy = codeOwnerConfigHierarchyProvider.get();
       CodeOwnerResolver codeOwnerResolver =
           codeOwnerResolverProvider.get().enforceVisibility(false);
-      return changedFiles
-          .getFromDiffCache(changeNotes.getProjectName(), patchSet.commitId())
+      CodeOwnerApprovalCheckInput input =
+          CodeOwnerApprovalCheckInput.createForComputingOwnedPaths(
+              codeOwnersConfig, codeOwnerResolver, changeNotes, accountIds);
+      return changedFiles.getFromDiffCache(changeNotes.getProjectName(), patchSet.commitId())
           .stream()
           .map(
               changedFile ->
@@ -480,20 +432,8 @@
                       codeOwnerResolver,
                       branch,
                       revision.orElse(null),
-                      /* globalCodeOwners= */ codeOwnerResolver.resolveGlobalCodeOwners(
-                          changeNotes.getProjectName()),
-                      // Do not check for implicit approvals since implicit approvals of other users
-                      // should be ignored. For the given account we do not need to check for
-                      // implicit approvals since all owned files are already covered by the
-                      // explicit approval.
-                      /* implicitApprover= */ null,
-                      /* reviewerAccountIds= */ ImmutableSet.of(),
-                      // Assume an explicit approval of the owners we want to check.
-                      /* approverAccountIds= */ accountIds,
-                      fallbackCodeOwners,
-                      /* overrides= */ ImmutableSet.of(),
                       changedFile,
-                      /* checkAllOwners= */ true));
+                      input));
     }
   }
 
@@ -538,14 +478,8 @@
       CodeOwnerResolver codeOwnerResolver,
       BranchNameKey branch,
       @Nullable ObjectId revision,
-      CodeOwnerResolverResult globalCodeOwners,
-      @Nullable Account.Id implicitApprover,
-      ImmutableSet<Account.Id> reviewerAccountIds,
-      ImmutableSet<Account.Id> approverAccountIds,
-      FallbackCodeOwners fallbackCodeOwners,
-      ImmutableSet<PatchSetApproval> overrides,
       ChangedFile changedFile,
-      boolean checkAllOwners) {
+      CodeOwnerApprovalCheckInput input) {
     try (Timer0.Context ctx = codeOwnerMetrics.computeFileStatus.start()) {
       logger.atFine().log("computing file status for %s", changedFile);
 
@@ -560,14 +494,8 @@
                           codeOwnerResolver,
                           branch,
                           revision,
-                          globalCodeOwners,
-                          implicitApprover,
-                          reviewerAccountIds,
-                          approverAccountIds,
-                          fallbackCodeOwners,
-                          overrides,
                           newPath,
-                          checkAllOwners));
+                          input));
 
       // Compute the code owner status for the old path, if the file was deleted or renamed.
       Optional<PathCodeOwnerStatus> oldPathStatus = Optional.empty();
@@ -584,14 +512,8 @@
                     codeOwnerResolver,
                     branch,
                     revision,
-                    globalCodeOwners,
-                    implicitApprover,
-                    reviewerAccountIds,
-                    approverAccountIds,
-                    fallbackCodeOwners,
-                    overrides,
                     changedFile.oldPath().get(),
-                    checkAllOwners));
+                    input));
       }
 
       FileCodeOwnerStatus fileCodeOwnerStatus =
@@ -606,21 +528,15 @@
       CodeOwnerResolver codeOwnerResolver,
       BranchNameKey branch,
       @Nullable ObjectId revision,
-      CodeOwnerResolverResult globalCodeOwners,
-      @Nullable Account.Id implicitApprover,
-      ImmutableSet<Account.Id> reviewerAccountIds,
-      ImmutableSet<Account.Id> approverAccountIds,
-      FallbackCodeOwners fallbackCodeOwners,
-      ImmutableSet<PatchSetApproval> overrides,
       Path absolutePath,
-      boolean checkAllOwners) {
+      CodeOwnerApprovalCheckInput input) {
     logger.atFine().log("computing path status for %s", absolutePath);
 
-    if (!overrides.isEmpty()) {
+    if (!input.overrides().isEmpty()) {
       logger.atFine().log(
           "the status for path %s is %s since an override is present (overrides = %s)",
-          absolutePath, CodeOwnerStatus.APPROVED.name(), overrides);
-      Optional<PatchSetApproval> override = overrides.stream().findAny();
+          absolutePath, CodeOwnerStatus.APPROVED.name(), input.overrides());
+      Optional<PatchSetApproval> override = input.overrides().stream().findAny();
       checkState(override.isPresent(), "no override found");
       return PathCodeOwnerStatus.create(
           absolutePath,
@@ -638,35 +554,36 @@
 
     boolean isGloballyApproved =
         isApproved(
-            globalCodeOwners,
+            input.globalCodeOwners(),
             CodeOwnerKind.GLOBAL_CODE_OWNER,
-            approverAccountIds,
-            implicitApprover,
+            input.approvers(),
+            input.implicitApprover().orElse(null),
             reason);
 
     if (isGloballyApproved) {
       codeOwnerStatus.set(CodeOwnerStatus.APPROVED);
     }
 
-    if (globalCodeOwners.ownedByAllUsers()) {
-      activeOwners.addAll(approverAccountIds);
-      activeOwners.addAll(reviewerAccountIds);
+    if (input.globalCodeOwners().ownedByAllUsers()) {
+      activeOwners.addAll(input.approvers());
+      activeOwners.addAll(input.reviewers());
     } else {
       activeOwners.addAll(
-          Sets.intersection(globalCodeOwners.codeOwnersAccountIds(), approverAccountIds));
+          Sets.intersection(input.globalCodeOwners().codeOwnersAccountIds(), input.approvers()));
       activeOwners.addAll(
-          Sets.intersection(globalCodeOwners.codeOwnersAccountIds(), reviewerAccountIds));
+          Sets.intersection(input.globalCodeOwners().codeOwnersAccountIds(), input.reviewers()));
     }
 
     // Only check recursively for all OWNERs in two scenarios:
     // 1. The path was not globally approved
     // 2. The path was globally approved but is not owned by all users and we
     //    want to calculate all ownerIds.
-    if (!isGloballyApproved || (checkAllOwners && !globalCodeOwners.ownedByAllUsers())) {
+    if (!isGloballyApproved
+        || (input.checkAllOwners() && !input.globalCodeOwners().ownedByAllUsers())) {
       logger.atFine().log("%s was not approved by a global code owner", absolutePath);
 
       if (isPending(
-          globalCodeOwners, CodeOwnerKind.GLOBAL_CODE_OWNER, reviewerAccountIds, reason)) {
+          input.globalCodeOwners(), CodeOwnerKind.GLOBAL_CODE_OWNER, input.reviewers(), reason)) {
         codeOwnerStatus.set(CodeOwnerStatus.PENDING);
       }
 
@@ -688,13 +605,13 @@
 
                 boolean ownedByAllUsers = codeOwners.ownedByAllUsers();
                 if (ownedByAllUsers) {
-                  activeOwners.addAll(approverAccountIds);
-                  activeOwners.addAll(reviewerAccountIds);
+                  activeOwners.addAll(input.approvers());
+                  activeOwners.addAll(input.reviewers());
                 } else {
                   activeOwners.addAll(
-                      Sets.intersection(codeOwners.codeOwnersAccountIds(), approverAccountIds));
+                      Sets.intersection(codeOwners.codeOwnersAccountIds(), input.approvers()));
                   activeOwners.addAll(
-                      Sets.intersection(codeOwners.codeOwnersAccountIds(), reviewerAccountIds));
+                      Sets.intersection(codeOwners.codeOwnersAccountIds(), input.reviewers()));
                 }
                 logger.atFine().log(
                     "code owners = %s (code owner kind = %s, code owner config folder path = %s,"
@@ -709,12 +626,16 @@
                 }
 
                 if (isApproved(
-                    codeOwners, codeOwnerKind, approverAccountIds, implicitApprover, reason)) {
+                    codeOwners,
+                    codeOwnerKind,
+                    input.approvers(),
+                    input.implicitApprover().orElse(null),
+                    reason)) {
                   codeOwnerStatus.set(CodeOwnerStatus.APPROVED);
                   // No need to recurse if we are not checking all owners or all owners are
                   // are already added.
-                  return checkAllOwners && !ownedByAllUsers;
-                } else if (isPending(codeOwners, codeOwnerKind, reviewerAccountIds, reason)) {
+                  return input.checkAllOwners() && !ownedByAllUsers;
+                } else if (isPending(codeOwners, codeOwnerKind, input.reviewers(), reason)) {
                   codeOwnerStatus.set(CodeOwnerStatus.PENDING);
 
                   // We need to continue to check if any of the higher-level code owners approved
@@ -742,25 +663,25 @@
         CodeOwnerStatus codeOwnerStatusForFallbackCodeOwners =
             getCodeOwnerStatusForFallbackCodeOwners(
                 codeOwnerStatus.get(),
-                implicitApprover,
-                reviewerAccountIds,
-                approverAccountIds,
-                fallbackCodeOwners,
+                input.implicitApprover().orElse(null),
+                input.reviewers(),
+                input.approvers(),
+                input.fallbackCodeOwners(),
                 absolutePath,
                 reason);
 
         if (codeOwnerStatusForFallbackCodeOwners.equals(CodeOwnerStatus.APPROVED)) {
-          activeOwners.addAll(approverAccountIds);
+          activeOwners.addAll(input.approvers());
         } else if (codeOwnerStatusForFallbackCodeOwners.equals(CodeOwnerStatus.PENDING)) {
-          switch (fallbackCodeOwners) {
+          switch (input.fallbackCodeOwners()) {
             case NONE:
               // do nothing, if codeOwnerStatus is PENDING, the reviewers that are code owners have
               // already been added to activeOwners
               break;
             case ALL_USERS:
               // all users are code owners
-              activeOwners.addAll(approverAccountIds);
-              activeOwners.addAll(reviewerAccountIds);
+              activeOwners.addAll(input.approvers());
+              activeOwners.addAll(input.reviewers());
               break;
           }
         }
@@ -797,7 +718,7 @@
             absolutePath,
             codeOwnerStatus.get(),
             reason.get(),
-            checkAllOwners ? Optional.of(activeOwners.build()) : Optional.empty());
+            input.checkAllOwners() ? Optional.of(activeOwners.build()) : Optional.empty());
     logger.atFine().log("pathCodeOwnerStatus = %s", pathCodeOwnerStatus);
     return pathCodeOwnerStatus;
   }
@@ -985,111 +906,6 @@
   }
 
   /**
-   * Gets the IDs of the accounts that are reviewer on the given change.
-   *
-   * @param changeNotes the change notes
-   */
-  private ImmutableSet<Account.Id> getReviewerAccountIds(
-      RequiredApproval requiredApproval, ChangeNotes changeNotes, Account.Id patchSetUploader) {
-    ImmutableSet<Account.Id> reviewerAccountIds =
-        changeNotes.getReviewers().byState(ReviewerStateInternal.REVIEWER);
-    if (requiredApproval.labelType().isIgnoreSelfApproval()
-        && reviewerAccountIds.contains(patchSetUploader)) {
-      logger.atFine().log(
-          "Removing patch set uploader %s from reviewers since the label of the required"
-              + " approval (%s) is configured to ignore self approvals",
-          patchSetUploader, requiredApproval.labelType());
-      return filterOutAccount(reviewerAccountIds, patchSetUploader);
-    }
-    return reviewerAccountIds;
-  }
-
-  /**
-   * Gets the IDs of the accounts that posted a patch set approval on the given revisions that
-   * counts as code owner approval.
-   *
-   * @param requiredApproval approval that is required from code owners to approve the files in a
-   *     change
-   */
-  private ImmutableSet<Account.Id> getApproverAccountIds(
-      ImmutableList<PatchSetApproval> currentPatchSetApprovals,
-      RequiredApproval requiredApproval,
-      Account.Id patchSetUploader) {
-    ImmutableSet<Account.Id> approverAccountIds =
-        currentPatchSetApprovals.stream()
-            .filter(requiredApproval::isApprovedBy)
-            .map(PatchSetApproval::accountId)
-            .collect(toImmutableSet());
-
-    if (requiredApproval.labelType().isIgnoreSelfApproval()
-        && approverAccountIds.contains(patchSetUploader)) {
-      logger.atFine().log(
-          "Removing patch set uploader %s from approvers since the label of the required"
-              + " approval (%s) is configured to ignore self approvals",
-          patchSetUploader, requiredApproval.labelType());
-      return filterOutAccount(approverAccountIds, patchSetUploader);
-    }
-
-    return approverAccountIds;
-  }
-
-  private ImmutableList<PatchSetApproval> getCurrentPatchSetApprovals(ChangeNotes changeNotes) {
-    try (Timer0.Context ctx = codeOwnerMetrics.computePatchSetApprovals.start()) {
-      return ImmutableList.copyOf(
-          approvalsUtil.byPatchSet(changeNotes, changeNotes.getCurrentPatchSet().id()));
-    }
-  }
-
-  private ImmutableSet<Account.Id> filterOutAccount(
-      ImmutableSet<Account.Id> accountIds, Account.Id accountIdToFilterOut) {
-    return accountIds.stream()
-        .filter(accountId -> !accountId.equals(accountIdToFilterOut))
-        .collect(toImmutableSet());
-  }
-
-  /**
-   * Gets the overrides that were applied on the change.
-   *
-   * @param overrideApprovals approvals that count as override for the code owners submit check.
-   * @param patchSetUploader account ID of the patch set uploader
-   * @return the overrides that were applied on the change
-   */
-  private ImmutableSet<PatchSetApproval> getOverride(
-      ImmutableList<PatchSetApproval> currentPatchSetApprovals,
-      ImmutableSet<RequiredApproval> overrideApprovals,
-      Account.Id patchSetUploader) {
-    ImmutableSet<RequiredApproval> overrideApprovalsThatIgnoreSelfApprovals =
-        overrideApprovals.stream()
-            .filter(overrideApproval -> overrideApproval.labelType().isIgnoreSelfApproval())
-            .collect(toImmutableSet());
-    return currentPatchSetApprovals.stream()
-        .filter(
-            approval -> {
-              // If the approval is from the patch set uploader and if it matches any of the labels
-              // for which self approvals are ignored, filter it out.
-              if (approval.accountId().equals(patchSetUploader)
-                  && overrideApprovalsThatIgnoreSelfApprovals.stream()
-                      .anyMatch(
-                          requiredApproval ->
-                              requiredApproval
-                                  .labelType()
-                                  .getLabelId()
-                                  .equals(approval.key().labelId()))) {
-                logger.atFine().log(
-                    "Filtered out self-override %s of patch set uploader",
-                    LabelVote.create(approval.label(), approval.value()));
-                return false;
-              }
-              return true;
-            })
-        .filter(
-            patchSetApproval ->
-                overrideApprovals.stream()
-                    .anyMatch(overrideApproval -> overrideApproval.isApprovedBy(patchSetApproval)))
-        .collect(toImmutableSet());
-  }
-
-  /**
    * Gets the current revision of the destination branch of the given change.
    *
    * <p>This is the revision from which the code owner configs should be read when computing code
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckInput.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckInput.java
new file mode 100644
index 0000000..cd2ef0e
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckInput.java
@@ -0,0 +1,361 @@
+// Copyright (C) 2022 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 static com.google.common.collect.ImmutableSet.toImmutableSet;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginProjectConfigSnapshot;
+import com.google.gerrit.plugins.codeowners.backend.config.RequiredApproval;
+import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
+import com.google.gerrit.server.approval.ApprovalsUtil;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
+import com.google.gerrit.server.util.LabelVote;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.util.Optional;
+
+/**
+ * Input for {@link CodeOwnerApprovalCheck}.
+ *
+ * <p>Provides all data that is needed to check the code owner approvals on a change.
+ */
+@AutoValue
+public abstract class CodeOwnerApprovalCheckInput {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  /**
+   * Gets the IDs of the accounts of all reviewers that can possibly code owner approve the change
+   * (if they are code owners).
+   *
+   * <p>If self approvals are ignored the patch set uploader is filtered out since in this case the
+   * patch set uploader cannot approve the change even if they are a code owner.
+   */
+  public abstract ImmutableSet<Account.Id> reviewers();
+
+  /**
+   * Gets the IDs of the accounts that have an approval on the current patch set that possibly
+   * counts as code owner approval (if they are code owners).
+   *
+   * <p>If self approvals are ignored the patch set uploader is filtered out since in this case the
+   * approval of the patch set uploader is ignored even if they are a code owner.
+   */
+  public abstract ImmutableSet<Account.Id> approvers();
+
+  /**
+   * Account from which an implicit code owner approval should be assumed.
+   *
+   * @see CodeOwnersPluginProjectConfigSnapshot#areImplicitApprovalsEnabled()
+   * @return the account of the change owner if implicit approvals are enabled, otherwise {@link
+   *     Optional#empty()}
+   */
+  public abstract Optional<Account.Id> implicitApprover();
+
+  /**
+   * Gets the approvals from the current patch set that count as code owner overrides.
+   *
+   * <p>If self approvals are ignored an override of the patch set uploader is filtered out since it
+   * doesn't count as code owner override.
+   */
+  public abstract ImmutableSet<PatchSetApproval> overrides();
+
+  /** Gets the configured global code owners. */
+  public abstract CodeOwnerResolverResult globalCodeOwners();
+
+  /** Gets the policy that defines who owns paths for which no code owners are defined. */
+  public abstract FallbackCodeOwners fallbackCodeOwners();
+
+  /**
+   * Whether all code owners should be checked. *
+   *
+   * <p>If {@code true} {@link PathCodeOwnerStatus#owners()} are expected to be set in {@link
+   * PathCodeOwnerStatus} instances that are created by {@link CodeOwnerApprovalCheck}.
+   */
+  public abstract boolean checkAllOwners();
+
+  /**
+   * Creates a {@link CodeOwnerApprovalCheckInput} instance for computing the paths in a change that
+   * are owned by the given accounts.
+   *
+   * @param codeOwnersConfig the code-owners plugin configuration
+   * @param codeOwnerResolver the {@link CodeOwnerResolver} that should be used to resolve the
+   *     configured global code owners
+   * @param changeNotes the notes of the change for which owned paths should be computed
+   * @param accounts the accounts for which the owned paths should be computed
+   * @return the created {@link CodeOwnerApprovalCheckInput} instance
+   */
+  public static CodeOwnerApprovalCheckInput createForComputingOwnedPaths(
+      CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig,
+      CodeOwnerResolver codeOwnerResolver,
+      ChangeNotes changeNotes,
+      ImmutableSet<Account.Id> accounts) {
+    CodeOwnerResolverResult globalCodeOwners =
+        codeOwnerResolver.resolveGlobalCodeOwners(changeNotes.getProjectName());
+    logger.atFine().log("global code owners = %s", globalCodeOwners);
+
+    FallbackCodeOwners fallbackCodeOwners = codeOwnersConfig.getFallbackCodeOwners();
+    logger.atFine().log("fallbackCodeOwner = %s", fallbackCodeOwners);
+
+    return create(
+        /* reviewers= */ ImmutableSet.of(),
+        /* approvers= */ accounts,
+        // Do not check for implicit approvals since implicit approvals of other users
+        // should be ignored. For the given account we do not need to check for
+        // implicit approvals since all owned files are already covered by the
+        // explicit approval.
+        /* implicitApprover= */ Optional.empty(),
+        /* overrides= */ ImmutableSet.of(),
+        globalCodeOwners,
+        fallbackCodeOwners,
+        /* checkAllOwners= */ true);
+  }
+
+  /**
+   * Creates a {@link CodeOwnerApprovalCheckInput} instance.
+   *
+   * @param reviewers the reviewers that can possibly code owner approve the change (if they are
+   *     code owners)
+   * @param approvers the accounts that have an approval on the current patch set that possibly
+   *     counts as code owner approval (if they are code owners)
+   * @param implicitApprover account from which an implicit code owner approval should be assumed
+   * @param overrides the approvals from the current patch set that count as code owner overrides
+   * @param globalCodeOwners the configured global code owners
+   * @param fallbackCodeOwners the policy that defines who owns paths for which no code owners are
+   *     defined
+   * @param checkAllOwners Whether all code owners are checked. If {@code true} {@link
+   *     PathCodeOwnerStatus#owners()} will be set in the the {@link PathCodeOwnerStatus} instances
+   *     that are created by {@link CodeOwnerApprovalCheck}. Checking all owners means that no
+   *     shortcuts can be applied, hence checking the code owner approvals with {@code
+   *     checkAllOwners=true} is more expensive.
+   * @return the created {@link CodeOwnerApprovalCheckInput} instance
+   */
+  private static CodeOwnerApprovalCheckInput create(
+      ImmutableSet<Account.Id> reviewers,
+      ImmutableSet<Account.Id> approvers,
+      Optional<Account.Id> implicitApprover,
+      ImmutableSet<PatchSetApproval> overrides,
+      CodeOwnerResolverResult globalCodeOwners,
+      FallbackCodeOwners fallbackCodeOwners,
+      boolean checkAllOwners) {
+    return new AutoValue_CodeOwnerApprovalCheckInput(
+        reviewers,
+        approvers,
+        implicitApprover,
+        overrides,
+        globalCodeOwners,
+        fallbackCodeOwners,
+        checkAllOwners);
+  }
+
+  /**
+   * Class to load all inputs that are required for checking the code owner approvals on a change.
+   */
+  public static class Loader {
+    /** Factory to create the {@link Loader} with injected dependencies. */
+    interface Factory {
+      Loader create(
+          CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig,
+          CodeOwnerResolver codeOwnerResolver,
+          ChangeNotes changeNotes);
+    }
+
+    private final ApprovalsUtil approvalsUtil;
+    private final CodeOwnerMetrics codeOwnerMetrics;
+    private final CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig;
+    private final CodeOwnerResolver codeOwnerResolver;
+    private final ChangeNotes changeNotes;
+
+    @Inject
+    Loader(
+        ApprovalsUtil approvalsUtil,
+        CodeOwnerMetrics codeOwnerMetrics,
+        @Assisted CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig,
+        @Assisted CodeOwnerResolver codeOwnerResolver,
+        @Assisted ChangeNotes changeNotes) {
+      this.approvalsUtil = approvalsUtil;
+      this.codeOwnerMetrics = codeOwnerMetrics;
+      this.codeOwnersConfig = codeOwnersConfig;
+      this.codeOwnerResolver = codeOwnerResolver;
+      this.changeNotes = changeNotes;
+    }
+
+    CodeOwnerApprovalCheckInput load() {
+      logger.atFine().log(
+          "requiredApproval = %s, overrideApprovals = %s",
+          codeOwnersConfig.getRequiredApproval().formatForLogging(),
+          RequiredApproval.formatForLogging(codeOwnersConfig.getOverrideApprovals()));
+      return CodeOwnerApprovalCheckInput.create(
+          getReviewers(),
+          getApprovers(),
+          getImplicitApprover(),
+          getOverrides(),
+          getGlobalCodeOwners(),
+          getFallbackCodeOwners(),
+          /* checkAllOwners= */ false);
+    }
+
+    /**
+     * Gets the IDs of the accounts of all reviewers that can possibly code owner approve the change
+     * (if they are code owners).
+     *
+     * <p>If self approvals are ignored the patch set uploader is filtered out since in this case
+     * the patch set uploader cannot approve the change even if they are a code owner.
+     */
+    private ImmutableSet<Account.Id> getReviewers() {
+      ImmutableSet<Account.Id> reviewerAccountIds =
+          changeNotes.getReviewers().byState(ReviewerStateInternal.REVIEWER);
+      RequiredApproval requiredApproval = codeOwnersConfig.getRequiredApproval();
+      Account.Id patchSetUploader = changeNotes.getCurrentPatchSet().uploader();
+      if (requiredApproval.labelType().isIgnoreSelfApproval()
+          && reviewerAccountIds.contains(patchSetUploader)) {
+        logger.atFine().log(
+            "Removing patch set uploader %s from reviewers since the label of the required"
+                + " approval (%s) is configured to ignore self approvals",
+            patchSetUploader, requiredApproval.labelType());
+        return filterOutAccount(reviewerAccountIds, patchSetUploader);
+      }
+      logger.atFine().log("reviewers = %s", reviewerAccountIds);
+      return reviewerAccountIds;
+    }
+
+    /**
+     * Gets the IDs of the accounts that have an approval on the current patch set that possibly
+     * counts as code owner approval (if they are code owners).
+     *
+     * <p>If self approvals are ignored the patch set uploader is filtered out since in this case
+     * the approval of the patch set uploader is ignored even if they are a code owner.
+     */
+    private ImmutableSet<Account.Id> getApprovers() {
+      ImmutableList<PatchSetApproval> currentPatchSetApprovals =
+          getCurrentPatchSetApprovals(changeNotes);
+      RequiredApproval requiredApproval = codeOwnersConfig.getRequiredApproval();
+      ImmutableSet<Account.Id> approverAccountIds =
+          currentPatchSetApprovals.stream()
+              .filter(requiredApproval::isApprovedBy)
+              .map(PatchSetApproval::accountId)
+              .collect(toImmutableSet());
+      Account.Id patchSetUploader = changeNotes.getCurrentPatchSet().uploader();
+      if (requiredApproval.labelType().isIgnoreSelfApproval()
+          && approverAccountIds.contains(patchSetUploader)) {
+        logger.atFine().log(
+            "Removing patch set uploader %s from approvers since the label of the required"
+                + " approval (%s) is configured to ignore self approvals",
+            patchSetUploader, requiredApproval.labelType());
+        return filterOutAccount(approverAccountIds, patchSetUploader);
+      }
+      logger.atFine().log("approvers = %s", approverAccountIds);
+      return approverAccountIds;
+    }
+
+    private Optional<Account.Id> getImplicitApprover() {
+      Account.Id changeOwner = changeNotes.getChange().getOwner();
+      Account.Id patchSetUploader = changeNotes.getCurrentPatchSet().uploader();
+      boolean implicitApprovalConfig = codeOwnersConfig.areImplicitApprovalsEnabled();
+      boolean enableImplicitApproval =
+          implicitApprovalConfig && changeOwner.equals(patchSetUploader);
+      logger.atFine().log(
+          "changeOwner = %d, patchSetUploader = %d, implict approval config = %s\n"
+              + "=> implicit approval is %s",
+          changeOwner.get(),
+          patchSetUploader.get(),
+          implicitApprovalConfig,
+          enableImplicitApproval ? "enabled" : "disabled");
+      return enableImplicitApproval ? Optional.of(changeOwner) : Optional.empty();
+    }
+
+    /**
+     * Gets the approvals from the current patch set that count as code owner overrides.
+     *
+     * <p>If self approvals are ignored an override of the patch set uploader is filtered out since
+     * it doesn't count as code owner override.
+     */
+    private ImmutableSet<PatchSetApproval> getOverrides() {
+      ImmutableList<PatchSetApproval> currentPatchSetApprovals =
+          getCurrentPatchSetApprovals(changeNotes);
+      ImmutableSortedSet<RequiredApproval> overrideApprovals =
+          codeOwnersConfig.getOverrideApprovals();
+      ImmutableSet<RequiredApproval> overrideApprovalsThatIgnoreSelfApprovals =
+          overrideApprovals.stream()
+              .filter(overrideApproval -> overrideApproval.labelType().isIgnoreSelfApproval())
+              .collect(toImmutableSet());
+      Account.Id patchSetUploader = changeNotes.getCurrentPatchSet().uploader();
+      ImmutableSet<PatchSetApproval> overrides =
+          currentPatchSetApprovals.stream()
+              .filter(
+                  approval -> {
+                    // If the approval is from the patch set uploader and if it matches any of the
+                    // labels
+                    // for which self approvals are ignored, filter it out.
+                    if (approval.accountId().equals(patchSetUploader)
+                        && overrideApprovalsThatIgnoreSelfApprovals.stream()
+                            .anyMatch(
+                                requiredApproval ->
+                                    requiredApproval
+                                        .labelType()
+                                        .getLabelId()
+                                        .equals(approval.key().labelId()))) {
+                      logger.atFine().log(
+                          "Filtered out self-override %s of patch set uploader",
+                          LabelVote.create(approval.label(), approval.value()));
+                      return false;
+                    }
+                    return true;
+                  })
+              .filter(
+                  patchSetApproval ->
+                      overrideApprovals.stream()
+                          .anyMatch(
+                              overrideApproval -> overrideApproval.isApprovedBy(patchSetApproval)))
+              .collect(toImmutableSet());
+      logger.atFine().log("hasOverride = %s (overrides = %s)", !overrides.isEmpty(), overrides);
+      return overrides;
+    }
+
+    private CodeOwnerResolverResult getGlobalCodeOwners() {
+      CodeOwnerResolverResult globalCodeOwners =
+          codeOwnerResolver.resolveGlobalCodeOwners(changeNotes.getProjectName());
+      logger.atFine().log("global code owners = %s", globalCodeOwners);
+      return globalCodeOwners;
+    }
+
+    private FallbackCodeOwners getFallbackCodeOwners() {
+      FallbackCodeOwners fallbackCodeOwners = codeOwnersConfig.getFallbackCodeOwners();
+      logger.atFine().log("fallbackCodeOwners = %s", fallbackCodeOwners);
+      return fallbackCodeOwners;
+    }
+
+    private ImmutableList<PatchSetApproval> getCurrentPatchSetApprovals(ChangeNotes changeNotes) {
+      try (Timer0.Context ctx = codeOwnerMetrics.computePatchSetApprovals.start()) {
+        return ImmutableList.copyOf(
+            approvalsUtil.byPatchSet(changeNotes, changeNotes.getCurrentPatchSet().id()));
+      }
+    }
+
+    private static ImmutableSet<Account.Id> filterOutAccount(
+        ImmutableSet<Account.Id> accountIds, Account.Id accountIdToFilterOut) {
+      return accountIds.stream()
+          .filter(accountId -> !accountId.equals(accountIdToFilterOut))
+          .collect(toImmutableSet());
+    }
+  }
+}
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 5b2ab1c..89fc56e 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginProjectConfigSnapshot.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginProjectConfigSnapshot.java
@@ -580,7 +580,7 @@
   /**
    * 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
+   * <p>If enabled, an implicit code owner approval from the change owner is assumed if the last
    * patch set was uploaded by the change owner.
    */
   public boolean areImplicitApprovalsEnabled() {
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/RequiredApproval.java b/java/com/google/gerrit/plugins/codeowners/backend/config/RequiredApproval.java
index 4cbdba0..9dd283d 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/config/RequiredApproval.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/RequiredApproval.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static java.util.Objects.requireNonNull;
 
 import com.google.auto.value.AutoValue;
@@ -25,6 +26,7 @@
 import com.google.gerrit.entities.LabelType;
 import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.server.project.ProjectState;
+import java.util.Collection;
 import java.util.Optional;
 
 /**
@@ -69,6 +71,18 @@
     return labelType().getName() + "+" + value();
   }
 
+  public final String formatForLogging() {
+    return String.format(
+        "%s (ignoreSelfApproval = %s)", toString(), labelType().isIgnoreSelfApproval());
+  }
+
+  public static String formatForLogging(Collection<RequiredApproval> requiredApprovals) {
+    return requiredApprovals.stream()
+        .map(RequiredApproval::formatForLogging)
+        .collect(toImmutableList())
+        .toString();
+  }
+
   /**
    * Parses a string-representation of a {@link RequiredApproval}.
    *