LCOV - code coverage report
Current view: top level - server/schema - MigrateLabelConfigToCopyCondition.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 98 98 100.0 %
Date: 2022-11-19 15:00:39 Functions: 24 24 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2022 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.schema;
      16             : 
      17             : import static java.util.stream.Collectors.joining;
      18             : 
      19             : import com.google.common.annotations.VisibleForTesting;
      20             : import com.google.common.base.Splitter;
      21             : import com.google.common.base.Strings;
      22             : import com.google.common.collect.ImmutableList;
      23             : import com.google.common.primitives.Shorts;
      24             : import com.google.gerrit.entities.PermissionRule;
      25             : import com.google.gerrit.entities.Project;
      26             : import com.google.gerrit.entities.RefNames;
      27             : import com.google.gerrit.extensions.client.ChangeKind;
      28             : import com.google.gerrit.server.GerritPersonIdent;
      29             : import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
      30             : import com.google.gerrit.server.git.GitRepositoryManager;
      31             : import com.google.gerrit.server.git.meta.MetaDataUpdate;
      32             : import com.google.gerrit.server.project.ProjectConfig;
      33             : import com.google.gerrit.server.project.ProjectLevelConfig;
      34             : import com.google.inject.Inject;
      35             : import java.io.IOException;
      36             : import java.util.ArrayList;
      37             : import java.util.Arrays;
      38             : import java.util.List;
      39             : import java.util.Objects;
      40             : import java.util.Optional;
      41             : import java.util.function.Consumer;
      42             : import org.eclipse.jgit.errors.ConfigInvalidException;
      43             : import org.eclipse.jgit.lib.Config;
      44             : import org.eclipse.jgit.lib.PersonIdent;
      45             : import org.eclipse.jgit.lib.Ref;
      46             : import org.eclipse.jgit.lib.Repository;
      47             : import org.eclipse.jgit.revwalk.RevCommit;
      48             : import org.eclipse.jgit.revwalk.RevWalk;
      49             : 
      50             : /**
      51             :  * Migrates all label configurations of a project to copy conditions.
      52             :  *
      53             :  * <p>The label configuration in {@code project.config} controls under which conditions approvals
      54             :  * should be copied to new patch sets:
      55             :  *
      56             :  * <ul>
      57             :  *   <li>old way: by setting boolean flags and copy values (fields {@code copyAnyScore}, {@code
      58             :  *       copyMinScore}, {@code copyMaxScore}, {@code copyAllScoresIfNoChange}, {@code
      59             :  *       copyAllScoresIfNoCodeChange}, {@code copyAllScoresOnMergeFirstParentUpdate}, {@code
      60             :  *       copyAllScoresOnTrivialRebase}, {@code copyAllScoresIfListOfFilesDidNotChange}, {@code
      61             :  *       copyValue})
      62             :  *   <li>new way: by setting a query as a copy condition (field {@code copyCondition})
      63             :  * </ul>
      64             :  *
      65             :  * <p>This class updates all label configurations in the {@code project.config} of the given
      66             :  * project:
      67             :  *
      68             :  * <ul>
      69             :  *   <li>it stores the conditions under which approvals should be copied to new patchs as a copy
      70             :  *       condition query (field {@code copyCondition})
      71             :  *   <li>it unsets all deprecated fields to control approval copying (fields {@code copyAnyScore},
      72             :  *       {@code copyMinScore}, {@code copyMaxScore}, {@code copyAllScoresIfNoChange}, {@code
      73             :  *       copyAllScoresIfNoCodeChange}, {@code copyAllScoresOnMergeFirstParentUpdate}, {@code
      74             :  *       copyAllScoresOnTrivialRebase}, {@code copyAllScoresIfListOfFilesDidNotChange}, {@code
      75             :  *       copyValue})
      76             :  * </ul>
      77             :  *
      78             :  * <p>This migration assumes {@code true} as default value for the {@code copyAllScoresIfNoChange}
      79             :  * flag since this default value was used for all labels that were created before this migration has
      80             :  * been run (for labels that are created after this migration has been run the default value for
      81             :  * this flag has been changed to {@code false}).
      82             :  */
      83             : public class MigrateLabelConfigToCopyCondition {
      84             :   public static final String COMMIT_MESSAGE = "Migrate label configs to copy conditions";
      85             : 
      86             :   @VisibleForTesting public static final String KEY_COPY_ANY_SCORE = "copyAnyScore";
      87             : 
      88             :   @VisibleForTesting public static final String KEY_COPY_MIN_SCORE = "copyMinScore";
      89             : 
      90             :   @VisibleForTesting public static final String KEY_COPY_MAX_SCORE = "copyMaxScore";
      91             : 
      92             :   @VisibleForTesting public static final String KEY_COPY_VALUE = "copyValue";
      93             : 
      94             :   @VisibleForTesting
      95             :   public static final String KEY_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE =
      96             :       "copyAllScoresOnMergeFirstParentUpdate";
      97             : 
      98             :   @VisibleForTesting
      99             :   public static final String KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE = "copyAllScoresOnTrivialRebase";
     100             : 
     101             :   @VisibleForTesting
     102             :   public static final String KEY_COPY_ALL_SCORES_IF_NO_CODE_CHANGE = "copyAllScoresIfNoCodeChange";
     103             : 
     104             :   @VisibleForTesting
     105             :   public static final String KEY_COPY_ALL_SCORES_IF_NO_CHANGE = "copyAllScoresIfNoChange";
     106             : 
     107             :   @VisibleForTesting
     108             :   public static final String KEY_COPY_ALL_SCORES_IF_LIST_OF_FILES_DID_NOT_CHANGE =
     109             :       "copyAllScoresIfListOfFilesDidNotChange";
     110             : 
     111             :   private final GitRepositoryManager repoManager;
     112             :   private final PersonIdent serverUser;
     113             : 
     114             :   @Inject
     115             :   public MigrateLabelConfigToCopyCondition(
     116           1 :       GitRepositoryManager repoManager, @GerritPersonIdent PersonIdent serverUser) {
     117           1 :     this.repoManager = repoManager;
     118           1 :     this.serverUser = serverUser;
     119           1 :   }
     120             : 
     121             :   /**
     122             :    * Executes the migration for the given project.
     123             :    *
     124             :    * @param projectName the name of the project for which the migration should be executed
     125             :    * @throws IOException thrown if an IO error occurs
     126             :    * @throws ConfigInvalidException thrown if the existing project.config is invalid and cannot be
     127             :    *     parsed
     128             :    */
     129             :   public void execute(Project.NameKey projectName) throws IOException, ConfigInvalidException {
     130           1 :     ProjectLevelConfig.Bare projectConfig =
     131             :         new ProjectLevelConfig.Bare(ProjectConfig.PROJECT_CONFIG);
     132           1 :     try (Repository repo = repoManager.openRepository(projectName);
     133           1 :         MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, projectName, repo)) {
     134           1 :       boolean isAlreadyMigrated = hasMigrationAlreadyRun(repo);
     135             : 
     136           1 :       projectConfig.load(projectName, repo);
     137             : 
     138           1 :       Config cfg = projectConfig.getConfig();
     139           1 :       String orgConfigAsText = cfg.toText();
     140           1 :       for (String labelName : cfg.getSubsections(ProjectConfig.LABEL)) {
     141           1 :         String newCopyCondition = computeCopyCondition(isAlreadyMigrated, cfg, labelName);
     142           1 :         if (!Strings.isNullOrEmpty(newCopyCondition)) {
     143           1 :           cfg.setString(
     144             :               ProjectConfig.LABEL, labelName, ProjectConfig.KEY_COPY_CONDITION, newCopyCondition);
     145             :         }
     146             : 
     147           1 :         unsetDeprecatedFields(cfg, labelName);
     148           1 :       }
     149             : 
     150           1 :       if (cfg.toText().equals(orgConfigAsText)) {
     151             :         // Config was not changed (ie. none of the label definitions had any deprecated field set).
     152           1 :         return;
     153             :       }
     154             : 
     155           1 :       md.getCommitBuilder().setAuthor(serverUser);
     156           1 :       md.getCommitBuilder().setCommitter(serverUser);
     157           1 :       md.setMessage(COMMIT_MESSAGE + "\n");
     158           1 :       projectConfig.commit(md);
     159           1 :     }
     160           1 :   }
     161             : 
     162             :   private static String computeCopyCondition(
     163             :       boolean isAlreadyMigrated, Config cfg, String labelName) {
     164           1 :     List<String> copyConditions = new ArrayList<>();
     165             : 
     166           1 :     ifTrue(cfg, labelName, KEY_COPY_ANY_SCORE, () -> copyConditions.add("is:ANY"));
     167           1 :     ifTrue(cfg, labelName, KEY_COPY_MIN_SCORE, () -> copyConditions.add("is:MIN"));
     168           1 :     ifTrue(cfg, labelName, KEY_COPY_MAX_SCORE, () -> copyConditions.add("is:MAX"));
     169           1 :     forEachSkipNullValues(
     170             :         cfg,
     171             :         labelName,
     172             :         KEY_COPY_VALUE,
     173           1 :         value -> copyConditions.add("is:" + quoteIfNegative(parseCopyValue(value))));
     174           1 :     ifTrue(
     175             :         cfg,
     176             :         labelName,
     177             :         KEY_COPY_ALL_SCORES_IF_NO_CHANGE,
     178           1 :         () -> copyConditions.add("changekind:" + ChangeKind.NO_CHANGE.name()));
     179             : 
     180             :     // If the migration has already been run on this project we must no longer assume true as
     181             :     // default value when copyAllScoresIfNoChange is unset. This is to ensure that the migration is
     182             :     // idempotent when copyAllScoresIfNoChange is set to false:
     183             :     //
     184             :     // 1. migration run:
     185             :     // Removes the copyAllScoresIfNoChange flag, but doesn't add anything to the copy condition.
     186             :     //
     187             :     // 2. migration run:
     188             :     // Since the copyAllScoresIfNoChange flag is no longer set, we would wrongly assume true now and
     189             :     // wrongly include "changekind:NO_CHANGE" into the copy condition. To prevent this we assume
     190             :     // false as default for copyAllScoresIfNoChange once the migration has been run, so that the 2.
     191             :     // migration run is a no-op.
     192           1 :     if (!isAlreadyMigrated) {
     193             :       // The default value for copyAllScoresIfNoChange is true, hence if this parameter is not set
     194             :       // we need to include "changekind:NO_CHANGE" into the copy condition.
     195           1 :       ifUnset(
     196             :           cfg,
     197             :           labelName,
     198             :           KEY_COPY_ALL_SCORES_IF_NO_CHANGE,
     199           1 :           () -> copyConditions.add("changekind:" + ChangeKind.NO_CHANGE.name()));
     200             :     }
     201             : 
     202           1 :     ifTrue(
     203             :         cfg,
     204             :         labelName,
     205             :         KEY_COPY_ALL_SCORES_IF_NO_CODE_CHANGE,
     206           1 :         () -> copyConditions.add("changekind:" + ChangeKind.NO_CODE_CHANGE.name()));
     207           1 :     ifTrue(
     208             :         cfg,
     209             :         labelName,
     210             :         KEY_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE,
     211           1 :         () -> copyConditions.add("changekind:" + ChangeKind.MERGE_FIRST_PARENT_UPDATE.name()));
     212           1 :     ifTrue(
     213             :         cfg,
     214             :         labelName,
     215             :         KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE,
     216           1 :         () -> copyConditions.add("changekind:" + ChangeKind.TRIVIAL_REBASE.name()));
     217           1 :     ifTrue(
     218             :         cfg,
     219             :         labelName,
     220             :         KEY_COPY_ALL_SCORES_IF_LIST_OF_FILES_DID_NOT_CHANGE,
     221           1 :         () -> copyConditions.add("has:unchanged-files"));
     222             : 
     223           1 :     if (copyConditions.isEmpty()) {
     224             :       // No copy conditions need to be added. Simply return the current copy condition as it is.
     225             :       // Returning here prevents that OR conditions are reordered and that parentheses are added
     226             :       // unnecessarily.
     227           1 :       return cfg.getString(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_COPY_CONDITION);
     228             :     }
     229             : 
     230           1 :     ifSet(
     231             :         cfg,
     232             :         labelName,
     233             :         ProjectConfig.KEY_COPY_CONDITION,
     234           1 :         copyCondition -> copyConditions.addAll(splitOrConditions(copyCondition)));
     235             : 
     236           1 :     return copyConditions.stream()
     237           1 :         .map(MigrateLabelConfigToCopyCondition::encloseInParenthesesIfNeeded)
     238           1 :         .sorted()
     239             :         // Remove duplicated OR conditions
     240           1 :         .distinct()
     241           1 :         .collect(joining(" OR "));
     242             :   }
     243             : 
     244             :   private static void ifSet(Config cfg, String labelName, String key, Consumer<String> consumer) {
     245           1 :     Optional.ofNullable(cfg.getString(ProjectConfig.LABEL, labelName, key)).ifPresent(consumer);
     246           1 :   }
     247             : 
     248             :   private static void ifUnset(Config cfg, String labelName, String key, Runnable runnable) {
     249           1 :     Optional<String> value =
     250           1 :         Optional.ofNullable(cfg.getString(ProjectConfig.LABEL, labelName, key));
     251           1 :     if (!value.isPresent()) {
     252           1 :       runnable.run();
     253             :     }
     254           1 :   }
     255             : 
     256             :   private static void ifTrue(Config cfg, String labelName, String key, Runnable runnable) {
     257           1 :     if (cfg.getBoolean(ProjectConfig.LABEL, labelName, key, /* defaultValue= */ false)) {
     258           1 :       runnable.run();
     259             :     }
     260           1 :   }
     261             : 
     262             :   private static void forEachSkipNullValues(
     263             :       Config cfg, String labelName, String key, Consumer<String> consumer) {
     264           1 :     Arrays.stream(cfg.getStringList(ProjectConfig.LABEL, labelName, key))
     265           1 :         .filter(Objects::nonNull)
     266           1 :         .forEach(consumer);
     267           1 :   }
     268             : 
     269             :   private static void unsetDeprecatedFields(Config cfg, String labelName) {
     270           1 :     cfg.unset(ProjectConfig.LABEL, labelName, KEY_COPY_ANY_SCORE);
     271           1 :     cfg.unset(ProjectConfig.LABEL, labelName, KEY_COPY_MIN_SCORE);
     272           1 :     cfg.unset(ProjectConfig.LABEL, labelName, KEY_COPY_MAX_SCORE);
     273           1 :     cfg.unset(ProjectConfig.LABEL, labelName, KEY_COPY_VALUE);
     274           1 :     cfg.unset(ProjectConfig.LABEL, labelName, KEY_COPY_ALL_SCORES_IF_NO_CHANGE);
     275           1 :     cfg.unset(ProjectConfig.LABEL, labelName, KEY_COPY_ALL_SCORES_IF_NO_CODE_CHANGE);
     276           1 :     cfg.unset(ProjectConfig.LABEL, labelName, KEY_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE);
     277           1 :     cfg.unset(ProjectConfig.LABEL, labelName, KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE);
     278           1 :     cfg.unset(ProjectConfig.LABEL, labelName, KEY_COPY_ALL_SCORES_IF_LIST_OF_FILES_DID_NOT_CHANGE);
     279           1 :   }
     280             : 
     281             :   private static ImmutableList<String> splitOrConditions(String copyCondition) {
     282           1 :     if (copyCondition.contains("(") || copyCondition.contains(")")) {
     283             :       // cannot parse complex predicate tree
     284           1 :       return ImmutableList.of(copyCondition);
     285             :     }
     286             : 
     287             :     // split query on OR, this way we can detect and remove duplicate OR conditions later
     288           1 :     return ImmutableList.copyOf(Splitter.on(" OR ").splitToList(copyCondition));
     289             :   }
     290             : 
     291             :   /**
     292             :    * Add parentheses around the given copyCondition if it consists out of 2 or more predicates and
     293             :    * if it isn't enclosed in parentheses yet.
     294             :    */
     295             :   private static String encloseInParenthesesIfNeeded(String copyCondition) {
     296           1 :     if (copyCondition.contains(" ")
     297           1 :         && !(copyCondition.startsWith("(") && copyCondition.endsWith(")"))) {
     298           1 :       return "(" + copyCondition + ")";
     299             :     }
     300           1 :     return copyCondition;
     301             :   }
     302             : 
     303             :   private static short parseCopyValue(String value) {
     304           1 :     return Shorts.checkedCast(PermissionRule.parseInt(value));
     305             :   }
     306             : 
     307             :   private static String quoteIfNegative(short value) {
     308           1 :     if (value < 0) {
     309           1 :       return "\"" + value + "\"";
     310             :     }
     311           1 :     return Integer.toString(value);
     312             :   }
     313             : 
     314             :   public static boolean hasMigrationAlreadyRun(Repository repo) throws IOException {
     315           1 :     try (RevWalk revWalk = new RevWalk(repo)) {
     316           1 :       Ref refsMetaConfig = repo.exactRef(RefNames.REFS_CONFIG);
     317           1 :       if (refsMetaConfig == null) {
     318           1 :         return false;
     319             :       }
     320           1 :       revWalk.markStart(revWalk.parseCommit(refsMetaConfig.getObjectId()));
     321             :       RevCommit commit;
     322           1 :       while ((commit = revWalk.next()) != null) {
     323           1 :         if (COMMIT_MESSAGE.equals(commit.getShortMessage())) {
     324           1 :           return true;
     325             :         }
     326             :       }
     327           1 :       return false;
     328           1 :     }
     329             :   }
     330             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750