blob: 1a7985d1f577b69efaee1e7f38b16bfafc49136a [file] [log] [blame]
// 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.config;
import static com.google.gerrit.server.project.ProjectCache.illegalState;
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.collect.Iterables;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigValidationPolicy;
import com.google.gerrit.plugins.codeowners.api.MergeCommitStrategy;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
import com.google.gerrit.plugins.codeowners.backend.FallbackCodeOwners;
import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jgit.lib.Config;
/**
* The configuration of the code-owners plugin.
*
* <p>The global configuration of the code-owners plugin is stored in the {@code gerrit.config} file
* in the {@code plugin.code-owners} subsection.
*
* <p>In addition there is configuration on project level that is stored in {@code
* code-owners.config} files that are stored in the {@code refs/meta/config} branches of the
* projects.
*
* <p>Parameters that are not set for a project are inherited from the parent project.
*/
@Singleton
public class CodeOwnersPluginConfiguration {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@VisibleForTesting public static final String SECTION_CODE_OWNERS = "codeOwners";
@VisibleForTesting
static final String KEY_ENABLE_EXPERIMENTAL_REST_ENDPOINTS = "enableExperimentalRestEndpoints";
private final String pluginName;
private final PluginConfigFactory pluginConfigFactory;
private final ProjectCache projectCache;
private final GeneralConfig generalConfig;
private final StatusConfig statusConfig;
private final BackendConfig backendConfig;
private final RequiredApprovalConfig requiredApprovalConfig;
private final OverrideApprovalConfig overrideApprovalConfig;
@Inject
CodeOwnersPluginConfiguration(
@PluginName String pluginName,
PluginConfigFactory pluginConfigFactory,
ProjectCache projectCache,
GeneralConfig generalConfig,
StatusConfig statusConfig,
BackendConfig backendConfig,
RequiredApprovalConfig requiredApprovalConfig,
OverrideApprovalConfig overrideApprovalConfig) {
this.pluginName = pluginName;
this.pluginConfigFactory = pluginConfigFactory;
this.projectCache = projectCache;
this.generalConfig = generalConfig;
this.statusConfig = statusConfig;
this.backendConfig = backendConfig;
this.requiredApprovalConfig = requiredApprovalConfig;
this.overrideApprovalConfig = overrideApprovalConfig;
}
/**
* Gets the file extension that is configured for the given project.
*
* @param project the project for which the configured file extension should be returned
* @return the file extension that is configured for the given project
*/
public Optional<String> getFileExtension(Project.NameKey project) {
requireNonNull(project, "project");
return generalConfig.getFileExtension(getPluginConfig(project));
}
/**
* Checks whether code owner configs in the given project are read-only.
*
* @param project the project for it should be checked whether code owner configs are read-only
* @return whether code owner configs in the given project are read-only
*/
public boolean areCodeOwnerConfigsReadOnly(Project.NameKey project) {
requireNonNull(project, "project");
return generalConfig.getReadOnly(getPluginConfig(project));
}
/**
* Whether code owner configs should be validated when a commit is received.
*
* @param project the project for it should be checked whether code owner configs should be
* validated when a commit is received
* @return whether code owner configs should be validated when a commit is received
*/
public CodeOwnerConfigValidationPolicy getCodeOwnerConfigValidationPolicyForCommitReceived(
Project.NameKey project) {
requireNonNull(project, "project");
return generalConfig.getCodeOwnerConfigValidationPolicyForCommitReceived(
project, getPluginConfig(project));
}
/**
* Whether code owner configs should be validated when a change is submitted.
*
* @param project the project for it should be checked whether code owner configs should be
* validated when a change is submitted
* @return whether code owner configs should be validated when a change is submitted
*/
public CodeOwnerConfigValidationPolicy getCodeOwnerConfigValidationPolicyForSubmit(
Project.NameKey project) {
requireNonNull(project, "project");
return generalConfig.getCodeOwnerConfigValidationPolicyForSubmit(
project, getPluginConfig(project));
}
/**
* Gets the merge commit strategy for the given project.
*
* @param project the project for which the merge commit strategy should be retrieved
* @return the merge commit strategy for the given project
*/
public MergeCommitStrategy getMergeCommitStrategy(Project.NameKey project) {
requireNonNull(project, "project");
return generalConfig.getMergeCommitStrategy(project, getPluginConfig(project));
}
/**
* Gets the fallback code owners for the given project.
*
* @param project the project for which the fallback code owners should be retrieved
* @return the fallback code owners for the given project
*/
public FallbackCodeOwners getFallbackCodeOwners(Project.NameKey project) {
requireNonNull(project, "project");
return generalConfig.getFallbackCodeOwners(project, getPluginConfig(project));
}
/**
* Checks whether an implicit code owner approval from the last uploader is assumed.
*
* @param project the project for it should be checked whether implict approvals are enabled
* @return whether an implicit code owner approval from the last uploader is assumed
*/
public boolean areImplicitApprovalsEnabled(Project.NameKey project) {
requireNonNull(project, "project");
LabelType requiredLabel = getRequiredApproval(project).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",
project, requiredLabel);
return false;
}
return generalConfig.getEnableImplicitApprovals(getPluginConfig(project));
}
/**
* Gets the global code owners of the given project.
*
* @param project the project for which the global code owners should be returned
* @return the global code owners of the given project
*/
public ImmutableSet<CodeOwnerReference> getGlobalCodeOwners(Project.NameKey project) {
requireNonNull(project, "project");
return generalConfig.getGlobalCodeOwners(getPluginConfig(project));
}
/**
* Gets the override info URL that is configured for the given project.
*
* @param project the project for which the configured override info URL should be returned
* @return the override info URL that is configured for the given project
*/
public Optional<String> getOverrideInfoUrl(Project.NameKey project) {
requireNonNull(project, "project");
return generalConfig.getOverrideInfoUrl(getPluginConfig(project));
}
/**
* Returns the email domains that are allowed to be used for code owners.
*
* @return the email domains that are allowed to be used for code owners, an empty set if all
* email domains are allowed (if {@code plugin.code-owners.allowedEmailDomain} is not set or
* set to an empty value)
*/
public ImmutableSet<String> getAllowedEmailDomains() {
return generalConfig.getAllowedEmailDomains();
}
/**
* Whether the code owners functionality is disabled for the given branch.
*
* <p>Callers must ensure that the project of the specified branch exists. If the project doesn't
* exist the call fails with {@link IllegalStateException}.
*
* <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 branchNameKey the branch and project 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(BranchNameKey branchNameKey) {
requireNonNull(branchNameKey, "branchNameKey");
Config pluginConfig = getPluginConfig(branchNameKey.project());
boolean isDisabled = statusConfig.isDisabledForBranch(pluginConfig, branchNameKey);
if (isDisabled) {
return true;
}
return isDisabled(branchNameKey.project());
}
/**
* Whether the code owners functionality is disabled for the given project.
*
* <p>Callers must ensure that the project of the specified branch exists. If the project doesn't
* exist the call fails with {@link IllegalStateException}.
*
* <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.
*
* @param project the project 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 project,
* otherwise {@code false}
*/
public boolean isDisabled(Project.NameKey project) {
requireNonNull(project, "project");
Config pluginConfig = getPluginConfig(project);
return statusConfig.isDisabledForProject(pluginConfig, project);
}
/**
* Returns the configured {@link CodeOwnerBackend} for the given branch.
*
* <p>Callers must ensure that the project of the specified branch exists. If the project doesn't
* exist the call fails with {@link IllegalStateException}.
*
* <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 branchNameKey project and 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(BranchNameKey branchNameKey) {
Config pluginConfig = getPluginConfig(branchNameKey.project());
// check if a branch specific backend is configured
Optional<CodeOwnerBackend> codeOwnerBackend =
backendConfig.getBackendForBranch(pluginConfig, branchNameKey);
if (codeOwnerBackend.isPresent()) {
return codeOwnerBackend.get();
}
return getBackend(branchNameKey.project());
}
/**
* Returns the configured {@link CodeOwnerBackend} for the given project.
*
* <p>Callers must ensure that the project exists. If the project doesn't exist the call fails
* with {@link IllegalStateException}.
*
* <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.
*
* @param project project for which the configured code owner backend should be returned
* @return the {@link CodeOwnerBackend} that should be used for the project
*/
public CodeOwnerBackend getBackend(Project.NameKey project) {
Config pluginConfig = getPluginConfig(project);
// check if a project specific backend is configured
Optional<CodeOwnerBackend> codeOwnerBackend =
backendConfig.getBackendForProject(pluginConfig, project);
if (codeOwnerBackend.isPresent()) {
return codeOwnerBackend.get();
}
// fall back to the default backend
return backendConfig.getDefaultBackend();
}
/**
* Returns the approval that is required from code owners to approve the files in a change of the
* given project.
*
* <p>Defines which approval counts as code owner approval.
*
* <p>Callers must ensure that the project of the specified branch exists. If the project doesn't
* exist the call fails with {@link IllegalStateException}.
*
* <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.
*
* @param project project for which the required approval should be returned
* @return the required code owner approval that should be used for the given project
*/
public RequiredApproval getRequiredApproval(Project.NameKey project) {
ImmutableList<RequiredApproval> configuredRequiredApprovalConfig =
getConfiguredRequiredApproval(requiredApprovalConfig, project);
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(project).orElseThrow(illegalState(project));
return requiredApprovalConfig.createDefault(projectState);
}
/**
* Returns the approvals that are required to override the code owners submit check for a change
* of the given project.
*
* <p>If multiple approvals are returned, any of them is sufficient to override the code owners
* submit check.
*
* <p>Callers must ensure that the project of the specified branch exists. If the project doesn't
* exist the call fails with {@link IllegalStateException}.
*
* <p>The override approval configuration is evaluated in the following order:
*
* <ul>
* <li>override approval configuration for project (with inheritance)
* <li>globally configured override approval
* </ul>
*
* <p>The first override approval configuration that exists counts and the evaluation is stopped.
*
* @param project project for which the override approval should be returned
* @return the override approvals that should be used for the given project, an empty set if no
* override approval is configured, in this case the override functionality is disabled
*/
public ImmutableSet<RequiredApproval> getOverrideApproval(Project.NameKey project) {
try {
return filterOutDuplicateRequiredApprovals(
getConfiguredRequiredApproval(overrideApprovalConfig, project));
} catch (InvalidPluginConfigurationException e) {
logger.atWarning().withCause(e).log(
"Ignoring invalid override approval configuration for project %s."
+ " Overrides are disabled.",
project.get());
}
return ImmutableSet.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 ImmutableSet<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 ImmutableSet.copyOf(requiredApprovalsByLabel.values());
}
/**
* Gets the required approvals that are configured for the given project.
*
* @param requiredApprovalConfig the config from which the required approvals should be read
* @param project the project for which the configured required approvals should be returned
* @return the required approvals that is configured for the given project, an empty list if no
* required approvals are configured
*/
private ImmutableList<RequiredApproval> getConfiguredRequiredApproval(
AbstractRequiredApprovalConfig requiredApprovalConfig, Project.NameKey project) {
Config pluginConfig = getPluginConfig(project);
ProjectState projectState = projectCache.get(project).orElseThrow(illegalState(project));
return requiredApprovalConfig.get(projectState, pluginConfig);
}
/**
* Checks whether experimental REST endpoints are enabled.
*
* @throws MethodNotAllowedException thrown if experimental REST endpoints are disabled
*/
public void checkExperimentalRestEndpointsEnabled() throws MethodNotAllowedException {
if (!areExperimentalRestEndpointsEnabled()) {
throw new MethodNotAllowedException("experimental code owners REST endpoints are disabled");
}
}
/** Whether experimental REST endpoints are enabled. */
public boolean areExperimentalRestEndpointsEnabled() {
try {
return pluginConfigFactory
.getFromGerritConfig(pluginName)
.getBoolean(KEY_ENABLE_EXPERIMENTAL_REST_ENDPOINTS, /* defaultValue= */ false);
} catch (IllegalArgumentException e) {
logger.atWarning().withCause(e).log(
"Value '%s' in gerrit.config (parameter plugin.%s.%s) is invalid.",
pluginConfigFactory
.getFromGerritConfig(pluginName)
.getString(KEY_ENABLE_EXPERIMENTAL_REST_ENDPOINTS),
pluginName,
KEY_ENABLE_EXPERIMENTAL_REST_ENDPOINTS);
return false;
}
}
/**
* Reads and returns the config from the {@code code-owners.config} file in {@code
* refs/meta/config} branch of the given project.
*
* @param project the project for which the code owners configurations should be returned
* @return the code owners configurations for the given project
*/
private Config getPluginConfig(Project.NameKey project) {
try {
return pluginConfigFactory.getProjectPluginConfigWithInheritance(project, pluginName);
} catch (NoSuchProjectException e) {
throw new IllegalStateException(
String.format(
"cannot get %s plugin config for non-existing project %s", pluginName, project),
e);
}
}
}