| // 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.common.collect.ImmutableSet.toImmutableSet; |
| import static com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS; |
| import static java.util.Objects.requireNonNull; |
| |
| import com.google.common.base.Strings; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Streams; |
| 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.plugins.codeowners.backend.CodeOwnerReference; |
| import com.google.gerrit.plugins.codeowners.backend.EnableImplicitApprovals; |
| import com.google.gerrit.plugins.codeowners.backend.FallbackCodeOwners; |
| import com.google.gerrit.plugins.codeowners.common.CodeOwnerConfigValidationPolicy; |
| import com.google.gerrit.plugins.codeowners.common.MergeCommitStrategy; |
| import com.google.gerrit.server.config.PluginConfig; |
| 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.gerrit.server.project.RefPatternMatcher; |
| import com.google.inject.Inject; |
| import com.google.inject.Singleton; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.Optional; |
| import java.util.regex.PatternSyntaxException; |
| import java.util.stream.Stream; |
| import org.eclipse.jgit.lib.Config; |
| |
| /** |
| * Class to read the general code owners configuration from {@code gerrit.config} and from {@code |
| * code-owners.config} in {@code refs/meta/config}. |
| * |
| * <p>Default values are configured in {@code gerrit.config}. |
| * |
| * <p>The default values can be overridden on project-level in {@code code-owners.config} in {@code |
| * refs/meta/config}. |
| * |
| * <p>Projects that have no configuration inherit the configuration from their parent projects. |
| */ |
| @Singleton |
| public class GeneralConfig { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| public static final String SECTION_VALIDATION = "validation"; |
| |
| public static final String KEY_FILE_EXTENSION = "fileExtension"; |
| public static final String KEY_READ_ONLY = "readOnly"; |
| public static final String KEY_EXEMPT_PURE_REVERTS = "exemptPureReverts"; |
| public static final String KEY_FALLBACK_CODE_OWNERS = "fallbackCodeOwners"; |
| public static final String KEY_ENABLE_VALIDATION_ON_COMMIT_RECEIVED = |
| "enableValidationOnCommitReceived"; |
| public static final String KEY_ENABLE_VALIDATION_ON_SUBMIT = "enableValidationOnSubmit"; |
| public static final String KEY_MAX_PATHS_IN_CHANGE_MESSAGES = "maxPathsInChangeMessages"; |
| public static final String KEY_MERGE_COMMIT_STRATEGY = "mergeCommitStrategy"; |
| public static final String KEY_GLOBAL_CODE_OWNER = "globalCodeOwner"; |
| public static final String KEY_EXEMPTED_USER = "exemptedUser"; |
| public static final String KEY_ENABLE_IMPLICIT_APPROVALS = "enableImplicitApprovals"; |
| public static final String KEY_OVERRIDE_INFO_URL = "overrideInfoUrl"; |
| public static final String KEY_INVALID_CODE_OWNER_CONFIG_INFO_URL = |
| "invalidCodeOwnerConfigInfoUrl"; |
| public static final String KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS = |
| "rejectNonResolvableCodeOwners"; |
| public static final String KEY_REJECT_NON_RESOLVABLE_IMPORTS = "rejectNonResolvableImports"; |
| |
| public static final int DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES = 50; |
| |
| private static final String KEY_ALLOWED_EMAIL_DOMAIN = "allowedEmailDomain"; |
| |
| private final String pluginName; |
| private final PluginConfig pluginConfigFromGerritConfig; |
| |
| @Inject |
| GeneralConfig(@PluginName String pluginName, PluginConfigFactory pluginConfigFactory) { |
| this.pluginName = pluginName; |
| this.pluginConfigFromGerritConfig = pluginConfigFactory.getFromGerritConfig(pluginName); |
| } |
| |
| /** |
| * 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"); |
| |
| List<CommitValidationMessage> validationMessages = new ArrayList<>(); |
| |
| try { |
| projectLevelConfig.getEnum( |
| SECTION_CODE_OWNERS, |
| /* subsection= */ null, |
| KEY_MERGE_COMMIT_STRATEGY, |
| MergeCommitStrategy.ALL_CHANGED_FILES); |
| } catch (IllegalArgumentException e) { |
| validationMessages.add( |
| new CommitValidationMessage( |
| String.format( |
| "Merge commit strategy '%s' that is configured in %s (parameter %s.%s) is invalid.", |
| projectLevelConfig.getString( |
| SECTION_CODE_OWNERS, /* subsection= */ null, KEY_MERGE_COMMIT_STRATEGY), |
| fileName, |
| SECTION_CODE_OWNERS, |
| KEY_MERGE_COMMIT_STRATEGY), |
| ValidationMessage.Type.ERROR)); |
| } |
| |
| try { |
| projectLevelConfig.getEnum( |
| SECTION_CODE_OWNERS, |
| /* subsection= */ null, |
| KEY_FALLBACK_CODE_OWNERS, |
| FallbackCodeOwners.NONE); |
| } catch (IllegalArgumentException e) { |
| validationMessages.add( |
| new CommitValidationMessage( |
| String.format( |
| "The value for fallback code owners '%s' that is configured in %s (parameter %s.%s) is invalid.", |
| projectLevelConfig.getString( |
| SECTION_CODE_OWNERS, /* subsection= */ null, KEY_FALLBACK_CODE_OWNERS), |
| fileName, |
| SECTION_CODE_OWNERS, |
| KEY_FALLBACK_CODE_OWNERS), |
| ValidationMessage.Type.ERROR)); |
| } |
| |
| try { |
| projectLevelConfig.getInt( |
| SECTION_CODE_OWNERS, |
| /* subsection= */ null, |
| KEY_MAX_PATHS_IN_CHANGE_MESSAGES, |
| DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES); |
| } catch (IllegalArgumentException e) { |
| validationMessages.add( |
| new CommitValidationMessage( |
| String.format( |
| "The value for max paths in change messages '%s' that is configured in %s" |
| + " (parameter %s.%s) is invalid.", |
| projectLevelConfig.getString( |
| SECTION_CODE_OWNERS, |
| /* subsection= */ null, |
| KEY_MAX_PATHS_IN_CHANGE_MESSAGES), |
| fileName, |
| SECTION_CODE_OWNERS, |
| KEY_MAX_PATHS_IN_CHANGE_MESSAGES), |
| ValidationMessage.Type.ERROR)); |
| } |
| |
| return ImmutableList.copyOf(validationMessages); |
| } |
| |
| /** |
| * Gets the file extension that should be used for code owner config files in the given project. |
| * |
| * @param pluginConfig the plugin config from which the file extension should be read. |
| * @return the file extension that should be used for code owner config files in the given |
| * project, {@link Optional#empty()} if no file extension should be used |
| */ |
| Optional<String> getFileExtension(Config pluginConfig) { |
| return getStringValue(pluginConfig, KEY_FILE_EXTENSION); |
| } |
| |
| /** |
| * 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) |
| */ |
| ImmutableSet<String> getAllowedEmailDomains() { |
| return Arrays.stream(pluginConfigFromGerritConfig.getStringList(KEY_ALLOWED_EMAIL_DOMAIN)) |
| .filter(emailDomain -> !Strings.isNullOrEmpty(emailDomain)) |
| .distinct() |
| .collect(toImmutableSet()); |
| } |
| |
| /** |
| * Gets the read-only configuration from the given plugin config with fallback to {@code |
| * gerrit.config}. |
| * |
| * <p>The read-only configuration controls whether code owner config files are read-only and all |
| * modifications of code owner config files should be rejected. |
| * |
| * @param project the project for which the read-only configuration should be read |
| * @param pluginConfig the plugin config from which the read-only configuration should be read. |
| * @return whether code owner config files are read-only |
| */ |
| boolean getReadOnly(Project.NameKey project, Config pluginConfig) { |
| return getBooleanConfig(project, pluginConfig, KEY_READ_ONLY, /* defaultValue= */ false); |
| } |
| |
| /** |
| * Gets the exempt-pure-reverts configuration from the given plugin config with fallback to {@code |
| * gerrit.config}. |
| * |
| * <p>The exempt-pure-reverts configuration controls whether pure revert changes are exempted from |
| * needing code owner approvals for submit. |
| * |
| * @param project the project for which the exempt-pure-revert configuration should be read |
| * @param pluginConfig the plugin config from which the read-only configuration should be read. |
| * @return whether pure reverts are exempted from needing code owner approvals for submit |
| */ |
| boolean getExemptPureReverts(Project.NameKey project, Config pluginConfig) { |
| return getBooleanConfig( |
| project, pluginConfig, KEY_EXEMPT_PURE_REVERTS, /* defaultValue= */ false); |
| } |
| |
| /** |
| * Gets the reject-non-resolvable-code-owners configuration from the given plugin config for the |
| * specified project with fallback to {@code gerrit.config}. |
| * |
| * <p>The reject-non-resolvable-code-owners configuration controls whether code owner config files |
| * with newly added non-resolvable code owners should be rejected on commit received and on |
| * submit. |
| * |
| * @param project the project for which the reject-non-resolvable-code-owners configuration should |
| * be read |
| * @param pluginConfig the plugin config from which the reject-non-resolvable-code-owners |
| * configuration should be read. |
| * @return whether code owner config files with newly added non-resolvable code owners should be |
| * rejected on commit received and on submit |
| */ |
| boolean getRejectNonResolvableCodeOwners(Project.NameKey project, Config pluginConfig) { |
| return getBooleanConfig( |
| project, pluginConfig, KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS, /* defaultValue= */ true); |
| } |
| |
| /** |
| * Gets the reject-non-resolvable-code-owners configuration from the given plugin config for the |
| * specified branch with fallback to {@code gerrit.config}. |
| * |
| * <p>If multiple branch-specific configurations match the specified branch, it is undefined which |
| * of the matching branch configurations takes precedence. |
| * |
| * <p>The reject-non-resolvable-code-owners configuration controls whether code owner config files |
| * with newly added non-resolvable code owners should be rejected on commit received and on |
| * submit. |
| * |
| * @param branchNameKey the branch and project for which the reject-non-resolvable-code-owners |
| * configuration should be read |
| * @param pluginConfig the plugin config from which the reject-non-resolvable-code-owners |
| * configuration should be read. |
| * @return whether code owner config files with newly added non-resolvable code owners should be |
| * rejected on commit received and on submit |
| */ |
| Optional<Boolean> getRejectNonResolvableCodeOwnersForBranch( |
| BranchNameKey branchNameKey, Config pluginConfig) { |
| return getCodeOwnerConfigValidationFlagForBranch( |
| KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS, branchNameKey, pluginConfig); |
| } |
| |
| /** |
| * Gets the reject-non-resolvable-imports configuration from the given plugin config for the |
| * specified project with fallback to {@code gerrit.config}. |
| * |
| * <p>The reject-non-resolvable-imports configuration controls whether code owner config files |
| * with newly added non-resolvable imports should be rejected on commit received and on submit. |
| * |
| * @param project the project for which the reject-non-resolvable-imports configuration should be |
| * read |
| * @param pluginConfig the plugin config from which the reject-non-resolvable-imports |
| * configuration should be read. |
| * @return whether code owner config files with newly added non-resolvable imports should be |
| * rejected on commit received and on submit |
| */ |
| boolean getRejectNonResolvableImports(Project.NameKey project, Config pluginConfig) { |
| return getBooleanConfig( |
| project, pluginConfig, KEY_REJECT_NON_RESOLVABLE_IMPORTS, /* defaultValue= */ true); |
| } |
| |
| /** |
| * Gets the reject-non-resolvable-imports configuration from the given plugin config for the |
| * specified branch with fallback to {@code gerrit.config}. |
| * |
| * <p>The reject-non-resolvable-imports configuration controls whether code owner config files |
| * with newly added non-resolvable imports should be rejected on commit received and on submit. |
| * |
| * @param branchNameKey the branch and project for which the reject-non-resolvable-imports |
| * configuration should be read |
| * @param pluginConfig the plugin config from which the reject-non-resolvable-imports |
| * configuration should be read. |
| * @return whether code owner config files with newly added non-resolvable imports should be |
| * rejected on commit received and on submit |
| */ |
| Optional<Boolean> getRejectNonResolvableImportsForBranch( |
| BranchNameKey branchNameKey, Config pluginConfig) { |
| return getCodeOwnerConfigValidationFlagForBranch( |
| KEY_REJECT_NON_RESOLVABLE_IMPORTS, branchNameKey, pluginConfig); |
| } |
| |
| private boolean getBooleanConfig( |
| Project.NameKey project, Config pluginConfig, String key, boolean defaultValue) { |
| requireNonNull(project, "project"); |
| requireNonNull(pluginConfig, "pluginConfig"); |
| requireNonNull(key, "key"); |
| |
| String value = pluginConfig.getString(SECTION_CODE_OWNERS, /* subsection= */ null, key); |
| if (value != null) { |
| try { |
| return pluginConfig.getBoolean( |
| SECTION_CODE_OWNERS, /* subsection= */ null, key, defaultValue); |
| } catch (IllegalArgumentException e) { |
| logger.atWarning().withCause(e).log( |
| "Ignoring invalid value %s for %s in '%s.config' of project %s." |
| + " Falling back to global config.", |
| value, key, pluginName, project.get()); |
| } |
| } |
| |
| try { |
| return pluginConfigFromGerritConfig.getBoolean(key, defaultValue); |
| } catch (IllegalArgumentException e) { |
| logger.atWarning().withCause(e).log( |
| "Ignoring invalid value %s for %s in gerrit.config (parameter" |
| + " plugin.%s.%s). Falling back to default value %s.", |
| pluginConfigFromGerritConfig.getString(key), key, pluginName, key, defaultValue); |
| return defaultValue; |
| } |
| } |
| |
| /** |
| * Gets the fallback code owners that own paths that have no defined code owners. |
| * |
| * @param project the project for which the fallback code owners should be read |
| * @param pluginConfig the plugin config from which the fallback code owners should be read |
| * @return the fallback code owners that own paths that have no defined code owners |
| */ |
| FallbackCodeOwners getFallbackCodeOwners(Project.NameKey project, Config pluginConfig) { |
| requireNonNull(project, "project"); |
| requireNonNull(pluginConfig, "pluginConfig"); |
| |
| String fallbackCodeOwnersString = |
| pluginConfig.getString( |
| SECTION_CODE_OWNERS, /* subsection= */ null, KEY_FALLBACK_CODE_OWNERS); |
| if (fallbackCodeOwnersString != null) { |
| try { |
| return pluginConfig.getEnum( |
| SECTION_CODE_OWNERS, |
| /* subsection= */ null, |
| KEY_FALLBACK_CODE_OWNERS, |
| FallbackCodeOwners.NONE); |
| } catch (IllegalArgumentException e) { |
| logger.atWarning().withCause(e).log( |
| "Ignoring invalid value %s for fallback code owners in '%s.config' of project %s." |
| + " Falling back to global config.", |
| fallbackCodeOwnersString, pluginName, project.get()); |
| } |
| } |
| |
| try { |
| return pluginConfigFromGerritConfig.getEnum( |
| KEY_FALLBACK_CODE_OWNERS, FallbackCodeOwners.NONE); |
| } catch (IllegalArgumentException e) { |
| logger.atWarning().withCause(e).log( |
| "Ignoring invalid value %s for fallback code owners in gerrit.config (parameter" |
| + " plugin.%s.%s). Falling back to default value %s.", |
| pluginConfigFromGerritConfig.getString(KEY_FALLBACK_CODE_OWNERS), |
| pluginName, |
| KEY_FALLBACK_CODE_OWNERS, |
| FallbackCodeOwners.NONE); |
| return FallbackCodeOwners.NONE; |
| } |
| } |
| |
| /** |
| * Gets the maximum number of paths that should be incuded in change messages. |
| * |
| * @param project the project for which the maximum number of paths in change messages should be |
| * read |
| * @param pluginConfig the plugin config from which the maximum number of paths in change messages |
| * should be read |
| * @return the maximum number of paths in change messages |
| */ |
| int getMaxPathsInChangeMessages(Project.NameKey project, Config pluginConfig) { |
| requireNonNull(project, "project"); |
| requireNonNull(pluginConfig, "pluginConfig"); |
| |
| String maxPathInChangeMessagesString = |
| pluginConfig.getString( |
| SECTION_CODE_OWNERS, /* subsection= */ null, KEY_MAX_PATHS_IN_CHANGE_MESSAGES); |
| if (maxPathInChangeMessagesString != null) { |
| try { |
| return pluginConfig.getInt( |
| SECTION_CODE_OWNERS, |
| /* subsection= */ null, |
| KEY_MAX_PATHS_IN_CHANGE_MESSAGES, |
| DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES); |
| } catch (IllegalArgumentException e) { |
| logger.atWarning().withCause(e).log( |
| "Ignoring invalid value %s for max paths in change messages in '%s.config' of" |
| + " project %s. Falling back to global config.", |
| maxPathInChangeMessagesString, pluginName, project.get()); |
| } |
| } |
| |
| try { |
| return pluginConfigFromGerritConfig.getInt( |
| KEY_MAX_PATHS_IN_CHANGE_MESSAGES, DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES); |
| } catch (IllegalArgumentException e) { |
| logger.atWarning().withCause(e).log( |
| "Ignoring invalid value %s for max paths in change messages in gerrit.config (parameter" |
| + " plugin.%s.%s). Falling back to default value %s.", |
| pluginConfigFromGerritConfig.getString(KEY_MAX_PATHS_IN_CHANGE_MESSAGES), |
| pluginName, |
| KEY_MAX_PATHS_IN_CHANGE_MESSAGES, |
| DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES); |
| return DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES; |
| } |
| } |
| |
| /** |
| * Gets the enable validation on commit received configuration from the given plugin config for |
| * the specified project with fallback to {@code gerrit.config} and default to {@code true}. |
| * |
| * <p>The enable validation on commit received controls whether code owner config files should be |
| * validated when a commit is received. |
| * |
| * @param project the project for which the enable validation on commit received configuration |
| * should be read |
| * @param pluginConfig the plugin config from which the enable validation on commit received |
| * configuration should be read |
| * @return whether code owner config files should be validated when a commit is received |
| */ |
| CodeOwnerConfigValidationPolicy getCodeOwnerConfigValidationPolicyForCommitReceived( |
| Project.NameKey project, Config pluginConfig) { |
| return getCodeOwnerConfigValidationPolicy( |
| KEY_ENABLE_VALIDATION_ON_COMMIT_RECEIVED, |
| project, |
| pluginConfig, |
| CodeOwnerConfigValidationPolicy.TRUE); |
| } |
| |
| /** |
| * Gets the enable validation on commit received configuration from the given plugin config for |
| * the specified branch. |
| * |
| * <p>If multiple branch-specific configurations match the specified branch, it is undefined which |
| * of the matching branch configurations takes precedence. |
| * |
| * <p>The enable validation on commit received controls whether code owner config files should be |
| * validated when a commit is received. |
| * |
| * @param branchNameKey the branch and project for which the enable validation on commit received |
| * configuration should be read |
| * @param pluginConfig the plugin config from which the enable validation on commit received |
| * configuration should be read |
| * @return the enable validation on commit received configuration that is configured for the |
| * branch, {@link Optional#empty()} if no branch specific configuration exists |
| */ |
| Optional<CodeOwnerConfigValidationPolicy> |
| getCodeOwnerConfigValidationPolicyForCommitReceivedForBranch( |
| BranchNameKey branchNameKey, Config pluginConfig) { |
| return getCodeOwnerConfigValidationPolicyForBranch( |
| KEY_ENABLE_VALIDATION_ON_COMMIT_RECEIVED, branchNameKey, pluginConfig); |
| } |
| |
| /** |
| * Gets the enable validation on submit configuration from the given plugin config for the |
| * specified project with fallback to {@code gerrit.config} and default to {@code true}. |
| * |
| * <p>The enable validation on submit controls whether code owner config files should be validated |
| * when a change is submitted. |
| * |
| * @param project the project for which the enable validation on submit configuration should be |
| * read |
| * @param pluginConfig the plugin config from which the enable validation on submit configuration |
| * should be read |
| * @return whether code owner config files should be validated when a change is submitted |
| */ |
| CodeOwnerConfigValidationPolicy getCodeOwnerConfigValidationPolicyForSubmit( |
| Project.NameKey project, Config pluginConfig) { |
| return getCodeOwnerConfigValidationPolicy( |
| KEY_ENABLE_VALIDATION_ON_SUBMIT, |
| project, |
| pluginConfig, |
| CodeOwnerConfigValidationPolicy.FALSE); |
| } |
| |
| /** |
| * Gets the enable validation on submit configuration from the given plugin config for the |
| * specified branch. |
| * |
| * <p>If multiple branch-specific configurations match the specified branch, it is undefined which |
| * of the matching branch configurations takes precedence. |
| * |
| * <p>The enable validation on submit controls whether code owner config files should be validated |
| * when a change is submitted. |
| * |
| * @param branchNameKey the branch and project for which the enable validation on submit |
| * configuration should be read |
| * @param pluginConfig the plugin config from which the enable validation on submit configuration |
| * should be read |
| * @return the enable validation on submit configuration that is configured for the branch, {@link |
| * Optional#empty()} if no branch specific configuration exists |
| */ |
| Optional<CodeOwnerConfigValidationPolicy> getCodeOwnerConfigValidationPolicyForSubmitForBranch( |
| BranchNameKey branchNameKey, Config pluginConfig) { |
| return getCodeOwnerConfigValidationPolicyForBranch( |
| KEY_ENABLE_VALIDATION_ON_SUBMIT, branchNameKey, pluginConfig); |
| } |
| |
| private Optional<CodeOwnerConfigValidationPolicy> getCodeOwnerConfigValidationPolicyForBranch( |
| String key, BranchNameKey branchNameKey, Config pluginConfig) { |
| requireNonNull(key, "key"); |
| requireNonNull(branchNameKey, "branchNameKey"); |
| requireNonNull(pluginConfig, "pluginConfig"); |
| |
| Optional<String> validationSectionForBranch = |
| getValidationSectionForBranch(branchNameKey, pluginConfig); |
| if (!validationSectionForBranch.isPresent()) { |
| return Optional.empty(); |
| } |
| |
| return getCodeOwnerConfigValidationPolicyForBranch( |
| validationSectionForBranch.get(), key, branchNameKey.project(), pluginConfig); |
| } |
| |
| private Optional<Boolean> getCodeOwnerConfigValidationFlagForBranch( |
| String key, BranchNameKey branchNameKey, Config pluginConfig) { |
| requireNonNull(key, "key"); |
| requireNonNull(branchNameKey, "branchNameKey"); |
| requireNonNull(pluginConfig, "pluginConfig"); |
| |
| Optional<String> validationSectionForBranch = |
| getValidationSectionForBranch(branchNameKey, pluginConfig); |
| if (!validationSectionForBranch.isPresent()) { |
| return Optional.empty(); |
| } |
| |
| return getCodeOwnerConfigValidationFlagForBranch( |
| validationSectionForBranch.get(), key, branchNameKey.project(), pluginConfig); |
| } |
| |
| private CodeOwnerConfigValidationPolicy getCodeOwnerConfigValidationPolicy( |
| String key, |
| Project.NameKey project, |
| Config pluginConfig, |
| CodeOwnerConfigValidationPolicy defaultValue) { |
| requireNonNull(key, "key"); |
| requireNonNull(project, "project"); |
| requireNonNull(pluginConfig, "pluginConfig"); |
| |
| String codeOwnerConfigValidationPolicyString = |
| pluginConfig.getString(SECTION_CODE_OWNERS, /* subsection= */ null, key); |
| if (codeOwnerConfigValidationPolicyString != null) { |
| try { |
| return pluginConfig.getEnum(SECTION_CODE_OWNERS, /* subsection= */ null, key, defaultValue); |
| } catch (IllegalArgumentException e) { |
| logger.atWarning().withCause(e).log( |
| "Ignoring invalid value %s for the code owner config validation policy in '%s.config'" |
| + " of project %s (parameter %s.%s). Falling back to global config.", |
| codeOwnerConfigValidationPolicyString, |
| pluginName, |
| project.get(), |
| SECTION_CODE_OWNERS, |
| key); |
| } |
| } |
| |
| try { |
| return pluginConfigFromGerritConfig.getEnum(key, defaultValue); |
| } catch (IllegalArgumentException e) { |
| logger.atWarning().withCause(e).log( |
| "Ignoring invalid value %s for the code owner config validation policy in gerrit.config" |
| + " (parameter plugin.%s.%s). Falling back to default value %s.", |
| pluginConfigFromGerritConfig.getString(key), pluginName, key, defaultValue); |
| return defaultValue; |
| } |
| } |
| |
| private Optional<String> getValidationSectionForBranch( |
| BranchNameKey branchNameKey, Config pluginConfig) { |
| ImmutableSet<String> matchingValidationSubsections = |
| pluginConfig.getSubsections(SECTION_VALIDATION).stream() |
| .filter( |
| refPattern -> { |
| try { |
| return RefPatternMatcher.getMatcher(refPattern) |
| .match(branchNameKey.branch(), /* user= */ null); |
| } catch (PatternSyntaxException e) { |
| logger.atWarning().withCause(e).log( |
| "invalid ref pattern %s for subsection %s.%s in %s.config of project %s", |
| refPattern, |
| SECTION_VALIDATION, |
| refPattern, |
| pluginName, |
| branchNameKey.project()); |
| return false; |
| } |
| }) |
| .collect(toImmutableSet()); |
| |
| if (matchingValidationSubsections.isEmpty()) { |
| return Optional.empty(); |
| } |
| |
| String matchingValidationSubsection = matchingValidationSubsections.asList().get(0); |
| if (matchingValidationSubsections.size() > 1) { |
| logger.atWarning().log( |
| "branch %s matches multiple %s subsections in %s.config of project %s: %s," |
| + " subsection %s takes precedence", |
| branchNameKey.branch(), |
| SECTION_VALIDATION, |
| pluginName, |
| branchNameKey.project(), |
| matchingValidationSubsections, |
| matchingValidationSubsection); |
| } |
| return Optional.of(matchingValidationSubsection); |
| } |
| |
| private Optional<CodeOwnerConfigValidationPolicy> getCodeOwnerConfigValidationPolicyForBranch( |
| String branchSubsection, String key, Project.NameKey project, Config pluginConfig) { |
| requireNonNull(branchSubsection, "branchSubsection"); |
| requireNonNull(key, "key"); |
| requireNonNull(project, "project"); |
| requireNonNull(pluginConfig, "pluginConfig"); |
| |
| String codeOwnerConfigValidationPolicyString = |
| pluginConfig.getString(SECTION_VALIDATION, branchSubsection, key); |
| if (codeOwnerConfigValidationPolicyString != null) { |
| try { |
| return Optional.of( |
| pluginConfig.getEnum( |
| SECTION_VALIDATION, branchSubsection, key, CodeOwnerConfigValidationPolicy.TRUE)); |
| } catch (IllegalArgumentException e) { |
| logger.atWarning().withCause(e).log( |
| "Ignoring invalid value %s for the code owner config validation policy in '%s.config'" |
| + " of project %s (parameter %s.%s.%s). Falling back to project-level setting.", |
| codeOwnerConfigValidationPolicyString, |
| pluginName, |
| project.get(), |
| SECTION_VALIDATION, |
| branchSubsection, |
| key); |
| } |
| } |
| return Optional.empty(); |
| } |
| |
| private Optional<Boolean> getCodeOwnerConfigValidationFlagForBranch( |
| String branchSubsection, String key, Project.NameKey project, Config pluginConfig) { |
| requireNonNull(branchSubsection, "branchSubsection"); |
| requireNonNull(key, "key"); |
| requireNonNull(project, "project"); |
| requireNonNull(pluginConfig, "pluginConfig"); |
| |
| String codeOwnerConfigValidationFlagString = |
| pluginConfig.getString(SECTION_VALIDATION, branchSubsection, key); |
| if (codeOwnerConfigValidationFlagString != null) { |
| try { |
| return Optional.of( |
| pluginConfig.getBoolean(SECTION_VALIDATION, branchSubsection, key, true)); |
| } catch (IllegalArgumentException e) { |
| logger.atWarning().withCause(e).log( |
| "Ignoring invalid value %s for %s.%s.%s in '%s.config' of project %s." |
| + " Falling back to project-level setting.", |
| codeOwnerConfigValidationFlagString, |
| SECTION_VALIDATION, |
| branchSubsection, |
| key, |
| pluginName, |
| project.get()); |
| } |
| } |
| return Optional.empty(); |
| } |
| |
| /** |
| * Gets the merge commit strategy from the given plugin config with fallback to {@code |
| * gerrit.config}. |
| * |
| * <p>The merge commit strategy defines for merge commits which files require code owner |
| * approvals. |
| * |
| * @param project the name of the project for which the merge commit strategy should be read |
| * @param pluginConfig the plugin config from which the merge commit strategy should be read |
| * @return the merge commit strategy that should be used |
| */ |
| MergeCommitStrategy getMergeCommitStrategy(Project.NameKey project, Config pluginConfig) { |
| requireNonNull(project, "project"); |
| requireNonNull(pluginConfig, "pluginConfig"); |
| |
| String mergeCommitStrategyString = |
| pluginConfig.getString( |
| SECTION_CODE_OWNERS, /* subsection= */ null, KEY_MERGE_COMMIT_STRATEGY); |
| if (mergeCommitStrategyString != null) { |
| try { |
| return pluginConfig.getEnum( |
| SECTION_CODE_OWNERS, |
| /* subsection= */ null, |
| KEY_MERGE_COMMIT_STRATEGY, |
| MergeCommitStrategy.ALL_CHANGED_FILES); |
| } catch (IllegalArgumentException e) { |
| logger.atWarning().withCause(e).log( |
| "Ignoring invalid value %s for merge commit stategy in '%s.config' of project %s." |
| + " Falling back to global config or default value.", |
| mergeCommitStrategyString, pluginName, project.get()); |
| } |
| } |
| |
| try { |
| return pluginConfigFromGerritConfig.getEnum( |
| KEY_MERGE_COMMIT_STRATEGY, MergeCommitStrategy.ALL_CHANGED_FILES); |
| } catch (IllegalArgumentException e) { |
| logger.atWarning().withCause(e).log( |
| "Ignoring invalid value %s for merge commit stategy in gerrit.config (parameter plugin.%s.%s)." |
| + " Falling back to default value %s.", |
| pluginConfigFromGerritConfig.getString(KEY_MERGE_COMMIT_STRATEGY), |
| pluginName, |
| KEY_MERGE_COMMIT_STRATEGY, |
| MergeCommitStrategy.ALL_CHANGED_FILES); |
| return MergeCommitStrategy.ALL_CHANGED_FILES; |
| } |
| } |
| |
| /** |
| * Gets whether an implicit code owner approvals are enabled from the given plugin config with |
| * fallback to {@code gerrit.config}. |
| * |
| * <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. |
| * |
| * @param project the name of the project for which the configuration should be read |
| * @param pluginConfig the plugin config from which the configuration should be read. |
| * @return whether an implicit code owner approval from the last uploader is assumed |
| */ |
| EnableImplicitApprovals getEnableImplicitApprovals(Project.NameKey project, Config pluginConfig) { |
| requireNonNull(project, "project"); |
| requireNonNull(pluginConfig, "pluginConfig"); |
| |
| String enableImplicitApprovalsString = |
| pluginConfig.getString( |
| SECTION_CODE_OWNERS, /* subsection= */ null, KEY_ENABLE_IMPLICIT_APPROVALS); |
| if (enableImplicitApprovalsString != null) { |
| try { |
| return pluginConfig.getEnum( |
| SECTION_CODE_OWNERS, |
| /* subsection= */ null, |
| KEY_ENABLE_IMPLICIT_APPROVALS, |
| EnableImplicitApprovals.FALSE); |
| } catch (IllegalArgumentException e) { |
| logger.atWarning().withCause(e).log( |
| "Ignoring invalid value %s for enabling implicit approvals in '%s.config' of project" |
| + " %s. Falling back to global config or default value.", |
| enableImplicitApprovalsString, pluginName, project.get()); |
| } |
| } |
| |
| try { |
| return pluginConfigFromGerritConfig.getEnum( |
| KEY_ENABLE_IMPLICIT_APPROVALS, EnableImplicitApprovals.FALSE); |
| } catch (IllegalArgumentException e) { |
| logger.atWarning().withCause(e).log( |
| "Ignoring invalid value %s for enabling implict approvals in gerrit.config (parameter" |
| + " plugin.%s.%s). Falling back to default value %s.", |
| pluginConfigFromGerritConfig.getString(KEY_ENABLE_IMPLICIT_APPROVALS), |
| pluginName, |
| KEY_ENABLE_IMPLICIT_APPROVALS, |
| EnableImplicitApprovals.FALSE); |
| return EnableImplicitApprovals.FALSE; |
| } |
| } |
| |
| /** |
| * Gets the users which are configured as global code owners from the given plugin config with |
| * fallback to {@code gerrit.config}. |
| * |
| * @param pluginConfig the plugin config from which the global code owners should be read. |
| * @return the users which are configured as global code owners |
| */ |
| ImmutableSet<CodeOwnerReference> getGlobalCodeOwners(Config pluginConfig) { |
| requireNonNull(pluginConfig, "pluginConfig"); |
| return getMultiValue(pluginConfig, KEY_GLOBAL_CODE_OWNER) |
| .map(CodeOwnerReference::create) |
| .collect(toImmutableSet()); |
| } |
| |
| /** |
| * Gets the users which are exempted from requiring code owner approvals. |
| * |
| * <p>If a user is exempted from requiring code owner approvals changes that are uploaded by this |
| * user are automatically code-owner approved. |
| * |
| * @param pluginConfig the plugin config from which the exempted users should be read. |
| * @return the users which are exempted from requiring code owner approvals |
| */ |
| ImmutableSet<String> getExemptedUsers(Config pluginConfig) { |
| requireNonNull(pluginConfig, "pluginConfig"); |
| return getMultiValue(pluginConfig, KEY_EXEMPTED_USER).collect(toImmutableSet()); |
| } |
| |
| /** |
| * Gets an URL that leads to an information page about overrides. |
| * |
| * <p>The URL is retrieved from the given plugin config, with fallback to the {@code |
| * gerrit.config}. |
| * |
| * @param pluginConfig the plugin config from which the override info URL should be read. |
| * @return URL that leads to an information page about overrides, {@link Optional#empty()} if no |
| * such URL is configured |
| */ |
| Optional<String> getOverrideInfoUrl(Config pluginConfig) { |
| return getStringValue(pluginConfig, KEY_OVERRIDE_INFO_URL); |
| } |
| |
| /** |
| * Gets an URL that leads to an information page about invalid code owner config files. |
| * |
| * <p>The URL is retrieved from the given plugin config, with fallback to the {@code |
| * gerrit.config}. |
| * |
| * @param pluginConfig the plugin config from which the invalid code owner config info URL should |
| * be read. |
| * @return URL that leads to an information page about invalid code owner config files, {@link |
| * Optional#empty()} if no such URL is configured |
| */ |
| Optional<String> getInvalidCodeOwnerConfigInfoUrl(Config pluginConfig) { |
| return getStringValue(pluginConfig, KEY_INVALID_CODE_OWNER_CONFIG_INFO_URL); |
| } |
| |
| private Optional<String> getStringValue(Config pluginConfig, String key) { |
| requireNonNull(pluginConfig, "pluginConfig"); |
| |
| String value = pluginConfig.getString(SECTION_CODE_OWNERS, /* subsection= */ null, key); |
| if (value != null) { |
| return Optional.of(value); |
| } |
| |
| return Optional.ofNullable(pluginConfigFromGerritConfig.getString(key)); |
| } |
| |
| /** |
| * Gets the values for a parameter that can be set multiple times with taking inherited values |
| * from {@code gerrit.config} into account. |
| * |
| * <p>The inherited values from {@code gerrit.config} are included into the returned list at the |
| * first position. This matches the behavior in {@link Config#getStringList(String, String, |
| * String)} that includes inherited values from the base config into the result list at the first |
| * position too. |
| * |
| * <p>The returned stream contains duplicates if the exact same value is set for different |
| * projects in the line of parent projects. |
| */ |
| private Stream<String> getMultiValue(Config pluginConfig, String key) { |
| return Streams.concat( |
| Arrays.stream(pluginConfigFromGerritConfig.getStringList(key)), |
| Arrays.stream( |
| pluginConfig.getStringList(SECTION_CODE_OWNERS, /* subsection= */ null, key))) |
| .filter(Objects::nonNull) |
| .filter(value -> !value.trim().isEmpty()); |
| } |
| } |