LCOV - code coverage report
Current view: top level - server/schema - MigrateLabelFunctionsToSubmitRequirement.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 161 179 89.9 %
Date: 2022-11-19 15:00:39 Functions: 17 18 94.4 %

          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 com.google.auto.value.AutoValue;
      18             : import com.google.common.collect.ImmutableList;
      19             : import com.google.gerrit.entities.LabelFunction;
      20             : import com.google.gerrit.entities.LabelType;
      21             : import com.google.gerrit.entities.Project;
      22             : import com.google.gerrit.entities.RefNames;
      23             : import com.google.gerrit.entities.SubmitRequirement;
      24             : import com.google.gerrit.entities.SubmitRequirementExpression;
      25             : import com.google.gerrit.server.GerritPersonIdent;
      26             : import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
      27             : import com.google.gerrit.server.git.GitRepositoryManager;
      28             : import com.google.gerrit.server.git.meta.MetaDataUpdate;
      29             : import com.google.gerrit.server.project.ProjectConfig;
      30             : import com.google.gerrit.server.project.ProjectLevelConfig;
      31             : import java.io.IOException;
      32             : import java.util.Arrays;
      33             : import java.util.Collection;
      34             : import java.util.HashMap;
      35             : import java.util.LinkedHashMap;
      36             : import java.util.List;
      37             : import java.util.Locale;
      38             : import java.util.Map;
      39             : import java.util.Optional;
      40             : import javax.inject.Inject;
      41             : import org.eclipse.jgit.errors.ConfigInvalidException;
      42             : import org.eclipse.jgit.lib.Config;
      43             : import org.eclipse.jgit.lib.ObjectReader;
      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             : import org.eclipse.jgit.treewalk.TreeWalk;
      50             : 
      51             : /**
      52             :  * A class with logic for migrating existing label functions to submit requirements and resetting
      53             :  * the label functions to {@link LabelFunction#NO_BLOCK}.
      54             :  *
      55             :  * <p>Important note: Callers should do this migration only if this gerrit installation has no
      56             :  * Prolog submit rules (i.e. no rules.pl file in refs/meta/config). Otherwise, the newly created
      57             :  * submit requirements might not behave as intended.
      58             :  *
      59             :  * <p>The conversion is done as follows:
      60             :  *
      61             :  * <ul>
      62             :  *   <li>MaxWithBlock is translated to submittableIf = label:$lbl=MAX AND -label:$lbl=MIN
      63             :  *   <li>MaxNoBlock is translated to submittableIf = label:$lbl=MAX
      64             :  *   <li>AnyWithBlock is translated to submittableIf = -label:$lbl=MIN
      65             :  *   <li>NoBlock/NoOp are translated to applicableIf = is:false (not applicable)
      66             :  *   <li>PatchSetLock labels are left as is
      67             :  * </ul>
      68             :  *
      69             :  * If the label has {@link LabelType#isIgnoreSelfApproval()}, the max vote is appended with the
      70             :  * 'user=non_uploader' argument.
      71             :  *
      72             :  * <p>For labels that were skipped, i.e. had only one "zero" predefined value, the migrator creates
      73             :  * a non-applicable submit-requirement for them. This is done so that if a parent project had a
      74             :  * submit-requirement with the same name, then it's not inherited by this project.
      75             :  *
      76             :  * <p>If there is an existing label and there exists a "submit requirement" with the same name, the
      77             :  * migrator checks if the submit-requirement to be created matches the one in project.config. If
      78             :  * they don't match, a warning message is printed, otherwise nothing happens. In either cases, the
      79             :  * existing submit-requirement is not altered.
      80             :  */
      81             : public class MigrateLabelFunctionsToSubmitRequirement {
      82             :   public static final String COMMIT_MSG = "Migrate label functions to submit requirements";
      83             :   private final GitRepositoryManager repoManager;
      84             :   private final PersonIdent serverUser;
      85             : 
      86           1 :   public enum Status {
      87             :     /**
      88             :      * The migrator updated the project config and created new submit requirements and/or did reset
      89             :      * label functions.
      90             :      */
      91           1 :     MIGRATED,
      92             : 
      93             :     /** The project had prolog rules, and the migration was skipped. */
      94           1 :     HAS_PROLOG,
      95             : 
      96             :     /**
      97             :      * The project was migrated with a previous run of this class. The migration for this run was
      98             :      * skipped.
      99             :      */
     100           1 :     PREVIOUSLY_MIGRATED,
     101             : 
     102             :     /**
     103             :      * Migration was run for the project but did not update the project.config because it was
     104             :      * up-to-date.
     105             :      */
     106           1 :     NO_CHANGE
     107             :   }
     108             : 
     109             :   @Inject
     110             :   public MigrateLabelFunctionsToSubmitRequirement(
     111           1 :       GitRepositoryManager repoManager, @GerritPersonIdent PersonIdent serverUser) {
     112           1 :     this.repoManager = repoManager;
     113           1 :     this.serverUser = serverUser;
     114           1 :   }
     115             : 
     116             :   /**
     117             :    * For each label function, create a corresponding submit-requirement and set the label function
     118             :    * to NO_BLOCK. Blocking label functions are substituted with blocking submit-requirements.
     119             :    * Non-blocking label functions are substituted with non-applicable submit requirements, allowing
     120             :    * the label vote to be surfaced as a trigger vote (optional label).
     121             :    *
     122             :    * @return {@link Status} reflecting the status of the migration.
     123             :    */
     124             :   public Status executeMigration(Project.NameKey project, UpdateUI ui)
     125             :       throws IOException, ConfigInvalidException {
     126           1 :     if (hasPrologRules(project)) {
     127           0 :       ui.message(String.format("Skipping project %s because it has prolog rules", project));
     128           0 :       return Status.HAS_PROLOG;
     129             :     }
     130           1 :     ProjectLevelConfig.Bare projectConfig =
     131             :         new ProjectLevelConfig.Bare(ProjectConfig.PROJECT_CONFIG);
     132           1 :     boolean migrationPerformed = false;
     133           1 :     try (Repository repo = repoManager.openRepository(project);
     134           1 :         MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, project, repo)) {
     135           1 :       if (hasMigrationAlreadyRun(repo)) {
     136           1 :         ui.message(
     137           1 :             String.format(
     138             :                 "Skipping migrating label functions to submit requirements for project '%s'"
     139             :                     + " because it has been previously migrated",
     140             :                 project));
     141           1 :         return Status.PREVIOUSLY_MIGRATED;
     142             :       }
     143           1 :       projectConfig.load(project, repo);
     144           1 :       Config cfg = projectConfig.getConfig();
     145           1 :       Map<String, LabelAttributes> labelTypes = getLabelTypes(cfg);
     146           1 :       Map<String, SubmitRequirement> existingSubmitRequirements = loadSubmitRequirements(cfg);
     147           1 :       boolean updated = false;
     148           1 :       for (Map.Entry<String, LabelAttributes> lt : labelTypes.entrySet()) {
     149           1 :         String labelName = lt.getKey();
     150           1 :         LabelAttributes attributes = lt.getValue();
     151           1 :         if (attributes.function().equals("PatchSetLock")) {
     152             :           // PATCH_SET_LOCK functions should be left as is
     153           1 :           continue;
     154             :         }
     155             :         // If the function is other than "NoBlock" we want to reset the label function regardless
     156             :         // of whether there exists a "submit requirement".
     157           1 :         if (!attributes.function().equals("NoBlock")) {
     158           1 :           updated = true;
     159           1 :           writeLabelFunction(cfg, labelName, "NoBlock");
     160             :         }
     161           1 :         Optional<SubmitRequirement> sr = createSrFromLabelDef(labelName, attributes);
     162           1 :         if (!sr.isPresent()) {
     163           1 :           continue;
     164             :         }
     165             :         // Make the operation idempotent by skipping creating the submit-requirement if one was
     166             :         // already created or previously existed.
     167           1 :         if (existingSubmitRequirements.containsKey(labelName.toLowerCase(Locale.ROOT))) {
     168           1 :           if (!sr.get()
     169           1 :               .equals(existingSubmitRequirements.get(labelName.toLowerCase(Locale.ROOT)))) {
     170           1 :             ui.message(
     171           1 :                 String.format(
     172             :                     "Warning: Skipping creating a submit requirement for label '%s'. An existing "
     173             :                         + "submit requirement is already present but its definition is not "
     174             :                         + "identical to the existing label definition.",
     175             :                     labelName));
     176             :           }
     177             :           continue;
     178             :         }
     179           1 :         updated = true;
     180           1 :         ui.message(
     181           1 :             String.format(
     182             :                 "Project %s: Creating a submit requirement for label %s", project, labelName));
     183           1 :         writeSubmitRequirement(cfg, sr.get());
     184           1 :       }
     185           1 :       if (updated) {
     186           1 :         commit(projectConfig, md);
     187           1 :         migrationPerformed = true;
     188             :       }
     189           1 :     }
     190           1 :     return migrationPerformed ? Status.MIGRATED : Status.NO_CHANGE;
     191             :   }
     192             : 
     193             :   /**
     194             :    * Returns a Map containing label names as string in keys along with some of its attributes (that
     195             :    * we need in the migration) like canOverride, ignoreSelfApproval and function in the value.
     196             :    */
     197             :   private Map<String, LabelAttributes> getLabelTypes(Config cfg) {
     198           1 :     Map<String, LabelAttributes> labelTypes = new HashMap<>();
     199           1 :     for (String labelName : cfg.getSubsections(ProjectConfig.LABEL)) {
     200           1 :       String function = cfg.getString(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_FUNCTION);
     201           1 :       boolean canOverride =
     202           1 :           cfg.getBoolean(
     203             :               ProjectConfig.LABEL,
     204             :               labelName,
     205             :               ProjectConfig.KEY_CAN_OVERRIDE,
     206             :               /* defaultValue= */ true);
     207           1 :       boolean ignoreSelfApproval =
     208           1 :           cfg.getBoolean(
     209             :               ProjectConfig.LABEL,
     210             :               labelName,
     211             :               ProjectConfig.KEY_IGNORE_SELF_APPROVAL,
     212             :               /* defaultValue= */ false);
     213             :       ImmutableList<String> values =
     214           1 :           ImmutableList.<String>builder()
     215           1 :               .addAll(
     216           1 :                   Arrays.asList(
     217           1 :                       cfg.getStringList(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_VALUE)))
     218           1 :               .build();
     219             :       LabelAttributes attributes =
     220           1 :           LabelAttributes.create(
     221           1 :               function == null ? "MaxWithBlock" : function,
     222             :               canOverride,
     223             :               ignoreSelfApproval,
     224             :               values);
     225           1 :       labelTypes.put(labelName, attributes);
     226           1 :     }
     227           1 :     return labelTypes;
     228             :   }
     229             : 
     230             :   private void writeSubmitRequirement(Config cfg, SubmitRequirement sr) {
     231           1 :     if (sr.description().isPresent()) {
     232           0 :       cfg.setString(
     233             :           ProjectConfig.SUBMIT_REQUIREMENT,
     234           0 :           sr.name(),
     235             :           ProjectConfig.KEY_SR_DESCRIPTION,
     236           0 :           sr.description().get());
     237             :     }
     238           1 :     if (sr.applicabilityExpression().isPresent()) {
     239           1 :       cfg.setString(
     240             :           ProjectConfig.SUBMIT_REQUIREMENT,
     241           1 :           sr.name(),
     242             :           ProjectConfig.KEY_SR_APPLICABILITY_EXPRESSION,
     243           1 :           sr.applicabilityExpression().get().expressionString());
     244             :     }
     245           1 :     cfg.setString(
     246             :         ProjectConfig.SUBMIT_REQUIREMENT,
     247           1 :         sr.name(),
     248             :         ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION,
     249           1 :         sr.submittabilityExpression().expressionString());
     250           1 :     if (sr.overrideExpression().isPresent()) {
     251           0 :       cfg.setString(
     252             :           ProjectConfig.SUBMIT_REQUIREMENT,
     253           0 :           sr.name(),
     254             :           ProjectConfig.KEY_SR_OVERRIDE_EXPRESSION,
     255           0 :           sr.overrideExpression().get().expressionString());
     256             :     }
     257           1 :     cfg.setBoolean(
     258             :         ProjectConfig.SUBMIT_REQUIREMENT,
     259           1 :         sr.name(),
     260             :         ProjectConfig.KEY_SR_OVERRIDE_IN_CHILD_PROJECTS,
     261           1 :         sr.allowOverrideInChildProjects());
     262           1 :   }
     263             : 
     264             :   private void writeLabelFunction(Config cfg, String labelName, String function) {
     265           1 :     cfg.setString(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_FUNCTION, function);
     266           1 :   }
     267             : 
     268             :   private void commit(ProjectLevelConfig.Bare projectConfig, MetaDataUpdate md) throws IOException {
     269           1 :     md.getCommitBuilder().setAuthor(serverUser);
     270           1 :     md.getCommitBuilder().setCommitter(serverUser);
     271           1 :     md.setMessage(COMMIT_MSG);
     272           1 :     projectConfig.commit(md);
     273           1 :   }
     274             : 
     275             :   private static Optional<SubmitRequirement> createSrFromLabelDef(
     276             :       String labelName, LabelAttributes attributes) {
     277           1 :     if (isLabelSkipped(attributes.values())) {
     278           1 :       return Optional.of(createNonApplicableSr(labelName, attributes.canOverride()));
     279           1 :     } else if (isBlockingOrRequiredLabel(attributes.function())) {
     280           1 :       return Optional.of(createBlockingOrRequiredSr(labelName, attributes));
     281             :     }
     282           1 :     return Optional.empty();
     283             :   }
     284             : 
     285             :   private static SubmitRequirement createNonApplicableSr(String labelName, boolean canOverride) {
     286           1 :     return SubmitRequirement.builder()
     287           1 :         .setName(labelName)
     288           1 :         .setApplicabilityExpression(SubmitRequirementExpression.of("is:false"))
     289           1 :         .setSubmittabilityExpression(SubmitRequirementExpression.create("is:true"))
     290           1 :         .setAllowOverrideInChildProjects(canOverride)
     291           1 :         .build();
     292             :   }
     293             : 
     294             :   /**
     295             :    * Create a "submit requirement" that is only satisfied if the label is voted with the max votes
     296             :    * and/or not voted by the min vote, according to the label attributes.
     297             :    */
     298             :   private static SubmitRequirement createBlockingOrRequiredSr(
     299             :       String labelName, LabelAttributes attributes) {
     300             :     SubmitRequirement.Builder builder =
     301           1 :         SubmitRequirement.builder()
     302           1 :             .setName(labelName)
     303           1 :             .setAllowOverrideInChildProjects(attributes.canOverride());
     304           1 :     String maxPart =
     305           1 :         String.format("label:%s=MAX", labelName)
     306           1 :             + (attributes.ignoreSelfApproval() ? ",user=non_uploader" : "");
     307           1 :     switch (attributes.function()) {
     308             :       case "MaxWithBlock":
     309           1 :         builder.setSubmittabilityExpression(
     310           1 :             SubmitRequirementExpression.create(
     311           1 :                 String.format("%s AND -label:%s=MIN", maxPart, labelName)));
     312           1 :         break;
     313             :       case "AnyWithBlock":
     314           1 :         builder.setSubmittabilityExpression(
     315           1 :             SubmitRequirementExpression.create(String.format("-label:%s=MIN", labelName)));
     316           1 :         break;
     317             :       case "MaxNoBlock":
     318           1 :         builder.setSubmittabilityExpression(SubmitRequirementExpression.create(maxPart));
     319           1 :         break;
     320             :       default:
     321             :         break;
     322             :     }
     323           1 :     return builder.build();
     324             :   }
     325             : 
     326             :   private static boolean isBlockingOrRequiredLabel(String function) {
     327           1 :     return function.equals("AnyWithBlock")
     328           1 :         || function.equals("MaxWithBlock")
     329           1 :         || function.equals("MaxNoBlock");
     330             :   }
     331             : 
     332             :   /**
     333             :    * Returns true if the label definition was skipped in the project, i.e. it had only one defined
     334             :    * value: zero.
     335             :    */
     336             :   private static boolean isLabelSkipped(List<String> values) {
     337           1 :     return values.isEmpty() || (values.size() == 1 && values.get(0).startsWith("0"));
     338             :   }
     339             : 
     340             :   public boolean anyProjectHasProlog(Collection<Project.NameKey> allProjects) throws IOException {
     341           0 :     for (Project.NameKey p : allProjects) {
     342           0 :       if (hasPrologRules(p)) {
     343           0 :         return true;
     344             :       }
     345           0 :     }
     346           0 :     return false;
     347             :   }
     348             : 
     349             :   private boolean hasPrologRules(Project.NameKey project) throws IOException {
     350           1 :     try (Repository repo = repoManager.openRepository(project);
     351           1 :         RevWalk rw = new RevWalk(repo);
     352           1 :         ObjectReader reader = rw.getObjectReader()) {
     353           1 :       Ref refsConfig = repo.exactRef(RefNames.REFS_CONFIG);
     354           1 :       if (refsConfig == null) {
     355             :         // Project does not have a refs/meta/config and no rules.pl consequently.
     356           0 :         return false;
     357             :       }
     358           1 :       RevCommit commit = repo.parseCommit(refsConfig.getObjectId());
     359           1 :       try (TreeWalk tw = TreeWalk.forPath(reader, "rules.pl", commit.getTree())) {
     360           1 :         if (tw != null) {
     361           0 :           return true;
     362             :         }
     363           0 :       }
     364             : 
     365           1 :       return false;
     366           0 :     }
     367             :   }
     368             : 
     369             :   /**
     370             :    * Returns a map containing submit requirement names in lower name as keys, with {@link
     371             :    * com.google.gerrit.entities.SubmitRequirement} as value.
     372             :    */
     373             :   private Map<String, SubmitRequirement> loadSubmitRequirements(Config rc) {
     374           1 :     Map<String, SubmitRequirement> allRequirements = new LinkedHashMap<>();
     375           1 :     for (String name : rc.getSubsections(ProjectConfig.SUBMIT_REQUIREMENT)) {
     376           1 :       String description =
     377           1 :           rc.getString(ProjectConfig.SUBMIT_REQUIREMENT, name, ProjectConfig.KEY_SR_DESCRIPTION);
     378           1 :       String applicabilityExpr =
     379           1 :           rc.getString(
     380             :               ProjectConfig.SUBMIT_REQUIREMENT,
     381             :               name,
     382             :               ProjectConfig.KEY_SR_APPLICABILITY_EXPRESSION);
     383           1 :       String submittabilityExpr =
     384           1 :           rc.getString(
     385             :               ProjectConfig.SUBMIT_REQUIREMENT,
     386             :               name,
     387             :               ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION);
     388           1 :       String overrideExpr =
     389           1 :           rc.getString(
     390             :               ProjectConfig.SUBMIT_REQUIREMENT, name, ProjectConfig.KEY_SR_OVERRIDE_EXPRESSION);
     391           1 :       boolean canInherit =
     392           1 :           rc.getBoolean(
     393             :               ProjectConfig.SUBMIT_REQUIREMENT,
     394             :               name,
     395             :               ProjectConfig.KEY_SR_OVERRIDE_IN_CHILD_PROJECTS,
     396             :               false);
     397             :       SubmitRequirement submitRequirement =
     398           1 :           SubmitRequirement.builder()
     399           1 :               .setName(name)
     400           1 :               .setDescription(Optional.ofNullable(description))
     401           1 :               .setApplicabilityExpression(SubmitRequirementExpression.of(applicabilityExpr))
     402           1 :               .setSubmittabilityExpression(SubmitRequirementExpression.create(submittabilityExpr))
     403           1 :               .setOverrideExpression(SubmitRequirementExpression.of(overrideExpr))
     404           1 :               .setAllowOverrideInChildProjects(canInherit)
     405           1 :               .build();
     406           1 :       allRequirements.put(name.toLowerCase(Locale.ROOT), submitRequirement);
     407           1 :     }
     408           1 :     return allRequirements;
     409             :   }
     410             : 
     411             :   private static boolean hasMigrationAlreadyRun(Repository repo) throws IOException {
     412           1 :     try (RevWalk revWalk = new RevWalk(repo)) {
     413           1 :       Ref refsMetaConfig = repo.exactRef(RefNames.REFS_CONFIG);
     414           1 :       if (refsMetaConfig == null) {
     415           0 :         return false;
     416             :       }
     417           1 :       revWalk.markStart(revWalk.parseCommit(refsMetaConfig.getObjectId()));
     418             :       RevCommit commit;
     419           1 :       while ((commit = revWalk.next()) != null) {
     420           1 :         if (COMMIT_MSG.equals(commit.getShortMessage())) {
     421           1 :           return true;
     422             :         }
     423             :       }
     424           1 :       return false;
     425           1 :     }
     426             :   }
     427             : 
     428             :   @AutoValue
     429           1 :   abstract static class LabelAttributes {
     430             :     abstract String function();
     431             : 
     432             :     abstract boolean canOverride();
     433             : 
     434             :     abstract boolean ignoreSelfApproval();
     435             : 
     436             :     abstract ImmutableList<String> values();
     437             : 
     438             :     static LabelAttributes create(
     439             :         String function,
     440             :         boolean canOverride,
     441             :         boolean ignoreSelfApproval,
     442             :         ImmutableList<String> values) {
     443           1 :       return new AutoValue_MigrateLabelFunctionsToSubmitRequirement_LabelAttributes(
     444             :           function, canOverride, ignoreSelfApproval, values);
     445             :     }
     446             :   }
     447             : }

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