All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.project.LabelConfigValidator Maven / Gradle / Ivy

The newest version!
// Copyright (C) 2022 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.server.project;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.LabelFunction;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.client.ChangeKind;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.git.meta.VersionedConfigFile;
import com.google.gerrit.server.git.validators.CommitValidationException;
import com.google.gerrit.server.git.validators.CommitValidationListener;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.gerrit.server.git.validators.ValidationMessage;
import com.google.gerrit.server.patch.DiffNotAvailableException;
import com.google.gerrit.server.patch.gitdiff.ModifiedFile;
import com.google.gerrit.server.query.approval.ApprovalQueryBuilder;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;

/**
 * Validates modifications to label configurations in the {@code project.config} file that is stored
 * in {@code refs/meta/config}.
 *
 * 

Rejects setting/changing deprecated fields that are no longer supported (fields {@code * copyAnyScore}, {@code copyMinScore}, {@code copyMaxScore}, {@code copyAllScoresIfNoChange}, * {@code copyAllScoresIfNoCodeChange}, {@code copyAllScoresOnMergeFirstParentUpdate}, {@code * copyAllScoresOnTrivialRebase}, {@code copyAllScoresIfListOfFilesDidNotChange}, {@code * copyValue}). * *

Updates that unset the deprecated fields or that don't touch them are allowed. */ @Singleton public class LabelConfigValidator implements CommitValidationListener { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); @VisibleForTesting public static final String KEY_COPY_ANY_SCORE = "copyAnyScore"; @VisibleForTesting public static final String KEY_COPY_MIN_SCORE = "copyMinScore"; @VisibleForTesting public static final String KEY_COPY_MAX_SCORE = "copyMaxScore"; @VisibleForTesting public static final String KEY_COPY_VALUE = "copyValue"; @VisibleForTesting public static final String KEY_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE = "copyAllScoresOnMergeFirstParentUpdate"; @VisibleForTesting public static final String KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE = "copyAllScoresOnTrivialRebase"; @VisibleForTesting public static final String KEY_COPY_ALL_SCORES_IF_NO_CODE_CHANGE = "copyAllScoresIfNoCodeChange"; @VisibleForTesting public static final String KEY_COPY_ALL_SCORES_IF_NO_CHANGE = "copyAllScoresIfNoChange"; @VisibleForTesting public static final String KEY_COPY_ALL_SCORES_IF_LIST_OF_FILES_DID_NOT_CHANGE = "copyAllScoresIfListOfFilesDidNotChange"; // Map of deprecated boolean flags to the predicates that should be used in the copy condition // instead. private static final ImmutableMap DEPRECATED_FLAGS = ImmutableMap.builder() .put(KEY_COPY_ANY_SCORE, "is:ANY") .put(KEY_COPY_MIN_SCORE, "is:MIN") .put(KEY_COPY_MAX_SCORE, "is:MAX") .put(KEY_COPY_ALL_SCORES_IF_NO_CHANGE, "changekind:" + ChangeKind.NO_CHANGE.name()) .put( KEY_COPY_ALL_SCORES_IF_NO_CODE_CHANGE, "changekind:" + ChangeKind.NO_CODE_CHANGE.name()) .put( KEY_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE, "changekind:" + ChangeKind.MERGE_FIRST_PARENT_UPDATE.name()) .put( KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE, "changekind:" + ChangeKind.TRIVIAL_REBASE.name()) .put(KEY_COPY_ALL_SCORES_IF_LIST_OF_FILES_DID_NOT_CHANGE, "has:unchanged-files") .build(); private final ApprovalQueryBuilder approvalQueryBuilder; public LabelConfigValidator(ApprovalQueryBuilder approvalQueryBuilder) { this.approvalQueryBuilder = approvalQueryBuilder; } @Override public List onCommitReceived(CommitReceivedEvent receiveEvent) throws CommitValidationException { try { if (!receiveEvent.refName.equals(RefNames.REFS_CONFIG) || !isFileChanged(receiveEvent, ProjectConfig.PROJECT_CONFIG)) { // The project.config file in refs/meta/config was not modified, hence we do not need to do // any validation and can return early. return ImmutableList.of(); } ImmutableList.Builder validationMessageBuilder = ImmutableList.builder(); // Load the new config Config newConfig; try { newConfig = loadNewConfig(receiveEvent); } catch (ConfigInvalidException e) { // The current config is invalid, hence we cannot inspect the delta. // Rejecting invalid configs is not the responsibility of this validator, hence ignore this // exception here. logger.atWarning().log( "cannot inspect the project config, because parsing %s from revision %s" + " in project %s failed: %s", ProjectConfig.PROJECT_CONFIG, receiveEvent.commit.name(), receiveEvent.getProjectNameKey(), e.getMessage()); return ImmutableList.of(); } // Load the old config @Nullable Config oldConfig = loadOldConfig(receiveEvent).orElse(null); for (String labelName : newConfig.getSubsections(ProjectConfig.LABEL)) { rejectSettingDeprecatedFlags(newConfig, oldConfig, labelName, validationMessageBuilder); rejectSettingCopyValues(newConfig, oldConfig, labelName, validationMessageBuilder); rejectSettingBlockingFunction(newConfig, oldConfig, labelName, validationMessageBuilder); rejectDeletingFunction(newConfig, oldConfig, labelName, validationMessageBuilder); rejectNonParseableCopyCondition(newConfig, oldConfig, labelName, validationMessageBuilder); } ImmutableList validationMessages = validationMessageBuilder.build(); if (validationMessages.stream().anyMatch(CommitValidationMessage::isError)) { throw new CommitValidationException( String.format( "invalid %s file in revision %s", ProjectConfig.PROJECT_CONFIG, receiveEvent.commit.getName()), validationMessages); } return validationMessages; } catch (IOException | DiffNotAvailableException e) { String errorMessage = String.format( "failed to validate file %s for revision %s in ref %s of project %s", ProjectConfig.PROJECT_CONFIG, receiveEvent.commit.getName(), RefNames.REFS_CONFIG, receiveEvent.getProjectNameKey()); logger.atSevere().withCause(e).log("%s", errorMessage); throw new CommitValidationException(errorMessage, e); } } private void rejectSettingDeprecatedFlags( Config newConfig, @Nullable Config oldConfig, String labelName, ImmutableList.Builder validationMessageBuilder) { for (String deprecatedFlag : DEPRECATED_FLAGS.keySet()) { if (flagChangedOrNewlySet(newConfig, oldConfig, labelName, deprecatedFlag)) { validationMessageBuilder.add( new CommitValidationMessage( String.format( "Parameter '%s.%s.%s' is deprecated and cannot be set," + " use '%s' in '%s.%s.%s' instead.", ProjectConfig.LABEL, labelName, deprecatedFlag, DEPRECATED_FLAGS.get(deprecatedFlag), ProjectConfig.LABEL, labelName, ProjectConfig.KEY_COPY_CONDITION), ValidationMessage.Type.ERROR)); } } } private void rejectSettingCopyValues( Config newConfig, @Nullable Config oldConfig, String labelName, ImmutableList.Builder validationMessageBuilder) { if (copyValuesChangedOrNewlySet(newConfig, oldConfig, labelName)) { validationMessageBuilder.add( new CommitValidationMessage( String.format( "Parameter '%s.%s.%s' is deprecated and cannot be set," + " use 'is:' in '%s.%s.%s' instead.", ProjectConfig.LABEL, labelName, KEY_COPY_VALUE, ProjectConfig.LABEL, labelName, ProjectConfig.KEY_COPY_CONDITION), ValidationMessage.Type.ERROR)); } } private void rejectSettingBlockingFunction( Config newConfig, @Nullable Config oldConfig, String labelName, ImmutableList.Builder validationMessageBuilder) { if (flagChangedOrNewlySet(newConfig, oldConfig, labelName, ProjectConfig.KEY_FUNCTION)) { String fnName = newConfig.getString(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_FUNCTION); Optional labelFn = LabelFunction.parse(fnName); if (labelFn.isPresent() && !isLabelFunctionAllowed(labelFn.get())) { validationMessageBuilder.add( new CommitValidationMessage( String.format( "Value '%s' of '%s.%s.%s' is not allowed and cannot be set." + " Label functions can only be set to {%s, %s, %s}." + " Use submit requirements instead of label functions.", fnName, ProjectConfig.LABEL, labelName, ProjectConfig.KEY_FUNCTION, LabelFunction.NO_BLOCK, LabelFunction.NO_OP, LabelFunction.PATCH_SET_LOCK), ValidationMessage.Type.ERROR)); } } } private void rejectDeletingFunction( Config newConfig, @Nullable Config oldConfig, String labelName, ImmutableList.Builder validationMessageBuilder) { // Ban deletion of label function since the default is MaxWithBlock which is deprecated if (flagDeleted(newConfig, oldConfig, labelName, ProjectConfig.KEY_FUNCTION)) { validationMessageBuilder.add( new CommitValidationMessage( String.format( "Cannot delete '%s.%s.%s'." + " Label functions can only be set to {%s, %s, %s}." + " Use submit requirements instead of label functions.", ProjectConfig.LABEL, labelName, ProjectConfig.KEY_FUNCTION, LabelFunction.NO_BLOCK, LabelFunction.NO_OP, LabelFunction.PATCH_SET_LOCK), ValidationMessage.Type.ERROR)); } } private void rejectNonParseableCopyCondition( Config newConfig, @Nullable Config oldConfig, String labelName, ImmutableList.Builder validationMessageBuilder) { if (flagChangedOrNewlySet(newConfig, oldConfig, labelName, ProjectConfig.KEY_COPY_CONDITION)) { String copyCondition = newConfig.getString(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_COPY_CONDITION); try { @SuppressWarnings("unused") var unused = approvalQueryBuilder.parse(copyCondition); } catch (QueryParseException e) { validationMessageBuilder.add( new CommitValidationMessage( String.format( "Cannot parse copy condition '%s' of label %s (parameter '%s.%s.%s'): %s", copyCondition, labelName, ProjectConfig.LABEL, labelName, ProjectConfig.KEY_COPY_CONDITION, e.getMessage()), // if the old copy condition is not parseable allow updating it even if the new copy // condition is also not parseable, only emit a warning in this case hasUnparseableOldCopyCondition(oldConfig, labelName) ? ValidationMessage.Type.WARNING : ValidationMessage.Type.ERROR)); } } } private boolean hasUnparseableOldCopyCondition(@Nullable Config oldConfig, String labelName) { if (oldConfig == null) { return false; } String oldCopyCondition = oldConfig.getString(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_COPY_CONDITION); try { @SuppressWarnings("unused") var unused = approvalQueryBuilder.parse(oldCopyCondition); return false; } catch (QueryParseException e) { return true; } } /** * Whether the given file was changed in the given revision. * * @param receiveEvent the receive event * @param fileName the name of the file */ private boolean isFileChanged(CommitReceivedEvent receiveEvent, String fileName) throws DiffNotAvailableException { Map fileDiffOutputs; if (receiveEvent.commit.getParentCount() > 0) { // normal commit with one parent: use listModifiedFilesAgainstParent with parentNum = 1 to // compare against the only parent (using parentNum = 0 to compare against the default parent // would also work) // merge commit with 2 or more parents: must use listModifiedFilesAgainstParent with parentNum // = 1 to compare against the first parent (using parentNum = 0 would compare against the // auto-merge) fileDiffOutputs = receiveEvent.diffOperations.loadModifiedFilesAgainstParentIfNecessary( receiveEvent.getProjectNameKey(), receiveEvent.commit, 1, /* enableRenameDetection= */ true); } else { // initial commit: must use listModifiedFilesAgainstParent with parentNum = 0 fileDiffOutputs = receiveEvent.diffOperations.loadModifiedFilesAgainstParentIfNecessary( receiveEvent.getProjectNameKey(), receiveEvent.commit, /* parentNum=*/ 0, /* enableRenameDetection= */ true); } return fileDiffOutputs.keySet().contains(fileName); } private Config loadNewConfig(CommitReceivedEvent receiveEvent) throws IOException, ConfigInvalidException { VersionedConfigFile bareConfig = new VersionedConfigFile(ProjectConfig.PROJECT_CONFIG); bareConfig.load(receiveEvent.project.getNameKey(), receiveEvent.revWalk, receiveEvent.commit); return bareConfig.getConfig(); } private Optional loadOldConfig(CommitReceivedEvent receiveEvent) throws IOException { if (receiveEvent.commit.getParentCount() == 0) { // initial commit, an old config doesn't exist return Optional.empty(); } try { VersionedConfigFile bareConfig = new VersionedConfigFile(ProjectConfig.PROJECT_CONFIG); bareConfig.load( receiveEvent.project.getNameKey(), receiveEvent.revWalk, receiveEvent.commit.getParent(0)); return Optional.of(bareConfig.getConfig()); } catch (ConfigInvalidException e) { // the old config is not parseable, treat this the same way as if an old config didn't exist // so that all parameters in the new config are validated logger.atWarning().log( "cannot inspect the old project config, because parsing %s from parent revision %s" + " in project %s failed: %s", ProjectConfig.PROJECT_CONFIG, receiveEvent.commit.name(), receiveEvent.getProjectNameKey(), e.getMessage()); return Optional.empty(); } } private static boolean flagChangedOrNewlySet( Config newConfig, @Nullable Config oldConfig, String labelName, String key) { if (oldConfig == null) { return newConfig.getNames(ProjectConfig.LABEL, labelName).contains(key); } // Use getString rather than getBoolean so that we do not have to deal with values that cannot // be parsed as a boolean. String oldValue = oldConfig.getString(ProjectConfig.LABEL, labelName, key); String newValue = newConfig.getString(ProjectConfig.LABEL, labelName, key); return newValue != null && !newValue.equals(oldValue); } private static boolean flagDeleted( Config newConfig, @Nullable Config oldConfig, String labelName, String key) { if (oldConfig == null) { return false; } String oldValue = oldConfig.getString(ProjectConfig.LABEL, labelName, key); String newValue = newConfig.getString(ProjectConfig.LABEL, labelName, key); return oldValue != null && newValue == null; } private static boolean copyValuesChangedOrNewlySet( Config newConfig, @Nullable Config oldConfig, String labelName) { if (oldConfig == null) { return newConfig.getNames(ProjectConfig.LABEL, labelName).contains(KEY_COPY_VALUE); } // Ignore the order in which the copy values are defined in the new and old config, since the // order doesn't matter for this parameter. ImmutableSet oldValues = ImmutableSet.copyOf( oldConfig.getStringList(ProjectConfig.LABEL, labelName, KEY_COPY_VALUE)); ImmutableSet newValues = ImmutableSet.copyOf( newConfig.getStringList(ProjectConfig.LABEL, labelName, KEY_COPY_VALUE)); return !newValues.isEmpty() && !Sets.difference(newValues, oldValues).isEmpty(); } private static boolean isLabelFunctionAllowed(LabelFunction labelFunction) { return labelFunction.equals(LabelFunction.NO_BLOCK) || labelFunction.equals(LabelFunction.NO_OP) || labelFunction.equals(LabelFunction.PATCH_SET_LOCK); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy