| // 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.ImmutableSet.toImmutableSet; |
| import static java.util.Objects.requireNonNull; |
| |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.entities.Account; |
| import com.google.gerrit.entities.BranchNameKey; |
| import com.google.gerrit.entities.Change; |
| import com.google.gerrit.entities.PatchSetApproval; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.exceptions.StorageException; |
| import com.google.gerrit.extensions.restapi.ResourceConflictException; |
| import com.google.gerrit.plugins.codeowners.api.CodeOwnerStatus; |
| import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration; |
| import com.google.gerrit.plugins.codeowners.config.RequiredApproval; |
| import com.google.gerrit.server.git.GitRepositoryManager; |
| import com.google.gerrit.server.logging.Metadata; |
| import com.google.gerrit.server.logging.TraceContext; |
| import com.google.gerrit.server.logging.TraceContext.TraceTimer; |
| import com.google.gerrit.server.notedb.ChangeNotes; |
| import com.google.gerrit.server.notedb.ReviewerStateInternal; |
| import com.google.gerrit.server.patch.PatchListNotAvailableException; |
| import com.google.gerrit.server.permissions.PermissionBackend; |
| import com.google.gerrit.server.permissions.PermissionBackendException; |
| import com.google.gerrit.server.permissions.ProjectPermission; |
| 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.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 CodeOwnerConfigScanner codeOwnerConfigScanner; |
| private final CodeOwnerConfigHierarchy codeOwnerConfigHierarchy; |
| private final Provider<CodeOwnerResolver> codeOwnerResolver; |
| |
| @Inject |
| CodeOwnerApprovalCheck( |
| PermissionBackend permissionBackend, |
| GitRepositoryManager repoManager, |
| CodeOwnersPluginConfiguration codeOwnersPluginConfiguration, |
| ChangedFiles changedFiles, |
| CodeOwnerConfigScanner codeOwnerConfigScanner, |
| CodeOwnerConfigHierarchy codeOwnerConfigHierarchy, |
| Provider<CodeOwnerResolver> codeOwnerResolver) { |
| this.permissionBackend = permissionBackend; |
| this.repoManager = repoManager; |
| this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration; |
| this.changedFiles = changedFiles; |
| this.codeOwnerConfigScanner = codeOwnerConfigScanner; |
| this.codeOwnerConfigHierarchy = codeOwnerConfigHierarchy; |
| this.codeOwnerResolver = codeOwnerResolver; |
| } |
| |
| /** |
| * 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, PatchListNotAvailableException { |
| requireNonNull(changeNotes, "changeNotes"); |
| logger.atFine().log( |
| "checking if change %d in project %s is submittable", |
| changeNotes.getChangeId().get(), changeNotes.getProjectName()); |
| boolean isSubmittable = |
| !getFileStatuses(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; |
| } |
| |
| /** |
| * 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 changeNotes the notes of the change for which the current code owner statuses should be |
| * returned |
| */ |
| public Stream<FileCodeOwnerStatus> getFileStatuses(ChangeNotes changeNotes) |
| throws ResourceConflictException, IOException, PatchListNotAvailableException { |
| requireNonNull(changeNotes, "changeNotes"); |
| try (TraceTimer traceTimer = |
| TraceContext.newTimer( |
| "Compute file statuses", |
| Metadata.builder() |
| .projectName(changeNotes.getProjectName().get()) |
| .changeId(changeNotes.getChangeId().get()) |
| .build())) { |
| RequiredApproval requiredApproval = |
| codeOwnersPluginConfiguration.getRequiredApproval(changeNotes.getProjectName()); |
| logger.atFine().log("requiredApproval = %s", requiredApproval); |
| |
| Optional<RequiredApproval> overrideApproval = |
| codeOwnersPluginConfiguration.getOverrideApproval(changeNotes.getProjectName()); |
| boolean hasOverride = |
| overrideApproval.isPresent() && hasOverride(overrideApproval.get(), changeNotes); |
| logger.atFine().log( |
| "hasOverride = %s (overrideApproval = %s)", hasOverride, overrideApproval); |
| |
| BranchNameKey branch = changeNotes.getChange().getDest(); |
| ObjectId revision = getDestBranchRevision(changeNotes.getChange()); |
| logger.atFine().log("dest branch %s has revision %s", branch.branch(), revision.name()); |
| |
| boolean enableImplicitApprovalFromUploader = |
| codeOwnersPluginConfiguration.areImplicitApprovalsEnabled(changeNotes.getProjectName()); |
| Account.Id patchSetUploader = changeNotes.getCurrentPatchSet().uploader(); |
| logger.atFine().log( |
| "patchSetUploader = %d, implicit approval from uploader is %s", |
| patchSetUploader.get(), enableImplicitApprovalFromUploader ? "enabled" : "disabled"); |
| |
| CodeOwnerResolverResult globalCodeOwners = |
| codeOwnerResolver |
| .get() |
| .enforceVisibility(false) |
| .resolve( |
| codeOwnersPluginConfiguration.getGlobalCodeOwners(changeNotes.getProjectName())); |
| logger.atFine().log("global code owners = %s", globalCodeOwners); |
| |
| // If the branch doesn't contain any code owner config file yet, we apply special logic |
| // (project |
| // owners count as code owners) to allow bootstrapping the code owner configuration in the |
| // branch. |
| boolean isBootstrapping = !codeOwnerConfigScanner.containsAnyCodeOwnerConfigFile(branch); |
| logger.atFine().log("isBootstrapping = %s", isBootstrapping); |
| |
| ImmutableSet<Account.Id> reviewerAccountIds = getReviewerAccountIds(changeNotes); |
| ImmutableSet<Account.Id> approverAccountIds = |
| getApproverAccountIds(requiredApproval, changeNotes); |
| logger.atFine().log("reviewers = %s, approvers = %s", reviewerAccountIds, approverAccountIds); |
| |
| return changedFiles |
| .compute(changeNotes.getProjectName(), changeNotes.getCurrentPatchSet().commitId()) |
| .stream() |
| .map( |
| changedFile -> |
| getFileStatus( |
| branch, |
| revision, |
| globalCodeOwners, |
| enableImplicitApprovalFromUploader, |
| patchSetUploader, |
| reviewerAccountIds, |
| approverAccountIds, |
| hasOverride, |
| isBootstrapping, |
| changedFile)); |
| } |
| } |
| |
| private FileCodeOwnerStatus getFileStatus( |
| BranchNameKey branch, |
| ObjectId revision, |
| CodeOwnerResolverResult globalCodeOwners, |
| boolean enableImplicitApprovalFromUploader, |
| Account.Id patchSetUploader, |
| ImmutableSet<Account.Id> reviewerAccountIds, |
| ImmutableSet<Account.Id> approverAccountIds, |
| boolean hasOverride, |
| boolean isBootstrapping, |
| ChangedFile changedFile) { |
| 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( |
| branch, |
| revision, |
| globalCodeOwners, |
| enableImplicitApprovalFromUploader, |
| patchSetUploader, |
| reviewerAccountIds, |
| approverAccountIds, |
| hasOverride, |
| isBootstrapping, |
| 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( |
| branch, |
| revision, |
| globalCodeOwners, |
| enableImplicitApprovalFromUploader, |
| patchSetUploader, |
| reviewerAccountIds, |
| approverAccountIds, |
| hasOverride, |
| isBootstrapping, |
| changedFile.oldPath().get())); |
| } |
| |
| FileCodeOwnerStatus fileCodeOwnerStatus = |
| FileCodeOwnerStatus.create(changedFile, newPathStatus, oldPathStatus); |
| logger.atFine().log("fileCodeOwnerStatus = %s", fileCodeOwnerStatus); |
| return fileCodeOwnerStatus; |
| } |
| |
| private PathCodeOwnerStatus getPathCodeOwnerStatus( |
| BranchNameKey branch, |
| ObjectId revision, |
| CodeOwnerResolverResult globalCodeOwners, |
| boolean enableImplicitApprovalFromUploader, |
| Account.Id patchSetUploader, |
| ImmutableSet<Account.Id> reviewerAccountIds, |
| ImmutableSet<Account.Id> approverAccountIds, |
| boolean hasOverride, |
| boolean isBootstrapping, |
| Path absolutePath) { |
| logger.atFine().log("computing path status for %s", absolutePath); |
| |
| if (hasOverride) { |
| logger.atFine().log( |
| "the status for path %s is %s since an override is present", |
| absolutePath, CodeOwnerStatus.APPROVED.name()); |
| return PathCodeOwnerStatus.create(absolutePath, CodeOwnerStatus.APPROVED); |
| } |
| |
| return isBootstrapping |
| ? getPathCodeOwnerStatusBootstrappingMode( |
| branch, |
| globalCodeOwners, |
| enableImplicitApprovalFromUploader, |
| patchSetUploader, |
| reviewerAccountIds, |
| approverAccountIds, |
| absolutePath) |
| : getPathCodeOwnerStatusRegularMode( |
| branch, |
| globalCodeOwners, |
| enableImplicitApprovalFromUploader, |
| patchSetUploader, |
| revision, |
| reviewerAccountIds, |
| approverAccountIds, |
| absolutePath); |
| } |
| |
| /** |
| * Gets the code owner status for the given path when the branch doesn't contain any code owner |
| * config file yet (bootstrapping mode). |
| * |
| * <p>If we are in bootstrapping mode we consider project owners as code owners. This allows |
| * bootstrapping the code owner configuration in the branch. |
| */ |
| private PathCodeOwnerStatus getPathCodeOwnerStatusBootstrappingMode( |
| BranchNameKey branch, |
| CodeOwnerResolverResult globalCodeOwners, |
| boolean enableImplicitApprovalFromUploader, |
| Account.Id patchSetUploader, |
| ImmutableSet<Account.Id> reviewerAccountIds, |
| ImmutableSet<Account.Id> approverAccountIds, |
| Path absolutePath) { |
| logger.atFine().log("computing path status for %s (bootstrapping mode)", absolutePath); |
| |
| CodeOwnerStatus codeOwnerStatus = CodeOwnerStatus.INSUFFICIENT_REVIEWERS; |
| if (isApprovedBootstrappingMode( |
| branch.project(), |
| absolutePath, |
| globalCodeOwners, |
| approverAccountIds, |
| enableImplicitApprovalFromUploader, |
| patchSetUploader)) { |
| codeOwnerStatus = CodeOwnerStatus.APPROVED; |
| } else if (isPendingBootstrappingMode( |
| branch.project(), absolutePath, globalCodeOwners, reviewerAccountIds)) { |
| codeOwnerStatus = CodeOwnerStatus.PENDING; |
| } |
| |
| PathCodeOwnerStatus pathCodeOwnerStatus = |
| PathCodeOwnerStatus.create(absolutePath, codeOwnerStatus); |
| logger.atFine().log("pathCodeOwnerStatus = %s", pathCodeOwnerStatus); |
| return pathCodeOwnerStatus; |
| } |
| |
| private boolean isApprovedBootstrappingMode( |
| Project.NameKey projectName, |
| Path absolutePath, |
| CodeOwnerResolverResult globalCodeOwners, |
| ImmutableSet<Account.Id> approverAccountIds, |
| boolean enableImplicitApprovalFromUploader, |
| Account.Id patchSetUploader) { |
| return (enableImplicitApprovalFromUploader |
| && isImplicitlyApprovedBootstrappingMode( |
| projectName, absolutePath, globalCodeOwners, patchSetUploader)) |
| || isExplicitlyApprovedBootstrappingMode( |
| projectName, absolutePath, globalCodeOwners, approverAccountIds); |
| } |
| |
| private boolean isImplicitlyApprovedBootstrappingMode( |
| Project.NameKey projectName, |
| Path absolutePath, |
| CodeOwnerResolverResult globalCodeOwners, |
| Account.Id patchSetUploader) { |
| if (isProjectOwner(projectName, patchSetUploader)) { |
| // 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. |
| logger.atFine().log( |
| "%s was implicitly approved by the patch set uploader who is a project owner", |
| absolutePath); |
| return true; |
| } |
| |
| if (globalCodeOwners.ownedByAllUsers() |
| || globalCodeOwners.codeOwnersAccountIds().contains(patchSetUploader)) { |
| // If the uploader of the patch set is a global code owner, there is an implicit code owner |
| // approval from the patch set uploader so that the path is automatically approved. |
| logger.atFine().log( |
| "%s was implicitly approved by the patch set uploader who is a global owner", |
| absolutePath); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| private boolean isExplicitlyApprovedBootstrappingMode( |
| Project.NameKey projectName, |
| Path absolutePath, |
| CodeOwnerResolverResult globalCodeOwners, |
| ImmutableSet<Account.Id> approverAccountIds) { |
| if (!Collections.disjoint(approverAccountIds, globalCodeOwners.codeOwnersAccountIds()) |
| || (globalCodeOwners.ownedByAllUsers() && !approverAccountIds.isEmpty())) { |
| // At least one of the global code owners approved the change. |
| logger.atFine().log("%s was approved by a global code owner", absolutePath); |
| return true; |
| } |
| |
| if (approverAccountIds.stream() |
| .anyMatch(approverAccountId -> isProjectOwner(projectName, approverAccountId))) { |
| // At least one of the approvers is a project owner and thus a code owner. |
| logger.atFine().log("%s was approved by a project owner", absolutePath); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| private boolean isPendingBootstrappingMode( |
| Project.NameKey projectName, |
| Path absolutePath, |
| CodeOwnerResolverResult globalCodeOwners, |
| ImmutableSet<Account.Id> reviewerAccountIds) { |
| if (reviewerAccountIds.stream() |
| .anyMatch(reviewerAccountId -> isProjectOwner(projectName, reviewerAccountId))) { |
| // At least one of the reviewers is a project owner and thus a code owner. |
| logger.atFine().log("%s is owned by a reviewer who is project owner", absolutePath); |
| return true; |
| } |
| |
| if (isPending(absolutePath, globalCodeOwners, reviewerAccountIds)) { |
| // At least one of the reviewers is a global code owner. |
| logger.atFine().log("%s is owned by a reviewer who is a global owner", absolutePath); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Gets the code owner status for the given path when the branch contains at least one code owner |
| * config file (regular mode). |
| */ |
| private PathCodeOwnerStatus getPathCodeOwnerStatusRegularMode( |
| BranchNameKey branch, |
| CodeOwnerResolverResult globalCodeOwners, |
| boolean enableImplicitApprovalFromUploader, |
| Account.Id patchSetUploader, |
| ObjectId revision, |
| ImmutableSet<Account.Id> reviewerAccountIds, |
| ImmutableSet<Account.Id> approverAccountIds, |
| Path absolutePath) { |
| logger.atFine().log("computing path status for %s (regular mode)", absolutePath); |
| |
| AtomicReference<CodeOwnerStatus> codeOwnerStatus = |
| new AtomicReference<>(CodeOwnerStatus.INSUFFICIENT_REVIEWERS); |
| |
| if (isApproved( |
| absolutePath, |
| globalCodeOwners, |
| approverAccountIds, |
| enableImplicitApprovalFromUploader, |
| patchSetUploader)) { |
| logger.atFine().log("%s was approved by a global code owner", absolutePath); |
| codeOwnerStatus.set(CodeOwnerStatus.APPROVED); |
| } else { |
| logger.atFine().log("%s was not approved by a global code owner", absolutePath); |
| |
| if (isPending(absolutePath, globalCodeOwners, reviewerAccountIds)) { |
| logger.atFine().log("%s is owned by a reviewer who is a global owner", absolutePath); |
| codeOwnerStatus.set(CodeOwnerStatus.PENDING); |
| } |
| |
| codeOwnerConfigHierarchy.visit( |
| 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>")); |
| |
| 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 or is a reviewer. |
| return true; |
| }); |
| } |
| |
| PathCodeOwnerStatus pathCodeOwnerStatus = |
| PathCodeOwnerStatus.create(absolutePath, codeOwnerStatus.get()); |
| logger.atFine().log("pathCodeOwnerStatus = %s", pathCodeOwnerStatus); |
| return pathCodeOwnerStatus; |
| } |
| |
| private boolean isApproved( |
| Path absolutePath, |
| CodeOwnerResolverResult codeOwners, |
| ImmutableSet<Account.Id> approverAccountIds, |
| boolean enableImplicitApprovalFromUploader, |
| Account.Id patchSetUploader) { |
| if (enableImplicitApprovalFromUploader |
| && (codeOwners.codeOwnersAccountIds().contains(patchSetUploader) |
| || 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. |
| logger.atFine().log("%s was implicitly approved by the patch set uploader", absolutePath); |
| return true; |
| } |
| |
| if (!Collections.disjoint(approverAccountIds, codeOwners.codeOwnersAccountIds()) |
| || (codeOwners.ownedByAllUsers() && !approverAccountIds.isEmpty())) { |
| // At least one of the global code owners approved the change. |
| logger.atFine().log("%s was explicitly approved by a code owner", absolutePath); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| private boolean isPending( |
| Path absolutePath, |
| CodeOwnerResolverResult codeOwners, |
| ImmutableSet<Account.Id> reviewerAccountIds) { |
| if (!Collections.disjoint(codeOwners.codeOwnersAccountIds(), reviewerAccountIds) |
| || (codeOwners.ownedByAllUsers() && !reviewerAccountIds.isEmpty())) { |
| logger.atFine().log("%s is owned by a reviewer", absolutePath); |
| 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 StorageException( |
| String.format( |
| "failed to check owner permission of project %s for account %d", |
| project.get(), accountId.get()), |
| e); |
| } |
| } |
| |
| /** |
| * Gets the code owners that own the given path according to the given code owner config. |
| * |
| * @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 |
| */ |
| private CodeOwnerResolverResult getCodeOwners( |
| CodeOwnerConfig codeOwnerConfig, Path absolutePath) { |
| return codeOwnerResolver |
| .get() |
| .enforceVisibility(false) |
| .resolvePathCodeOwners(codeOwnerConfig, absolutePath); |
| } |
| |
| /** |
| * Gets the IDs of the accounts that are reviewer on the given change. |
| * |
| * @param changeNotes the change notes |
| */ |
| private ImmutableSet<Account.Id> getReviewerAccountIds(ChangeNotes changeNotes) { |
| return changeNotes.getReviewers().byState(ReviewerStateInternal.REVIEWER); |
| } |
| |
| /** |
| * 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 |
| * @param changeNotes the change notes |
| */ |
| private ImmutableSet<Account.Id> getApproverAccountIds( |
| RequiredApproval requiredApproval, ChangeNotes changeNotes) { |
| return changeNotes.getApprovals().get(changeNotes.getCurrentPatchSet().id()).stream() |
| .filter(requiredApproval::isApprovedBy) |
| .map(PatchSetApproval::accountId) |
| .collect(toImmutableSet()); |
| } |
| |
| /** |
| * Checks whether the given change has an override approval. |
| * |
| * @param overrideApproval approval that is required to override the code owners submit check. |
| * @param changeNotes the change notes |
| * @return whether the given change has an override approval |
| */ |
| private boolean hasOverride(RequiredApproval overrideApproval, ChangeNotes changeNotes) { |
| return changeNotes.getApprovals().get(changeNotes.getCurrentPatchSet().id()).stream() |
| .anyMatch(overrideApproval::isApprovedBy); |
| } |
| |
| /** |
| * 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()); |
| } |
| } |
| } |