blob: 2077d644ee9740463fd8a8d26ff5a070300ba96f [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.backend.config;
import static com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS;
import static java.util.Objects.requireNonNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackendId;
import com.google.gerrit.plugins.codeowners.backend.PathExpressions;
import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.gerrit.server.git.validators.ValidationMessage;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.Optional;
import org.eclipse.jgit.lib.Config;
/**
* Class to read the {@link CodeOwnerBackend} configuration from {@code gerrit.config} and from
* {@code code-owners.config} in {@code refs/meta/config}.
*
* <p>The default {@link CodeOwnerBackend} is configured in {@code gerrit.config} by the {@code
* plugin.code-owners.backend} parameter.
*
* <p>On project-level the {@link CodeOwnerBackend} is configured in {@code code-owners.config} in
* {@code refs/meta/config}. The {@code codeOwners.backend} parameter configures the {@link
* CodeOwnerBackend} for the project and the {@code codeOwners.<branch>.backend} parameter for a
* branch.
*
* <p>Projects that have no {@link CodeOwnerBackend} configuration inherit the configuration from
* their parent projects.
*/
@Singleton
@VisibleForTesting
public class BackendConfig {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@VisibleForTesting public static final String KEY_BACKEND = "backend";
@VisibleForTesting public static final String KEY_PATH_EXPRESSIONS = "pathExpressions";
private final String pluginName;
private final DynamicMap<CodeOwnerBackend> codeOwnerBackends;
/** The name of the configured code owners default backend. */
private final String defaultBackendName;
/** The configured default path expressions. */
private final Optional<PathExpressions> defaultPathExpressions;
@Inject
BackendConfig(
@PluginName String pluginName,
PluginConfigFactory pluginConfigFactory,
DynamicMap<CodeOwnerBackend> codeOwnerBackends) {
this.pluginName = pluginName;
this.codeOwnerBackends = codeOwnerBackends;
this.defaultBackendName =
pluginConfigFactory
.getFromGerritConfig(pluginName)
.getString(KEY_BACKEND, CodeOwnerBackendId.FIND_OWNERS.getBackendId());
String defaultPathExpressionsName =
pluginConfigFactory
.getFromGerritConfig(pluginName)
.getString(KEY_PATH_EXPRESSIONS, /* defaultValue= */ null);
this.defaultPathExpressions = PathExpressions.tryParse(defaultPathExpressionsName);
if (!Strings.isNullOrEmpty(defaultPathExpressionsName)
&& !this.defaultPathExpressions.isPresent()) {
logger.atWarning().log(
"Path expressions '%s' that are configured in gerrit.config"
+ " (parameter plugin.%s.%s) not found.",
defaultPathExpressionsName, pluginName, KEY_PATH_EXPRESSIONS);
}
}
/**
* Validates the backend configuration in the given project level configuration.
*
* @param fileName the name of the config file
* @param projectLevelConfig the project level plugin configuration
* @return list of validation messages for validation errors, empty list if there are no
* validation errors
*/
ImmutableList<CommitValidationMessage> validateProjectLevelConfig(
String fileName, Config projectLevelConfig) {
requireNonNull(fileName, "fileName");
requireNonNull(projectLevelConfig, "projectLevelConfig");
ImmutableList.Builder<CommitValidationMessage> validationMessages = ImmutableList.builder();
String backendName = projectLevelConfig.getString(SECTION_CODE_OWNERS, null, KEY_BACKEND);
if (backendName != null) {
if (!lookupBackend(backendName).isPresent()) {
validationMessages.add(
new CommitValidationMessage(
String.format(
"Code owner backend '%s' that is configured in %s (parameter %s.%s) not found.",
backendName, fileName, SECTION_CODE_OWNERS, KEY_BACKEND),
ValidationMessage.Type.ERROR));
}
}
String pathExpressionsName =
projectLevelConfig.getString(SECTION_CODE_OWNERS, null, KEY_PATH_EXPRESSIONS);
if (!Strings.isNullOrEmpty(pathExpressionsName)
&& !PathExpressions.tryParse(pathExpressionsName).isPresent()) {
validationMessages.add(
new CommitValidationMessage(
String.format(
"Path expressions '%s' that are configured in %s (parameter %s.%s) not found.",
pathExpressionsName, fileName, SECTION_CODE_OWNERS, KEY_PATH_EXPRESSIONS),
ValidationMessage.Type.ERROR));
}
for (String subsection : projectLevelConfig.getSubsections(SECTION_CODE_OWNERS)) {
backendName = projectLevelConfig.getString(SECTION_CODE_OWNERS, subsection, KEY_BACKEND);
if (backendName != null) {
if (!lookupBackend(backendName).isPresent()) {
validationMessages.add(
new CommitValidationMessage(
String.format(
"Code owner backend '%s' that is configured in %s (parameter %s.%s.%s) not found.",
backendName, fileName, SECTION_CODE_OWNERS, subsection, KEY_BACKEND),
ValidationMessage.Type.ERROR));
}
}
pathExpressionsName =
projectLevelConfig.getString(SECTION_CODE_OWNERS, subsection, KEY_PATH_EXPRESSIONS);
if (!Strings.isNullOrEmpty(pathExpressionsName)
&& !PathExpressions.tryParse(pathExpressionsName).isPresent()) {
validationMessages.add(
new CommitValidationMessage(
String.format(
"Path expressions '%s' that are configured in %s (parameter %s.%s.%s) not found.",
pathExpressionsName,
fileName,
SECTION_CODE_OWNERS,
subsection,
KEY_PATH_EXPRESSIONS),
ValidationMessage.Type.ERROR));
}
}
return validationMessages.build();
}
/**
* Gets the code owner backend that is configured for the given branch.
*
* <p>The code owner backend configuration is evaluated in the following order:
*
* <ul>
* <li>backend configuration for branch by full name (with inheritance)
* <li>backend configuration for branch by short name (with inheritance)
* </ul>
*
* @param pluginConfig the plugin config from which the code owner backend should be read.
* @param branch the project and branch for which the configured code owner backend should be read
* @return the code owner backend that is configured for the given branch, {@link
* Optional#empty()} if there is no branch-specific code owner backend configuration
*/
Optional<CodeOwnerBackend> getBackendForBranch(Config pluginConfig, BranchNameKey branch) {
requireNonNull(pluginConfig, "pluginConfig");
requireNonNull(branch, "branch");
// check for branch specific backend by full branch name
Optional<CodeOwnerBackend> backend =
getBackendForBranch(pluginConfig, branch.project(), branch.branch());
if (!backend.isPresent()) {
// check for branch specific backend by short branch name
backend = getBackendForBranch(pluginConfig, branch.project(), branch.shortName());
}
return backend;
}
private Optional<CodeOwnerBackend> getBackendForBranch(
Config pluginConfig, Project.NameKey project, String branch) {
String backendName = pluginConfig.getString(SECTION_CODE_OWNERS, branch, KEY_BACKEND);
if (backendName == null) {
return Optional.empty();
}
return Optional.of(
lookupBackend(backendName)
.orElseThrow(
() -> {
InvalidPluginConfigurationException e =
new InvalidPluginConfigurationException(
pluginName,
String.format(
"Code owner backend '%s' that is configured for project %s in"
+ " %s.config (parameter %s.%s.%s) not found.",
backendName,
project,
pluginName,
SECTION_CODE_OWNERS,
branch,
KEY_BACKEND));
logger.atSevere().log("%s", e.getMessage());
return e;
}));
}
/**
* Gets the code owner backend that is configured for the given project.
*
* @param pluginConfig the plugin config from which the code owner backend should be read.
* @param project the project for which the configured code owner backend should be read
* @return the code owner backend that is configured for the given project, {@link
* Optional#empty()} if there is no project-specific code owner backend configuration
*/
Optional<CodeOwnerBackend> getBackendForProject(Config pluginConfig, Project.NameKey project) {
requireNonNull(pluginConfig, "pluginConfig");
requireNonNull(project, "project");
String backendName = pluginConfig.getString(SECTION_CODE_OWNERS, null, KEY_BACKEND);
if (backendName == null) {
return Optional.empty();
}
return Optional.of(
lookupBackend(backendName)
.orElseThrow(
() -> {
InvalidPluginConfigurationException e =
new InvalidPluginConfigurationException(
pluginName,
String.format(
"Code owner backend '%s' that is configured for project %s in"
+ " %s.config (parameter %s.%s) not found.",
backendName, project, pluginName, SECTION_CODE_OWNERS, KEY_BACKEND));
logger.atSevere().log("%s", e.getMessage());
return e;
}));
}
/** Gets the default code owner backend. */
public CodeOwnerBackend getDefaultBackend() {
return lookupBackend(defaultBackendName)
.orElseThrow(
() -> {
InvalidPluginConfigurationException e =
new InvalidPluginConfigurationException(
pluginName,
String.format(
"Code owner backend '%s' that is configured in gerrit.config"
+ " (parameter plugin.%s.%s) not found.",
defaultBackendName, pluginName, KEY_BACKEND));
logger.atSevere().log("%s", e.getMessage());
return e;
});
}
private Optional<CodeOwnerBackend> lookupBackend(String backendName) {
// We must use "gerrit" as plugin name since DynamicMapProvider#get() hard-codes "gerrit" as
// plugin name.
return Optional.ofNullable(codeOwnerBackends.get("gerrit", backendName));
}
/**
* Gets the path expressions that are configured for the given branch.
*
* <p>The path expressions configuration is evaluated in the following order:
*
* <ul>
* <li>path expressions for branch by full name (with inheritance)
* <li>path expressions for branch by short name (with inheritance)
* </ul>
*
* @param pluginConfig the plugin config from which the path expressions should be read.
* @param branch the project and branch for which the configured path expressions should be read
* @return the path expressions that are configured for the given branch, {@link Optional#empty()}
* if there is no branch-specific path expressions configuration
*/
Optional<PathExpressions> getPathExpressionsForBranch(Config pluginConfig, BranchNameKey branch) {
requireNonNull(pluginConfig, "pluginConfig");
requireNonNull(branch, "branch");
// check for branch specific path expressions by full branch name
Optional<PathExpressions> pathExpressions =
getPathExpressionsForBranch(pluginConfig, branch.project(), branch.branch());
if (!pathExpressions.isPresent()) {
// check for branch specific path expressions by short branch name
pathExpressions =
getPathExpressionsForBranch(pluginConfig, branch.project(), branch.shortName());
}
return pathExpressions;
}
private Optional<PathExpressions> getPathExpressionsForBranch(
Config pluginConfig, Project.NameKey project, String branch) {
String pathExpressionsName =
pluginConfig.getString(SECTION_CODE_OWNERS, branch, KEY_PATH_EXPRESSIONS);
if (Strings.isNullOrEmpty(pathExpressionsName)) {
return Optional.empty();
}
Optional<PathExpressions> pathExpressions = PathExpressions.tryParse(pathExpressionsName);
if (!pathExpressions.isPresent()) {
logger.atWarning().log(
"Path expressions '%s' that are configured for project %s in"
+ " %s.config (parameter %s.%s.%s) not found. Falling back to default path"
+ " expressions.",
pathExpressionsName,
project,
pluginName,
SECTION_CODE_OWNERS,
branch,
KEY_PATH_EXPRESSIONS);
}
return pathExpressions;
}
/**
* Gets the path expressions that are configured for the given project.
*
* @param pluginConfig the plugin config from which the path expressions should be read.
* @param project the project for which the configured path expressions should be read
* @return the path expressions that are configured for the given project, {@link
* Optional#empty()} if there is no project-specific path expression configuration
*/
Optional<PathExpressions> getPathExpressionsForProject(
Config pluginConfig, Project.NameKey project) {
requireNonNull(pluginConfig, "pluginConfig");
requireNonNull(project, "project");
String pathExpressionsName =
pluginConfig.getString(SECTION_CODE_OWNERS, null, KEY_PATH_EXPRESSIONS);
if (Strings.isNullOrEmpty(pathExpressionsName)) {
return Optional.empty();
}
Optional<PathExpressions> pathExpressions = PathExpressions.tryParse(pathExpressionsName);
if (!pathExpressions.isPresent()) {
logger.atWarning().log(
"Path expressions '%s' that are configured for project %s in"
+ " %s.config (parameter %s.%s) not found. Falling back to default path"
+ " expressions.",
pathExpressionsName, project, pluginName, SECTION_CODE_OWNERS, KEY_PATH_EXPRESSIONS);
}
return pathExpressions;
}
/** Gets the default path expressions. */
public Optional<PathExpressions> getDefaultPathExpressions() {
return defaultPathExpressions;
}
}