| // 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; |
| } |
| } |