| // Copyright (C) 2021 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.base.Strings; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.entities.LabelType; |
| import com.google.gerrit.entities.SubmitRecord; |
| import com.google.gerrit.entities.SubmitRecord.Label; |
| import com.google.gerrit.entities.SubmitRequirement; |
| import com.google.gerrit.entities.SubmitRequirementExpression; |
| import com.google.gerrit.entities.SubmitRequirementExpressionResult; |
| import com.google.gerrit.entities.SubmitRequirementExpressionResult.Status; |
| import com.google.gerrit.entities.SubmitRequirementResult; |
| import com.google.gerrit.server.query.change.ChangeData; |
| import com.google.gerrit.server.query.change.ChangeQueryBuilder; |
| import com.google.gerrit.server.rules.DefaultSubmitRule; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.stream.Collectors; |
| import org.eclipse.jgit.lib.ObjectId; |
| |
| /** |
| * Convert {@link com.google.gerrit.entities.SubmitRecord} entities to {@link |
| * com.google.gerrit.entities.SubmitRequirementResult}s. |
| */ |
| public class SubmitRequirementsAdapter { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| private SubmitRequirementsAdapter() {} |
| |
| /** |
| * Retrieve legacy submit records (created by label functions and other {@link |
| * com.google.gerrit.server.rules.SubmitRule}s) and convert them to submit requirement results. |
| */ |
| public static ImmutableMap<SubmitRequirement, SubmitRequirementResult> getLegacyRequirements( |
| ChangeData cd) { |
| // We use SubmitRuleOptions.defaults() which does not recompute submit rules for closed changes. |
| // This doesn't have an effect since we never call this class (i.e. to evaluate submit |
| // requirements) for closed changes. |
| List<SubmitRecord> records = cd.submitRecords(SubmitRuleOptions.defaults()); |
| boolean areForced = |
| records.stream().anyMatch(record -> SubmitRecord.Status.FORCED.equals(record.status)); |
| List<LabelType> labelTypes = cd.getLabelTypes().getLabelTypes(); |
| ObjectId commitId = cd.currentPatchSet().commitId(); |
| Map<String, List<SubmitRequirementResult>> srsByName = |
| records.stream() |
| // Filter out the "FORCED" submit record. This is a marker submit record that was just |
| // used to indicate that all other records were forced. "FORCED" means that the change |
| // was pushed with the %submit option bypassing submit rules. |
| .filter(r -> !SubmitRecord.Status.FORCED.equals(r.status)) |
| .map(r -> createResult(r, labelTypes, commitId, areForced)) |
| .flatMap(List::stream) |
| .collect(Collectors.groupingBy(sr -> sr.submitRequirement().name())); |
| |
| // We convert submit records to submit requirements by generating a separate |
| // submit requirement result for each available label in each submit record. |
| // The SR status is derived from the label status of the submit record. |
| // This conversion might result in duplicate entries. |
| // One such example can be a prolog rule emitting the same label name twice. |
| // Another case might happen if two different submit rules emit the same label |
| // name. In such cases, we need to merge these entries and return a single submit |
| // requirement result. If both entries agree in their status, return any of them. |
| // Otherwise, favour the entry that is blocking submission. |
| ImmutableMap.Builder<SubmitRequirement, SubmitRequirementResult> result = |
| ImmutableMap.builder(); |
| for (Map.Entry<String, List<SubmitRequirementResult>> entry : srsByName.entrySet()) { |
| if (entry.getValue().size() == 1) { |
| SubmitRequirementResult srResult = entry.getValue().iterator().next(); |
| result.put(srResult.submitRequirement(), srResult); |
| continue; |
| } |
| // If all submit requirements with the same name match in status, return the first one. |
| List<SubmitRequirementResult> resultsSameName = entry.getValue(); |
| boolean allNonBlocking = resultsSameName.stream().allMatch(sr -> sr.fulfilled()); |
| if (allNonBlocking) { |
| result.put(resultsSameName.get(0).submitRequirement(), resultsSameName.get(0)); |
| } else { |
| // Otherwise, return the first submit requirement result that is blocking submission. |
| Optional<SubmitRequirementResult> nonFulfilled = |
| resultsSameName.stream().filter(sr -> !sr.fulfilled()).findFirst(); |
| if (nonFulfilled.isPresent()) { |
| result.put(nonFulfilled.get().submitRequirement(), nonFulfilled.get()); |
| } |
| } |
| } |
| return result.build(); |
| } |
| |
| @VisibleForTesting |
| static ImmutableList<SubmitRequirementResult> createResult( |
| SubmitRecord record, List<LabelType> labelTypes, ObjectId psCommitId, boolean isForced) { |
| ImmutableList<SubmitRequirementResult> results; |
| if (record.ruleName != null && record.ruleName.equals(DefaultSubmitRule.RULE_NAME)) { |
| results = createFromDefaultSubmitRecord(record.labels, labelTypes, psCommitId, isForced); |
| } else { |
| results = createFromCustomSubmitRecord(record, psCommitId, isForced); |
| } |
| logger.atFine().log("Converted submit record %s to submit requirements %s", record, results); |
| return results; |
| } |
| |
| private static ImmutableList<SubmitRequirementResult> createFromDefaultSubmitRecord( |
| @Nullable List<Label> labels, |
| List<LabelType> labelTypes, |
| ObjectId psCommitId, |
| boolean isForced) { |
| ImmutableList.Builder<SubmitRequirementResult> result = ImmutableList.builder(); |
| if (labels == null) { |
| return result.build(); |
| } |
| for (Label label : labels) { |
| if (skipSubmitRequirementFor(label)) { |
| continue; |
| } |
| Optional<LabelType> maybeLabelType = getLabelType(labelTypes, label.label); |
| if (!maybeLabelType.isPresent()) { |
| // Label type might have been removed from the project config. We don't have information |
| // if it was blocking or not, hence we skip the label. |
| continue; |
| } |
| LabelType labelType = maybeLabelType.get(); |
| if (!isBlocking(labelType)) { |
| continue; |
| } |
| ImmutableList<String> atoms = toExpressionAtomList(labelType); |
| SubmitRequirement.Builder req = |
| SubmitRequirement.builder() |
| .setName(label.label) |
| .setSubmittabilityExpression(toExpression(atoms)) |
| .setAllowOverrideInChildProjects(labelType.isCanOverride()); |
| result.add( |
| SubmitRequirementResult.builder() |
| .legacy(Optional.of(true)) |
| .submitRequirement(req.build()) |
| .submittabilityExpressionResult( |
| createExpressionResult(toExpression(atoms), mapStatus(label), atoms)) |
| .patchSetCommitId(psCommitId) |
| .forced(Optional.of(isForced)) |
| .build()); |
| } |
| return result.build(); |
| } |
| |
| private static ImmutableList<SubmitRequirementResult> createFromCustomSubmitRecord( |
| SubmitRecord record, ObjectId psCommitId, boolean isForced) { |
| String ruleName = record.ruleName != null ? record.ruleName : "Custom-Rule"; |
| if (record.labels == null || record.labels.isEmpty()) { |
| SubmitRequirement sr = |
| SubmitRequirement.builder() |
| .setName(ruleName) |
| .setSubmittabilityExpression( |
| SubmitRequirementExpression.create(String.format("rule:%s", ruleName))) |
| .setAllowOverrideInChildProjects(false) |
| .build(); |
| return ImmutableList.of( |
| SubmitRequirementResult.builder() |
| .legacy(Optional.of(true)) |
| .submitRequirement(sr) |
| .submittabilityExpressionResult( |
| createExpressionResult( |
| sr.submittabilityExpression(), |
| mapStatus(record), |
| ImmutableList.of(ruleName), |
| record.errorMessage)) |
| .patchSetCommitId(psCommitId) |
| .forced(Optional.of(isForced)) |
| .build()); |
| } |
| ImmutableList.Builder<SubmitRequirementResult> result = ImmutableList.builder(); |
| for (Label label : record.labels) { |
| if (skipSubmitRequirementFor(label) |
| || |
| // If SubmitRecord is a PASS, then skip all the requirements |
| // that are not a PASS as they would block the overall submit requirement |
| // status from being a PASS |
| (mapStatus(record) == Status.PASS && mapStatus(label) != Status.PASS)) { |
| continue; |
| } |
| String expressionString = String.format("label:%s=%s", label.label, ruleName); |
| SubmitRequirement sr = |
| SubmitRequirement.builder() |
| .setName(label.label) |
| .setSubmittabilityExpression(SubmitRequirementExpression.create(expressionString)) |
| .setAllowOverrideInChildProjects(false) |
| .build(); |
| result.add( |
| SubmitRequirementResult.builder() |
| .legacy(Optional.of(true)) |
| .submitRequirement(sr) |
| .submittabilityExpressionResult( |
| createExpressionResult( |
| sr.submittabilityExpression(), |
| mapStatus(label), |
| ImmutableList.of(expressionString))) |
| .patchSetCommitId(psCommitId) |
| .build()); |
| } |
| return result.build(); |
| } |
| |
| private static boolean isBlocking(LabelType labelType) { |
| return labelType.getFunction().isBlock() || labelType.getFunction().isRequired(); |
| } |
| |
| private static SubmitRequirementExpression toExpression(List<String> atoms) { |
| return SubmitRequirementExpression.create(String.join(" ", atoms)); |
| } |
| |
| private static ImmutableList<String> toExpressionAtomList(LabelType lt) { |
| String ignoreSelfApproval = |
| lt.isIgnoreSelfApproval() ? ",user=" + ChangeQueryBuilder.ARG_ID_NON_UPLOADER : ""; |
| switch (lt.getFunction()) { |
| case MAX_WITH_BLOCK: |
| return ImmutableList.of( |
| String.format("label:%s=MAX", lt.getName()) + ignoreSelfApproval, |
| String.format("-label:%s=MIN", lt.getName())); |
| case ANY_WITH_BLOCK: |
| return ImmutableList.of(String.format(String.format("-label:%s=MIN", lt.getName()))); |
| case MAX_NO_BLOCK: |
| return ImmutableList.of( |
| String.format(String.format("label:%s=MAX", lt.getName())) + ignoreSelfApproval); |
| case NO_BLOCK: |
| case NO_OP: |
| case PATCH_SET_LOCK: |
| default: |
| return ImmutableList.of(); |
| } |
| } |
| |
| private static Status mapStatus(Label label) { |
| SubmitRequirementExpressionResult.Status status = Status.PASS; |
| switch (label.status) { |
| case OK: |
| case MAY: |
| status = Status.PASS; |
| break; |
| case REJECT: |
| case NEED: |
| case IMPOSSIBLE: |
| status = Status.FAIL; |
| break; |
| } |
| return status; |
| } |
| |
| private static Status mapStatus(SubmitRecord submitRecord) { |
| switch (submitRecord.status) { |
| case OK: |
| case CLOSED: |
| case FORCED: |
| return Status.PASS; |
| case NOT_READY: |
| return Status.FAIL; |
| case RULE_ERROR: |
| default: |
| return Status.ERROR; |
| } |
| } |
| |
| private static SubmitRequirementExpressionResult createExpressionResult( |
| SubmitRequirementExpression expression, Status status, ImmutableList<String> atoms) { |
| return SubmitRequirementExpressionResult.create( |
| expression, |
| status, |
| status == Status.PASS ? atoms : ImmutableList.of(), |
| status == Status.FAIL ? atoms : ImmutableList.of()); |
| } |
| |
| private static SubmitRequirementExpressionResult createExpressionResult( |
| SubmitRequirementExpression expression, |
| Status status, |
| ImmutableList<String> atoms, |
| String errorMessage) { |
| return SubmitRequirementExpressionResult.create( |
| expression, |
| status, |
| status == Status.PASS ? atoms : ImmutableList.of(), |
| status == Status.FAIL ? atoms : ImmutableList.of(), |
| Optional.ofNullable(Strings.emptyToNull(errorMessage))); |
| } |
| |
| private static Optional<LabelType> getLabelType(List<LabelType> labelTypes, String labelName) { |
| List<LabelType> label = |
| labelTypes.stream() |
| .filter(lt -> lt.getName().equals(labelName)) |
| .collect(Collectors.toList()); |
| if (label.isEmpty()) { |
| // Label might have been removed from the project. |
| logger.atFine().log("Label '%s' was not found for the project.", labelName); |
| return Optional.empty(); |
| } else if (label.size() > 1) { |
| logger.atWarning().log("Found more than one label definition for label name '%s'", labelName); |
| return Optional.empty(); |
| } |
| return Optional.of(label.get(0)); |
| } |
| |
| /** |
| * Returns true if we should skip creating a "submit requirement" result out of the "submit |
| * record" label. |
| */ |
| private static boolean skipSubmitRequirementFor(SubmitRecord.Label label) { |
| return label.status == SubmitRecord.Label.Status.MAY; |
| } |
| } |