| // 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.schema; | |
| import static com.google.gerrit.server.project.ProjectConfig.RULES_PL_FILE; | |
| import com.google.auto.value.AutoValue; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.gerrit.entities.LabelFunction; | |
| import com.google.gerrit.entities.LabelType; | |
| import com.google.gerrit.entities.Project; | |
| import com.google.gerrit.entities.RefNames; | |
| import com.google.gerrit.entities.SubmitRequirement; | |
| import com.google.gerrit.entities.SubmitRequirementExpression; | |
| import com.google.gerrit.server.GerritPersonIdent; | |
| import com.google.gerrit.server.extensions.events.GitReferenceUpdated; | |
| import com.google.gerrit.server.git.GitRepositoryManager; | |
| import com.google.gerrit.server.git.meta.MetaDataUpdate; | |
| import com.google.gerrit.server.project.ProjectConfig; | |
| import com.google.gerrit.server.project.ProjectLevelConfig; | |
| import java.io.IOException; | |
| import java.util.Arrays; | |
| import java.util.Collection; | |
| import java.util.HashMap; | |
| import java.util.LinkedHashMap; | |
| import java.util.List; | |
| import java.util.Locale; | |
| import java.util.Map; | |
| import java.util.Optional; | |
| import java.util.stream.Collectors; | |
| import javax.inject.Inject; | |
| import org.eclipse.jgit.errors.ConfigInvalidException; | |
| import org.eclipse.jgit.lib.Config; | |
| import org.eclipse.jgit.lib.ObjectReader; | |
| import org.eclipse.jgit.lib.PersonIdent; | |
| import org.eclipse.jgit.lib.Ref; | |
| import org.eclipse.jgit.lib.Repository; | |
| import org.eclipse.jgit.revwalk.RevCommit; | |
| import org.eclipse.jgit.revwalk.RevWalk; | |
| import org.eclipse.jgit.treewalk.TreeWalk; | |
| /** | |
| * A class with logic for migrating existing label functions to submit requirements and resetting | |
| * the label functions to {@link LabelFunction#NO_BLOCK}. | |
| * | |
| * <p>Important note: Callers should do this migration only if this gerrit installation has no | |
| * Prolog submit rules (i.e. no rules.pl file in refs/meta/config). Otherwise, the newly created | |
| * submit requirements might not behave as intended. | |
| * | |
| * <p>The conversion is done as follows: | |
| * | |
| * <ul> | |
| * <li>MaxWithBlock is translated to submittableIf = label:$lbl=MAX AND -label:$lbl=MIN | |
| * <li>MaxNoBlock is translated to submittableIf = label:$lbl=MAX | |
| * <li>AnyWithBlock is translated to submittableIf = -label:$lbl=MIN | |
| * <li>NoBlock/NoOp are translated to applicableIf = is:false (not applicable) | |
| * <li>PatchSetLock labels are left as is | |
| * </ul> | |
| * | |
| * If the label has {@link LabelType#isIgnoreSelfApproval()}, the max vote is appended with the | |
| * 'user=non_uploader' argument. | |
| * | |
| * <p>For labels that were skipped, i.e. had only one "zero" predefined value, the migrator creates | |
| * a non-applicable submit-requirement for them. This is done so that if a parent project had a | |
| * submit-requirement with the same name, then it's not inherited by this project. | |
| * | |
| * <p>If there is an existing label and there exists a "submit requirement" with the same name, the | |
| * migrator checks if the submit-requirement to be created matches the one in project.config. If | |
| * they don't match, a warning message is printed, otherwise nothing happens. In either cases, the | |
| * existing submit-requirement is not altered. | |
| */ | |
| public class MigrateLabelFunctionsToSubmitRequirement { | |
| public static final String COMMIT_MSG = "Migrate label functions to submit requirements"; | |
| private final GitRepositoryManager repoManager; | |
| private final PersonIdent serverUser; | |
| public enum Status { | |
| /** | |
| * The migrator updated the project config and created new submit requirements and/or did reset | |
| * label functions. | |
| */ | |
| MIGRATED, | |
| /** The project had prolog rules, and the migration was skipped. */ | |
| HAS_PROLOG, | |
| /** | |
| * The project was migrated with a previous run of this class. The migration for this run was | |
| * skipped. | |
| */ | |
| PREVIOUSLY_MIGRATED, | |
| /** | |
| * Migration was run for the project but did not update the project.config because it was | |
| * up-to-date. | |
| */ | |
| NO_CHANGE | |
| } | |
| @Inject | |
| public MigrateLabelFunctionsToSubmitRequirement( | |
| GitRepositoryManager repoManager, @GerritPersonIdent PersonIdent serverUser) { | |
| this.repoManager = repoManager; | |
| this.serverUser = serverUser; | |
| } | |
| /** | |
| * For each label function, create a corresponding submit-requirement and set the label function | |
| * to NO_BLOCK. Blocking label functions are substituted with blocking submit-requirements. | |
| * Non-blocking label functions are substituted with non-applicable submit requirements, allowing | |
| * the label vote to be surfaced as a trigger vote (optional label). | |
| * | |
| * @return {@link Status} reflecting the status of the migration. | |
| */ | |
| public Status executeMigration(Project.NameKey project, UpdateUI ui) | |
| throws IOException, ConfigInvalidException { | |
| if (hasPrologRules(project)) { | |
| ui.message(String.format("Skipping project %s because it has prolog rules", project)); | |
| return Status.HAS_PROLOG; | |
| } | |
| ProjectLevelConfig.Bare projectConfig = | |
| new ProjectLevelConfig.Bare(ProjectConfig.PROJECT_CONFIG); | |
| boolean migrationPerformed = false; | |
| try (Repository repo = repoManager.openRepository(project); | |
| MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, project, repo)) { | |
| if (hasMigrationAlreadyRun(repo)) { | |
| ui.message( | |
| String.format( | |
| "Skipping migrating label functions to submit requirements for project '%s'" | |
| + " because it has been previously migrated", | |
| project)); | |
| return Status.PREVIOUSLY_MIGRATED; | |
| } | |
| projectConfig.load(project, repo); | |
| Config cfg = projectConfig.getConfig(); | |
| Map<String, LabelAttributes> labelTypes = getLabelTypes(cfg); | |
| Map<String, SubmitRequirement> existingSubmitRequirements = loadSubmitRequirements(cfg); | |
| boolean updated = false; | |
| for (Map.Entry<String, LabelAttributes> lt : labelTypes.entrySet()) { | |
| String labelName = lt.getKey(); | |
| LabelAttributes attributes = lt.getValue(); | |
| if (attributes.function().equals("PatchSetLock")) { | |
| // PATCH_SET_LOCK functions should be left as is | |
| continue; | |
| } | |
| // If the function is other than "NoBlock" we want to reset the label function regardless | |
| // of whether there exists a "submit requirement". | |
| if (!attributes.function().equals("NoBlock")) { | |
| updated = true; | |
| writeLabelFunction(cfg, labelName, "NoBlock"); | |
| } | |
| Optional<SubmitRequirement> sr = createSrFromLabelDef(labelName, attributes); | |
| if (!sr.isPresent()) { | |
| continue; | |
| } | |
| // Make the operation idempotent by skipping creating the submit-requirement if one was | |
| // already created or previously existed. | |
| if (existingSubmitRequirements.containsKey(labelName.toLowerCase(Locale.ROOT))) { | |
| if (!sr.get() | |
| .equals(existingSubmitRequirements.get(labelName.toLowerCase(Locale.ROOT)))) { | |
| ui.message( | |
| String.format( | |
| "Warning: Skipping creating a submit requirement for label '%s'. An existing " | |
| + "submit requirement is already present but its definition is not " | |
| + "identical to the existing label definition.", | |
| labelName)); | |
| } | |
| continue; | |
| } | |
| updated = true; | |
| ui.message( | |
| String.format( | |
| "Project %s: Creating a submit requirement for label %s", project, labelName)); | |
| writeSubmitRequirement(cfg, sr.get()); | |
| } | |
| if (updated) { | |
| commit(projectConfig, md); | |
| migrationPerformed = true; | |
| } | |
| } | |
| return migrationPerformed ? Status.MIGRATED : Status.NO_CHANGE; | |
| } | |
| /** | |
| * Returns a Map containing label names as string in keys along with some of its attributes (that | |
| * we need in the migration) like canOverride, ignoreSelfApproval and function in the value. | |
| */ | |
| private Map<String, LabelAttributes> getLabelTypes(Config cfg) { | |
| Map<String, LabelAttributes> labelTypes = new HashMap<>(); | |
| for (String labelName : cfg.getSubsections(ProjectConfig.LABEL)) { | |
| String function = cfg.getString(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_FUNCTION); | |
| boolean canOverride = | |
| cfg.getBoolean( | |
| ProjectConfig.LABEL, | |
| labelName, | |
| ProjectConfig.KEY_CAN_OVERRIDE, | |
| /* defaultValue= */ true); | |
| boolean ignoreSelfApproval = | |
| cfg.getBoolean( | |
| ProjectConfig.LABEL, | |
| labelName, | |
| ProjectConfig.KEY_IGNORE_SELF_APPROVAL, | |
| /* defaultValue= */ false); | |
| ImmutableList<String> values = | |
| ImmutableList.<String>builder() | |
| .addAll( | |
| Arrays.asList( | |
| cfg.getStringList(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_VALUE))) | |
| .build(); | |
| ImmutableList<String> refPatterns = | |
| ImmutableList.<String>builder() | |
| .addAll( | |
| Arrays.asList( | |
| cfg.getStringList(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_BRANCH))) | |
| .build(); | |
| LabelAttributes attributes = | |
| LabelAttributes.create( | |
| function == null ? "MaxWithBlock" : function, | |
| canOverride, | |
| ignoreSelfApproval, | |
| values, | |
| refPatterns); | |
| labelTypes.put(labelName, attributes); | |
| } | |
| return labelTypes; | |
| } | |
| private void writeSubmitRequirement(Config cfg, SubmitRequirement sr) { | |
| if (sr.description().isPresent()) { | |
| cfg.setString( | |
| ProjectConfig.SUBMIT_REQUIREMENT, | |
| sr.name(), | |
| ProjectConfig.KEY_SR_DESCRIPTION, | |
| sr.description().get()); | |
| } | |
| if (sr.applicabilityExpression().isPresent()) { | |
| cfg.setString( | |
| ProjectConfig.SUBMIT_REQUIREMENT, | |
| sr.name(), | |
| ProjectConfig.KEY_SR_APPLICABILITY_EXPRESSION, | |
| sr.applicabilityExpression().get().expressionString()); | |
| } | |
| cfg.setString( | |
| ProjectConfig.SUBMIT_REQUIREMENT, | |
| sr.name(), | |
| ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION, | |
| sr.submittabilityExpression().expressionString()); | |
| if (sr.overrideExpression().isPresent()) { | |
| cfg.setString( | |
| ProjectConfig.SUBMIT_REQUIREMENT, | |
| sr.name(), | |
| ProjectConfig.KEY_SR_OVERRIDE_EXPRESSION, | |
| sr.overrideExpression().get().expressionString()); | |
| } | |
| cfg.setBoolean( | |
| ProjectConfig.SUBMIT_REQUIREMENT, | |
| sr.name(), | |
| ProjectConfig.KEY_SR_OVERRIDE_IN_CHILD_PROJECTS, | |
| sr.allowOverrideInChildProjects()); | |
| } | |
| private void writeLabelFunction(Config cfg, String labelName, String function) { | |
| cfg.setString(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_FUNCTION, function); | |
| } | |
| private void commit(ProjectLevelConfig.Bare projectConfig, MetaDataUpdate md) throws IOException { | |
| md.getCommitBuilder().setAuthor(serverUser); | |
| md.getCommitBuilder().setCommitter(serverUser); | |
| md.setMessage(COMMIT_MSG); | |
| projectConfig.commit(md); | |
| } | |
| private static Optional<SubmitRequirement> createSrFromLabelDef( | |
| String labelName, LabelAttributes attributes) { | |
| if (isLabelSkipped(attributes.values())) { | |
| return Optional.of(createNonApplicableSr(labelName, attributes.canOverride())); | |
| } else if (isBlockingOrRequiredLabel(attributes.function())) { | |
| return Optional.of(createBlockingOrRequiredSr(labelName, attributes)); | |
| } | |
| return Optional.empty(); | |
| } | |
| private static SubmitRequirement createNonApplicableSr(String labelName, boolean canOverride) { | |
| return SubmitRequirement.builder() | |
| .setName(labelName) | |
| .setApplicabilityExpression(SubmitRequirementExpression.of("is:false")) | |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("is:true")) | |
| .setAllowOverrideInChildProjects(canOverride) | |
| .build(); | |
| } | |
| /** | |
| * Create a "submit requirement" that is only satisfied if the label is voted with the max votes | |
| * and/or not voted by the min vote, according to the label attributes. | |
| */ | |
| private static SubmitRequirement createBlockingOrRequiredSr( | |
| String labelName, LabelAttributes attributes) { | |
| SubmitRequirement.Builder builder = | |
| SubmitRequirement.builder() | |
| .setName(labelName) | |
| .setAllowOverrideInChildProjects(attributes.canOverride()); | |
| String maxPart = | |
| String.format("label:%s=MAX", labelName) | |
| + (attributes.ignoreSelfApproval() ? ",user=non_uploader" : ""); | |
| switch (attributes.function()) { | |
| case "MaxWithBlock": | |
| builder.setSubmittabilityExpression( | |
| SubmitRequirementExpression.create( | |
| String.format("%s AND -label:%s=MIN", maxPart, labelName))); | |
| break; | |
| case "AnyWithBlock": | |
| builder.setSubmittabilityExpression( | |
| SubmitRequirementExpression.create(String.format("-label:%s=MIN", labelName))); | |
| break; | |
| case "MaxNoBlock": | |
| builder.setSubmittabilityExpression(SubmitRequirementExpression.create(maxPart)); | |
| break; | |
| default: | |
| break; | |
| } | |
| if (!attributes.refPatterns().isEmpty()) { | |
| builder.setApplicabilityExpression( | |
| SubmitRequirementExpression.of( | |
| String.join( | |
| " OR ", | |
| attributes.refPatterns().stream() | |
| .map(b -> "branch:\\\"" + b + "\\\"") | |
| .collect(Collectors.toList())))); | |
| } | |
| return builder.build(); | |
| } | |
| private static boolean isBlockingOrRequiredLabel(String function) { | |
| return function.equals("AnyWithBlock") | |
| || function.equals("MaxWithBlock") | |
| || function.equals("MaxNoBlock"); | |
| } | |
| /** | |
| * Returns true if the label definition was skipped in the project, i.e. it had only one defined | |
| * value: zero. | |
| */ | |
| private static boolean isLabelSkipped(List<String> values) { | |
| return values.isEmpty() || (values.size() == 1 && values.get(0).startsWith("0")); | |
| } | |
| public boolean anyProjectHasProlog(Collection<Project.NameKey> allProjects) throws IOException { | |
| for (Project.NameKey p : allProjects) { | |
| if (hasPrologRules(p)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| private boolean hasPrologRules(Project.NameKey project) throws IOException { | |
| try (Repository repo = repoManager.openRepository(project); | |
| RevWalk rw = new RevWalk(repo); | |
| ObjectReader reader = rw.getObjectReader()) { | |
| Ref refsConfig = repo.exactRef(RefNames.REFS_CONFIG); | |
| if (refsConfig == null) { | |
| // Project does not have a refs/meta/config and no rules.pl consequently. | |
| return false; | |
| } | |
| RevCommit commit = repo.parseCommit(refsConfig.getObjectId()); | |
| try (TreeWalk tw = TreeWalk.forPath(reader, RULES_PL_FILE, commit.getTree())) { | |
| if (tw != null) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| } | |
| /** | |
| * Returns a map containing submit requirement names in lower name as keys, with {@link | |
| * com.google.gerrit.entities.SubmitRequirement} as value. | |
| */ | |
| private Map<String, SubmitRequirement> loadSubmitRequirements(Config rc) { | |
| Map<String, SubmitRequirement> allRequirements = new LinkedHashMap<>(); | |
| for (String name : rc.getSubsections(ProjectConfig.SUBMIT_REQUIREMENT)) { | |
| String description = | |
| rc.getString(ProjectConfig.SUBMIT_REQUIREMENT, name, ProjectConfig.KEY_SR_DESCRIPTION); | |
| String applicabilityExpr = | |
| rc.getString( | |
| ProjectConfig.SUBMIT_REQUIREMENT, | |
| name, | |
| ProjectConfig.KEY_SR_APPLICABILITY_EXPRESSION); | |
| String submittabilityExpr = | |
| rc.getString( | |
| ProjectConfig.SUBMIT_REQUIREMENT, | |
| name, | |
| ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION); | |
| String overrideExpr = | |
| rc.getString( | |
| ProjectConfig.SUBMIT_REQUIREMENT, name, ProjectConfig.KEY_SR_OVERRIDE_EXPRESSION); | |
| boolean canInherit = | |
| rc.getBoolean( | |
| ProjectConfig.SUBMIT_REQUIREMENT, | |
| name, | |
| ProjectConfig.KEY_SR_OVERRIDE_IN_CHILD_PROJECTS, | |
| false); | |
| SubmitRequirement submitRequirement = | |
| SubmitRequirement.builder() | |
| .setName(name) | |
| .setDescription(Optional.ofNullable(description)) | |
| .setApplicabilityExpression(SubmitRequirementExpression.of(applicabilityExpr)) | |
| .setSubmittabilityExpression(SubmitRequirementExpression.create(submittabilityExpr)) | |
| .setOverrideExpression(SubmitRequirementExpression.of(overrideExpr)) | |
| .setAllowOverrideInChildProjects(canInherit) | |
| .build(); | |
| allRequirements.put(name.toLowerCase(Locale.ROOT), submitRequirement); | |
| } | |
| return allRequirements; | |
| } | |
| private static boolean hasMigrationAlreadyRun(Repository repo) throws IOException { | |
| try (RevWalk revWalk = new RevWalk(repo)) { | |
| Ref refsMetaConfig = repo.exactRef(RefNames.REFS_CONFIG); | |
| if (refsMetaConfig == null) { | |
| return false; | |
| } | |
| revWalk.markStart(revWalk.parseCommit(refsMetaConfig.getObjectId())); | |
| RevCommit commit; | |
| while ((commit = revWalk.next()) != null) { | |
| if (COMMIT_MSG.equals(commit.getShortMessage())) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| } | |
| @AutoValue | |
| abstract static class LabelAttributes { | |
| abstract String function(); | |
| abstract boolean canOverride(); | |
| abstract boolean ignoreSelfApproval(); | |
| abstract ImmutableList<String> values(); | |
| abstract ImmutableList<String> refPatterns(); | |
| static LabelAttributes create( | |
| String function, | |
| boolean canOverride, | |
| boolean ignoreSelfApproval, | |
| ImmutableList<String> values, | |
| ImmutableList<String> refPatterns) { | |
| return new AutoValue_MigrateLabelFunctionsToSubmitRequirement_LabelAttributes( | |
| function, canOverride, ignoreSelfApproval, values, refPatterns); | |
| } | |
| } | |
| } |