Line data Source code
1 : // Copyright (C) 2014 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.approval;
16 :
17 : import static com.google.common.collect.ImmutableList.toImmutableList;
18 : import static com.google.gerrit.server.project.ProjectCache.illegalState;
19 :
20 : import com.google.auto.value.AutoValue;
21 : import com.google.common.annotations.VisibleForTesting;
22 : import com.google.common.collect.HashBasedTable;
23 : import com.google.common.collect.ImmutableList;
24 : import com.google.common.collect.ImmutableSet;
25 : import com.google.common.collect.Table;
26 : import com.google.common.flogger.FluentLogger;
27 : import com.google.gerrit.entities.Account;
28 : import com.google.gerrit.entities.LabelType;
29 : import com.google.gerrit.entities.LabelTypes;
30 : import com.google.gerrit.entities.PatchSet;
31 : import com.google.gerrit.entities.PatchSetApproval;
32 : import com.google.gerrit.entities.Project;
33 : import com.google.gerrit.exceptions.StorageException;
34 : import com.google.gerrit.extensions.client.ChangeKind;
35 : import com.google.gerrit.index.query.QueryParseException;
36 : import com.google.gerrit.server.PatchSetUtil;
37 : import com.google.gerrit.server.change.ChangeKindCache;
38 : import com.google.gerrit.server.change.LabelNormalizer;
39 : import com.google.gerrit.server.git.GitRepositoryManager;
40 : import com.google.gerrit.server.logging.Metadata;
41 : import com.google.gerrit.server.logging.TraceContext;
42 : import com.google.gerrit.server.logging.TraceContext.TraceTimer;
43 : import com.google.gerrit.server.notedb.ChangeNotes;
44 : import com.google.gerrit.server.project.ProjectCache;
45 : import com.google.gerrit.server.project.ProjectState;
46 : import com.google.gerrit.server.query.approval.ApprovalContext;
47 : import com.google.gerrit.server.query.approval.ApprovalQueryBuilder;
48 : import com.google.gerrit.server.util.ManualRequestContext;
49 : import com.google.gerrit.server.util.OneOffRequestContext;
50 : import com.google.inject.Inject;
51 : import com.google.inject.Singleton;
52 : import java.io.IOException;
53 : import java.util.Map;
54 : import java.util.Optional;
55 : import org.eclipse.jgit.lib.Config;
56 : import org.eclipse.jgit.lib.Repository;
57 : import org.eclipse.jgit.revwalk.RevWalk;
58 :
59 : /**
60 : * Computes copied approvals for a given patch set.
61 : *
62 : * <p>Approvals are copied if:
63 : *
64 : * <ul>
65 : * <li>the approval on the previous patch set matches the copy condition of its label
66 : * <li>the approval is not overridden by a current approval on the patch set
67 : * </ul>
68 : *
69 : * <p>Callers should store the copied approvals in NoteDb when a new patch set is created.
70 : */
71 : @Singleton
72 : @VisibleForTesting
73 : public class ApprovalCopier {
74 146 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
75 :
76 : @AutoValue
77 51 : public abstract static class Result {
78 : /**
79 : * Approvals that have been copied from the previous patch set.
80 : *
81 : * <p>An approval is copied if:
82 : *
83 : * <ul>
84 : * <li>the approval on the previous patch set matches the copy condition of its label
85 : * <li>the approval is not overridden by a current approval on the patch set
86 : * </ul>
87 : */
88 : public abstract ImmutableSet<PatchSetApproval> copiedApprovals();
89 :
90 : /**
91 : * Approvals on the previous patch set that have not been copied to the patch set.
92 : *
93 : * <p>These approvals didn't match the copy condition of their labels and hence haven't been
94 : * copied.
95 : *
96 : * <p>Only returns non-copied approvals of the previous patch set. Approvals from earlier patch
97 : * sets that were outdated before are not included.
98 : */
99 : public abstract ImmutableSet<PatchSetApproval> outdatedApprovals();
100 :
101 : static Result empty() {
102 1 : return create(
103 1 : /* copiedApprovals= */ ImmutableSet.of(), /* outdatedApprovals= */ ImmutableSet.of());
104 : }
105 :
106 : @VisibleForTesting
107 : public static Result create(
108 : ImmutableSet<PatchSetApproval> copiedApprovals,
109 : ImmutableSet<PatchSetApproval> outdatedApprovals) {
110 51 : return new AutoValue_ApprovalCopier_Result(copiedApprovals, outdatedApprovals);
111 : }
112 : }
113 :
114 : private final GitRepositoryManager repoManager;
115 : private final ProjectCache projectCache;
116 : private final ChangeKindCache changeKindCache;
117 : private final PatchSetUtil psUtil;
118 : private final LabelNormalizer labelNormalizer;
119 : private final ApprovalQueryBuilder approvalQueryBuilder;
120 : private final OneOffRequestContext requestContext;
121 :
122 : @Inject
123 : ApprovalCopier(
124 : GitRepositoryManager repoManager,
125 : ProjectCache projectCache,
126 : ChangeKindCache changeKindCache,
127 : PatchSetUtil psUtil,
128 : LabelNormalizer labelNormalizer,
129 : ApprovalQueryBuilder approvalQueryBuilder,
130 146 : OneOffRequestContext requestContext) {
131 146 : this.repoManager = repoManager;
132 146 : this.projectCache = projectCache;
133 146 : this.changeKindCache = changeKindCache;
134 146 : this.psUtil = psUtil;
135 146 : this.labelNormalizer = labelNormalizer;
136 146 : this.approvalQueryBuilder = approvalQueryBuilder;
137 146 : this.requestContext = requestContext;
138 146 : }
139 :
140 : /**
141 : * Returns all copied approvals that apply to the given patch set.
142 : *
143 : * <p>Approvals are copied if:
144 : *
145 : * <ul>
146 : * <li>the approval on the previous patch set matches the copy condition of its label
147 : * <li>the approval is not overridden by a current approval on the patch set
148 : * </ul>
149 : */
150 : @VisibleForTesting
151 : public Result forPatchSet(ChangeNotes notes, PatchSet ps, RevWalk rw, Config repoConfig) {
152 : ProjectState project;
153 51 : try (TraceTimer traceTimer =
154 51 : TraceContext.newTimer(
155 : "Computing labels for patch set",
156 51 : Metadata.builder()
157 51 : .changeId(notes.load().getChangeId().get())
158 51 : .patchSetId(ps.id().get())
159 51 : .build())) {
160 51 : project =
161 : projectCache
162 51 : .get(notes.getProjectName())
163 51 : .orElseThrow(illegalState(notes.getProjectName()));
164 51 : return computeForPatchSet(project.getLabelTypes(), notes, ps, rw, repoConfig);
165 : }
166 : }
167 :
168 : /**
169 : * Returns all follow-up patch sets of the given patch set to which the given approval is
170 : * copyable.
171 : *
172 : * <p>An approval is considered as copyable to a follow-up patch set if it matches the copy rules
173 : * of the label and it is copyable to all intermediate follow-up patch sets as well.
174 : *
175 : * <p>The returned follow-up patch sets are returned in the order of their patch set IDs.
176 : *
177 : * <p>Note: This method only checks the copy rules to detect if the approval is copyable. There
178 : * are other factors, not checked here, that can prevent the copying of the approval to the
179 : * returned follow-up patch sets (e.g. if they already have a matching non-copy approval that
180 : * prevents the copying).
181 : *
182 : * @param changeNotes the change notes
183 : * @param sourcePatchSet the patch set on which the approval was applied
184 : * @param approverId the account ID of the user that applied the approval
185 : * @param label the label of the approval that was applied
186 : * @param approvalValue the value of the approval that was applied
187 : * @return the follow-up patch sets to which the approval is copyable, ordered by patch set ID
188 : */
189 : public ImmutableList<PatchSet.Id> forApproval(
190 : ChangeNotes changeNotes,
191 : PatchSet sourcePatchSet,
192 : Account.Id approverId,
193 : String label,
194 : short approvalValue)
195 : throws IOException {
196 2 : ImmutableList.Builder<PatchSet.Id> targetPatchSetsBuilder = ImmutableList.builder();
197 :
198 2 : Optional<LabelType> labelType =
199 : projectCache
200 2 : .get(changeNotes.getProjectName())
201 2 : .orElseThrow(illegalState(changeNotes.getProjectName()))
202 2 : .getLabelTypes()
203 2 : .byLabel(label);
204 2 : if (!labelType.isPresent()) {
205 : // no label type exists for this label, hence this approval cannot be copied
206 0 : return ImmutableList.of();
207 : }
208 :
209 2 : try (Repository repo = repoManager.openRepository(changeNotes.getProjectName());
210 2 : RevWalk revWalk = new RevWalk(repo)) {
211 2 : ImmutableList<PatchSet.Id> followUpPatchSets =
212 2 : changeNotes.getPatchSets().keySet().stream()
213 2 : .filter(psId -> psId.get() > sourcePatchSet.id().get())
214 2 : .collect(toImmutableList());
215 2 : PatchSet priorPatchSet = sourcePatchSet;
216 :
217 : // Iterate over the follow-up patch sets in order to copy the approval from their prior patch
218 : // set if possible (copy from PS N-1 to PS N).
219 2 : for (PatchSet.Id followUpPatchSetId : followUpPatchSets) {
220 2 : PatchSet followUpPatchSet = psUtil.get(changeNotes, followUpPatchSetId);
221 2 : ChangeKind changeKind =
222 2 : changeKindCache.getChangeKind(
223 2 : changeNotes.getProjectName(),
224 : revWalk,
225 2 : repo.getConfig(),
226 2 : priorPatchSet.commitId(),
227 2 : followUpPatchSet.commitId());
228 2 : boolean isMerge = isMerge(changeNotes.getProjectName(), revWalk, followUpPatchSet);
229 :
230 2 : if (canCopy(
231 : changeNotes,
232 2 : priorPatchSet.id(),
233 : followUpPatchSet,
234 : approverId,
235 2 : labelType.get(),
236 : approvalValue,
237 : changeKind,
238 : isMerge,
239 : revWalk,
240 2 : repo.getConfig())) {
241 1 : targetPatchSetsBuilder.add(followUpPatchSetId);
242 : } else {
243 : // The approval is not copyable to this follow-up patch set.
244 : // This means it's also not copyable to any further follow-up patch set and we should stop
245 : // the loop here.
246 : break;
247 : }
248 1 : priorPatchSet = followUpPatchSet;
249 1 : }
250 : }
251 2 : return targetPatchSetsBuilder.build();
252 : }
253 :
254 : private boolean canCopy(
255 : ChangeNotes changeNotes,
256 : PatchSet.Id sourcePatchSetId,
257 : PatchSet targetPatchSet,
258 : Account.Id approverId,
259 : LabelType labelType,
260 : short approvalValue,
261 : ChangeKind changeKind,
262 : boolean isMerge,
263 : RevWalk revWalk,
264 : Config repoConfig) {
265 15 : if (!labelType.getCopyCondition().isPresent()) {
266 4 : return false;
267 : }
268 15 : ApprovalContext ctx =
269 15 : ApprovalContext.create(
270 : changeNotes,
271 : sourcePatchSetId,
272 : approverId,
273 : labelType,
274 : approvalValue,
275 : targetPatchSet,
276 : changeKind,
277 : isMerge,
278 : revWalk,
279 : repoConfig);
280 : try {
281 : // Use a request context to run checks as an internal user with expanded visibility. This is
282 : // so that the output of the copy condition does not depend on who is running the current
283 : // request (e.g. a group used in this query might not be visible to the person sending this
284 : // request).
285 15 : try (ManualRequestContext ignored = requestContext.open()) {
286 15 : return approvalQueryBuilder
287 15 : .parse(labelType.getCopyCondition().get())
288 15 : .asMatchable()
289 15 : .match(ctx);
290 : }
291 0 : } catch (QueryParseException e) {
292 0 : logger.atWarning().withCause(e).log(
293 : "Unable to copy label because config is invalid. This should have been caught before.");
294 0 : return false;
295 : }
296 : }
297 :
298 : private Result computeForPatchSet(
299 : LabelTypes labelTypes,
300 : ChangeNotes notes,
301 : PatchSet targetPatchSet,
302 : RevWalk rw,
303 : Config repoConfig) {
304 51 : Project.NameKey projectName = notes.getProjectName();
305 51 : PatchSet.Id targetPsId = targetPatchSet.id();
306 :
307 : // Bail out immediately if this is the first patch set. Return only approvals granted on the
308 : // given patch set.
309 51 : if (targetPsId.get() == 1) {
310 1 : return Result.empty();
311 : }
312 51 : Map.Entry<PatchSet.Id, PatchSet> priorPatchSet =
313 51 : notes.load().getPatchSets().lowerEntry(targetPsId);
314 51 : if (priorPatchSet == null) {
315 0 : return Result.empty();
316 : }
317 :
318 51 : Table<String, Account.Id, PatchSetApproval> currentApprovalsByUser = HashBasedTable.create();
319 51 : ImmutableList<PatchSetApproval> nonCopiedApprovalsForGivenPatchSet =
320 51 : notes.load().getApprovals().onlyNonCopied().get(targetPatchSet.id());
321 51 : nonCopiedApprovalsForGivenPatchSet.forEach(
322 1 : psa -> currentApprovalsByUser.put(psa.label(), psa.accountId(), psa));
323 :
324 51 : Table<String, Account.Id, PatchSetApproval> copiedApprovalsByUser = HashBasedTable.create();
325 51 : ImmutableSet.Builder<PatchSetApproval> outdatedApprovalsBuilder = ImmutableSet.builder();
326 :
327 51 : ImmutableList<PatchSetApproval> priorApprovals =
328 51 : notes.load().getApprovals().all().get(priorPatchSet.getKey());
329 :
330 : // Add labels from the previous patch set to the result in case the label isn't already there
331 : // and settings as well as change kind allow copying.
332 51 : ChangeKind changeKind =
333 51 : changeKindCache.getChangeKind(
334 : projectName,
335 : rw,
336 : repoConfig,
337 51 : priorPatchSet.getValue().commitId(),
338 51 : targetPatchSet.commitId());
339 51 : boolean isMerge = isMerge(projectName, rw, targetPatchSet);
340 51 : logger.atFine().log(
341 : "change kind for patch set %d of change %d against prior patch set %s is %s",
342 51 : targetPatchSet.id().get(),
343 51 : targetPatchSet.id().changeId().get(),
344 51 : priorPatchSet.getValue().id().changeId(),
345 : changeKind);
346 :
347 51 : for (PatchSetApproval priorPsa : priorApprovals) {
348 15 : if (priorPsa.value() == 0) {
349 : // approvals with a zero vote record the deletion of a vote,
350 : // they should neither be copied nor be reported as outdated, hence just skip them
351 2 : continue;
352 : }
353 :
354 15 : Optional<LabelType> labelType = labelTypes.byLabel(priorPsa.labelId());
355 15 : if (!labelType.isPresent()) {
356 2 : logger.atFine().log(
357 : "approval %d on label %s of patch set %d of change %d cannot be copied"
358 : + " to patch set %d because the label no longer exists on project %s",
359 2 : priorPsa.value(),
360 2 : priorPsa.label(),
361 2 : priorPsa.key().patchSetId().get(),
362 2 : priorPsa.key().patchSetId().changeId().get(),
363 2 : targetPsId.get(),
364 : projectName);
365 2 : outdatedApprovalsBuilder.add(priorPsa);
366 2 : continue;
367 : }
368 14 : if (canCopy(
369 : notes,
370 14 : priorPsa.patchSetId(),
371 : targetPatchSet,
372 14 : priorPsa.accountId(),
373 14 : labelType.get(),
374 14 : priorPsa.value(),
375 : changeKind,
376 : isMerge,
377 : rw,
378 : repoConfig)) {
379 12 : if (!currentApprovalsByUser.contains(priorPsa.label(), priorPsa.accountId())) {
380 12 : copiedApprovalsByUser.put(
381 12 : priorPsa.label(),
382 12 : priorPsa.accountId(),
383 12 : priorPsa.copyWithPatchSet(targetPatchSet.id()));
384 : }
385 : } else {
386 10 : outdatedApprovalsBuilder.add(priorPsa);
387 10 : continue;
388 : }
389 12 : }
390 :
391 51 : ImmutableSet<PatchSetApproval> copiedApprovals =
392 51 : labelNormalizer.normalize(notes, copiedApprovalsByUser.values()).getNormalized();
393 51 : return Result.create(copiedApprovals, outdatedApprovalsBuilder.build());
394 : }
395 :
396 : private boolean isMerge(Project.NameKey project, RevWalk rw, PatchSet patchSet) {
397 : try {
398 51 : return rw.parseCommit(patchSet.commitId()).getParentCount() > 1;
399 0 : } catch (IOException e) {
400 0 : throw new StorageException(
401 0 : String.format(
402 : "failed to check if patch set %d of change %s in project %s is a merge commit",
403 0 : patchSet.id().get(), patchSet.id().changeId(), project),
404 : e);
405 : }
406 : }
407 : }
|