Line data Source code
1 : // Copyright (C) 2016 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.git.receive;
16 :
17 : import static com.google.common.collect.ImmutableList.toImmutableList;
18 : import static com.google.common.collect.ImmutableSet.toImmutableSet;
19 : import static com.google.gerrit.server.change.ReviewerModifier.newReviewerInputFromCommitIdentity;
20 : import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
21 : import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromReviewers;
22 : import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
23 : import static com.google.gerrit.server.project.ProjectCache.illegalState;
24 : import static java.util.stream.Collectors.joining;
25 : import static org.eclipse.jgit.lib.Constants.R_HEADS;
26 :
27 : import com.google.common.base.Strings;
28 : import com.google.common.collect.ImmutableList;
29 : import com.google.common.collect.Streams;
30 : import com.google.common.flogger.FluentLogger;
31 : import com.google.gerrit.common.Nullable;
32 : import com.google.gerrit.entities.Account;
33 : import com.google.gerrit.entities.Change;
34 : import com.google.gerrit.entities.LabelType;
35 : import com.google.gerrit.entities.PatchSet;
36 : import com.google.gerrit.entities.PatchSetApproval;
37 : import com.google.gerrit.entities.PatchSetInfo;
38 : import com.google.gerrit.entities.SubmissionId;
39 : import com.google.gerrit.extensions.api.changes.NotifyHandling;
40 : import com.google.gerrit.extensions.api.changes.ReviewerInput;
41 : import com.google.gerrit.extensions.client.ChangeKind;
42 : import com.google.gerrit.extensions.client.ReviewerState;
43 : import com.google.gerrit.extensions.registration.DynamicItem;
44 : import com.google.gerrit.extensions.restapi.BadRequestException;
45 : import com.google.gerrit.extensions.restapi.RestApiException;
46 : import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
47 : import com.google.gerrit.server.ChangeMessagesUtil;
48 : import com.google.gerrit.server.ChangeUtil;
49 : import com.google.gerrit.server.PatchSetUtil;
50 : import com.google.gerrit.server.account.AccountCache;
51 : import com.google.gerrit.server.account.AccountResolver;
52 : import com.google.gerrit.server.account.AccountState;
53 : import com.google.gerrit.server.approval.ApprovalCopier;
54 : import com.google.gerrit.server.approval.ApprovalsUtil;
55 : import com.google.gerrit.server.change.ChangeKindCache;
56 : import com.google.gerrit.server.change.EmailNewPatchSet;
57 : import com.google.gerrit.server.change.NotifyResolver;
58 : import com.google.gerrit.server.change.ReviewerModifier;
59 : import com.google.gerrit.server.change.ReviewerModifier.InternalReviewerInput;
60 : import com.google.gerrit.server.change.ReviewerModifier.ReviewerModification;
61 : import com.google.gerrit.server.change.ReviewerModifier.ReviewerModificationList;
62 : import com.google.gerrit.server.change.ReviewerOp;
63 : import com.google.gerrit.server.config.AnonymousCowardName;
64 : import com.google.gerrit.server.config.UrlFormatter;
65 : import com.google.gerrit.server.extensions.events.CommentAdded;
66 : import com.google.gerrit.server.extensions.events.RevisionCreated;
67 : import com.google.gerrit.server.git.MergedByPushOp;
68 : import com.google.gerrit.server.git.receive.ReceiveCommits.MagicBranchInput;
69 : import com.google.gerrit.server.mail.MailUtil.MailRecipients;
70 : import com.google.gerrit.server.notedb.ChangeNotes;
71 : import com.google.gerrit.server.notedb.ChangeUpdate;
72 : import com.google.gerrit.server.permissions.PermissionBackendException;
73 : import com.google.gerrit.server.project.ProjectCache;
74 : import com.google.gerrit.server.project.ProjectState;
75 : import com.google.gerrit.server.query.change.ChangeData;
76 : import com.google.gerrit.server.update.BatchUpdateOp;
77 : import com.google.gerrit.server.update.ChangeContext;
78 : import com.google.gerrit.server.update.Context;
79 : import com.google.gerrit.server.update.PostUpdateContext;
80 : import com.google.gerrit.server.update.RepoContext;
81 : import com.google.gerrit.server.util.LabelVote;
82 : import com.google.gerrit.server.util.RequestScopePropagator;
83 : import com.google.gerrit.server.validators.ValidationException;
84 : import com.google.inject.Inject;
85 : import com.google.inject.assistedinject.Assisted;
86 : import com.google.inject.util.Providers;
87 : import java.io.IOException;
88 : import java.util.HashMap;
89 : import java.util.List;
90 : import java.util.Map;
91 : import java.util.Optional;
92 : import java.util.Set;
93 : import java.util.stream.Stream;
94 : import org.eclipse.jgit.errors.ConfigInvalidException;
95 : import org.eclipse.jgit.lib.ObjectId;
96 : import org.eclipse.jgit.revwalk.RevCommit;
97 : import org.eclipse.jgit.revwalk.RevWalk;
98 : import org.eclipse.jgit.transport.PushCertificate;
99 : import org.eclipse.jgit.transport.ReceiveCommand;
100 :
101 : public class ReplaceOp implements BatchUpdateOp {
102 37 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
103 :
104 : public interface Factory {
105 : ReplaceOp create(
106 : ProjectState projectState,
107 : Change change,
108 : boolean checkMergedInto,
109 : @Nullable String mergeResultRevId,
110 : @Assisted("priorPatchSetId") PatchSet.Id priorPatchSetId,
111 : @Assisted("priorCommitId") ObjectId priorCommit,
112 : @Assisted("patchSetId") PatchSet.Id patchSetId,
113 : @Assisted("commitId") ObjectId commitId,
114 : PatchSetInfo info,
115 : List<String> groups,
116 : @Nullable MagicBranchInput magicBranch,
117 : @Nullable PushCertificate pushCertificate,
118 : RequestScopePropagator requestScopePropagator);
119 : }
120 :
121 : private static final String CHANGE_IS_CLOSED = "change is closed";
122 :
123 : private final AccountCache accountCache;
124 : private final AccountResolver accountResolver;
125 : private final String anonymousCowardName;
126 : private final ApprovalsUtil approvalsUtil;
127 : private final ChangeData.Factory changeDataFactory;
128 : private final ChangeKindCache changeKindCache;
129 : private final ChangeMessagesUtil cmUtil;
130 : private final EmailNewPatchSet.Factory emailNewPatchSetFactory;
131 : private final RevisionCreated revisionCreated;
132 : private final CommentAdded commentAdded;
133 : private final MergedByPushOp.Factory mergedByPushOpFactory;
134 : private final PatchSetUtil psUtil;
135 : private final ProjectCache projectCache;
136 : private final ReviewerModifier reviewerModifier;
137 : private final DynamicItem<UrlFormatter> urlFormatter;
138 :
139 : private final ProjectState projectState;
140 : private final Change change;
141 : private final boolean checkMergedInto;
142 : private final String mergeResultRevId;
143 : private final PatchSet.Id priorPatchSetId;
144 : private final ObjectId priorCommitId;
145 : private final PatchSet.Id patchSetId;
146 : private final ObjectId commitId;
147 : private final PatchSetInfo info;
148 : private final MagicBranchInput magicBranch;
149 : private final PushCertificate pushCertificate;
150 : private final RequestScopePropagator requestScopePropagator;
151 : private List<String> groups;
152 :
153 37 : private final Map<String, Short> approvals = new HashMap<>();
154 : private RevCommit commit;
155 : private ReceiveCommand cmd;
156 : private ChangeNotes notes;
157 : private PatchSet newPatchSet;
158 : private ChangeKind changeKind;
159 : private String mailMessage;
160 : private ApprovalCopier.Result approvalCopierResult;
161 : private String rejectMessage;
162 : private MergedByPushOp mergedByPushOp;
163 : private ReviewerModificationList reviewerAdditions;
164 : private MailRecipients oldRecipients;
165 :
166 : @Inject
167 : ReplaceOp(
168 : AccountCache accountCache,
169 : AccountResolver accountResolver,
170 : @AnonymousCowardName String anonymousCowardName,
171 : ApprovalsUtil approvalsUtil,
172 : ChangeData.Factory changeDataFactory,
173 : ChangeKindCache changeKindCache,
174 : ChangeMessagesUtil cmUtil,
175 : RevisionCreated revisionCreated,
176 : CommentAdded commentAdded,
177 : MergedByPushOp.Factory mergedByPushOpFactory,
178 : PatchSetUtil psUtil,
179 : ProjectCache projectCache,
180 : EmailNewPatchSet.Factory emailNewPatchSetFactory,
181 : ReviewerModifier reviewerModifier,
182 : DynamicItem<UrlFormatter> urlFormatter,
183 : @Assisted ProjectState projectState,
184 : @Assisted Change change,
185 : @Assisted boolean checkMergedInto,
186 : @Assisted @Nullable String mergeResultRevId,
187 : @Assisted("priorPatchSetId") PatchSet.Id priorPatchSetId,
188 : @Assisted("priorCommitId") ObjectId priorCommitId,
189 : @Assisted("patchSetId") PatchSet.Id patchSetId,
190 : @Assisted("commitId") ObjectId commitId,
191 : @Assisted PatchSetInfo info,
192 : @Assisted List<String> groups,
193 : @Assisted @Nullable MagicBranchInput magicBranch,
194 : @Assisted @Nullable PushCertificate pushCertificate,
195 37 : @Assisted RequestScopePropagator requestScopePropagator) {
196 37 : this.accountCache = accountCache;
197 37 : this.accountResolver = accountResolver;
198 37 : this.anonymousCowardName = anonymousCowardName;
199 37 : this.approvalsUtil = approvalsUtil;
200 37 : this.changeDataFactory = changeDataFactory;
201 37 : this.changeKindCache = changeKindCache;
202 37 : this.cmUtil = cmUtil;
203 37 : this.revisionCreated = revisionCreated;
204 37 : this.commentAdded = commentAdded;
205 37 : this.mergedByPushOpFactory = mergedByPushOpFactory;
206 37 : this.psUtil = psUtil;
207 37 : this.projectCache = projectCache;
208 37 : this.emailNewPatchSetFactory = emailNewPatchSetFactory;
209 37 : this.reviewerModifier = reviewerModifier;
210 37 : this.urlFormatter = urlFormatter;
211 :
212 37 : this.projectState = projectState;
213 37 : this.change = change;
214 37 : this.checkMergedInto = checkMergedInto;
215 37 : this.mergeResultRevId = mergeResultRevId;
216 37 : this.priorPatchSetId = priorPatchSetId;
217 37 : this.priorCommitId = priorCommitId.copy();
218 37 : this.patchSetId = patchSetId;
219 37 : this.commitId = commitId.copy();
220 37 : this.info = info;
221 37 : this.groups = groups;
222 37 : this.magicBranch = magicBranch;
223 37 : this.pushCertificate = pushCertificate;
224 37 : this.requestScopePropagator = requestScopePropagator;
225 37 : }
226 :
227 : @Override
228 : public void updateRepo(RepoContext ctx) throws Exception {
229 37 : commit = ctx.getRevWalk().parseCommit(commitId);
230 37 : ctx.getRevWalk().parseBody(commit);
231 37 : changeKind =
232 37 : changeKindCache.getChangeKind(
233 37 : projectState.getNameKey(),
234 37 : ctx.getRevWalk(),
235 37 : ctx.getRepoView().getConfig(),
236 : priorCommitId,
237 : commitId);
238 :
239 37 : if (checkMergedInto) {
240 0 : String mergedInto = findMergedInto(ctx, change.getDest().branch(), commit);
241 0 : if (mergedInto != null) {
242 0 : mergedByPushOp =
243 0 : mergedByPushOpFactory.create(
244 : requestScopePropagator,
245 : patchSetId,
246 : new SubmissionId(change),
247 : mergedInto,
248 : mergeResultRevId);
249 : }
250 : }
251 :
252 37 : cmd = new ReceiveCommand(ObjectId.zeroId(), commitId, patchSetId.toRefName());
253 37 : ctx.addRefUpdate(cmd);
254 37 : }
255 :
256 : @Override
257 : public boolean updateChange(ChangeContext ctx)
258 : throws RestApiException, IOException, PermissionBackendException, ConfigInvalidException,
259 : ValidationException {
260 37 : notes = ctx.getNotes();
261 37 : Change change = notes.getChange();
262 37 : if (change == null || change.isClosed()) {
263 0 : rejectMessage = CHANGE_IS_CLOSED;
264 0 : return false;
265 : }
266 37 : if (groups.isEmpty()) {
267 6 : PatchSet prevPs = psUtil.current(notes);
268 6 : groups = prevPs != null ? prevPs.groups() : ImmutableList.of();
269 : }
270 :
271 37 : ChangeData cd = changeDataFactory.create(ctx.getNotes());
272 37 : oldRecipients = getRecipientsFromReviewers(cd.reviewers());
273 :
274 37 : ChangeUpdate update = ctx.getUpdate(patchSetId);
275 37 : update.setSubjectForCommit("Create patch set " + patchSetId.get());
276 :
277 37 : String reviewMessage = null;
278 37 : String psDescription = null;
279 37 : if (magicBranch != null) {
280 37 : reviewMessage = magicBranch.message;
281 37 : psDescription = magicBranch.message;
282 37 : approvals.putAll(magicBranch.labels);
283 37 : Set<String> hashtags = magicBranch.hashtags;
284 37 : if (hashtags != null && !hashtags.isEmpty()) {
285 3 : hashtags.addAll(notes.getHashtags());
286 3 : update.setHashtags(hashtags);
287 : }
288 37 : if (magicBranch.topic != null && !magicBranch.topic.equals(ctx.getChange().getTopic())) {
289 : try {
290 0 : update.setTopic(magicBranch.topic);
291 0 : } catch (ValidationException ex) {
292 0 : throw new BadRequestException(ex.getMessage());
293 0 : }
294 : }
295 37 : if (magicBranch.removePrivate) {
296 3 : change.setPrivate(false);
297 3 : update.setPrivate(false);
298 37 : } else if (magicBranch.isPrivate) {
299 1 : change.setPrivate(true);
300 1 : update.setPrivate(true);
301 : }
302 37 : if (magicBranch.ready) {
303 4 : change.setWorkInProgress(false);
304 4 : change.setReviewStarted(true);
305 4 : update.setWorkInProgress(false);
306 37 : } else if (magicBranch.workInProgress) {
307 5 : change.setWorkInProgress(true);
308 5 : update.setWorkInProgress(true);
309 : }
310 37 : if (magicBranch.ignoreAttentionSet) {
311 0 : update.ignoreFurtherAttentionSetUpdates();
312 : }
313 : }
314 :
315 37 : newPatchSet =
316 37 : psUtil.insert(
317 37 : ctx.getRevWalk(),
318 : update,
319 : patchSetId,
320 : commitId,
321 : groups,
322 37 : pushCertificate != null ? pushCertificate.toTextWithSignature() : null,
323 : psDescription);
324 :
325 37 : update.setPsDescription(psDescription);
326 37 : MailRecipients fromFooters = getRecipientsFromFooters(accountResolver, commit.getFooterLines());
327 37 : approvalsUtil.addApprovalsForNewPatchSet(
328 37 : update, projectState.getLabelTypes(), newPatchSet, ctx.getUser(), approvals);
329 :
330 37 : reviewerAdditions =
331 37 : reviewerModifier.prepare(
332 37 : ctx.getNotes(),
333 37 : ctx.getUser(),
334 37 : getReviewerInputs(magicBranch, fromFooters, ctx.getChange(), info),
335 : true);
336 37 : Optional<ReviewerModification> reviewerError =
337 37 : reviewerAdditions.getFailures().stream().findFirst();
338 37 : if (reviewerError.isPresent()) {
339 0 : throw new UnprocessableEntityException(reviewerError.get().result.error);
340 : }
341 37 : reviewerAdditions.updateChange(ctx, newPatchSet);
342 :
343 : // Check if approvals are changing with this update. If so, add the current user (aka the
344 : // approver) as a reviewers because all approvers must also be reviewers.
345 : // Note that this is done separately as addReviewers is filtering out the change owner as a
346 : // reviewer which is needed in several other code paths.
347 37 : if (magicBranch != null && !magicBranch.labels.isEmpty()) {
348 4 : update.putReviewer(ctx.getAccountId(), REVIEWER);
349 : }
350 :
351 37 : approvalCopierResult =
352 37 : approvalsUtil.copyApprovalsToNewPatchSet(
353 37 : ctx.getNotes(), newPatchSet, ctx.getRevWalk(), ctx.getRepoView().getConfig(), update);
354 :
355 37 : mailMessage = insertChangeMessage(update, ctx, reviewMessage);
356 37 : if (mergedByPushOp == null) {
357 37 : resetChange(ctx);
358 : } else {
359 0 : mergedByPushOp.setPatchSetProvider(Providers.of(newPatchSet)).updateChange(ctx);
360 : }
361 :
362 37 : return true;
363 : }
364 :
365 : private ImmutableList<ReviewerInput> getReviewerInputs(
366 : @Nullable MagicBranchInput magicBranch,
367 : MailRecipients fromFooters,
368 : Change change,
369 : PatchSetInfo psInfo) {
370 : // Disable individual emails when adding reviewers, as all reviewers will receive the single
371 : // bulk new change email.
372 37 : Stream<ReviewerInput> inputs =
373 37 : Streams.concat(
374 37 : Streams.stream(
375 37 : newReviewerInputFromCommitIdentity(
376 : change,
377 37 : psInfo.getCommitId(),
378 37 : psInfo.getAuthor().getAccount(),
379 : NotifyHandling.NONE,
380 37 : newPatchSet.uploader())),
381 37 : Streams.stream(
382 37 : newReviewerInputFromCommitIdentity(
383 : change,
384 37 : psInfo.getCommitId(),
385 37 : psInfo.getCommitter().getAccount(),
386 : NotifyHandling.NONE,
387 37 : newPatchSet.uploader())));
388 37 : if (magicBranch != null) {
389 37 : inputs =
390 37 : Streams.concat(
391 : inputs,
392 37 : magicBranch.getCombinedReviewers(fromFooters).stream()
393 37 : .map(r -> newReviewerInput(r, ReviewerState.REVIEWER)),
394 37 : magicBranch.getCombinedCcs(fromFooters).stream()
395 37 : .map(r -> newReviewerInput(r, ReviewerState.CC)));
396 : }
397 37 : return inputs.collect(toImmutableList());
398 : }
399 :
400 : private static InternalReviewerInput newReviewerInput(String reviewer, ReviewerState state) {
401 : // Disable individual emails when adding reviewers, as all reviewers will receive the single
402 : // bulk new patch set email.
403 4 : InternalReviewerInput input =
404 4 : ReviewerModifier.newReviewerInput(reviewer, state, NotifyHandling.NONE);
405 :
406 : // Ignore failures for reasons like the reviewer being inactive or being unable to see the
407 : // change. See discussion in ChangeInserter.
408 4 : input.otherFailureBehavior = ReviewerModifier.FailureBehavior.IGNORE_EXCEPT_NOT_FOUND;
409 :
410 4 : return input;
411 : }
412 :
413 : private String insertChangeMessage(ChangeUpdate update, ChangeContext ctx, String reviewMessage) {
414 37 : String approvalMessage =
415 37 : ApprovalsUtil.renderMessageWithApprovals(
416 37 : patchSetId.get(), approvals, scanLabels(ctx, approvals));
417 37 : String kindMessage = changeKindMessage(changeKind);
418 37 : StringBuilder message = new StringBuilder(approvalMessage);
419 37 : if (!Strings.isNullOrEmpty(kindMessage)) {
420 14 : message.append(kindMessage);
421 : } else {
422 36 : message.append('.');
423 : }
424 37 : if (!Strings.isNullOrEmpty(reviewMessage)) {
425 4 : message.append("\n\n").append(reviewMessage);
426 : }
427 37 : approvalsUtil
428 37 : .formatApprovalCopierResult(approvalCopierResult, projectState.getLabelTypes())
429 37 : .ifPresent(
430 : msg -> {
431 12 : if (Strings.isNullOrEmpty(reviewMessage) || !reviewMessage.endsWith("\n")) {
432 12 : message.append("\n");
433 : }
434 12 : message.append("\n").append(msg);
435 12 : });
436 37 : boolean workInProgress = ctx.getChange().isWorkInProgress();
437 37 : if (magicBranch != null && magicBranch.workInProgress) {
438 5 : workInProgress = true;
439 : }
440 37 : return cmUtil.setChangeMessage(
441 37 : update, message.toString(), ChangeMessagesUtil.uploadedPatchSetTag(workInProgress));
442 : }
443 :
444 : @Nullable
445 : private String changeKindMessage(ChangeKind changeKind) {
446 37 : switch (changeKind) {
447 : case MERGE_FIRST_PARENT_UPDATE:
448 1 : return ": New merge patch set was added with a new first parent relative to Patch Set "
449 1 : + priorPatchSetId.get()
450 : + ".";
451 : case TRIVIAL_REBASE:
452 5 : return ": Patch Set " + priorPatchSetId.get() + " was rebased.";
453 : case NO_CHANGE:
454 8 : return ": New patch set was added with same tree, parent "
455 8 : + (commit.getParentCount() != 1 ? "trees" : "tree")
456 : + ", and commit message as Patch Set "
457 8 : + priorPatchSetId.get()
458 : + ".";
459 : case NO_CODE_CHANGE:
460 6 : return ": Commit message was updated.";
461 : case REWORK:
462 : default:
463 36 : return null;
464 : }
465 : }
466 :
467 : private Map<String, PatchSetApproval> scanLabels(
468 : ChangeContext ctx, Map<String, Short> approvals) {
469 37 : Map<String, PatchSetApproval> current = new HashMap<>();
470 : // We optimize here and only retrieve current when approvals provided
471 37 : if (!approvals.isEmpty()) {
472 : for (PatchSetApproval a :
473 4 : approvalsUtil.byPatchSetUser(ctx.getNotes(), priorPatchSetId, ctx.getAccountId())) {
474 4 : if (a.isLegacySubmit()) {
475 0 : continue;
476 : }
477 :
478 4 : projectState
479 4 : .getLabelTypes()
480 4 : .byLabel(a.labelId())
481 4 : .ifPresent(l -> current.put(l.getName(), a));
482 4 : }
483 : }
484 37 : return current;
485 : }
486 :
487 : private void resetChange(ChangeContext ctx) {
488 37 : Change change = ctx.getChange();
489 37 : if (!change.currentPatchSetId().equals(priorPatchSetId)) {
490 0 : return;
491 : }
492 :
493 37 : if (magicBranch != null && magicBranch.topic != null) {
494 0 : change.setTopic(magicBranch.topic);
495 : }
496 37 : change.setStatus(Change.Status.NEW);
497 37 : change.setCurrentPatchSet(info);
498 :
499 37 : List<String> idList = ChangeUtil.getChangeIdsFromFooter(commit, urlFormatter.get());
500 37 : change.setKey(Change.key(idList.get(idList.size() - 1).trim()));
501 37 : }
502 :
503 : @Override
504 : public void postUpdate(PostUpdateContext ctx) throws Exception {
505 37 : reviewerAdditions.postUpdate(ctx);
506 :
507 : // TODO(dborowitz): Merge email templates so we only have to send one.
508 37 : emailNewPatchSetFactory
509 37 : .create(
510 : ctx,
511 : newPatchSet,
512 : mailMessage,
513 37 : approvalCopierResult.outdatedApprovals(),
514 37 : Streams.concat(
515 37 : oldRecipients.getReviewers().stream(),
516 37 : reviewerAdditions.flattenResults(ReviewerOp.Result::addedReviewers).stream()
517 37 : .map(PatchSetApproval::accountId))
518 37 : .collect(toImmutableSet()),
519 37 : Streams.concat(
520 37 : oldRecipients.getCcOnly().stream(),
521 37 : reviewerAdditions.flattenResults(ReviewerOp.Result::addedCCs).stream())
522 37 : .collect(toImmutableSet()),
523 : changeKind,
524 37 : notes.getMetaId())
525 37 : .setRequestScopePropagator(requestScopePropagator)
526 37 : .sendAsync();
527 :
528 37 : NotifyResolver.Result notify = ctx.getNotify(notes.getChangeId());
529 37 : revisionCreated.fire(
530 37 : ctx.getChangeData(notes), newPatchSet, ctx.getAccount(), ctx.getWhen(), notify);
531 : try {
532 37 : fireApprovalsEvent(ctx);
533 0 : } catch (Exception e) {
534 0 : logger.atWarning().withCause(e).log("comment-added event invocation failed");
535 37 : }
536 37 : if (mergedByPushOp != null) {
537 0 : mergedByPushOp.postUpdate(ctx);
538 : }
539 37 : }
540 :
541 : private void fireApprovalsEvent(PostUpdateContext ctx) {
542 37 : if (approvals.isEmpty()) {
543 37 : return;
544 : }
545 : /* For labels that are not set in this operation, show the "current" value
546 : * of 0, and no oldValue as the value was not modified by this operation.
547 : * For labels that are set in this operation, the value was modified, so
548 : * show a transition from an oldValue of 0 to the new value.
549 : */
550 4 : List<LabelType> labels =
551 : projectCache
552 4 : .get(ctx.getProject())
553 4 : .orElseThrow(illegalState(ctx.getProject()))
554 4 : .getLabelTypes(notes)
555 4 : .getLabelTypes();
556 4 : Map<String, Short> allApprovals = new HashMap<>();
557 4 : Map<String, Short> oldApprovals = new HashMap<>();
558 4 : for (LabelType lt : labels) {
559 4 : allApprovals.put(lt.getName(), (short) 0);
560 4 : oldApprovals.put(lt.getName(), null);
561 4 : }
562 4 : for (Map.Entry<String, Short> entry : approvals.entrySet()) {
563 4 : if (entry.getValue() != 0) {
564 4 : allApprovals.put(entry.getKey(), entry.getValue());
565 4 : oldApprovals.put(entry.getKey(), (short) 0);
566 : }
567 4 : }
568 4 : commentAdded.fire(
569 4 : ctx.getChangeData(notes),
570 : newPatchSet,
571 4 : ctx.getAccount(),
572 : null,
573 : allApprovals,
574 : oldApprovals,
575 4 : ctx.getWhen());
576 4 : }
577 :
578 : public PatchSet getPatchSet() {
579 6 : return newPatchSet;
580 : }
581 :
582 : public Change getChange() {
583 0 : return notes.getChange();
584 : }
585 :
586 : public String getRejectMessage() {
587 37 : return rejectMessage;
588 : }
589 :
590 : public Optional<String> getOutdatedApprovalsMessage() {
591 37 : if (approvalCopierResult == null || approvalCopierResult.outdatedApprovals().isEmpty()) {
592 35 : return Optional.empty();
593 : }
594 :
595 11 : return Optional.of(
596 : "The following approvals got outdated and were removed:\n"
597 11 : + approvalCopierResult.outdatedApprovals().stream()
598 11 : .map(
599 : outdatedApproval ->
600 11 : String.format(
601 : "* %s by %s",
602 11 : LabelVote.create(outdatedApproval.label(), outdatedApproval.value())
603 11 : .format(),
604 11 : getNameFor(outdatedApproval.accountId())))
605 11 : .sorted()
606 11 : .collect(joining("\n")));
607 : }
608 :
609 : private String getNameFor(Account.Id accountId) {
610 11 : Optional<Account> account = accountCache.get(accountId).map(AccountState::account);
611 11 : String name = null;
612 11 : if (account.isPresent()) {
613 11 : name = account.get().fullName();
614 11 : if (name == null) {
615 0 : name = account.get().preferredEmail();
616 : }
617 : }
618 11 : if (name == null) {
619 0 : name = anonymousCowardName + " #" + accountId;
620 : }
621 11 : return name;
622 : }
623 :
624 : public ReceiveCommand getCommand() {
625 0 : return cmd;
626 : }
627 :
628 : @Nullable
629 : private static String findMergedInto(Context ctx, String first, RevCommit commit) {
630 : try {
631 0 : RevWalk rw = ctx.getRevWalk();
632 0 : Optional<ObjectId> firstId = ctx.getRepoView().getRef(first);
633 0 : if (firstId.isPresent() && rw.isMergedInto(commit, rw.parseCommit(firstId.get()))) {
634 0 : return first;
635 : }
636 :
637 0 : for (Map.Entry<String, ObjectId> e : ctx.getRepoView().getRefs(R_HEADS).entrySet()) {
638 0 : if (rw.isMergedInto(commit, rw.parseCommit(e.getValue()))) {
639 0 : return R_HEADS + e.getKey();
640 : }
641 0 : }
642 0 : return null;
643 0 : } catch (IOException e) {
644 0 : logger.atWarning().withCause(e).log("Can't check for already submitted change");
645 0 : return null;
646 : }
647 : }
648 : }
|