Line data Source code
1 : // Copyright (C) 2021 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.project;
16 :
17 : import com.google.common.annotations.VisibleForTesting;
18 : import com.google.common.base.Strings;
19 : import com.google.common.collect.ImmutableList;
20 : import com.google.common.collect.ImmutableMap;
21 : import com.google.common.flogger.FluentLogger;
22 : import com.google.gerrit.common.Nullable;
23 : import com.google.gerrit.entities.LabelType;
24 : import com.google.gerrit.entities.SubmitRecord;
25 : import com.google.gerrit.entities.SubmitRecord.Label;
26 : import com.google.gerrit.entities.SubmitRequirement;
27 : import com.google.gerrit.entities.SubmitRequirementExpression;
28 : import com.google.gerrit.entities.SubmitRequirementExpressionResult;
29 : import com.google.gerrit.entities.SubmitRequirementExpressionResult.Status;
30 : import com.google.gerrit.entities.SubmitRequirementResult;
31 : import com.google.gerrit.server.query.change.ChangeData;
32 : import com.google.gerrit.server.query.change.ChangeQueryBuilder;
33 : import com.google.gerrit.server.rules.DefaultSubmitRule;
34 : import java.util.List;
35 : import java.util.Map;
36 : import java.util.Optional;
37 : import java.util.stream.Collectors;
38 : import org.eclipse.jgit.lib.ObjectId;
39 :
40 : /**
41 : * Convert {@link com.google.gerrit.entities.SubmitRecord} entities to {@link
42 : * com.google.gerrit.entities.SubmitRequirementResult}s.
43 : */
44 : public class SubmitRequirementsAdapter {
45 103 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
46 :
47 : private SubmitRequirementsAdapter() {}
48 :
49 : /**
50 : * Retrieve legacy submit records (created by label functions and other {@link
51 : * com.google.gerrit.server.rules.SubmitRule}s) and convert them to submit requirement results.
52 : */
53 : public static Map<SubmitRequirement, SubmitRequirementResult> getLegacyRequirements(
54 : ChangeData cd) {
55 : // We use SubmitRuleOptions.defaults() which does not recompute submit rules for closed changes.
56 : // This doesn't have an effect since we never call this class (i.e. to evaluate submit
57 : // requirements) for closed changes.
58 103 : List<SubmitRecord> records = cd.submitRecords(SubmitRuleOptions.defaults());
59 103 : boolean areForced =
60 103 : records.stream().anyMatch(record -> SubmitRecord.Status.FORCED.equals(record.status));
61 103 : List<LabelType> labelTypes = cd.getLabelTypes().getLabelTypes();
62 103 : ObjectId commitId = cd.currentPatchSet().commitId();
63 103 : Map<String, List<SubmitRequirementResult>> srsByName =
64 103 : records.stream()
65 : // Filter out the "FORCED" submit record. This is a marker submit record that was just
66 : // used to indicate that all other records were forced. "FORCED" means that the change
67 : // was pushed with the %submit option bypassing submit rules.
68 103 : .filter(r -> !SubmitRecord.Status.FORCED.equals(r.status))
69 103 : .map(r -> createResult(r, labelTypes, commitId, areForced))
70 103 : .flatMap(List::stream)
71 103 : .collect(Collectors.groupingBy(sr -> sr.submitRequirement().name()));
72 :
73 : // We convert submit records to submit requirements by generating a separate
74 : // submit requirement result for each available label in each submit record.
75 : // The SR status is derived from the label status of the submit record.
76 : // This conversion might result in duplicate entries.
77 : // One such example can be a prolog rule emitting the same label name twice.
78 : // Another case might happen if two different submit rules emit the same label
79 : // name. In such cases, we need to merge these entries and return a single submit
80 : // requirement result. If both entries agree in their status, return any of them.
81 : // Otherwise, favour the entry that is blocking submission.
82 : ImmutableMap.Builder<SubmitRequirement, SubmitRequirementResult> result =
83 103 : ImmutableMap.builder();
84 103 : for (Map.Entry<String, List<SubmitRequirementResult>> entry : srsByName.entrySet()) {
85 103 : if (entry.getValue().size() == 1) {
86 103 : SubmitRequirementResult srResult = entry.getValue().iterator().next();
87 103 : result.put(srResult.submitRequirement(), srResult);
88 103 : continue;
89 : }
90 : // If all submit requirements with the same name match in status, return the first one.
91 2 : List<SubmitRequirementResult> resultsSameName = entry.getValue();
92 2 : boolean allNonBlocking = resultsSameName.stream().allMatch(sr -> sr.fulfilled());
93 2 : if (allNonBlocking) {
94 1 : result.put(resultsSameName.get(0).submitRequirement(), resultsSameName.get(0));
95 : } else {
96 : // Otherwise, return the first submit requirement result that is blocking submission.
97 2 : Optional<SubmitRequirementResult> nonFulfilled =
98 2 : resultsSameName.stream().filter(sr -> !sr.fulfilled()).findFirst();
99 2 : if (nonFulfilled.isPresent()) {
100 2 : result.put(nonFulfilled.get().submitRequirement(), nonFulfilled.get());
101 : }
102 : }
103 2 : }
104 103 : return result.build();
105 : }
106 :
107 : @VisibleForTesting
108 : static List<SubmitRequirementResult> createResult(
109 : SubmitRecord record, List<LabelType> labelTypes, ObjectId psCommitId, boolean isForced) {
110 : List<SubmitRequirementResult> results;
111 103 : if (record.ruleName != null && record.ruleName.equals(DefaultSubmitRule.RULE_NAME)) {
112 103 : results = createFromDefaultSubmitRecord(record.labels, labelTypes, psCommitId, isForced);
113 : } else {
114 14 : results = createFromCustomSubmitRecord(record, psCommitId, isForced);
115 : }
116 103 : logger.atFine().log("Converted submit record %s to submit requirements %s", record, results);
117 103 : return results;
118 : }
119 :
120 : private static List<SubmitRequirementResult> createFromDefaultSubmitRecord(
121 : @Nullable List<Label> labels,
122 : List<LabelType> labelTypes,
123 : ObjectId psCommitId,
124 : boolean isForced) {
125 103 : ImmutableList.Builder<SubmitRequirementResult> result = ImmutableList.builder();
126 103 : if (labels == null) {
127 0 : return result.build();
128 : }
129 103 : for (Label label : labels) {
130 103 : if (skipSubmitRequirementFor(label)) {
131 8 : continue;
132 : }
133 103 : Optional<LabelType> maybeLabelType = getLabelType(labelTypes, label.label);
134 103 : if (!maybeLabelType.isPresent()) {
135 : // Label type might have been removed from the project config. We don't have information
136 : // if it was blocking or not, hence we skip the label.
137 1 : continue;
138 : }
139 103 : LabelType labelType = maybeLabelType.get();
140 103 : if (!isBlocking(labelType)) {
141 0 : continue;
142 : }
143 103 : ImmutableList<String> atoms = toExpressionAtomList(labelType);
144 : SubmitRequirement.Builder req =
145 103 : SubmitRequirement.builder()
146 103 : .setName(label.label)
147 103 : .setSubmittabilityExpression(toExpression(atoms))
148 103 : .setAllowOverrideInChildProjects(labelType.isCanOverride());
149 103 : result.add(
150 103 : SubmitRequirementResult.builder()
151 103 : .legacy(Optional.of(true))
152 103 : .submitRequirement(req.build())
153 103 : .submittabilityExpressionResult(
154 103 : createExpressionResult(toExpression(atoms), mapStatus(label), atoms))
155 103 : .patchSetCommitId(psCommitId)
156 103 : .forced(Optional.of(isForced))
157 103 : .build());
158 103 : }
159 103 : return result.build();
160 : }
161 :
162 : private static List<SubmitRequirementResult> createFromCustomSubmitRecord(
163 : SubmitRecord record, ObjectId psCommitId, boolean isForced) {
164 14 : String ruleName = record.ruleName != null ? record.ruleName : "Custom-Rule";
165 14 : if (record.labels == null || record.labels.isEmpty()) {
166 : SubmitRequirement sr =
167 7 : SubmitRequirement.builder()
168 7 : .setName(ruleName)
169 7 : .setSubmittabilityExpression(
170 7 : SubmitRequirementExpression.create(String.format("rule:%s", ruleName)))
171 7 : .setAllowOverrideInChildProjects(false)
172 7 : .build();
173 7 : return ImmutableList.of(
174 7 : SubmitRequirementResult.builder()
175 7 : .legacy(Optional.of(true))
176 7 : .submitRequirement(sr)
177 7 : .submittabilityExpressionResult(
178 7 : createExpressionResult(
179 7 : sr.submittabilityExpression(),
180 7 : mapStatus(record),
181 7 : ImmutableList.of(ruleName),
182 : record.errorMessage))
183 7 : .patchSetCommitId(psCommitId)
184 7 : .forced(Optional.of(isForced))
185 7 : .build());
186 : }
187 9 : ImmutableList.Builder<SubmitRequirementResult> result = ImmutableList.builder();
188 9 : for (Label label : record.labels) {
189 9 : if (skipSubmitRequirementFor(label)) {
190 1 : continue;
191 : }
192 9 : String expressionString = String.format("label:%s=%s", label.label, ruleName);
193 : SubmitRequirement sr =
194 9 : SubmitRequirement.builder()
195 9 : .setName(label.label)
196 9 : .setSubmittabilityExpression(SubmitRequirementExpression.create(expressionString))
197 9 : .setAllowOverrideInChildProjects(false)
198 9 : .build();
199 9 : result.add(
200 9 : SubmitRequirementResult.builder()
201 9 : .legacy(Optional.of(true))
202 9 : .submitRequirement(sr)
203 9 : .submittabilityExpressionResult(
204 9 : createExpressionResult(
205 9 : sr.submittabilityExpression(),
206 9 : mapStatus(label),
207 9 : ImmutableList.of(expressionString)))
208 9 : .patchSetCommitId(psCommitId)
209 9 : .build());
210 9 : }
211 9 : return result.build();
212 : }
213 :
214 : private static boolean isBlocking(LabelType labelType) {
215 103 : return labelType.getFunction().isBlock() || labelType.getFunction().isRequired();
216 : }
217 :
218 : private static SubmitRequirementExpression toExpression(List<String> atoms) {
219 103 : return SubmitRequirementExpression.create(String.join(" ", atoms));
220 : }
221 :
222 : private static ImmutableList<String> toExpressionAtomList(LabelType lt) {
223 : String ignoreSelfApproval =
224 103 : lt.isIgnoreSelfApproval() ? ",user=" + ChangeQueryBuilder.ARG_ID_NON_UPLOADER : "";
225 103 : switch (lt.getFunction()) {
226 : case MAX_WITH_BLOCK:
227 103 : return ImmutableList.of(
228 103 : String.format("label:%s=MAX", lt.getName()) + ignoreSelfApproval,
229 103 : String.format("-label:%s=MIN", lt.getName()));
230 : case ANY_WITH_BLOCK:
231 2 : return ImmutableList.of(String.format(String.format("-label:%s=MIN", lt.getName())));
232 : case MAX_NO_BLOCK:
233 3 : return ImmutableList.of(
234 3 : String.format(String.format("label:%s=MAX", lt.getName())) + ignoreSelfApproval);
235 : case NO_BLOCK:
236 : case NO_OP:
237 : case PATCH_SET_LOCK:
238 : default:
239 0 : return ImmutableList.of();
240 : }
241 : }
242 :
243 : private static Status mapStatus(Label label) {
244 103 : SubmitRequirementExpressionResult.Status status = Status.PASS;
245 103 : switch (label.status) {
246 : case OK:
247 : case MAY:
248 58 : status = Status.PASS;
249 58 : break;
250 : case REJECT:
251 : case NEED:
252 : case IMPOSSIBLE:
253 103 : status = Status.FAIL;
254 : break;
255 : }
256 103 : return status;
257 : }
258 :
259 : private static Status mapStatus(SubmitRecord submitRecord) {
260 7 : switch (submitRecord.status) {
261 : case OK:
262 : case CLOSED:
263 : case FORCED:
264 6 : return Status.PASS;
265 : case NOT_READY:
266 6 : return Status.FAIL;
267 : case RULE_ERROR:
268 : default:
269 2 : return Status.ERROR;
270 : }
271 : }
272 :
273 : private static SubmitRequirementExpressionResult createExpressionResult(
274 : SubmitRequirementExpression expression, Status status, ImmutableList<String> atoms) {
275 103 : return SubmitRequirementExpressionResult.create(
276 : expression,
277 : status,
278 103 : status == Status.PASS ? atoms : ImmutableList.of(),
279 103 : status == Status.FAIL ? atoms : ImmutableList.of());
280 : }
281 :
282 : private static SubmitRequirementExpressionResult createExpressionResult(
283 : SubmitRequirementExpression expression,
284 : Status status,
285 : ImmutableList<String> atoms,
286 : String errorMessage) {
287 7 : return SubmitRequirementExpressionResult.create(
288 : expression,
289 : status,
290 7 : status == Status.PASS ? atoms : ImmutableList.of(),
291 7 : status == Status.FAIL ? atoms : ImmutableList.of(),
292 7 : Optional.ofNullable(Strings.emptyToNull(errorMessage)));
293 : }
294 :
295 : private static Optional<LabelType> getLabelType(List<LabelType> labelTypes, String labelName) {
296 103 : List<LabelType> label =
297 103 : labelTypes.stream()
298 103 : .filter(lt -> lt.getName().equals(labelName))
299 103 : .collect(Collectors.toList());
300 103 : if (label.isEmpty()) {
301 : // Label might have been removed from the project.
302 1 : logger.atFine().log("Label '%s' was not found for the project.", labelName);
303 1 : return Optional.empty();
304 103 : } else if (label.size() > 1) {
305 0 : logger.atWarning().log("Found more than one label definition for label name '%s'", labelName);
306 0 : return Optional.empty();
307 : }
308 103 : return Optional.of(label.get(0));
309 : }
310 :
311 : /**
312 : * Returns true if we should skip creating a "submit requirement" result out of the "submit
313 : * record" label.
314 : */
315 : private static boolean skipSubmitRequirementFor(SubmitRecord.Label label) {
316 103 : return label.status == SubmitRecord.Label.Status.MAY;
317 : }
318 : }
|