Line data Source code
1 : // Copyright (C) 2013 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.change;
16 :
17 : import static com.google.common.base.Preconditions.checkState;
18 : import static com.google.common.collect.ImmutableList.toImmutableList;
19 : import static com.google.common.collect.ImmutableSet.toImmutableSet;
20 : import static com.google.gerrit.entities.Change.INITIAL_PATCH_SET_ID;
21 : import static com.google.gerrit.server.change.ReviewerModifier.newReviewerInputFromCommitIdentity;
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.Objects.requireNonNull;
25 :
26 : import com.google.common.base.MoreObjects;
27 : import com.google.common.collect.ImmutableList;
28 : import com.google.common.collect.ImmutableListMultimap;
29 : import com.google.common.collect.Iterables;
30 : import com.google.common.collect.Streams;
31 : import com.google.common.flogger.FluentLogger;
32 : import com.google.errorprone.annotations.CanIgnoreReturnValue;
33 : import com.google.gerrit.common.Nullable;
34 : import com.google.gerrit.entities.Account;
35 : import com.google.gerrit.entities.BranchNameKey;
36 : import com.google.gerrit.entities.Change;
37 : import com.google.gerrit.entities.LabelType;
38 : import com.google.gerrit.entities.LabelTypes;
39 : import com.google.gerrit.entities.PatchSet;
40 : import com.google.gerrit.entities.PatchSetApproval;
41 : import com.google.gerrit.entities.PatchSetInfo;
42 : import com.google.gerrit.entities.SubmissionId;
43 : import com.google.gerrit.extensions.api.changes.NotifyHandling;
44 : import com.google.gerrit.extensions.client.ReviewerState;
45 : import com.google.gerrit.extensions.registration.DynamicItem;
46 : import com.google.gerrit.extensions.restapi.BadRequestException;
47 : import com.google.gerrit.extensions.restapi.ResourceConflictException;
48 : import com.google.gerrit.extensions.restapi.RestApiException;
49 : import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
50 : import com.google.gerrit.server.ChangeMessagesUtil;
51 : import com.google.gerrit.server.ChangeUtil;
52 : import com.google.gerrit.server.PatchSetUtil;
53 : import com.google.gerrit.server.approval.ApprovalsUtil;
54 : import com.google.gerrit.server.change.ReviewerModifier.InternalReviewerInput;
55 : import com.google.gerrit.server.change.ReviewerModifier.ReviewerModification;
56 : import com.google.gerrit.server.change.ReviewerModifier.ReviewerModificationList;
57 : import com.google.gerrit.server.config.SendEmailExecutor;
58 : import com.google.gerrit.server.config.UrlFormatter;
59 : import com.google.gerrit.server.events.CommitReceivedEvent;
60 : import com.google.gerrit.server.extensions.events.CommentAdded;
61 : import com.google.gerrit.server.extensions.events.RevisionCreated;
62 : import com.google.gerrit.server.git.GroupCollector;
63 : import com.google.gerrit.server.git.validators.CommitValidationException;
64 : import com.google.gerrit.server.git.validators.CommitValidators;
65 : import com.google.gerrit.server.mail.send.CreateChangeSender;
66 : import com.google.gerrit.server.mail.send.MessageIdGenerator;
67 : import com.google.gerrit.server.notedb.ChangeUpdate;
68 : import com.google.gerrit.server.patch.AutoMerger;
69 : import com.google.gerrit.server.patch.PatchSetInfoFactory;
70 : import com.google.gerrit.server.permissions.PermissionBackend;
71 : import com.google.gerrit.server.permissions.PermissionBackendException;
72 : import com.google.gerrit.server.project.ProjectCache;
73 : import com.google.gerrit.server.project.ProjectState;
74 : import com.google.gerrit.server.ssh.NoSshInfo;
75 : import com.google.gerrit.server.update.ChangeContext;
76 : import com.google.gerrit.server.update.Context;
77 : import com.google.gerrit.server.update.InsertChangeOp;
78 : import com.google.gerrit.server.update.PostUpdateContext;
79 : import com.google.gerrit.server.update.RepoContext;
80 : import com.google.gerrit.server.util.CommitMessageUtil;
81 : import com.google.gerrit.server.util.RequestScopePropagator;
82 : import com.google.gerrit.server.validators.ValidationException;
83 : import com.google.inject.Inject;
84 : import com.google.inject.assistedinject.Assisted;
85 : import java.io.IOException;
86 : import java.util.Collections;
87 : import java.util.HashMap;
88 : import java.util.List;
89 : import java.util.Map;
90 : import java.util.Optional;
91 : import java.util.concurrent.ExecutorService;
92 : import java.util.concurrent.Future;
93 : import org.eclipse.jgit.errors.ConfigInvalidException;
94 : import org.eclipse.jgit.lib.ObjectId;
95 : import org.eclipse.jgit.revwalk.RevCommit;
96 : import org.eclipse.jgit.revwalk.RevWalk;
97 : import org.eclipse.jgit.transport.ReceiveCommand;
98 :
99 : public class ChangeInserter implements InsertChangeOp {
100 103 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
101 :
102 : public interface Factory {
103 : ChangeInserter create(Change.Id cid, ObjectId commitId, String refName);
104 : }
105 :
106 : private final PermissionBackend permissionBackend;
107 : private final ProjectCache projectCache;
108 : private final PatchSetInfoFactory patchSetInfoFactory;
109 : private final PatchSetUtil psUtil;
110 : private final ApprovalsUtil approvalsUtil;
111 : private final ChangeMessagesUtil cmUtil;
112 : private final CreateChangeSender.Factory createChangeSenderFactory;
113 : private final ExecutorService sendEmailExecutor;
114 : private final CommitValidators.Factory commitValidatorsFactory;
115 : private final RevisionCreated revisionCreated;
116 : private final CommentAdded commentAdded;
117 : private final ReviewerModifier reviewerModifier;
118 : private final MessageIdGenerator messageIdGenerator;
119 : private final DynamicItem<UrlFormatter> urlFormatter;
120 : private final AutoMerger autoMerger;
121 :
122 : private final Change.Id changeId;
123 : private final PatchSet.Id psId;
124 : private final ObjectId commitId;
125 : private final String refName;
126 :
127 : // Fields exposed as setters.
128 : private PatchSet.Id cherryPickOf;
129 : private Change.Status status;
130 : private String topic;
131 : private String message;
132 : private String patchSetDescription;
133 : private boolean isPrivate;
134 : private boolean workInProgress;
135 103 : private List<String> groups = Collections.emptyList();
136 103 : private ImmutableListMultimap<String, String> validationOptions = ImmutableListMultimap.of();
137 103 : private boolean validate = true;
138 : private Map<String, Short> approvals;
139 : private RequestScopePropagator requestScopePropagator;
140 : private boolean fireRevisionCreated;
141 : private boolean sendMail;
142 : private boolean updateRef;
143 : private Change.Id revertOf;
144 : private ImmutableList<InternalReviewerInput> reviewerInputs;
145 :
146 : // Fields set during the insertion process.
147 : private ReceiveCommand cmd;
148 : private Change change;
149 : private String changeMessage;
150 : private PatchSetInfo patchSetInfo;
151 : private PatchSet patchSet;
152 : private String pushCert;
153 : private ProjectState projectState;
154 : private ReviewerModificationList reviewerAdditions;
155 :
156 : @Inject
157 : ChangeInserter(
158 : PermissionBackend permissionBackend,
159 : ProjectCache projectCache,
160 : PatchSetInfoFactory patchSetInfoFactory,
161 : PatchSetUtil psUtil,
162 : ApprovalsUtil approvalsUtil,
163 : ChangeMessagesUtil cmUtil,
164 : CreateChangeSender.Factory createChangeSenderFactory,
165 : @SendEmailExecutor ExecutorService sendEmailExecutor,
166 : CommitValidators.Factory commitValidatorsFactory,
167 : CommentAdded commentAdded,
168 : RevisionCreated revisionCreated,
169 : ReviewerModifier reviewerModifier,
170 : MessageIdGenerator messageIdGenerator,
171 : DynamicItem<UrlFormatter> urlFormatter,
172 : AutoMerger autoMerger,
173 : @Assisted Change.Id changeId,
174 : @Assisted ObjectId commitId,
175 103 : @Assisted String refName) {
176 103 : this.permissionBackend = permissionBackend;
177 103 : this.projectCache = projectCache;
178 103 : this.patchSetInfoFactory = patchSetInfoFactory;
179 103 : this.psUtil = psUtil;
180 103 : this.approvalsUtil = approvalsUtil;
181 103 : this.cmUtil = cmUtil;
182 103 : this.createChangeSenderFactory = createChangeSenderFactory;
183 103 : this.sendEmailExecutor = sendEmailExecutor;
184 103 : this.commitValidatorsFactory = commitValidatorsFactory;
185 103 : this.revisionCreated = revisionCreated;
186 103 : this.commentAdded = commentAdded;
187 103 : this.reviewerModifier = reviewerModifier;
188 103 : this.messageIdGenerator = messageIdGenerator;
189 103 : this.urlFormatter = urlFormatter;
190 103 : this.autoMerger = autoMerger;
191 :
192 103 : this.changeId = changeId;
193 103 : this.psId = PatchSet.id(changeId, INITIAL_PATCH_SET_ID);
194 103 : this.commitId = commitId.copy();
195 103 : this.refName = refName;
196 103 : this.reviewerInputs = ImmutableList.of();
197 103 : this.approvals = Collections.emptyMap();
198 103 : this.fireRevisionCreated = true;
199 103 : this.sendMail = true;
200 103 : this.updateRef = true;
201 103 : }
202 :
203 : @Override
204 : public Change createChange(Context ctx) throws IOException {
205 103 : change =
206 : new Change(
207 103 : getChangeKey(ctx.getRevWalk()),
208 : changeId,
209 103 : ctx.getAccountId(),
210 103 : BranchNameKey.create(ctx.getProject(), refName),
211 103 : ctx.getWhen());
212 103 : change.setStatus(MoreObjects.firstNonNull(status, Change.Status.NEW));
213 103 : change.setTopic(topic);
214 103 : change.setCherryPickOf(cherryPickOf);
215 103 : change.setPrivate(isPrivate);
216 103 : change.setWorkInProgress(workInProgress);
217 103 : change.setReviewStarted(!workInProgress);
218 103 : change.setRevertOf(revertOf);
219 103 : return change;
220 : }
221 :
222 : private Change.Key getChangeKey(RevWalk rw) throws IOException {
223 103 : RevCommit commit = rw.parseCommit(commitId);
224 103 : rw.parseBody(commit);
225 103 : List<String> idList = ChangeUtil.getChangeIdsFromFooter(commit, urlFormatter.get());
226 103 : if (!idList.isEmpty()) {
227 103 : return Change.key(idList.get(idList.size() - 1).trim());
228 : }
229 : // A Change-Id is generated for the review, but not appended to the commit message.
230 : // This can happen if requireChangeId is false.
231 9 : return CommitMessageUtil.generateKey();
232 : }
233 :
234 : public PatchSet.Id getPatchSetId() {
235 102 : return psId;
236 : }
237 :
238 : public ObjectId getCommitId() {
239 4 : return commitId;
240 : }
241 :
242 : public Change getChange() {
243 32 : checkState(change != null, "getChange() only valid after creating change");
244 32 : return change;
245 : }
246 :
247 : @CanIgnoreReturnValue
248 : public ChangeInserter setTopic(String topic) {
249 101 : checkState(change == null, "setTopic(String) only valid before creating change");
250 101 : this.topic = topic;
251 101 : return this;
252 : }
253 :
254 : @CanIgnoreReturnValue
255 : public ChangeInserter setCherryPickOf(PatchSet.Id cherryPickOf) {
256 10 : this.cherryPickOf = cherryPickOf;
257 10 : return this;
258 : }
259 :
260 : @CanIgnoreReturnValue
261 : public ChangeInserter setMessage(String message) {
262 102 : this.message = message;
263 102 : return this;
264 : }
265 :
266 : @CanIgnoreReturnValue
267 : public ChangeInserter setPatchSetDescription(String patchSetDescription) {
268 88 : this.patchSetDescription = patchSetDescription;
269 88 : return this;
270 : }
271 :
272 : @CanIgnoreReturnValue
273 : public ChangeInserter setValidate(boolean validate) {
274 92 : this.validate = validate;
275 92 : return this;
276 : }
277 :
278 : @CanIgnoreReturnValue
279 : public ChangeInserter setReviewersAndCcs(
280 : Iterable<Account.Id> reviewers, Iterable<Account.Id> ccs) {
281 0 : return setReviewersAndCcsAsStrings(
282 0 : Iterables.transform(reviewers, Account.Id::toString),
283 0 : Iterables.transform(ccs, Account.Id::toString));
284 : }
285 :
286 : @CanIgnoreReturnValue
287 : public ChangeInserter setReviewersAndCcsIgnoreVisibility(
288 : Iterable<Account.Id> reviewers, Iterable<Account.Id> ccs) {
289 15 : return setReviewersAndCcsAsStrings(
290 15 : Iterables.transform(reviewers, Account.Id::toString),
291 15 : Iterables.transform(ccs, Account.Id::toString),
292 : /* skipVisibilityCheck= */ true);
293 : }
294 :
295 : @CanIgnoreReturnValue
296 : public ChangeInserter setReviewersAndCcsAsStrings(
297 : Iterable<String> reviewers, Iterable<String> ccs) {
298 88 : return setReviewersAndCcsAsStrings(reviewers, ccs, /* skipVisibilityCheck= */ false);
299 : }
300 :
301 : @CanIgnoreReturnValue
302 : private ChangeInserter setReviewersAndCcsAsStrings(
303 : Iterable<String> reviewers, Iterable<String> ccs, boolean skipVisibilityCheck) {
304 92 : reviewerInputs =
305 92 : Streams.concat(
306 92 : Streams.stream(reviewers)
307 92 : .distinct()
308 92 : .map(id -> newReviewerInput(id, ReviewerState.REVIEWER, skipVisibilityCheck)),
309 92 : Streams.stream(ccs)
310 92 : .distinct()
311 92 : .map(id -> newReviewerInput(id, ReviewerState.CC, skipVisibilityCheck)))
312 92 : .collect(toImmutableList());
313 92 : return this;
314 : }
315 :
316 : @CanIgnoreReturnValue
317 : public ChangeInserter setPrivate(boolean isPrivate) {
318 101 : checkState(change == null, "setPrivate(boolean) only valid before creating change");
319 101 : this.isPrivate = isPrivate;
320 101 : return this;
321 : }
322 :
323 : @CanIgnoreReturnValue
324 : public ChangeInserter setWorkInProgress(boolean workInProgress) {
325 101 : this.workInProgress = workInProgress;
326 101 : return this;
327 : }
328 :
329 : @CanIgnoreReturnValue
330 : public ChangeInserter setStatus(Change.Status status) {
331 7 : checkState(change == null, "setStatus(Change.Status) only valid before creating change");
332 7 : this.status = status;
333 7 : return this;
334 : }
335 :
336 : @CanIgnoreReturnValue
337 : public ChangeInserter setGroups(List<String> groups) {
338 101 : requireNonNull(groups, "groups may not be empty");
339 101 : checkState(patchSet == null, "setGroups(List<String>) only valid before creating change");
340 101 : this.groups = groups;
341 101 : return this;
342 : }
343 :
344 : @CanIgnoreReturnValue
345 : public ChangeInserter setValidationOptions(
346 : ImmutableListMultimap<String, String> validationOptions) {
347 19 : requireNonNull(validationOptions, "validationOptions may not be null");
348 19 : checkState(
349 : patchSet == null,
350 : "setValidationOptions(ImmutableListMultimap<String, String>) only valid before creating a"
351 : + " change");
352 19 : this.validationOptions = validationOptions;
353 19 : return this;
354 : }
355 :
356 : @CanIgnoreReturnValue
357 : public ChangeInserter setFireRevisionCreated(boolean fireRevisionCreated) {
358 1 : this.fireRevisionCreated = fireRevisionCreated;
359 1 : return this;
360 : }
361 :
362 : @CanIgnoreReturnValue
363 : public ChangeInserter setSendMail(boolean sendMail) {
364 88 : this.sendMail = sendMail;
365 88 : return this;
366 : }
367 :
368 : @CanIgnoreReturnValue
369 : public ChangeInserter setRequestScopePropagator(RequestScopePropagator r) {
370 88 : this.requestScopePropagator = r;
371 88 : return this;
372 : }
373 :
374 : @CanIgnoreReturnValue
375 : public ChangeInserter setRevertOf(Change.Id revertOf) {
376 19 : this.revertOf = revertOf;
377 19 : return this;
378 : }
379 :
380 : public void setPushCertificate(String cert) {
381 0 : pushCert = cert;
382 0 : }
383 :
384 : public PatchSet getPatchSet() {
385 0 : checkState(patchSet != null, "getPatchSet() only valid after creating change");
386 0 : return patchSet;
387 : }
388 :
389 : @CanIgnoreReturnValue
390 : public ChangeInserter setApprovals(Map<String, Short> approvals) {
391 89 : this.approvals = approvals;
392 89 : return this;
393 : }
394 :
395 : /**
396 : * Set whether to include the new patch set ref update in this update.
397 : *
398 : * <p>If false, the caller is responsible for creating the patch set ref <strong>before</strong>
399 : * executing the containing {@code BatchUpdate}.
400 : *
401 : * <p>Should not be used in new code, as it doesn't result in a single atomic batch ref update for
402 : * code and NoteDb meta refs.
403 : *
404 : * @param updateRef whether to update the ref during {@link #updateRepo(RepoContext)}.
405 : */
406 : @Deprecated
407 : @CanIgnoreReturnValue
408 : public ChangeInserter setUpdateRef(boolean updateRef) {
409 1 : this.updateRef = updateRef;
410 1 : return this;
411 : }
412 :
413 : @Nullable
414 : public String getChangeMessage() {
415 0 : if (message == null) {
416 0 : return null;
417 : }
418 0 : checkState(changeMessage != null, "getChangeMessage() only valid after inserting change");
419 0 : return changeMessage;
420 : }
421 :
422 : public ReceiveCommand getCommand() {
423 0 : return cmd;
424 : }
425 :
426 : @Override
427 : public void updateRepo(RepoContext ctx) throws ResourceConflictException, IOException {
428 103 : cmd = new ReceiveCommand(ObjectId.zeroId(), commitId, psId.toRefName());
429 103 : projectState = projectCache.get(ctx.getProject()).orElseThrow(illegalState(ctx.getProject()));
430 103 : validate(ctx);
431 103 : if (!updateRef) {
432 1 : return;
433 : }
434 103 : ctx.addRefUpdate(cmd);
435 103 : Optional<ReceiveCommand> autoMerge =
436 103 : autoMerger.createAutoMergeCommitIfNecessary(
437 103 : ctx.getRepoView(),
438 103 : ctx.getRevWalk(),
439 103 : ctx.getInserter(),
440 103 : ctx.getRevWalk().parseCommit(commitId));
441 103 : if (autoMerge.isPresent()) {
442 29 : ctx.addRefUpdate(autoMerge.get());
443 : }
444 103 : }
445 :
446 : @Override
447 : public boolean updateChange(ChangeContext ctx)
448 : throws RestApiException, IOException, PermissionBackendException, ConfigInvalidException {
449 103 : change = ctx.getChange(); // Use defensive copy created by ChangeControl.
450 103 : patchSetInfo =
451 103 : patchSetInfoFactory.get(ctx.getRevWalk(), ctx.getRevWalk().parseCommit(commitId), psId);
452 103 : ctx.getChange().setCurrentPatchSet(patchSetInfo);
453 :
454 103 : ChangeUpdate update = ctx.getUpdate(psId);
455 103 : update.setChangeId(change.getKey().get());
456 103 : update.setSubjectForCommit("Create change");
457 103 : update.setBranch(change.getDest().branch());
458 : try {
459 103 : update.setTopic(change.getTopic());
460 0 : } catch (ValidationException ex) {
461 0 : throw new BadRequestException(ex.getMessage());
462 103 : }
463 103 : update.setPsDescription(patchSetDescription);
464 103 : update.setPrivate(isPrivate);
465 103 : update.setWorkInProgress(workInProgress);
466 103 : if (revertOf != null) {
467 14 : update.setRevertOf(revertOf.get());
468 : }
469 103 : if (cherryPickOf != null) {
470 8 : update.setCherryPickOf(cherryPickOf.getCommaSeparatedChangeAndPatchSetId());
471 : }
472 :
473 103 : List<String> newGroups = groups;
474 103 : if (newGroups.isEmpty()) {
475 45 : newGroups = GroupCollector.getDefaultGroups(commitId);
476 : }
477 103 : patchSet =
478 103 : psUtil.insert(
479 103 : ctx.getRevWalk(), update, psId, commitId, newGroups, pushCert, patchSetDescription);
480 :
481 : /* TODO: fixStatusToMerged is used here because the tests
482 : * (byStatusClosed() in AbstractQueryChangesTest)
483 : * insert changes that are already merged,
484 : * and setStatus may not be used to set the Status to merged
485 : *
486 : * is it possible to make the tests use the merge code path,
487 : * instead of setting the status directly?
488 : */
489 103 : if (change.getStatus() == Change.Status.MERGED) {
490 7 : update.fixStatusToMerged(new SubmissionId(change));
491 : } else {
492 103 : update.setStatus(change.getStatus());
493 : }
494 :
495 103 : reviewerAdditions =
496 103 : reviewerModifier.prepare(ctx.getNotes(), ctx.getUser(), getReviewerInputs(), true);
497 103 : Optional<ReviewerModification> reviewerError =
498 103 : reviewerAdditions.getFailures().stream().findFirst();
499 103 : if (reviewerError.isPresent()) {
500 3 : throw new UnprocessableEntityException(reviewerError.get().result.error);
501 : }
502 103 : reviewerAdditions.updateChange(ctx, patchSet);
503 :
504 103 : LabelTypes labelTypes = projectState.getLabelTypes();
505 103 : approvalsUtil.addApprovalsForNewPatchSet(
506 103 : update, labelTypes, patchSet, ctx.getUser(), approvals);
507 :
508 : // Check if approvals are changing with this update. If so, add the current user (aka the
509 : // approver) as a reviewers because all approvers must also be reviewers.
510 : // Note that this is done separately as addReviewers is filtering out the change owner as a
511 : // reviewer which is needed in several other code paths.
512 103 : if (!approvals.isEmpty()) {
513 4 : update.putReviewer(ctx.getAccountId(), REVIEWER);
514 : }
515 103 : if (message != null) {
516 102 : changeMessage =
517 102 : cmUtil.setChangeMessage(
518 102 : update, message, ChangeMessagesUtil.uploadedPatchSetTag(workInProgress));
519 : }
520 103 : return true;
521 : }
522 :
523 : @Override
524 : public void postUpdate(PostUpdateContext ctx) throws Exception {
525 103 : reviewerAdditions.postUpdate(ctx);
526 103 : NotifyResolver.Result notify = ctx.getNotify(change.getId());
527 103 : if (sendMail && notify.shouldNotify()) {
528 103 : Runnable sender =
529 103 : new Runnable() {
530 : @Override
531 : public void run() {
532 : try {
533 103 : CreateChangeSender emailSender =
534 103 : createChangeSenderFactory.create(change.getProject(), change.getId());
535 103 : emailSender.setFrom(change.getOwner());
536 103 : emailSender.setPatchSet(patchSet, patchSetInfo);
537 103 : emailSender.setNotify(notify);
538 103 : emailSender.addReviewers(
539 103 : reviewerAdditions.flattenResults(ReviewerOp.Result::addedReviewers).stream()
540 103 : .map(PatchSetApproval::accountId)
541 103 : .collect(toImmutableSet()));
542 103 : emailSender.addReviewersByEmail(
543 103 : reviewerAdditions.flattenResults(ReviewerOp.Result::addedReviewersByEmail));
544 103 : emailSender.addExtraCC(
545 103 : reviewerAdditions.flattenResults(ReviewerOp.Result::addedCCs));
546 103 : emailSender.addExtraCCByEmail(
547 103 : reviewerAdditions.flattenResults(ReviewerOp.Result::addedCCsByEmail));
548 103 : emailSender.setMessageId(
549 103 : messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), patchSet.id()));
550 103 : emailSender.send();
551 0 : } catch (Exception e) {
552 0 : logger.atSevere().withCause(e).log(
553 0 : "Cannot send email for new change %s", change.getId());
554 103 : }
555 103 : }
556 :
557 : @Override
558 : public String toString() {
559 0 : return "send-email newchange";
560 : }
561 : };
562 103 : if (requestScopePropagator != null) {
563 : @SuppressWarnings("unused")
564 88 : Future<?> possiblyIgnoredError =
565 88 : sendEmailExecutor.submit(requestScopePropagator.wrap(sender));
566 88 : } else {
567 48 : sender.run();
568 : }
569 : }
570 :
571 : /* For labels that are not set in this operation, show the "current" value
572 : * of 0, and no oldValue as the value was not modified by this operation.
573 : * For labels that are set in this operation, the value was modified, so
574 : * show a transition from an oldValue of 0 to the new value.
575 : */
576 103 : if (fireRevisionCreated) {
577 103 : revisionCreated.fire(
578 103 : ctx.getChangeData(change), patchSet, ctx.getAccount(), ctx.getWhen(), notify);
579 103 : if (approvals != null && !approvals.isEmpty()) {
580 4 : List<LabelType> labels = projectState.getLabelTypes(change.getDest()).getLabelTypes();
581 4 : Map<String, Short> allApprovals = new HashMap<>();
582 4 : Map<String, Short> oldApprovals = new HashMap<>();
583 4 : for (LabelType lt : labels) {
584 4 : allApprovals.put(lt.getName(), (short) 0);
585 4 : oldApprovals.put(lt.getName(), null);
586 4 : }
587 4 : for (Map.Entry<String, Short> entry : approvals.entrySet()) {
588 4 : if (entry.getValue() != 0) {
589 4 : allApprovals.put(entry.getKey(), entry.getValue());
590 4 : oldApprovals.put(entry.getKey(), (short) 0);
591 : }
592 4 : }
593 4 : commentAdded.fire(
594 4 : ctx.getChangeData(change),
595 : patchSet,
596 4 : ctx.getAccount(),
597 : null,
598 : allApprovals,
599 : oldApprovals,
600 4 : ctx.getWhen());
601 : }
602 : }
603 103 : }
604 :
605 : private void validate(RepoContext ctx) throws IOException, ResourceConflictException {
606 103 : if (!validate) {
607 92 : return;
608 : }
609 :
610 : try {
611 48 : try (CommitReceivedEvent event =
612 : new CommitReceivedEvent(
613 : cmd,
614 48 : projectState.getProject(),
615 48 : change.getDest().branch(),
616 : validationOptions,
617 48 : ctx.getRepoView().getConfig(),
618 48 : ctx.getRevWalk().getObjectReader(),
619 : commitId,
620 48 : ctx.getIdentifiedUser())) {
621 48 : commitValidatorsFactory
622 48 : .forGerritCommits(
623 48 : permissionBackend.user(ctx.getUser()).project(ctx.getProject()),
624 48 : BranchNameKey.create(ctx.getProject(), refName),
625 48 : ctx.getIdentifiedUser(),
626 : new NoSshInfo(),
627 48 : ctx.getRevWalk(),
628 : change)
629 48 : .validate(event);
630 : }
631 1 : } catch (CommitValidationException e) {
632 1 : throw new ResourceConflictException(e.getFullMessage());
633 48 : }
634 48 : }
635 :
636 : private static InternalReviewerInput newReviewerInput(
637 : String reviewer, ReviewerState state, boolean skipVisibilityCheck) {
638 : // Disable individual emails when adding reviewers, as all reviewers will receive the single
639 : // bulk new change email.
640 10 : InternalReviewerInput input =
641 10 : ReviewerModifier.newReviewerInput(reviewer, state, NotifyHandling.NONE);
642 :
643 : // Ignore failures for reasons like the reviewer being inactive or being unable to see the
644 : // change. This is required for the push path, where it automatically sets reviewers from
645 : // certain commit footers: putting a nonexistent user in a footer should not cause an error. In
646 : // theory we could provide finer control to do this for some reviewers and not others, but it's
647 : // not worth complicating the ChangeInserter interface further at this time.
648 10 : input.otherFailureBehavior = ReviewerModifier.FailureBehavior.IGNORE_EXCEPT_NOT_FOUND;
649 :
650 10 : input.skipVisibilityCheck = skipVisibilityCheck;
651 :
652 10 : return input;
653 : }
654 :
655 : private ImmutableList<InternalReviewerInput> getReviewerInputs() {
656 103 : return Streams.concat(
657 103 : reviewerInputs.stream(),
658 103 : Streams.stream(
659 103 : newReviewerInputFromCommitIdentity(
660 : change,
661 103 : patchSetInfo.getCommitId(),
662 103 : patchSetInfo.getAuthor().getAccount(),
663 : NotifyHandling.NONE,
664 103 : change.getOwner())),
665 103 : Streams.stream(
666 103 : newReviewerInputFromCommitIdentity(
667 : change,
668 103 : patchSetInfo.getCommitId(),
669 103 : patchSetInfo.getCommitter().getAccount(),
670 : NotifyHandling.NONE,
671 103 : change.getOwner())))
672 103 : .collect(toImmutableList());
673 : }
674 : }
|