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 : }
|