blob: 7bd097139805dc1b1f8358b7d16e9c5617f15b31 [file] [log] [blame]
// 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.noSuchProject;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.annotations.PluginName;
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.assistedinject.Assisted;
import java.util.Arrays;
import java.util.Set;
import org.eclipse.jgit.lib.Config;
/**
* Class to read the {@code code-owners.config} file in the {@code refs/meta/config} branch of a
* project with taking inherited config parameters from parent projects into account.
*
* <p>For inheriting config parameters from parent projects we rely on base config support in JGit's
* {@link Config} class.
*
* <p>For single-value parameters (string, boolean, enum, int, long) this means:
*
* <ul>
* <li>If a parameter is not set, it is read from the parent project.
* <li>If a parameter is set, it overrides any value that is set in the parent project.
* </ul>
*
* <p>For multi-value parameters (string list) this means:
*
* <ul>
* <li>If a parameter is not set, the values are read from the parent projects.
* <li>If any value for the parameter is set, it is added to the inherited value list (the
* inherited value list is extended).
* <li>If the exact same value is set for different projects in the line of parent projects this
* value appears multiple times in the value list (list may contain duplicates).
* <li>The inherited value list cannot be overridden (this means the inherited values cannot be
* unset/overridden).
* </ul>
*
* <p>Please note that this inheritance behavior is different from what {@link
* com.google.gerrit.server.config.PluginConfigFactory} does. {@code PluginConfigFactory} has 2
* modes:
*
* <ul>
* <li>merge = false: Inherited list values are overridden.
* <li>merge = true: Inherited list values are extended the same way as in this class, but for
* single-value parameters the inherited value from the parent project takes precedence.
* </ul>
*
* <p>For the {@code code-owners.config} we want that:
*
* <ul>
* <li>Single-value parameters override inherited settings so that they can be controlled per
* project (e.g. whether validation of OWNERS files should be done).
* <li>Multi-value parameters cannot be overridden, but only extended (e.g. this allows to enforce
* global code owners or exempted users globally).
* </ul>
*/
public class CodeOwnersPluginConfig {
public interface Factory {
CodeOwnersPluginConfig create(Project.NameKey projectName);
}
private static final String CONFIG_EXTENSION = ".config";
private final String pluginName;
private final ProjectCache projectCache;
private final Project.NameKey projectName;
private Config config;
@Inject
CodeOwnersPluginConfig(
@PluginName String pluginName,
ProjectCache projectCache,
@Assisted Project.NameKey projectName) {
this.pluginName = pluginName;
this.projectCache = projectCache;
this.projectName = projectName;
}
public Config get() {
if (config == null) {
config = load();
}
return config;
}
/**
* Load the {@code code-owners.config} file of the project and sets all parent {@code
* code-owners.config}s as base configs.
*
* @throws IllegalStateException if the project doesn't exist
*/
private Config load() {
try {
ProjectState projectState =
projectCache.get(projectName).orElseThrow(noSuchProject(projectName));
String fileName = pluginName + CONFIG_EXTENSION;
Config mergedConfig = null;
// Iterate in-order from All-Projects through the project hierarchy to this project. For each
// project read the code-owners.config and set the parent code-owners.config as base config.
for (ProjectState p : projectState.treeInOrder()) {
Config currentConfig = p.getConfig(fileName).get();
if (mergedConfig == null) {
mergedConfig = currentConfig;
} else {
mergedConfig = createConfigWithBase(currentConfig, mergedConfig);
}
}
return mergedConfig;
} catch (NoSuchProjectException e) {
throw new IllegalStateException(
String.format(
"cannot get %s plugin config for non-existing project %s", pluginName, projectName),
e);
}
}
/**
* Creates a copy of the given {@code config} with the given {@code baseConfig} as base config.
*
* <p>JGit doesn't allow to set a base config on an existing {@link Config}. Hence create a new
* (empty) config with the base config and then copy over all sections and subsection.
*
* @param config config that should be copied
* @param baseConfig config that should be set as base config
*/
private Config createConfigWithBase(Config config, Config baseConfig) {
// Create a new Config with the parent Config as base config.
Config configWithBase = new Config(baseConfig);
// Copy all sections and subsections from the given config.
for (String section : config.getSections()) {
for (String name : config.getNames(section)) {
configWithBase.setStringList(
section,
/* subsection = */ null,
name,
Arrays.asList(config.getStringList(section, /* subsection = */ null, name)));
}
for (String subsection : config.getSubsections(section)) {
Set<String> allNames = config.getNames(section, subsection);
if (allNames.isEmpty()) {
// Set empty subsection.
configWithBase.setString(section, subsection, /* name= */ null, /* value= */ null);
} else {
for (String name : allNames) {
configWithBase.setStringList(
section,
subsection,
name,
Arrays.asList(config.getStringList(section, subsection, name)));
}
}
}
}
return configWithBase;
}
}