| // Copyright (C) 2021 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.gerrit.plugins.codeowners.backend.config; |
| |
| import static com.google.gerrit.server.project.ProjectCache.illegalState; |
| import static java.util.Objects.requireNonNull; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.ImmutableSetMultimap; |
| import com.google.common.collect.ImmutableSortedSet; |
| import com.google.common.collect.Iterables; |
| 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.LabelType; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend; |
| import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference; |
| import com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException; |
| import com.google.gerrit.plugins.codeowners.backend.EnableImplicitApprovals; |
| import com.google.gerrit.plugins.codeowners.backend.FallbackCodeOwners; |
| import com.google.gerrit.plugins.codeowners.backend.PathExpressions; |
| import com.google.gerrit.plugins.codeowners.common.CodeOwnerConfigValidationPolicy; |
| import com.google.gerrit.plugins.codeowners.common.MergeCommitStrategy; |
| import com.google.gerrit.server.account.Emails; |
| import com.google.gerrit.server.project.ProjectCache; |
| import com.google.gerrit.server.project.ProjectState; |
| import com.google.inject.Inject; |
| import com.google.inject.assistedinject.Assisted; |
| import java.io.IOException; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.Optional; |
| import org.eclipse.jgit.lib.Config; |
| |
| /** Snapshot of the project-specific code-owners plugin configuration. */ |
| public class CodeOwnersPluginProjectConfigSnapshot { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| public interface Factory { |
| CodeOwnersPluginProjectConfigSnapshot create(Project.NameKey projectName); |
| } |
| |
| private final ProjectCache projectCache; |
| private final Emails emails; |
| private final BackendConfig backendConfig; |
| private final GeneralConfig generalConfig; |
| private final OverrideApprovalConfig overrideApprovalConfig; |
| private final RequiredApprovalConfig requiredApprovalConfig; |
| private final StatusConfig statusConfig; |
| private final Project.NameKey projectName; |
| private final Config pluginConfig; |
| |
| @Nullable private Optional<String> fileExtension; |
| @Nullable private Boolean enableCodeOwnerConfigFilesWithFileExtensions; |
| @Nullable private Boolean codeOwnerConfigsReadOnly; |
| @Nullable private Boolean exemptPureReverts; |
| @Nullable private Boolean rejectNonResolvableCodeOwners; |
| @Nullable private Boolean rejectNonResolvableImports; |
| |
| @Nullable |
| private CodeOwnerConfigValidationPolicy codeOwnerConfigValidationPolicyForCommitReceived; |
| |
| @Nullable private CodeOwnerConfigValidationPolicy codeOwnerConfigValidationPolicyForSubmit; |
| @Nullable private MergeCommitStrategy mergeCommitStrategy; |
| @Nullable private FallbackCodeOwners fallbackCodeOwners; |
| @Nullable private Integer maxPathsInChangeMessages; |
| @Nullable private Boolean enableAsyncMessageOnAddReviewer; |
| @Nullable private ImmutableSet<CodeOwnerReference> globalCodeOwners; |
| @Nullable private ImmutableSet<Account.Id> exemptedAccounts; |
| @Nullable private Optional<String> overrideInfoUrl; |
| @Nullable private Optional<String> invalidCodeOwnerConfigInfoUrl; |
| private Map<String, Boolean> disabledByBranch = new HashMap<>(); |
| @Nullable private Boolean isDisabled; |
| private Map<String, CodeOwnerBackend> backendByBranch = new HashMap<>(); |
| @Nullable private CodeOwnerBackend backend; |
| private Map<String, Optional<PathExpressions>> pathExpressionsByBranch = new HashMap<>(); |
| @Nullable private Optional<PathExpressions> pathExpressions; |
| @Nullable private Boolean implicitApprovalsEnabled; |
| @Nullable private RequiredApproval requiredApproval; |
| @Nullable private ImmutableSortedSet<RequiredApproval> overrideApprovals; |
| |
| @Inject |
| CodeOwnersPluginProjectConfigSnapshot( |
| CodeOwnersPluginConfig.Factory codeOwnersPluginConfigFactory, |
| ProjectCache projectCache, |
| Emails emails, |
| BackendConfig backendConfig, |
| GeneralConfig generalConfig, |
| OverrideApprovalConfig overrideApprovalConfig, |
| RequiredApprovalConfig requiredApprovalConfig, |
| StatusConfig statusConfig, |
| @Assisted Project.NameKey projectName) { |
| this.projectCache = projectCache; |
| this.emails = emails; |
| this.backendConfig = backendConfig; |
| this.generalConfig = generalConfig; |
| this.overrideApprovalConfig = overrideApprovalConfig; |
| this.requiredApprovalConfig = requiredApprovalConfig; |
| this.statusConfig = statusConfig; |
| this.projectName = projectName; |
| this.pluginConfig = codeOwnersPluginConfigFactory.create(projectName).get(); |
| } |
| |
| /** Gets the file extension of code owner config files, if any configured. */ |
| public Optional<String> getFileExtension() { |
| if (fileExtension == null) { |
| fileExtension = generalConfig.getFileExtension(pluginConfig); |
| } |
| return fileExtension; |
| } |
| |
| /** Whether file extensions for code owner config files are enabled. */ |
| public boolean enableCodeOwnerConfigFilesWithFileExtensions() { |
| if (enableCodeOwnerConfigFilesWithFileExtensions == null) { |
| enableCodeOwnerConfigFilesWithFileExtensions = |
| generalConfig.enableCodeOwnerConfigFilesWithFileExtensions(projectName, pluginConfig); |
| } |
| return enableCodeOwnerConfigFilesWithFileExtensions; |
| } |
| |
| /** Whether code owner configs are read-only. */ |
| public boolean areCodeOwnerConfigsReadOnly() { |
| if (codeOwnerConfigsReadOnly == null) { |
| codeOwnerConfigsReadOnly = generalConfig.getReadOnly(projectName, pluginConfig); |
| } |
| return codeOwnerConfigsReadOnly; |
| } |
| |
| /** Whether pure revert changes are exempted from needing code owner approvals for submit. */ |
| public boolean arePureRevertsExempted() { |
| if (exemptPureReverts == null) { |
| exemptPureReverts = generalConfig.getExemptPureReverts(projectName, pluginConfig); |
| } |
| return exemptPureReverts; |
| } |
| |
| /** |
| * Whether newly added non-resolvable code owners should be rejected on commit received and |
| * submit. |
| * |
| * @param branchName the branch for which it should be checked whether non-resolvable code owners |
| * should be rejected |
| */ |
| public boolean rejectNonResolvableCodeOwners(String branchName) { |
| if (rejectNonResolvableCodeOwners == null) { |
| rejectNonResolvableCodeOwners = readRejectNonResolvableCodeOwners(branchName); |
| } |
| return rejectNonResolvableCodeOwners; |
| } |
| |
| private boolean readRejectNonResolvableCodeOwners(String branchName) { |
| requireNonNull(branchName, "branchName"); |
| |
| Optional<Boolean> branchSpecificFlag = |
| generalConfig.getRejectNonResolvableCodeOwnersForBranch( |
| BranchNameKey.create(projectName, branchName), pluginConfig); |
| if (branchSpecificFlag.isPresent()) { |
| return branchSpecificFlag.get(); |
| } |
| |
| return generalConfig.getRejectNonResolvableCodeOwners(projectName, pluginConfig); |
| } |
| |
| /** |
| * Whether newly added non-resolvable imports should be rejected on commit received and submit. |
| * |
| * @param branchName the branch for which it should be checked whether non-resolvable imports |
| * should be rejected |
| */ |
| public boolean rejectNonResolvableImports(String branchName) { |
| if (rejectNonResolvableImports == null) { |
| rejectNonResolvableImports = readRejectNonResolvableImports(branchName); |
| } |
| return rejectNonResolvableImports; |
| } |
| |
| private boolean readRejectNonResolvableImports(String branchName) { |
| requireNonNull(branchName, "branchName"); |
| |
| Optional<Boolean> branchSpecificFlag = |
| generalConfig.getRejectNonResolvableImportsForBranch( |
| BranchNameKey.create(projectName, branchName), pluginConfig); |
| if (branchSpecificFlag.isPresent()) { |
| return branchSpecificFlag.get(); |
| } |
| |
| return generalConfig.getRejectNonResolvableImports(projectName, pluginConfig); |
| } |
| |
| /** |
| * Whether code owner configs should be validated when a commit is received. |
| * |
| * @param branchName the branch for which it should be checked whether code owner configs should |
| * be validated on commit received |
| */ |
| public CodeOwnerConfigValidationPolicy getCodeOwnerConfigValidationPolicyForCommitReceived( |
| String branchName) { |
| if (codeOwnerConfigValidationPolicyForCommitReceived == null) { |
| codeOwnerConfigValidationPolicyForCommitReceived = |
| readCodeOwnerConfigValidationPolicyForCommitReceived(branchName); |
| } |
| return codeOwnerConfigValidationPolicyForCommitReceived; |
| } |
| |
| private CodeOwnerConfigValidationPolicy readCodeOwnerConfigValidationPolicyForCommitReceived( |
| String branchName) { |
| requireNonNull(branchName, "branchName"); |
| |
| Optional<CodeOwnerConfigValidationPolicy> branchSpecificPolicy = |
| generalConfig.getCodeOwnerConfigValidationPolicyForCommitReceivedForBranch( |
| BranchNameKey.create(projectName, branchName), pluginConfig); |
| if (branchSpecificPolicy.isPresent()) { |
| return branchSpecificPolicy.get(); |
| } |
| |
| return generalConfig.getCodeOwnerConfigValidationPolicyForCommitReceived( |
| projectName, pluginConfig); |
| } |
| |
| /** |
| * Whether code owner configs should be validated when a change is submitted. |
| * |
| * @param branchName the branch for which it should be checked whether code owner configs should |
| * be validated on submit |
| */ |
| public CodeOwnerConfigValidationPolicy getCodeOwnerConfigValidationPolicyForSubmit( |
| String branchName) { |
| if (codeOwnerConfigValidationPolicyForSubmit == null) { |
| codeOwnerConfigValidationPolicyForSubmit = |
| readCodeOwnerConfigValidationPolicyForSubmit(branchName); |
| } |
| return codeOwnerConfigValidationPolicyForSubmit; |
| } |
| |
| private CodeOwnerConfigValidationPolicy readCodeOwnerConfigValidationPolicyForSubmit( |
| String branchName) { |
| requireNonNull(branchName, "branchName"); |
| |
| Optional<CodeOwnerConfigValidationPolicy> branchSpecificPolicy = |
| generalConfig.getCodeOwnerConfigValidationPolicyForSubmitForBranch( |
| BranchNameKey.create(projectName, branchName), pluginConfig); |
| if (branchSpecificPolicy.isPresent()) { |
| return branchSpecificPolicy.get(); |
| } |
| |
| return generalConfig.getCodeOwnerConfigValidationPolicyForSubmit(projectName, pluginConfig); |
| } |
| |
| /** Gets the merge commit strategy. */ |
| public MergeCommitStrategy getMergeCommitStrategy() { |
| if (mergeCommitStrategy == null) { |
| mergeCommitStrategy = generalConfig.getMergeCommitStrategy(projectName, pluginConfig); |
| } |
| return mergeCommitStrategy; |
| } |
| |
| /** Gets the fallback code owners. */ |
| public FallbackCodeOwners getFallbackCodeOwners() { |
| if (fallbackCodeOwners == null) { |
| fallbackCodeOwners = generalConfig.getFallbackCodeOwners(projectName, pluginConfig); |
| } |
| return fallbackCodeOwners; |
| } |
| |
| /** Gets the max paths in change messages. */ |
| public int getMaxPathsInChangeMessages() { |
| if (maxPathsInChangeMessages == null) { |
| maxPathsInChangeMessages = |
| generalConfig.getMaxPathsInChangeMessages(projectName, pluginConfig); |
| } |
| return maxPathsInChangeMessages; |
| } |
| |
| /** |
| * Gets whether code owner change messages that are added when a code owner is added as a reviewer |
| * should be posted asynchronously. |
| */ |
| public boolean enableAsyncMessageOnAddReviewer() { |
| if (enableAsyncMessageOnAddReviewer == null) { |
| enableAsyncMessageOnAddReviewer = |
| generalConfig.enableAsyncMessageOnAddReviewer(projectName, pluginConfig); |
| } |
| return enableAsyncMessageOnAddReviewer; |
| } |
| |
| /** Gets the global code owners. */ |
| public ImmutableSet<CodeOwnerReference> getGlobalCodeOwners() { |
| if (globalCodeOwners == null) { |
| globalCodeOwners = generalConfig.getGlobalCodeOwners(pluginConfig); |
| } |
| return globalCodeOwners; |
| } |
| |
| /** Gets the accounts that are exempted from requiring code owner approvals. */ |
| public ImmutableSet<Account.Id> getExemptedAccounts() { |
| if (exemptedAccounts == null) { |
| exemptedAccounts = lookupExemptedAccounts(); |
| } |
| return exemptedAccounts; |
| } |
| |
| private ImmutableSet<Account.Id> lookupExemptedAccounts() { |
| ImmutableSet<String> exemptedUsers = generalConfig.getExemptedUsers(pluginConfig); |
| |
| try { |
| ImmutableSetMultimap<String, Account.Id> exemptedAccounts = |
| emails.getAccountsFor(exemptedUsers.toArray(new String[0])); |
| |
| exemptedUsers.stream() |
| .filter(exemptedUser -> !exemptedAccounts.containsKey(exemptedUser)) |
| .forEach( |
| exemptedUser -> |
| logger.atWarning().log( |
| "Ignoring exempted user %s for project %s: not found", |
| exemptedUser, projectName)); |
| |
| return ImmutableSet.copyOf(exemptedAccounts.values()); |
| } catch (IOException e) { |
| throw new CodeOwnersInternalServerErrorException( |
| String.format( |
| "Failed to resolve exempted users %s on project %s", exemptedUsers, projectName), |
| e); |
| } |
| } |
| |
| /** Gets the override info URL that is configured. */ |
| public Optional<String> getOverrideInfoUrl() { |
| if (overrideInfoUrl == null) { |
| overrideInfoUrl = generalConfig.getOverrideInfoUrl(pluginConfig); |
| } |
| return overrideInfoUrl; |
| } |
| |
| /** Gets the invalid code owner config info URL that is configured. */ |
| public Optional<String> getInvalidCodeOwnerConfigInfoUrl() { |
| if (invalidCodeOwnerConfigInfoUrl == null) { |
| invalidCodeOwnerConfigInfoUrl = generalConfig.getInvalidCodeOwnerConfigInfoUrl(pluginConfig); |
| } |
| return invalidCodeOwnerConfigInfoUrl; |
| } |
| |
| /** |
| * Whether the code owners functionality is disabled for the given branch. |
| * |
| * <p>The configuration is evaluated in the following order: |
| * |
| * <ul> |
| * <li>disabled configuration for the branch (with inheritance) |
| * <li>disabled configuration for the project (with inheritance) |
| * <li>hard-coded default (not disabled) |
| * </ul> |
| * |
| * <p>The first disabled configuration that exists counts and the evaluation is stopped. |
| * |
| * @param branchName the branch for which it should be checked whether the code owners |
| * functionality is disabled |
| * @return {@code true} if the code owners functionality is disabled for the given branch, |
| * otherwise {@code false} |
| */ |
| public boolean isDisabled(String branchName) { |
| requireNonNull(branchName, "branchName"); |
| |
| BranchNameKey branchNameKey = BranchNameKey.create(projectName, branchName); |
| return disabledByBranch.computeIfAbsent( |
| branchNameKey.branch(), |
| b -> { |
| boolean isDisabled = statusConfig.isDisabledForBranch(pluginConfig, branchNameKey); |
| if (isDisabled) { |
| return true; |
| } |
| return isDisabled(); |
| }); |
| } |
| |
| /** |
| * Whether the code owners functionality is disabled for the given project. |
| * |
| * <p>The configuration is evaluated in the following order: |
| * |
| * <ul> |
| * <li>disabled configuration for the project (with inheritance) |
| * <li>hard-coded default (not disabled) |
| * </ul> |
| * |
| * <p>The first disabled configuration that exists counts and the evaluation is stopped. |
| * |
| * @return {@code true} if the code owners functionality is disabled, otherwise {@code false} |
| */ |
| public boolean isDisabled() { |
| if (isDisabled == null) { |
| isDisabled = statusConfig.isDisabledForProject(pluginConfig, projectName); |
| } |
| return isDisabled; |
| } |
| |
| /** |
| * Returns the configured {@link CodeOwnerBackend} for the given branch. |
| * |
| * <p>The code owner backend configuration is evaluated in the following order: |
| * |
| * <ul> |
| * <li>backend configuration for branch (with inheritance, first by full branch name, then by |
| * short branch name) |
| * <li>backend configuration for project (with inheritance) |
| * <li>default backend (first globally configured backend, then hard-coded default backend) |
| * </ul> |
| * |
| * <p>The first code owner backend configuration that exists counts and the evaluation is stopped. |
| * |
| * @param branchName the branch for which the configured code owner backend should be returned |
| * @return the {@link CodeOwnerBackend} that should be used for the branch |
| */ |
| public CodeOwnerBackend getBackend(String branchName) { |
| requireNonNull(branchName, "branchName"); |
| |
| BranchNameKey branchNameKey = BranchNameKey.create(projectName, branchName); |
| return backendByBranch.computeIfAbsent( |
| branchNameKey.branch(), |
| b -> { |
| Optional<CodeOwnerBackend> codeOwnerBackend = |
| backendConfig.getBackendForBranch( |
| pluginConfig, BranchNameKey.create(projectName, branchName)); |
| if (codeOwnerBackend.isPresent()) { |
| return codeOwnerBackend.get(); |
| } |
| return getBackend(); |
| }); |
| } |
| |
| /** |
| * Returns the configured {@link CodeOwnerBackend}. |
| * |
| * <p>The code owner backend configuration is evaluated in the following order: |
| * |
| * <ul> |
| * <li>backend configuration for project (with inheritance) |
| * <li>default backend (first globally configured backend, then hard-coded default backend) |
| * </ul> |
| * |
| * <p>The first code owner backend configuration that exists counts and the evaluation is stopped. |
| * |
| * @return the {@link CodeOwnerBackend} that should be used |
| */ |
| public CodeOwnerBackend getBackend() { |
| if (backend == null) { |
| backend = readBackend(); |
| } |
| return backend; |
| } |
| |
| private CodeOwnerBackend readBackend() { |
| // check if a project specific backend is configured |
| Optional<CodeOwnerBackend> codeOwnerBackend = |
| backendConfig.getBackendForProject(pluginConfig, projectName); |
| if (codeOwnerBackend.isPresent()) { |
| return codeOwnerBackend.get(); |
| } |
| |
| // fall back to the default backend |
| return backendConfig.getDefaultBackend(); |
| } |
| |
| /** |
| * Returns the configured {@link PathExpressions} for the given branch. |
| * |
| * <p>The path expression configuration is evaluated in the following order: |
| * |
| * <ul> |
| * <li>path expression configuration for branch (with inheritance, first by full branch name, |
| * then by short branch name) |
| * <li>path expressions configuration for project (with inheritance) |
| * </ul> |
| * |
| * <p>The first path expressions configuration that exists counts and the evaluation is stopped. |
| * |
| * @param branchName the branch for which the configured path expressions should be returned |
| * @return the {@link PathExpressions} that should be used for the branch, {@link |
| * Optional#empty()} if no path expressions are configured for the branch |
| */ |
| public Optional<PathExpressions> getPathExpressions(String branchName) { |
| requireNonNull(branchName, "branchName"); |
| |
| BranchNameKey branchNameKey = BranchNameKey.create(projectName, branchName); |
| return pathExpressionsByBranch.computeIfAbsent( |
| branchNameKey.branch(), |
| b -> { |
| Optional<PathExpressions> pathExpressions = |
| backendConfig.getPathExpressionsForBranch( |
| pluginConfig, BranchNameKey.create(projectName, branchName)); |
| if (pathExpressions.isPresent()) { |
| return pathExpressions; |
| } |
| return getPathExpressions(); |
| }); |
| } |
| |
| /** |
| * Returns the configured {@link PathExpressions}. |
| * |
| * <p>The path expression configuration is evaluated in the following order: |
| * |
| * <ul> |
| * <li>path expression configuration for project (with inheritance) |
| * <li>default path expressions (globally configured path expressions) |
| * </ul> |
| * |
| * <p>The first path expression configuration that exists counts and the evaluation is stopped. |
| * |
| * @return the {@link PathExpressions} that should be used, {@link Optional#empty()} if no path |
| * expressions are configured |
| */ |
| public Optional<PathExpressions> getPathExpressions() { |
| if (pathExpressions == null) { |
| pathExpressions = readPathExpressions(); |
| } |
| return pathExpressions; |
| } |
| |
| private Optional<PathExpressions> readPathExpressions() { |
| // check if project specific path expressions are configured |
| Optional<PathExpressions> pathExpressions = |
| backendConfig.getPathExpressionsForProject(pluginConfig, projectName); |
| if (pathExpressions.isPresent()) { |
| return pathExpressions; |
| } |
| |
| // fall back to the default path expressions |
| return backendConfig.getDefaultPathExpressions(); |
| } |
| |
| /** |
| * Checks whether implicit code owner approvals are enabled. |
| * |
| * <p>If enabled, an implict code owner approval from the change owner is assumed if the last |
| * patch set was uploaded by the change owner. |
| */ |
| public boolean areImplicitApprovalsEnabled() { |
| if (implicitApprovalsEnabled == null) { |
| implicitApprovalsEnabled = readImplicitApprovalsEnabled(); |
| } |
| return implicitApprovalsEnabled; |
| } |
| |
| private boolean readImplicitApprovalsEnabled() { |
| EnableImplicitApprovals enableImplicitApprovals = |
| generalConfig.getEnableImplicitApprovals(projectName, pluginConfig); |
| switch (enableImplicitApprovals) { |
| case FALSE: |
| logger.atFine().log("implicit approvals on project %s are disabled", projectName); |
| return false; |
| case TRUE: |
| LabelType requiredLabel = getRequiredApproval().labelType(); |
| if (requiredLabel.isIgnoreSelfApproval()) { |
| logger.atFine().log( |
| "ignoring implicit approval configuration on project %s since the label of the required" |
| + " approval (%s) is configured to ignore self approvals", |
| projectName, requiredLabel); |
| return false; |
| } |
| return true; |
| case FORCED: |
| logger.atFine().log("implicit approvals on project %s are enforced", projectName); |
| return true; |
| } |
| throw new IllegalStateException( |
| String.format( |
| "unknown value %s for enableImplicitApprovals configuration in project %s", |
| enableImplicitApprovals, projectName)); |
| } |
| |
| /** |
| * Returns the approval that is required from code owners to approve the files in a change. |
| * |
| * <p>Defines which approval counts as code owner approval. |
| * |
| * <p>The code owner required approval configuration is evaluated in the following order: |
| * |
| * <ul> |
| * <li>required approval configuration for project (with inheritance) |
| * <li>globally configured required approval |
| * <li>hard-coded default required approval |
| * </ul> |
| * |
| * <p>The first required code owner approval configuration that exists counts and the evaluation |
| * is stopped. |
| * |
| * <p>If the code owner configuration contains multiple required approvals values, the last value |
| * is used. |
| * |
| * @return the required code owner approval that should be used |
| */ |
| public RequiredApproval getRequiredApproval() { |
| if (requiredApproval == null) { |
| requiredApproval = readRequiredApproval(); |
| } |
| return requiredApproval; |
| } |
| |
| private RequiredApproval readRequiredApproval() { |
| ImmutableList<RequiredApproval> configuredRequiredApprovalConfig = |
| getConfiguredRequiredApproval(requiredApprovalConfig); |
| if (!configuredRequiredApprovalConfig.isEmpty()) { |
| // There can be only one required approval. If multiple ones are configured just use the last |
| // one, this is also what Config#getString(String, String, String) does. |
| return Iterables.getLast(configuredRequiredApprovalConfig); |
| } |
| |
| // fall back to hard-coded default required approval |
| ProjectState projectState = |
| projectCache.get(projectName).orElseThrow(illegalState(projectName)); |
| return requiredApprovalConfig.createDefault(projectState); |
| } |
| |
| /** |
| * Returns the approvals that are required to override the code owners submit check for a change. |
| * |
| * <p>If multiple approvals are returned, any of them is sufficient to override the code owners |
| * submit check. |
| * |
| * <p>The override approval configuration is read from: |
| * |
| * <ul> |
| * <li>the override approval configuration for project (with inheritance) |
| * <li>the globally configured override approval |
| * </ul> |
| * |
| * <p>Override approvals that are configured on project-level extend the inherited override |
| * approval configuration. |
| * |
| * <p>The returned override approvals are sorted alphabetically by their string representation |
| * (e.g. {@code Owners-Override+1}). |
| * |
| * @return the override approvals that should be used, an empty set if no override approval is |
| * configured, in this case the override functionality is disabled |
| */ |
| public ImmutableSortedSet<RequiredApproval> getOverrideApprovals() { |
| if (overrideApprovals == null) { |
| overrideApprovals = readOverrideApprovals(); |
| } |
| return overrideApprovals; |
| } |
| |
| private ImmutableSortedSet<RequiredApproval> readOverrideApprovals() { |
| try { |
| return ImmutableSortedSet.copyOf( |
| filterOutDuplicateRequiredApprovals( |
| getConfiguredRequiredApproval(overrideApprovalConfig))); |
| } catch (InvalidPluginConfigurationException e) { |
| logger.atWarning().withCause(e).log( |
| "Ignoring invalid override approval configuration for project %s." |
| + " Overrides are disabled.", |
| projectName.get()); |
| } |
| |
| return ImmutableSortedSet.of(); |
| } |
| |
| /** |
| * Filters out duplicate required approvals from the input list. |
| * |
| * <p>The following entries are considered as duplicate: |
| * |
| * <ul> |
| * <li>exact identical required approvals (e.g. "Code-Review+2" and "Code-Review+2") |
| * <li>required approvals with the same label name and a higher value (e.g. "Code-Review+2" is |
| * not needed if "Code-Review+1" is already contained, since "Code-Review+1" covers all |
| * "Code-Review" approvals >= 1) |
| * </ul> |
| */ |
| private Collection<RequiredApproval> filterOutDuplicateRequiredApprovals( |
| ImmutableList<RequiredApproval> requiredApprovals) { |
| Map<String, RequiredApproval> requiredApprovalsByLabel = new HashMap<>(); |
| for (RequiredApproval requiredApproval : requiredApprovals) { |
| String labelName = requiredApproval.labelType().getName(); |
| RequiredApproval otherRequiredApproval = requiredApprovalsByLabel.get(labelName); |
| if (otherRequiredApproval != null |
| && otherRequiredApproval.value() <= requiredApproval.value()) { |
| continue; |
| } |
| requiredApprovalsByLabel.put(labelName, requiredApproval); |
| } |
| return requiredApprovalsByLabel.values(); |
| } |
| |
| /** |
| * Gets the required approvals that are configured. |
| * |
| * @param requiredApprovalConfig the config from which the required approvals should be read |
| * @return the required approvals that are configured, an empty list if no required approvals are |
| * configured |
| */ |
| private ImmutableList<RequiredApproval> getConfiguredRequiredApproval( |
| AbstractRequiredApprovalConfig requiredApprovalConfig) { |
| ProjectState projectState = |
| projectCache.get(projectName).orElseThrow(illegalState(projectName)); |
| return requiredApprovalConfig.get(projectState, pluginConfig); |
| } |
| } |