| // Copyright (C) 2020 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.gerrit.plugins.codeowners.backend; |
| |
| import static com.google.common.base.Preconditions.checkState; |
| import static com.google.common.collect.ImmutableList.toImmutableList; |
| import static com.google.common.collect.ImmutableSet.toImmutableSet; |
| import static java.util.Objects.requireNonNull; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.entities.Account; |
| import com.google.gerrit.entities.BranchNameKey; |
| import com.google.gerrit.entities.Change; |
| import com.google.gerrit.entities.LabelValue; |
| import com.google.gerrit.entities.PatchSet; |
| import com.google.gerrit.entities.PatchSetApproval; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.extensions.restapi.BadRequestException; |
| import com.google.gerrit.extensions.restapi.ResourceConflictException; |
| import com.google.gerrit.metrics.Timer0; |
| import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration; |
| import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginProjectConfigSnapshot; |
| import com.google.gerrit.plugins.codeowners.backend.config.RequiredApproval; |
| 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.permissions.PermissionBackend; |
| import com.google.gerrit.server.permissions.PermissionBackendException; |
| import com.google.gerrit.server.permissions.ProjectPermission; |
| 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; |
| import java.io.IOException; |
| import java.nio.file.Path; |
| import java.util.Collections; |
| import java.util.Optional; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.concurrent.atomic.AtomicReference; |
| import java.util.stream.Stream; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| |
| /** |
| * Class to check code owner approvals on a change. |
| * |
| * <p>Every file, or rather touched path, in a change needs to be approved by a code owner: |
| * |
| * <ul> |
| * <li>New file: requires approval on the new path |
| * <li>Modified file: requires approval on the old/new path (old path == new path) |
| * <li>Deleted file: requires approval on the old path |
| * <li>Renamed file: requires approval on the old and new path (equivalent to delete old path + |
| * add new path) |
| * <li>Copied file: requires approval on the new path (an approval on the old path is not needed |
| * since the file at this path was not touched) |
| * </ul> |
| */ |
| @Singleton |
| public class CodeOwnerApprovalCheck { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| private final PermissionBackend permissionBackend; |
| private final GitRepositoryManager repoManager; |
| private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration; |
| private final ChangedFiles changedFiles; |
| private final PureRevertCache pureRevertCache; |
| private final Provider<CodeOwnerConfigHierarchy> codeOwnerConfigHierarchyProvider; |
| private final Provider<CodeOwnerResolver> codeOwnerResolverProvider; |
| private final ApprovalsUtil approvalsUtil; |
| private final CodeOwnerMetrics codeOwnerMetrics; |
| |
| @Inject |
| CodeOwnerApprovalCheck( |
| PermissionBackend permissionBackend, |
| GitRepositoryManager repoManager, |
| CodeOwnersPluginConfiguration codeOwnersPluginConfiguration, |
| ChangedFiles changedFiles, |
| PureRevertCache pureRevertCache, |
| Provider<CodeOwnerConfigHierarchy> codeOwnerConfigHierarchyProvider, |
| Provider<CodeOwnerResolver> codeOwnerResolverProvider, |
| ApprovalsUtil approvalsUtil, |
| CodeOwnerMetrics codeOwnerMetrics) { |
| this.permissionBackend = permissionBackend; |
| this.repoManager = repoManager; |
| this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration; |
| this.changedFiles = changedFiles; |
| this.pureRevertCache = pureRevertCache; |
| this.codeOwnerConfigHierarchyProvider = codeOwnerConfigHierarchyProvider; |
| this.codeOwnerResolverProvider = codeOwnerResolverProvider; |
| this.approvalsUtil = approvalsUtil; |
| this.codeOwnerMetrics = codeOwnerMetrics; |
| } |
| |
| /** |
| * Returns the paths of the files in the given patch set that are owned by the specified account. |
| * |
| * @param changeNotes the change notes for which the owned files should be returned |
| * @param patchSet the patch set for which the owned files should be returned |
| * @param accountId account ID of the code owner for which the owned files should be returned |
| * @param start number of owned paths to skip |
| * @param limit the max number of owned paths that should be returned (0 = unlimited) |
| * @return the paths of the files in the given patch set that are owned by the specified account |
| * @throws ResourceConflictException if the destination branch of the change no longer exists |
| */ |
| public ImmutableList<Path> getOwnedPaths( |
| ChangeNotes changeNotes, PatchSet patchSet, Account.Id accountId, int start, int limit) |
| throws ResourceConflictException { |
| try (Timer0.Context ctx = codeOwnerMetrics.computeOwnedPaths.start()) { |
| logger.atFine().log( |
| "compute owned paths for account %d (project = %s, change = %d, patch set = %d," |
| + " start = %d, limit = %d)", |
| accountId.get(), |
| changeNotes.getProjectName(), |
| changeNotes.getChangeId().get(), |
| patchSet.id().get(), |
| start, |
| limit); |
| Stream<Path> ownedPaths = |
| getFileStatusesForAccount(changeNotes, patchSet, accountId) |
| .flatMap( |
| fileCodeOwnerStatus -> |
| Stream.of( |
| fileCodeOwnerStatus.newPathStatus(), |
| fileCodeOwnerStatus.oldPathStatus()) |
| .filter(Optional::isPresent) |
| .map(Optional::get)) |
| .filter( |
| pathCodeOwnerStatus -> pathCodeOwnerStatus.status() == CodeOwnerStatus.APPROVED) |
| .map(PathCodeOwnerStatus::path); |
| if (start > 0) { |
| ownedPaths = ownedPaths.skip(start); |
| } |
| if (limit > 0) { |
| ownedPaths = ownedPaths.limit(limit); |
| } |
| return ownedPaths.collect(toImmutableList()); |
| } catch (IOException | DiffNotAvailableException e) { |
| throw new CodeOwnersInternalServerErrorException( |
| String.format( |
| "failed to compute owned paths of patch set %s for account %d", |
| patchSet.id(), accountId.get()), |
| e); |
| } |
| } |
| |
| /** |
| * Whether the given change has sufficient code owner approvals to be submittable. |
| * |
| * @param changeNotes the change notes |
| * @return whether the given change has sufficient code owner approvals to be submittable |
| */ |
| public boolean isSubmittable(ChangeNotes changeNotes) |
| throws ResourceConflictException, IOException, DiffNotAvailableException { |
| requireNonNull(changeNotes, "changeNotes"); |
| logger.atFine().log( |
| "checking if change %d in project %s is submittable", |
| changeNotes.getChangeId().get(), changeNotes.getProjectName()); |
| CodeOwnerConfigHierarchy codeOwnerConfigHierarchy = codeOwnerConfigHierarchyProvider.get(); |
| CodeOwnerResolver codeOwnerResolver = codeOwnerResolverProvider.get().enforceVisibility(false); |
| try { |
| boolean isSubmittable = |
| !getFileStatuses(codeOwnerConfigHierarchy, codeOwnerResolver, changeNotes) |
| .anyMatch( |
| fileStatus -> |
| (fileStatus.newPathStatus().isPresent() |
| && fileStatus.newPathStatus().get().status() |
| != CodeOwnerStatus.APPROVED) |
| || (fileStatus.oldPathStatus().isPresent() |
| && fileStatus.oldPathStatus().get().status() |
| != CodeOwnerStatus.APPROVED)); |
| logger.atFine().log( |
| "change %d in project %s %s submittable", |
| changeNotes.getChangeId().get(), |
| changeNotes.getProjectName(), |
| isSubmittable ? "is" : "is not"); |
| return isSubmittable; |
| } finally { |
| codeOwnerMetrics.codeOwnerConfigBackendReadsPerChange.record( |
| codeOwnerConfigHierarchy.getCodeOwnerConfigCounters().getBackendReadCount()); |
| codeOwnerMetrics.codeOwnerConfigCacheReadsPerChange.record( |
| codeOwnerConfigHierarchy.getCodeOwnerConfigCounters().getCacheReadCount()); |
| codeOwnerMetrics.codeOwnerResolutionsPerChange.record( |
| codeOwnerResolver.getCodeOwnerCounters().getResolutionCount()); |
| codeOwnerMetrics.codeOwnerConfigCacheReadsPerChange.record( |
| codeOwnerResolver.getCodeOwnerCounters().getCacheReadCount()); |
| } |
| } |
| |
| /** |
| * Gets the code owner statuses for all files/paths that were changed in the current revision of |
| * the given change as a set. |
| * |
| * @param start number of file statuses to skip |
| * @param limit the max number of file statuses that should be returned (0 = unlimited) |
| * @see #getFileStatuses(CodeOwnerConfigHierarchy, CodeOwnerResolver, ChangeNotes) |
| */ |
| public ImmutableSet<FileCodeOwnerStatus> getFileStatusesAsSet( |
| ChangeNotes changeNotes, int start, int limit) |
| throws ResourceConflictException, IOException, DiffNotAvailableException { |
| requireNonNull(changeNotes, "changeNotes"); |
| try (Timer0.Context ctx = codeOwnerMetrics.computeFileStatuses.start()) { |
| logger.atFine().log( |
| "compute file statuses (project = %s, change = %d, start = %d, limit = %d)", |
| changeNotes.getProjectName(), changeNotes.getChangeId().get(), start, limit); |
| Stream<FileCodeOwnerStatus> fileStatuses = |
| getFileStatuses( |
| codeOwnerConfigHierarchyProvider.get(), |
| codeOwnerResolverProvider.get().enforceVisibility(false), |
| changeNotes); |
| if (start > 0) { |
| fileStatuses = fileStatuses.skip(start); |
| } |
| if (limit > 0) { |
| fileStatuses = fileStatuses.limit(limit); |
| } |
| return fileStatuses.collect(toImmutableSet()); |
| } |
| } |
| |
| /** |
| * Gets the code owner statuses for all files/paths that were changed in the current revision of |
| * the given change. |
| * |
| * <p>The code owner statuses tell the user which code owner approvals are still missing in order |
| * to make the change submittable. |
| * |
| * <p>Computing the code owner statuses for non-current revisions is not supported since the |
| * semantics are unclear, e.g.: |
| * |
| * <ul> |
| * <li>non-current revisions are never submittable, hence asking which code owner approvals are |
| * still missing in order to make the revision submittable makes no sense |
| * <li>the code owner status {@link CodeOwnerStatus#PENDING} doesn't make sense for an old |
| * revision, from the perspective of the change owner this status looks like the change is |
| * waiting for the code owner to approve, but since voting on old revisions is not possible |
| * the code-owner is not able to provide this approval |
| * <li>the code owner statuses are computed from the approvals that were given by code owners, |
| * the UI always shows the current approval even when looking at an old revision, showing |
| * code owner statuses that mismatch the shown approvals (because they are computed from |
| * approvals that were present on an old revision) would only confuse users |
| * </ul> |
| * |
| * @param codeOwnerConfigHierarchy {@link CodeOwnerConfigHierarchy} instance that should be used |
| * to iterate over code owner config hierarchies |
| * @param changeNotes the notes of the change for which the current code owner statuses should be |
| * returned |
| */ |
| private Stream<FileCodeOwnerStatus> getFileStatuses( |
| CodeOwnerConfigHierarchy codeOwnerConfigHierarchy, |
| CodeOwnerResolver codeOwnerResolver, |
| ChangeNotes changeNotes) |
| throws ResourceConflictException, IOException, DiffNotAvailableException { |
| requireNonNull(changeNotes, "changeNotes"); |
| try (Timer0.Context ctx = codeOwnerMetrics.prepareFileStatusComputation.start()) { |
| logger.atFine().log( |
| "prepare stream to compute file statuses (project = %s, change = %d)", |
| changeNotes.getProjectName(), changeNotes.getChangeId().get()); |
| |
| 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); |
| if (exemptedAccounts.contains(patchSetUploader)) { |
| logger.atFine().log( |
| "patch set uploader %d is exempted from requiring code owner approvals", |
| patchSetUploader.get()); |
| return getAllPathsAsApproved( |
| changeNotes, |
| changeNotes.getCurrentPatchSet(), |
| String.format( |
| "patch set uploader %s is exempted from requiring code owner approvals", |
| AccountTemplateUtil.getAccountTemplate(patchSetUploader))); |
| } |
| |
| boolean arePureRevertsExempted = codeOwnersConfig.arePureRevertsExempted(); |
| logger.atFine().log("arePureRevertsExempted = %s", arePureRevertsExempted); |
| if (arePureRevertsExempted && isPureRevert(changeNotes)) { |
| logger.atFine().log( |
| "change is a pure revert and is exempted from requiring code owner approvals"); |
| return getAllPathsAsApproved( |
| changeNotes, |
| changeNotes.getCurrentPatchSet(), |
| "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(); |
| ObjectId revision = getDestBranchRevision(changeNotes.getChange()); |
| logger.atFine().log("dest branch %s has revision %s", branch.branch(), revision.name()); |
| |
| 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(); |
| |
| return changedFiles |
| .getFromDiffCache( |
| changeNotes.getProjectName(), changeNotes.getCurrentPatchSet().commitId()) |
| .stream() |
| .map( |
| changedFile -> |
| getFileStatus( |
| codeOwnerConfigHierarchy, |
| codeOwnerResolver, |
| branch, |
| revision, |
| globalCodeOwners, |
| enableImplicitApproval ? changeOwner : null, |
| reviewerAccountIds, |
| approverAccountIds, |
| fallbackCodeOwners, |
| overrides, |
| changedFile)); |
| } |
| } |
| |
| /** |
| * Gets the code owner status for all files/paths that were changed in the current revision of the |
| * given change assuming that there is only an approval from the given account. |
| * |
| * <p>This method doesn't take approvals from other users and global code owners into account. |
| * |
| * <p>The purpose of this method is to find the files/paths in a change that are owned by the |
| * given account. |
| * |
| * @param changeNotes the notes of the change for which the code owner statuses should be returned |
| * @param patchSet the patch set for which the code owner statuses should be returned |
| * @param accountId the ID of the account for which an approval should be assumed |
| */ |
| @VisibleForTesting |
| public Stream<FileCodeOwnerStatus> getFileStatusesForAccount( |
| ChangeNotes changeNotes, PatchSet patchSet, Account.Id accountId) |
| throws ResourceConflictException, IOException, DiffNotAvailableException { |
| requireNonNull(changeNotes, "changeNotes"); |
| requireNonNull(patchSet, "patchSet"); |
| requireNonNull(accountId, "accountId"); |
| try (Timer0.Context ctx = codeOwnerMetrics.prepareFileStatusComputationForAccount.start()) { |
| logger.atFine().log( |
| "prepare stream to compute file statuses for account %d (project = %s, change = %d," |
| + " patch set = %d)", |
| accountId.get(), |
| changeNotes.getProjectName(), |
| changeNotes.getChangeId().get(), |
| patchSet.id().get()); |
| |
| CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig = |
| codeOwnersPluginConfiguration.getProjectConfig(changeNotes.getProjectName()); |
| |
| RequiredApproval requiredApproval = codeOwnersConfig.getRequiredApproval(); |
| logger.atFine().log("requiredApproval = %s", requiredApproval); |
| |
| BranchNameKey branch = changeNotes.getChange().getDest(); |
| ObjectId revision = getDestBranchRevision(changeNotes.getChange()); |
| logger.atFine().log("dest branch %s has revision %s", branch.branch(), revision.name()); |
| |
| boolean isProjectOwner = isProjectOwner(changeNotes.getProjectName(), accountId); |
| FallbackCodeOwners fallbackCodeOwners = codeOwnersConfig.getFallbackCodeOwners(); |
| logger.atFine().log( |
| "fallbackCodeOwner = %s, isProjectOwner = %s", fallbackCodeOwners, isProjectOwner); |
| |
| CodeOwnerConfigHierarchy codeOwnerConfigHierarchy = codeOwnerConfigHierarchyProvider.get(); |
| CodeOwnerResolver codeOwnerResolver = |
| codeOwnerResolverProvider.get().enforceVisibility(false); |
| return changedFiles.getFromDiffCache(changeNotes.getProjectName(), patchSet.commitId()) |
| .stream() |
| .map( |
| changedFile -> |
| getFileStatus( |
| codeOwnerConfigHierarchy, |
| codeOwnerResolver, |
| branch, |
| revision, |
| /* globalCodeOwners= */ CodeOwnerResolverResult.createEmpty(), |
| // 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 given account. |
| /* approverAccountIds= */ ImmutableSet.of(accountId), |
| fallbackCodeOwners, |
| /* overrides= */ ImmutableSet.of(), |
| changedFile)); |
| } |
| } |
| |
| private boolean isPureRevert(ChangeNotes changeNotes) throws IOException { |
| try { |
| return changeNotes.getChange().getRevertOf() != null |
| && pureRevertCache.isPureRevert(changeNotes); |
| } catch (BadRequestException e) { |
| throw new CodeOwnersInternalServerErrorException( |
| String.format( |
| "failed to check if change %s in project %s is a pure revert", |
| changeNotes.getChangeId(), changeNotes.getProjectName()), |
| e); |
| } |
| } |
| |
| private Stream<FileCodeOwnerStatus> getAllPathsAsApproved( |
| ChangeNotes changeNotes, PatchSet patchSet, String reason) |
| throws IOException, DiffNotAvailableException { |
| logger.atFine().log("all paths are approved (reason = %s)", reason); |
| return changedFiles.getFromDiffCache(changeNotes.getProjectName(), patchSet.commitId()).stream() |
| .map( |
| changedFile -> |
| FileCodeOwnerStatus.create( |
| changedFile, |
| changedFile |
| .newPath() |
| .map( |
| newPath -> |
| PathCodeOwnerStatus.create( |
| newPath, CodeOwnerStatus.APPROVED, reason)), |
| changedFile |
| .oldPath() |
| .map( |
| oldPath -> |
| PathCodeOwnerStatus.create( |
| oldPath, CodeOwnerStatus.APPROVED, reason)))); |
| } |
| |
| private FileCodeOwnerStatus getFileStatus( |
| CodeOwnerConfigHierarchy codeOwnerConfigHierarchy, |
| CodeOwnerResolver codeOwnerResolver, |
| BranchNameKey branch, |
| ObjectId revision, |
| CodeOwnerResolverResult globalCodeOwners, |
| @Nullable Account.Id implicitApprover, |
| ImmutableSet<Account.Id> reviewerAccountIds, |
| ImmutableSet<Account.Id> approverAccountIds, |
| FallbackCodeOwners fallbackCodeOwners, |
| ImmutableSet<PatchSetApproval> overrides, |
| ChangedFile changedFile) { |
| try (Timer0.Context ctx = codeOwnerMetrics.computeFileStatus.start()) { |
| logger.atFine().log("computing file status for %s", changedFile); |
| |
| // Compute the code owner status for the new path, if there is a new path. |
| Optional<PathCodeOwnerStatus> newPathStatus = |
| changedFile |
| .newPath() |
| .map( |
| newPath -> |
| getPathCodeOwnerStatus( |
| codeOwnerConfigHierarchy, |
| codeOwnerResolver, |
| branch, |
| revision, |
| globalCodeOwners, |
| implicitApprover, |
| reviewerAccountIds, |
| approverAccountIds, |
| fallbackCodeOwners, |
| overrides, |
| newPath)); |
| |
| // Compute the code owner status for the old path, if the file was deleted or renamed. |
| Optional<PathCodeOwnerStatus> oldPathStatus = Optional.empty(); |
| if (changedFile.isDeletion() || changedFile.isRename()) { |
| checkState( |
| changedFile.oldPath().isPresent(), "old path must be present for deletion/rename"); |
| logger.atFine().log( |
| "file was %s (old path = %s)", |
| changedFile.isDeletion() ? "deleted" : "renamed", changedFile.oldPath().get()); |
| oldPathStatus = |
| Optional.of( |
| getPathCodeOwnerStatus( |
| codeOwnerConfigHierarchy, |
| codeOwnerResolver, |
| branch, |
| revision, |
| globalCodeOwners, |
| implicitApprover, |
| reviewerAccountIds, |
| approverAccountIds, |
| fallbackCodeOwners, |
| overrides, |
| changedFile.oldPath().get())); |
| } |
| |
| FileCodeOwnerStatus fileCodeOwnerStatus = |
| FileCodeOwnerStatus.create(changedFile, newPathStatus, oldPathStatus); |
| logger.atFine().log("fileCodeOwnerStatus = %s", fileCodeOwnerStatus); |
| return fileCodeOwnerStatus; |
| } |
| } |
| |
| private PathCodeOwnerStatus getPathCodeOwnerStatus( |
| CodeOwnerConfigHierarchy codeOwnerConfigHierarchy, |
| CodeOwnerResolver codeOwnerResolver, |
| BranchNameKey branch, |
| ObjectId revision, |
| CodeOwnerResolverResult globalCodeOwners, |
| @Nullable Account.Id implicitApprover, |
| ImmutableSet<Account.Id> reviewerAccountIds, |
| ImmutableSet<Account.Id> approverAccountIds, |
| FallbackCodeOwners fallbackCodeOwners, |
| ImmutableSet<PatchSetApproval> overrides, |
| Path absolutePath) { |
| logger.atFine().log("computing path status for %s", absolutePath); |
| |
| if (!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(); |
| checkState(override.isPresent(), "no override found"); |
| return PathCodeOwnerStatus.create( |
| absolutePath, |
| CodeOwnerStatus.APPROVED, |
| String.format( |
| "override approval %s by %s is present", |
| override.get().label() + LabelValue.formatValue(override.get().value()), |
| AccountTemplateUtil.getAccountTemplate(override.get().accountId()))); |
| } |
| |
| AtomicReference<CodeOwnerStatus> codeOwnerStatus = |
| new AtomicReference<>(CodeOwnerStatus.INSUFFICIENT_REVIEWERS); |
| AtomicReference<String> reason = new AtomicReference<>(/* initialValue= */ null); |
| |
| if (isApproved( |
| globalCodeOwners, |
| CodeOwnerKind.GLOBAL_CODE_OWNER, |
| approverAccountIds, |
| implicitApprover, |
| reason)) { |
| codeOwnerStatus.set(CodeOwnerStatus.APPROVED); |
| } else { |
| logger.atFine().log("%s was not approved by a global code owner", absolutePath); |
| |
| if (isPending( |
| globalCodeOwners, CodeOwnerKind.GLOBAL_CODE_OWNER, reviewerAccountIds, reason)) { |
| codeOwnerStatus.set(CodeOwnerStatus.PENDING); |
| } |
| |
| AtomicBoolean hasRevelantCodeOwnerDefinitions = new AtomicBoolean(false); |
| AtomicBoolean parentCodeOwnersAreIgnored = new AtomicBoolean(false); |
| codeOwnerConfigHierarchy.visitForFile( |
| branch, |
| revision, |
| absolutePath, |
| (PathCodeOwnersVisitor) |
| pathCodeOwners -> { |
| CodeOwnerKind codeOwnerKind = |
| RefNames.REFS_CONFIG.equals(pathCodeOwners.getCodeOwnerConfig().key().ref()) |
| ? CodeOwnerKind.DEFAULT_CODE_OWNER |
| : CodeOwnerKind.REGULAR_CODE_OWNER; |
| |
| CodeOwnerResolverResult codeOwners = |
| resolveCodeOwners(codeOwnerResolver, pathCodeOwners); |
| logger.atFine().log( |
| "code owners = %s (code owner kind = %s, code owner config folder path = %s," |
| + " file name = %s)", |
| codeOwners, |
| codeOwnerKind, |
| pathCodeOwners.getCodeOwnerConfig().key().folderPath(), |
| pathCodeOwners.getCodeOwnerConfig().key().fileName().orElse("<default>")); |
| |
| if (codeOwners.hasRevelantCodeOwnerDefinitions()) { |
| hasRevelantCodeOwnerDefinitions.set(true); |
| } |
| |
| if (isApproved( |
| codeOwners, codeOwnerKind, approverAccountIds, implicitApprover, reason)) { |
| codeOwnerStatus.set(CodeOwnerStatus.APPROVED); |
| return false; |
| } else if (isPending(codeOwners, codeOwnerKind, reviewerAccountIds, reason)) { |
| 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 or is a reviewer. |
| return true; |
| }, |
| codeOwnerConfigKey -> { |
| logger.atFine().log( |
| "code owner config %s ignores parent code owners for %s", |
| codeOwnerConfigKey, absolutePath); |
| parentCodeOwnersAreIgnored.set(true); |
| }); |
| |
| // If no code owners have been defined for the file and if parent code owners are not ignored, |
| // the fallback code owners apply if they are configured. We can skip checking them if we |
| // already found that the file was approved. |
| if (codeOwnerStatus.get() != CodeOwnerStatus.APPROVED |
| && !hasRevelantCodeOwnerDefinitions.get() |
| && !parentCodeOwnersAreIgnored.get()) { |
| CodeOwnerStatus codeOwnerStatusForFallbackCodeOwners = |
| getCodeOwnerStatusForFallbackCodeOwners( |
| codeOwnerStatus.get(), |
| branch, |
| implicitApprover, |
| reviewerAccountIds, |
| approverAccountIds, |
| fallbackCodeOwners, |
| absolutePath, |
| reason); |
| // Merge codeOwnerStatusForFallbackCodeOwners into codeOwnerStatus: |
| // * codeOwnerStatus is the code owner status without taking fallback code owners into |
| // account |
| // * codeOwnerStatusForFallbackCodeOwners is the code owner status for fallback code owners |
| // only |
| // When merging both the "better" code owner status should take precedence (APPROVED is |
| // better than PENDING which is better than INSUFFICIENT_REVIEWERS): |
| // * if codeOwnerStatus == APPROVED we do not compute the code owner status for the fallback |
| // code owners and never reach this point. Hence we can ignore this case below. |
| // * if codeOwnerStatus == PENDING (e.g. because a global code owner is a reviewer) we must |
| // override it if codeOwnerStatusForFallbackCodeOwners is APPROVED |
| // * if codeOwnerStatus == INSUFFICIENT_REVIEWERS we must override it if |
| // codeOwnerStatusForFallbackCodeOwners is PENDING or APPROVED |
| // This means if codeOwnerStatusForFallbackCodeOwners is INSUFFICIENT_REVIEWERS it is never |
| // "better" than codeOwnerStatus, hence in this case we do not override codeOwnerStatus. |
| // On the other hand if codeOwnerStatusForFallbackCodeOwners is PENDING or APPROVED (aka not |
| // INSUFFICIENT_REVIEWERS) it is always as good or "better" than codeOwnerStatus (which can |
| // only be INSUFFICIENT_REVIEWERS or PENDING at this point), hence in this case we can/must |
| // override codeOwnerStatus. |
| if (!codeOwnerStatusForFallbackCodeOwners.equals(CodeOwnerStatus.INSUFFICIENT_REVIEWERS)) { |
| codeOwnerStatus.set(codeOwnerStatusForFallbackCodeOwners); |
| } |
| } |
| } |
| |
| logger.atFine().log( |
| "%s has code owner status %s (reason = %s)", |
| absolutePath, codeOwnerStatus.get(), reason.get() != null ? reason.get() : "n/a"); |
| PathCodeOwnerStatus pathCodeOwnerStatus = |
| PathCodeOwnerStatus.create(absolutePath, codeOwnerStatus.get(), reason.get()); |
| logger.atFine().log("pathCodeOwnerStatus = %s", pathCodeOwnerStatus); |
| return pathCodeOwnerStatus; |
| } |
| |
| /** |
| * Gets the code owner status for the given path when project owners are configured as fallback |
| * code owners. |
| */ |
| private CodeOwnerStatus getCodeOwnerStatusForProjectOwnersAsFallbackCodeOwners( |
| BranchNameKey branch, |
| @Nullable Account.Id implicitApprover, |
| ImmutableSet<Account.Id> reviewerAccountIds, |
| ImmutableSet<Account.Id> approverAccountIds, |
| Path absolutePath, |
| AtomicReference<String> reason) { |
| logger.atFine().log( |
| "computing code owner status for %s with project owners as fallback code owners", |
| absolutePath); |
| |
| CodeOwnerStatus codeOwnerStatus = CodeOwnerStatus.INSUFFICIENT_REVIEWERS; |
| if (isApprovedByProjectOwner(branch.project(), approverAccountIds, implicitApprover, reason)) { |
| codeOwnerStatus = CodeOwnerStatus.APPROVED; |
| } else if (isPendingByProjectOwner(branch.project(), reviewerAccountIds, reason)) { |
| codeOwnerStatus = CodeOwnerStatus.PENDING; |
| } |
| |
| return codeOwnerStatus; |
| } |
| |
| private boolean isApprovedByProjectOwner( |
| Project.NameKey projectName, |
| ImmutableSet<Account.Id> approverAccountIds, |
| @Nullable Account.Id implicitApprover, |
| AtomicReference<String> reason) { |
| return (implicitApprover != null |
| && isImplicitlyApprovedByProjectOwner(projectName, implicitApprover, reason)) |
| || isExplicitlyApprovedByProjectOwner(projectName, approverAccountIds, reason); |
| } |
| |
| private boolean isImplicitlyApprovedByProjectOwner( |
| Project.NameKey projectName, Account.Id implicitApprover, AtomicReference<String> reason) { |
| requireNonNull(implicitApprover, "implicitApprover"); |
| if (isProjectOwner(projectName, implicitApprover)) { |
| // The uploader of the patch set is a project owner and thus a code owner. This means there |
| // is an implicit code owner approval from the patch set uploader so that the path is |
| // automatically approved. |
| reason.set( |
| String.format( |
| "implicitly approved by the patch set uploader %s who is a %s" |
| + " (all project owners are %ss)", |
| AccountTemplateUtil.getAccountTemplate(implicitApprover), |
| CodeOwnerKind.FALLBACK_CODE_OWNER.getDisplayName(), |
| CodeOwnerKind.FALLBACK_CODE_OWNER.getDisplayName())); |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean isExplicitlyApprovedByProjectOwner( |
| Project.NameKey projectName, |
| ImmutableSet<Account.Id> approverAccountIds, |
| AtomicReference<String> reason) { |
| Optional<Account.Id> approver = |
| approverAccountIds.stream() |
| .filter(approverAccountId -> isProjectOwner(projectName, approverAccountId)) |
| .findAny(); |
| if (approver.isPresent()) { |
| // At least one of the approvers is a project owner and thus a code owner. |
| reason.set( |
| String.format( |
| "approved by %s who is a %s (all project owners are %ss)", |
| AccountTemplateUtil.getAccountTemplate(approver.get()), |
| CodeOwnerKind.FALLBACK_CODE_OWNER.getDisplayName(), |
| CodeOwnerKind.FALLBACK_CODE_OWNER.getDisplayName())); |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean isPendingByProjectOwner( |
| Project.NameKey projectName, |
| ImmutableSet<Account.Id> reviewerAccountIds, |
| AtomicReference<String> reason) { |
| Optional<Account.Id> reviewer = |
| reviewerAccountIds.stream() |
| .filter(reviewerAccountId -> isProjectOwner(projectName, reviewerAccountId)) |
| .findAny(); |
| if (reviewer.isPresent()) { |
| // At least one of the reviewers is a project owner and thus a code owner. |
| reason.set( |
| String.format( |
| "reviewer %s is a %s (all project owners are %ss)", |
| AccountTemplateUtil.getAccountTemplate(reviewer.get()), |
| CodeOwnerKind.FALLBACK_CODE_OWNER.getDisplayName(), |
| CodeOwnerKind.FALLBACK_CODE_OWNER.getDisplayName())); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Computes the code owner status for the given path based on the configured fallback code owners. |
| */ |
| private CodeOwnerStatus getCodeOwnerStatusForFallbackCodeOwners( |
| CodeOwnerStatus codeOwnerStatus, |
| BranchNameKey branch, |
| @Nullable Account.Id implicitApprover, |
| ImmutableSet<Account.Id> reviewerAccountIds, |
| ImmutableSet<Account.Id> approverAccountIds, |
| FallbackCodeOwners fallbackCodeOwners, |
| Path absolutePath, |
| AtomicReference<String> reason) { |
| logger.atFine().log( |
| "getting code owner status for fallback code owners (fallback code owners = %s)", |
| fallbackCodeOwners); |
| switch (fallbackCodeOwners) { |
| case NONE: |
| logger.atFine().log("no fallback code owners"); |
| return codeOwnerStatus; |
| case PROJECT_OWNERS: |
| return getCodeOwnerStatusForProjectOwnersAsFallbackCodeOwners( |
| branch, implicitApprover, reviewerAccountIds, approverAccountIds, absolutePath, reason); |
| case ALL_USERS: |
| return getCodeOwnerStatusIfAllUsersAreCodeOwners( |
| implicitApprover, reviewerAccountIds, approverAccountIds, absolutePath, reason); |
| } |
| |
| throw new CodeOwnersInternalServerErrorException( |
| String.format("unknown fallback code owners configured: %s", fallbackCodeOwners)); |
| } |
| |
| /** Computes the code owner status for the given path assuming that all users are code owners. */ |
| private CodeOwnerStatus getCodeOwnerStatusIfAllUsersAreCodeOwners( |
| @Nullable Account.Id implicitApprover, |
| ImmutableSet<Account.Id> reviewerAccountIds, |
| ImmutableSet<Account.Id> approverAccountIds, |
| Path absolutePath, |
| AtomicReference<String> reason) { |
| logger.atFine().log( |
| "getting code owner status for fallback code owners (all users are fallback code owners)"); |
| |
| if (implicitApprover != null) { |
| reason.set( |
| String.format( |
| "implicitly approved by the patch set uploader %s who is a %s" |
| + " (all users are %ss)", |
| AccountTemplateUtil.getAccountTemplate(implicitApprover), |
| CodeOwnerKind.FALLBACK_CODE_OWNER.getDisplayName(), |
| CodeOwnerKind.FALLBACK_CODE_OWNER.getDisplayName())); |
| return CodeOwnerStatus.APPROVED; |
| } |
| |
| if (!approverAccountIds.isEmpty()) { |
| Optional<Account.Id> approver = approverAccountIds.stream().findAny(); |
| checkState(approver.isPresent(), "no approver found"); |
| reason.set( |
| String.format( |
| "approved by %s who is a %s (all users are %ss)", |
| AccountTemplateUtil.getAccountTemplate(approver.get()), |
| CodeOwnerKind.FALLBACK_CODE_OWNER.getDisplayName(), |
| CodeOwnerKind.FALLBACK_CODE_OWNER.getDisplayName())); |
| return CodeOwnerStatus.APPROVED; |
| } else if (!reviewerAccountIds.isEmpty()) { |
| Optional<Account.Id> reviewer = reviewerAccountIds.stream().findAny(); |
| checkState(reviewer.isPresent(), "no reviewer found"); |
| reason.set( |
| String.format( |
| "reviewer %s is a %s (all users are %ss)", |
| AccountTemplateUtil.getAccountTemplate(reviewer.get()), |
| CodeOwnerKind.FALLBACK_CODE_OWNER.getDisplayName(), |
| CodeOwnerKind.FALLBACK_CODE_OWNER.getDisplayName())); |
| return CodeOwnerStatus.PENDING; |
| } |
| |
| logger.atFine().log("%s has no fallback code owner as a reviewer", absolutePath); |
| return CodeOwnerStatus.INSUFFICIENT_REVIEWERS; |
| } |
| |
| /** |
| * Checks whether the given path was implicitly or explicitly approved. |
| * |
| * @param codeOwners users that own the path |
| * @param codeOwnerKind the kind of the given {@code codeOwners} |
| * @param approverAccountIds the IDs of the accounts that have approved the change |
| * @param implicitApprover the ID of the account the could be an implicit approver (aka last patch |
| * set uploader) |
| * @param reason {@link AtomicReference} on which the reason is being set if the path is approved |
| * @return whether the path was approved |
| */ |
| private boolean isApproved( |
| CodeOwnerResolverResult codeOwners, |
| CodeOwnerKind codeOwnerKind, |
| ImmutableSet<Account.Id> approverAccountIds, |
| @Nullable Account.Id implicitApprover, |
| AtomicReference<String> reason) { |
| if (implicitApprover != null) { |
| if (codeOwners.codeOwnersAccountIds().contains(implicitApprover) |
| || codeOwners.ownedByAllUsers()) { |
| // If the uploader of the patch set owns the path, there is an implicit code owner |
| // approval from the patch set uploader so that the path is automatically approved. |
| reason.set( |
| String.format( |
| "implicitly approved by the patch set uploader %s who is a %s%s", |
| AccountTemplateUtil.getAccountTemplate(implicitApprover), |
| codeOwnerKind.getDisplayName(), |
| codeOwners.ownedByAllUsers() |
| ? String.format(" (all users are %ss)", codeOwnerKind.getDisplayName()) |
| : "")); |
| return true; |
| } |
| } |
| |
| if (!Collections.disjoint(approverAccountIds, codeOwners.codeOwnersAccountIds()) |
| || (codeOwners.ownedByAllUsers() && !approverAccountIds.isEmpty())) { |
| // At least one of the code owners approved the change. |
| Optional<Account.Id> approver = |
| codeOwners.ownedByAllUsers() |
| ? approverAccountIds.stream().findAny() |
| : approverAccountIds.stream() |
| .filter(accountId -> codeOwners.codeOwnersAccountIds().contains(accountId)) |
| .findAny(); |
| checkState(approver.isPresent(), "no approver found"); |
| reason.set( |
| String.format( |
| "approved by %s who is a %s%s", |
| AccountTemplateUtil.getAccountTemplate(approver.get()), |
| codeOwnerKind.getDisplayName(), |
| codeOwners.ownedByAllUsers() |
| ? String.format(" (all users are %ss)", codeOwnerKind.getDisplayName()) |
| : "")); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Checks whether any of the reviewers is a code owner of the path. |
| * |
| * @param codeOwners users that own the path |
| * @param codeOwnerKind the kind of the given {@code codeOwners} |
| * @param reviewerAccountIds the IDs of the accounts that are reviewer of the change |
| * @param reason {@link AtomicReference} on which the reason is being set if the status for the |
| * path is {@code PENDING} |
| * @return whether the path was approved |
| */ |
| private boolean isPending( |
| CodeOwnerResolverResult codeOwners, |
| CodeOwnerKind codeOwnerKind, |
| ImmutableSet<Account.Id> reviewerAccountIds, |
| AtomicReference<String> reason) { |
| if (!Collections.disjoint(codeOwners.codeOwnersAccountIds(), reviewerAccountIds) |
| || (codeOwners.ownedByAllUsers() && !reviewerAccountIds.isEmpty())) { |
| Optional<Account.Id> reviewer = |
| codeOwners.ownedByAllUsers() |
| ? reviewerAccountIds.stream().findAny() |
| : reviewerAccountIds.stream() |
| .filter(accountId -> codeOwners.codeOwnersAccountIds().contains(accountId)) |
| .findAny(); |
| checkState(reviewer.isPresent(), "no reviewer found"); |
| reason.set( |
| String.format( |
| "reviewer %s is a %s%s", |
| AccountTemplateUtil.getAccountTemplate(reviewer.get()), |
| codeOwnerKind.getDisplayName(), |
| codeOwners.ownedByAllUsers() |
| ? String.format(" (all users are %ss)", codeOwnerKind.getDisplayName()) |
| : "")); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| /** Whether the given account is a project owner of the given project. */ |
| private boolean isProjectOwner(Project.NameKey project, Account.Id accountId) { |
| try { |
| boolean isProjectOwner = |
| permissionBackend |
| .absentUser(accountId) |
| .project(project) |
| .test(ProjectPermission.WRITE_CONFIG); |
| if (isProjectOwner) { |
| logger.atFine().log("Account %d is a project owner", accountId.get()); |
| } |
| return isProjectOwner; |
| } catch (PermissionBackendException e) { |
| throw new CodeOwnersInternalServerErrorException( |
| String.format( |
| "failed to check owner permission of project %s for account %d", |
| project.get(), accountId.get()), |
| e); |
| } |
| } |
| |
| /** |
| * Resolves the given path code owners. |
| * |
| * @param codeOwnerResolver the {@code CodeOwnerResolver} that should be used to resolve code |
| * owners |
| * @param pathCodeOwners the path code owners that should be resolved |
| */ |
| private CodeOwnerResolverResult resolveCodeOwners( |
| CodeOwnerResolver codeOwnerResolver, PathCodeOwners pathCodeOwners) { |
| return codeOwnerResolver.resolvePathCodeOwners(pathCodeOwners); |
| } |
| |
| /** |
| * 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(), |
| /** revWalk */ |
| null, |
| /** repoConfig */ |
| null)); |
| } |
| } |
| |
| 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 |
| * owners for the files that are touched in the change. |
| * |
| * @throws ResourceConflictException thrown if the destination branch is not found, e.g. when the |
| * branch got deleted after the change was created |
| */ |
| private ObjectId getDestBranchRevision(Change change) |
| throws IOException, ResourceConflictException { |
| try (Repository repository = repoManager.openRepository(change.getProject()); |
| RevWalk rw = new RevWalk(repository)) { |
| Ref ref = repository.exactRef(change.getDest().branch()); |
| if (ref == null) { |
| throw new ResourceConflictException("destination branch not found"); |
| } |
| return rw.parseCommit(ref.getObjectId()); |
| } |
| } |
| } |