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.notedb;
16 :
17 : import static com.google.common.base.MoreObjects.firstNonNull;
18 : import static com.google.common.base.Preconditions.checkArgument;
19 : import static com.google.common.base.Preconditions.checkState;
20 : import static com.google.gerrit.entities.RefNames.changeMetaRef;
21 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_ASSIGNEE;
22 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_ATTENTION;
23 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_BRANCH;
24 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CHANGE_ID;
25 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CHERRY_PICK_OF;
26 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_COMMIT;
27 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_COPIED_LABEL;
28 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CURRENT;
29 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_GROUPS;
30 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_HASHTAGS;
31 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_LABEL;
32 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_PATCH_SET;
33 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_PATCH_SET_DESCRIPTION;
34 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_PRIVATE;
35 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_REAL_USER;
36 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_REVERT_OF;
37 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_STATUS;
38 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_SUBJECT;
39 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_SUBMISSION_ID;
40 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_SUBMITTED_WITH;
41 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_TAG;
42 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_TOPIC;
43 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_WORK_IN_PROGRESS;
44 : import static com.google.gerrit.server.notedb.NoteDbUtil.sanitizeFooter;
45 : import static com.google.gerrit.server.project.ProjectCache.illegalState;
46 : import static java.util.Comparator.naturalOrder;
47 : import static java.util.Objects.requireNonNull;
48 : import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
49 :
50 : import com.google.common.annotations.VisibleForTesting;
51 : import com.google.common.base.Joiner;
52 : import com.google.common.base.Strings;
53 : import com.google.common.collect.ImmutableList;
54 : import com.google.common.collect.ImmutableSet;
55 : import com.google.common.collect.ImmutableTable;
56 : import com.google.common.collect.Iterables;
57 : import com.google.common.collect.Table;
58 : import com.google.common.collect.Table.Cell;
59 : import com.google.common.collect.TreeBasedTable;
60 : import com.google.gerrit.common.Nullable;
61 : import com.google.gerrit.entities.Account;
62 : import com.google.gerrit.entities.Address;
63 : import com.google.gerrit.entities.AttentionSetUpdate;
64 : import com.google.gerrit.entities.AttentionSetUpdate.Operation;
65 : import com.google.gerrit.entities.Change;
66 : import com.google.gerrit.entities.Comment;
67 : import com.google.gerrit.entities.HumanComment;
68 : import com.google.gerrit.entities.LabelId;
69 : import com.google.gerrit.entities.PatchSet;
70 : import com.google.gerrit.entities.PatchSetApproval;
71 : import com.google.gerrit.entities.Project;
72 : import com.google.gerrit.entities.RobotComment;
73 : import com.google.gerrit.entities.SubmissionId;
74 : import com.google.gerrit.entities.SubmitRecord;
75 : import com.google.gerrit.entities.SubmitRequirementResult;
76 : import com.google.gerrit.exceptions.StorageException;
77 : import com.google.gerrit.extensions.client.ReviewerState;
78 : import com.google.gerrit.server.CurrentUser;
79 : import com.google.gerrit.server.GerritPersonIdent;
80 : import com.google.gerrit.server.account.ServiceUserClassifier;
81 : import com.google.gerrit.server.approval.PatchSetApprovalUuidGenerator;
82 : import com.google.gerrit.server.project.ProjectCache;
83 : import com.google.gerrit.server.util.AttentionSetUtil;
84 : import com.google.gerrit.server.util.LabelVote;
85 : import com.google.gerrit.server.validators.ValidationException;
86 : import com.google.inject.assistedinject.Assisted;
87 : import com.google.inject.assistedinject.AssistedInject;
88 : import java.io.IOException;
89 : import java.time.Instant;
90 : import java.util.ArrayList;
91 : import java.util.Collection;
92 : import java.util.Comparator;
93 : import java.util.HashMap;
94 : import java.util.HashSet;
95 : import java.util.LinkedHashMap;
96 : import java.util.List;
97 : import java.util.Map;
98 : import java.util.Objects;
99 : import java.util.Optional;
100 : import java.util.Set;
101 : import java.util.stream.Collectors;
102 : import java.util.stream.Stream;
103 : import org.eclipse.jgit.errors.ConfigInvalidException;
104 : import org.eclipse.jgit.lib.CommitBuilder;
105 : import org.eclipse.jgit.lib.ObjectId;
106 : import org.eclipse.jgit.lib.ObjectInserter;
107 : import org.eclipse.jgit.lib.PersonIdent;
108 : import org.eclipse.jgit.notes.NoteMap;
109 : import org.eclipse.jgit.revwalk.FooterKey;
110 : import org.eclipse.jgit.revwalk.RevCommit;
111 : import org.eclipse.jgit.revwalk.RevWalk;
112 :
113 : /**
114 : * A delta to apply to a change.
115 : *
116 : * <p>This delta will become two unique commits: one in the AllUsers repo that will contain the
117 : * draft comments on this change and one in the notes branch that will contain approvals, reviewers,
118 : * change status, subject, submit records, the change message, and published comments. There are
119 : * limitations on the set of modifications that can be handled in a single update. In particular,
120 : * there is a single author and timestamp for each update.
121 : *
122 : * <p>This class is not thread-safe.
123 : *
124 : * <p>NOTE: This class also serializes the change in a custom storage format, used in NoteDB. All
125 : * changes to the storage format must be both forward and backward compatible, see comment on {@link
126 : * ChangeNotesParser}.
127 : *
128 : * <p>Such changes include e.g. introducing/removing footers, modifying footer formats, mutations of
129 : * the attached {@link ChangeRevisionNote}.
130 : */
131 : public class ChangeUpdate extends AbstractChangeUpdate {
132 : public interface Factory {
133 : ChangeUpdate create(ChangeNotes notes, CurrentUser user, Instant when);
134 :
135 : ChangeUpdate create(
136 : ChangeNotes notes, CurrentUser user, Instant when, Comparator<String> labelNameComparator);
137 : }
138 :
139 : private final NoteDbUpdateManager.Factory updateManagerFactory;
140 : private final ChangeDraftUpdate.Factory draftUpdateFactory;
141 : private final RobotCommentUpdate.Factory robotCommentUpdateFactory;
142 : private final DeleteCommentRewriter.Factory deleteCommentRewriterFactory;
143 : private final ServiceUserClassifier serviceUserClassifier;
144 : private final PatchSetApprovalUuidGenerator patchSetApprovalUuidGenerator;
145 :
146 : private final Table<String, Account.Id, Optional<PatchSetApproval>> approvals;
147 103 : private final List<PatchSetApproval> copiedApprovals = new ArrayList<>();
148 103 : private final Map<Account.Id, ReviewerStateInternal> reviewers = new LinkedHashMap<>();
149 103 : private final Map<Address, ReviewerStateInternal> reviewersByEmail = new LinkedHashMap<>();
150 103 : private final List<HumanComment> comments = new ArrayList<>();
151 :
152 : private String commitSubject;
153 : private String subject;
154 : private String changeId;
155 : private String branch;
156 : private Change.Status status;
157 : private List<SubmitRecord> submitRecords;
158 : private String submissionId;
159 : private String topic;
160 : private String commit;
161 : private Map<Account.Id, AttentionSetUpdate> plannedAttentionSetUpdates;
162 : private boolean ignoreFurtherAttentionSetUpdates;
163 : private Optional<Account.Id> assignee;
164 : private Set<String> hashtags;
165 : private String changeMessage;
166 : private String tag;
167 : private PatchSetState psState;
168 : private Iterable<String> groups;
169 : private String pushCert;
170 : private boolean isAllowWriteToNewtRef;
171 : private String psDescription;
172 : private boolean currentPatchSet;
173 : private Boolean isPrivate;
174 : private Boolean workInProgress;
175 : private Integer revertOf;
176 : // If null, the update does not modify the field. Otherwise, it updates the field with the
177 : // new value or resets if cherryPickOf == Optional.empty().
178 : private Optional<String> cherryPickOf;
179 :
180 : private ChangeDraftUpdate draftUpdate;
181 : private RobotCommentUpdate robotCommentUpdate;
182 : private DeleteCommentRewriter deleteCommentRewriter;
183 : private DeleteChangeMessageRewriter deleteChangeMessageRewriter;
184 : private List<SubmitRequirementResult> submitRequirementResults;
185 :
186 103 : private ImmutableList.Builder<AttentionSetUpdate> attentionSetUpdatesBuilder =
187 103 : ImmutableList.builder();
188 :
189 : @SuppressWarnings("UnusedMethod")
190 : @AssistedInject
191 : private ChangeUpdate(
192 : @GerritPersonIdent PersonIdent serverIdent,
193 : NoteDbUpdateManager.Factory updateManagerFactory,
194 : ChangeDraftUpdate.Factory draftUpdateFactory,
195 : RobotCommentUpdate.Factory robotCommentUpdateFactory,
196 : DeleteCommentRewriter.Factory deleteCommentRewriterFactory,
197 : ProjectCache projectCache,
198 : ServiceUserClassifier serviceUserClassifier,
199 : PatchSetApprovalUuidGenerator patchSetApprovalUuidGenerator,
200 : @Assisted ChangeNotes notes,
201 : @Assisted CurrentUser user,
202 : @Assisted Instant when,
203 : ChangeNoteUtil noteUtil) {
204 103 : this(
205 : serverIdent,
206 : updateManagerFactory,
207 : draftUpdateFactory,
208 : robotCommentUpdateFactory,
209 : deleteCommentRewriterFactory,
210 : serviceUserClassifier,
211 : patchSetApprovalUuidGenerator,
212 : notes,
213 : user,
214 : when,
215 : projectCache
216 103 : .get(notes.getProjectName())
217 103 : .orElseThrow(illegalState(notes.getProjectName()))
218 103 : .getLabelTypes()
219 103 : .nameComparator(),
220 : noteUtil);
221 103 : }
222 :
223 : private static Table<String, Account.Id, Optional<PatchSetApproval>> approvals(
224 : Comparator<String> nameComparator) {
225 103 : return TreeBasedTable.create(nameComparator, naturalOrder());
226 : }
227 :
228 : @AssistedInject
229 : private ChangeUpdate(
230 : @GerritPersonIdent PersonIdent serverIdent,
231 : NoteDbUpdateManager.Factory updateManagerFactory,
232 : ChangeDraftUpdate.Factory draftUpdateFactory,
233 : RobotCommentUpdate.Factory robotCommentUpdateFactory,
234 : DeleteCommentRewriter.Factory deleteCommentRewriterFactory,
235 : ServiceUserClassifier serviceUserClassifier,
236 : PatchSetApprovalUuidGenerator patchSetApprovalUuidGenerator,
237 : @Assisted ChangeNotes notes,
238 : @Assisted CurrentUser user,
239 : @Assisted Instant when,
240 : @Assisted Comparator<String> labelNameComparator,
241 : ChangeNoteUtil noteUtil) {
242 103 : super(notes, user, serverIdent, noteUtil, when);
243 103 : this.updateManagerFactory = updateManagerFactory;
244 103 : this.draftUpdateFactory = draftUpdateFactory;
245 103 : this.robotCommentUpdateFactory = robotCommentUpdateFactory;
246 103 : this.deleteCommentRewriterFactory = deleteCommentRewriterFactory;
247 103 : this.serviceUserClassifier = serviceUserClassifier;
248 103 : this.patchSetApprovalUuidGenerator = patchSetApprovalUuidGenerator;
249 103 : this.approvals = approvals(labelNameComparator);
250 103 : }
251 :
252 : public ObjectId commit() throws IOException {
253 2 : try (NoteDbUpdateManager updateManager = updateManagerFactory.create(getProjectName())) {
254 2 : updateManager.add(this);
255 2 : updateManager.execute();
256 : }
257 2 : return getResult();
258 : }
259 :
260 : public void setChangeId(String changeId) {
261 103 : String old = getChange().getKey().get();
262 103 : checkArgument(
263 103 : old.equals(changeId),
264 : "The Change-Id was already set to %s, so we cannot set this Change-Id: %s",
265 : old,
266 : changeId);
267 103 : this.changeId = changeId;
268 103 : }
269 :
270 : public void setBranch(String branch) {
271 103 : this.branch = branch;
272 103 : }
273 :
274 : public void setStatus(Change.Status status) {
275 103 : checkArgument(status != Change.Status.MERGED, "use merge(RequestId, Iterable<SubmitRecord>)");
276 103 : this.status = status;
277 103 : }
278 :
279 : public void fixStatusToMerged(SubmissionId submissionId) {
280 16 : checkArgument(submissionId != null, "submission id must be set for merged changes");
281 16 : this.status = Change.Status.MERGED;
282 16 : this.submissionId = submissionId.toString();
283 16 : }
284 :
285 : public void putApproval(String label, short value) {
286 68 : putApprovalFor(getAccountId(), label, value);
287 68 : }
288 :
289 : public void putApprovalFor(Account.Id reviewer, String label, short value) {
290 : PatchSetApproval psa =
291 68 : PatchSetApproval.builder()
292 68 : .key(PatchSetApproval.key(getPatchSetId(), reviewer, LabelId.create(label)))
293 68 : .value(value)
294 68 : .granted(when)
295 68 : .uuid(patchSetApprovalUuidGenerator.get(getPatchSetId(), reviewer, label, value, when))
296 68 : .build();
297 68 : approvals.put(label, reviewer, Optional.of(psa));
298 68 : }
299 :
300 : public ImmutableTable<String, Account.Id, Optional<PatchSetApproval>> getApprovals() {
301 7 : return ImmutableTable.copyOf(approvals);
302 : }
303 :
304 : void removeApproval(String label) {
305 1 : removeApprovalFor(getAccountId(), label);
306 1 : }
307 :
308 : public void removeApprovalFor(Account.Id reviewer, String label) {
309 11 : approvals.put(label, reviewer, Optional.empty());
310 11 : }
311 :
312 : /**
313 : * We expect the {@code copied} flag of {@code copiedPatchSetApproval} to be set, since this
314 : * method is only meant for copied approvals.
315 : */
316 : public void putCopiedApproval(PatchSetApproval copiedPatchSetApproval) {
317 13 : checkArgument(copiedPatchSetApproval.copied(), "Approval that should be copied is not copied.");
318 13 : copiedApprovals.add(copiedPatchSetApproval);
319 13 : }
320 :
321 : public void removeCopiedApprovalFor(
322 : @Nullable Account.Id realUserId, Account.Id reviewerId, String label) {
323 : PatchSetApproval.Builder psaBuilder =
324 1 : PatchSetApproval.builder()
325 1 : .copied(true)
326 1 : .key(PatchSetApproval.key(getPatchSetId(), reviewerId, LabelId.create(label)))
327 1 : .value(0)
328 1 : .uuid(Optional.empty())
329 1 : .granted(when);
330 :
331 1 : if (realUserId != null) {
332 1 : psaBuilder.realAccountId(realUserId);
333 : }
334 :
335 1 : copiedApprovals.add(psaBuilder.build());
336 1 : }
337 :
338 : public void merge(SubmissionId submissionId, Iterable<SubmitRecord> submitRecords) {
339 55 : this.status = Change.Status.MERGED;
340 55 : this.submissionId = submissionId.toString();
341 55 : this.submitRecords = ImmutableList.copyOf(submitRecords);
342 55 : checkArgument(!this.submitRecords.isEmpty(), "no submit records specified at submit time");
343 55 : }
344 :
345 : public void setSubjectForCommit(String commitSubject) {
346 103 : this.commitSubject = commitSubject;
347 103 : }
348 :
349 : public void setSubject(String subject) {
350 0 : this.subject = subject;
351 0 : }
352 :
353 : @VisibleForTesting
354 : ObjectId getCommit() {
355 1 : return ObjectId.fromString(commit);
356 : }
357 :
358 : public void setChangeMessage(String changeMessage) {
359 103 : this.changeMessage = changeMessage;
360 103 : }
361 :
362 : public void setTag(String tag) {
363 103 : this.tag = tag;
364 103 : }
365 :
366 : public void setPsDescription(String psDescription) {
367 103 : this.psDescription = psDescription;
368 103 : }
369 :
370 : public void putSubmitRequirementResults(Collection<SubmitRequirementResult> rs) {
371 54 : if (submitRequirementResults == null) {
372 54 : submitRequirementResults = new ArrayList<>();
373 : }
374 54 : submitRequirementResults.addAll(rs);
375 54 : }
376 :
377 : public void putComment(Comment.Status status, HumanComment c) {
378 29 : verifyComment(c);
379 29 : createDraftUpdateIfNull();
380 29 : if (status == HumanComment.Status.DRAFT) {
381 21 : draftUpdate.putComment(c);
382 : } else {
383 26 : comments.add(c);
384 26 : draftUpdate.markCommentPublished(c);
385 : }
386 29 : }
387 :
388 : public void putRobotComment(RobotComment c) {
389 9 : verifyComment(c);
390 9 : createRobotCommentUpdateIfNull();
391 9 : robotCommentUpdate.putComment(c);
392 9 : }
393 :
394 : public void deleteComment(HumanComment c) {
395 10 : verifyComment(c);
396 10 : createDraftUpdateIfNull().deleteComment(c);
397 10 : }
398 :
399 : public void deleteCommentByRewritingHistory(String uuid, String newMessage) {
400 3 : deleteCommentRewriter =
401 3 : deleteCommentRewriterFactory.create(getChange().getId(), uuid, newMessage);
402 3 : }
403 :
404 : public void deleteChangeMessageByRewritingHistory(String targetMessageId, String newMessage) {
405 1 : deleteChangeMessageRewriter =
406 1 : new DeleteChangeMessageRewriter(getChange().getId(), targetMessageId, newMessage);
407 1 : }
408 :
409 : @VisibleForTesting
410 : ChangeDraftUpdate createDraftUpdateIfNull() {
411 29 : if (draftUpdate == null) {
412 29 : ChangeNotes notes = getNotes();
413 29 : if (notes != null) {
414 29 : draftUpdate = draftUpdateFactory.create(notes, accountId, realAccountId, authorIdent, when);
415 : } else {
416 : // tests will always take the notes != null path above.
417 0 : draftUpdate =
418 0 : draftUpdateFactory.create(getChange(), accountId, realAccountId, authorIdent, when);
419 : }
420 : }
421 29 : return draftUpdate;
422 : }
423 :
424 : private void createRobotCommentUpdateIfNull() {
425 9 : if (robotCommentUpdate == null) {
426 9 : ChangeNotes notes = getNotes();
427 9 : if (notes != null) {
428 9 : robotCommentUpdate =
429 9 : robotCommentUpdateFactory.create(notes, accountId, realAccountId, authorIdent, when);
430 : } else {
431 0 : robotCommentUpdate =
432 0 : robotCommentUpdateFactory.create(
433 0 : getChange(), accountId, realAccountId, authorIdent, when);
434 : }
435 : }
436 9 : }
437 :
438 : public void setTopic(String topic) throws ValidationException {
439 :
440 103 : if (isIllegalTopic(topic)) {
441 2 : throw new ValidationException("topic can't contain quotation marks.");
442 : }
443 103 : this.topic = Strings.nullToEmpty(topic);
444 103 : }
445 :
446 : public void setCommit(RevWalk rw, ObjectId id) throws IOException {
447 1 : setCommit(rw, id, null);
448 1 : }
449 :
450 : public void setCommit(RevWalk rw, ObjectId id, String pushCert) throws IOException {
451 103 : RevCommit commit = rw.parseCommit(id);
452 103 : rw.parseBody(commit);
453 103 : this.commit = commit.name();
454 103 : subject = commit.getShortMessage();
455 103 : this.pushCert = pushCert;
456 103 : }
457 :
458 : public void setHashtags(Set<String> hashtags) {
459 9 : this.hashtags = hashtags;
460 9 : }
461 :
462 : /**
463 : * Adds attention set updates that should be stored in NoteDb.
464 : *
465 : * <p>If invoked multiple times with attention set updates for the same user, only the attention
466 : * set update of the first invocation is stored for this user and further attention set updates
467 : * for this user are silently ignored. This means if callers invoke this method multiple times
468 : * with attention set updates for the same user, they must ensure that the first call is being
469 : * done with the attention set update that should take precedence.
470 : *
471 : * @param updates Attention set updates that should be performed. The updates must not have any
472 : * timestamp set ({@link AttentionSetUpdate#timestamp()} must return {@code null}). This is
473 : * because the timestamp of all performed updates is always the timestamp of when the NoteDb
474 : * commit is created. Each of the provided updates must be for a different user, if there are
475 : * multiple updates for the same user the update is rejected.
476 : * @throws IllegalArgumentException thrown if any of the provided updates has a timestamp set, or
477 : * if the provided set of updates contains multiple updates for the same user
478 : */
479 : public void addToPlannedAttentionSetUpdates(Set<AttentionSetUpdate> updates) {
480 103 : if (updates == null || updates.isEmpty() || ignoreFurtherAttentionSetUpdates) {
481 : // No updates to do. Robots don't change attention set.
482 103 : return;
483 : }
484 74 : checkArgument(
485 74 : updates.stream().noneMatch(a -> a.timestamp() != null),
486 : "must not specify timestamp for write");
487 :
488 74 : checkArgument(
489 74 : updates.stream().map(AttentionSetUpdate::account).distinct().count() == updates.size(),
490 : "must not specify multiple updates for single user");
491 :
492 74 : if (plannedAttentionSetUpdates == null) {
493 74 : plannedAttentionSetUpdates = new HashMap<>();
494 : }
495 :
496 74 : Set<Account.Id> currentAccountUpdates =
497 74 : plannedAttentionSetUpdates.values().stream()
498 74 : .map(AttentionSetUpdate::account)
499 74 : .collect(Collectors.toSet());
500 74 : updates.stream()
501 74 : .filter(u -> !currentAccountUpdates.contains(u.account()))
502 74 : .forEach(u -> plannedAttentionSetUpdates.putIfAbsent(u.account(), u));
503 74 : }
504 :
505 : public void addToPlannedAttentionSetUpdates(AttentionSetUpdate update) {
506 70 : addToPlannedAttentionSetUpdates(ImmutableSet.of(update));
507 70 : }
508 :
509 : public ImmutableList<AttentionSetUpdate> getAttentionSetUpdates() {
510 103 : return attentionSetUpdatesBuilder.build();
511 : }
512 :
513 : public void setAssignee(Account.Id assignee) {
514 7 : checkArgument(assignee != null, "use removeAssignee");
515 7 : this.assignee = Optional.of(assignee);
516 7 : }
517 :
518 : public void removeAssignee() {
519 2 : this.assignee = Optional.empty();
520 2 : }
521 :
522 : public Map<Account.Id, ReviewerStateInternal> getReviewers() {
523 42 : return reviewers;
524 : }
525 :
526 : public void putReviewer(Account.Id reviewer, ReviewerStateInternal type) {
527 75 : checkArgument(type != ReviewerStateInternal.REMOVED, "invalid ReviewerType");
528 75 : reviewers.put(reviewer, type);
529 75 : }
530 :
531 : public void removeReviewer(Account.Id reviewer) {
532 17 : reviewers.put(reviewer, ReviewerStateInternal.REMOVED);
533 17 : }
534 :
535 : public void putReviewerByEmail(Address reviewer, ReviewerStateInternal type) {
536 11 : checkArgument(type != ReviewerStateInternal.REMOVED, "invalid ReviewerType");
537 11 : reviewersByEmail.put(reviewer, type);
538 11 : }
539 :
540 : public void removeReviewerByEmail(Address reviewer) {
541 8 : reviewersByEmail.put(reviewer, ReviewerStateInternal.REMOVED);
542 8 : }
543 :
544 : public void setPatchSetState(PatchSetState psState) {
545 2 : this.psState = psState;
546 2 : }
547 :
548 : public void setCurrentPatchSet() {
549 12 : this.currentPatchSet = true;
550 12 : }
551 :
552 : public void setGroups(List<String> groups) {
553 103 : requireNonNull(groups, "groups may not be null");
554 103 : this.groups = groups;
555 103 : }
556 :
557 : public void setRevertOf(int revertOf) {
558 15 : int ownId = getId().get();
559 15 : checkArgument(ownId != revertOf, "A change cannot revert itself");
560 15 : this.revertOf = revertOf;
561 15 : rootOnly = true;
562 15 : }
563 :
564 : public void setCherryPickOf(String cherryPickOf) {
565 11 : checkArgument(cherryPickOf != null, "use resetCherryPickOf");
566 11 : this.cherryPickOf = Optional.of(cherryPickOf);
567 11 : }
568 :
569 : public void resetCherryPickOf() {
570 2 : this.cherryPickOf = Optional.empty();
571 2 : }
572 :
573 : /** Returns the tree id for the updated tree */
574 : @Nullable
575 : private ObjectId storeRevisionNotes(RevWalk rw, ObjectInserter inserter, ObjectId curr)
576 : throws ConfigInvalidException, IOException {
577 103 : if (submitRequirementResults == null && comments.isEmpty() && pushCert == null) {
578 103 : return null;
579 : }
580 64 : RevisionNoteMap<ChangeRevisionNote> rnm = getRevisionNoteMap(rw, curr);
581 :
582 64 : RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(rnm);
583 64 : for (HumanComment c : comments) {
584 26 : c.tag = tag;
585 26 : cache.get(c.getCommitId()).putComment(c);
586 26 : }
587 64 : if (submitRequirementResults != null) {
588 54 : if (submitRequirementResults.isEmpty()) {
589 54 : ObjectId latestPsCommitId =
590 54 : Iterables.getLast(getNotes().getPatchSets().values()).commitId();
591 54 : cache.get(latestPsCommitId).createEmptySubmitRequirementResults();
592 54 : } else {
593 : // Clear any previously stored SRs first. The SRs in this update will overwrite any
594 : // previously stored SRs (e.g. if the change is abandoned (SRs stored) -> un-abandoned ->
595 : // merged).
596 2 : submitRequirementResults.stream()
597 2 : .map(SubmitRequirementResult::patchSetCommitId)
598 2 : .distinct()
599 2 : .forEach(commit -> cache.get(commit).clearSubmitRequirementResults());
600 2 : for (SubmitRequirementResult sr : submitRequirementResults) {
601 2 : cache.get(sr.patchSetCommitId()).putSubmitRequirementResult(sr);
602 2 : }
603 : }
604 : }
605 64 : if (pushCert != null) {
606 1 : checkState(commit != null);
607 1 : cache.get(ObjectId.fromString(commit)).setPushCertificate(pushCert);
608 : }
609 64 : Map<ObjectId, RevisionNoteBuilder> builders = cache.getBuilders();
610 64 : checkComments(rnm.revisionNotes, builders);
611 :
612 64 : for (Map.Entry<ObjectId, RevisionNoteBuilder> e : builders.entrySet()) {
613 64 : ObjectId data = inserter.insert(OBJ_BLOB, e.getValue().build(noteUtil.getChangeNoteJson()));
614 64 : rnm.noteMap.set(e.getKey(), data);
615 64 : }
616 :
617 64 : return rnm.noteMap.writeTree(inserter);
618 : }
619 :
620 : private RevisionNoteMap<ChangeRevisionNote> getRevisionNoteMap(RevWalk rw, ObjectId curr)
621 : throws ConfigInvalidException, IOException {
622 64 : if (curr.equals(ObjectId.zeroId())) {
623 0 : return RevisionNoteMap.emptyMap();
624 : }
625 : // The old ChangeNotes may have already parsed the revision notes. We can reuse them as long as
626 : // the ref hasn't advanced.
627 64 : ChangeNotes notes = getNotes();
628 64 : if (notes != null && notes.revisionNoteMap != null) {
629 1 : ObjectId idFromNotes = firstNonNull(notes.load().getRevision(), ObjectId.zeroId());
630 1 : if (idFromNotes.equals(curr)) {
631 1 : return notes.revisionNoteMap;
632 : }
633 : }
634 64 : NoteMap noteMap = NoteMap.read(rw.getObjectReader(), rw.parseCommit(curr));
635 : // Even though reading from changes might not be enabled, we need to
636 : // parse any existing revision notes so we can merge them.
637 64 : return RevisionNoteMap.parse(
638 64 : noteUtil.getChangeNoteJson(), rw.getObjectReader(), noteMap, HumanComment.Status.PUBLISHED);
639 : }
640 :
641 : private void checkComments(
642 : Map<ObjectId, ChangeRevisionNote> existingNotes,
643 : Map<ObjectId, RevisionNoteBuilder> toUpdate) {
644 : // Prohibit various kinds of illegal operations on comments.
645 64 : Set<Comment.Key> existing = new HashSet<>();
646 64 : for (ChangeRevisionNote rn : existingNotes.values()) {
647 24 : for (Comment c : rn.getEntities()) {
648 17 : existing.add(c.key);
649 17 : if (draftUpdate != null) {
650 : // Take advantage of an existing update on All-Users to prune any
651 : // published comments from drafts. NoteDbUpdateManager takes care of
652 : // ensuring that this update is applied before its dependent draft
653 : // update.
654 : //
655 : // Deleting aggressively in this way, combined with filtering out
656 : // duplicate published/draft comments in ChangeNotes#getDraftComments,
657 : // makes up for the fact that updates between the change repo and
658 : // All-Users are not atomic.
659 : //
660 : // TODO(dborowitz): We might want to distinguish between deleted
661 : // drafts that we're fixing up after the fact by putting them in a
662 : // separate commit. But note that we don't care much about the commit
663 : // graph of the draft ref, particularly because the ref is completely
664 : // deleted when all drafts are gone.
665 16 : draftUpdate.deleteComment(c.getCommitId(), c.key);
666 : }
667 17 : }
668 24 : }
669 :
670 64 : for (RevisionNoteBuilder b : toUpdate.values()) {
671 64 : for (Comment c : b.put.values()) {
672 26 : if (existing.contains(c.key)) {
673 0 : throw new StorageException("Cannot update existing published comment: " + c);
674 : }
675 26 : }
676 64 : }
677 64 : }
678 :
679 : @Override
680 : protected String getRefName() {
681 103 : return changeMetaRef(getId());
682 : }
683 :
684 : @Override
685 : protected boolean bypassMaxUpdates() {
686 2 : return isAbandonChange() || isAttentionSetChangeOnly();
687 : }
688 :
689 : private boolean isAbandonChange() {
690 2 : return status != null && status.isClosed();
691 : }
692 :
693 : private boolean isAttentionSetChangeOnly() {
694 2 : return (plannedAttentionSetUpdates != null
695 2 : && plannedAttentionSetUpdates.size() > 0
696 2 : && doesNotHaveChangesAffectingAttentionSet());
697 : }
698 :
699 : private boolean doesNotHaveChangesAffectingAttentionSet() {
700 1 : return comments.isEmpty()
701 1 : && reviewers.isEmpty()
702 1 : && reviewersByEmail.isEmpty()
703 1 : && approvals.isEmpty()
704 : && workInProgress == null;
705 : }
706 :
707 : @Override
708 : protected CommitBuilder applyImpl(RevWalk rw, ObjectInserter ins, ObjectId curr)
709 : throws IOException {
710 103 : checkState(
711 : deleteCommentRewriter == null && deleteChangeMessageRewriter == null,
712 : "cannot update and rewrite ref in one BatchUpdate");
713 :
714 103 : PatchSet.Id patchSetId = psId != null ? psId : getChange().currentPatchSetId();
715 103 : StringBuilder msg = new StringBuilder();
716 103 : if (commitSubject != null) {
717 103 : msg.append(commitSubject);
718 : } else {
719 82 : msg.append("Update patch set ").append(patchSetId.get());
720 : }
721 103 : msg.append("\n\n");
722 :
723 103 : if (changeMessage != null) {
724 103 : msg.append(changeMessage);
725 103 : msg.append("\n\n");
726 : }
727 :
728 103 : addPatchSetFooter(msg, patchSetId);
729 :
730 103 : if (currentPatchSet) {
731 12 : addFooter(msg, FOOTER_CURRENT, Boolean.TRUE);
732 : }
733 :
734 103 : if (psDescription != null) {
735 24 : addFooter(msg, FOOTER_PATCH_SET_DESCRIPTION, psDescription);
736 : }
737 :
738 103 : if (changeId != null) {
739 103 : addFooter(msg, FOOTER_CHANGE_ID, changeId);
740 : }
741 :
742 103 : if (subject != null) {
743 103 : addFooter(msg, FOOTER_SUBJECT, subject);
744 : }
745 :
746 103 : if (branch != null) {
747 103 : addFooter(msg, FOOTER_BRANCH, branch);
748 : }
749 :
750 103 : if (status != null) {
751 103 : addFooter(msg, FOOTER_STATUS, status.name().toLowerCase());
752 103 : if (status.equals(Change.Status.ABANDONED)) {
753 22 : clearAttentionSet("Change was abandoned");
754 : }
755 103 : if (status.equals(Change.Status.MERGED)) {
756 57 : clearAttentionSet("Change was submitted");
757 : }
758 : }
759 :
760 103 : if (topic != null) {
761 103 : addFooter(msg, FOOTER_TOPIC, topic);
762 : }
763 :
764 103 : if (commit != null) {
765 103 : addFooter(msg, FOOTER_COMMIT, commit);
766 : }
767 :
768 103 : if (assignee != null) {
769 7 : if (assignee.isPresent()) {
770 7 : addFooter(msg, FOOTER_ASSIGNEE);
771 7 : noteUtil.appendAccountIdIdentString(msg, assignee.get()).append('\n');
772 : } else {
773 2 : addFooter(msg, FOOTER_ASSIGNEE).append('\n');
774 : }
775 : }
776 :
777 103 : Joiner comma = Joiner.on(',');
778 103 : if (hashtags != null) {
779 9 : addFooter(msg, FOOTER_HASHTAGS, comma.join(hashtags));
780 : }
781 :
782 103 : if (tag != null) {
783 103 : addFooter(msg, FOOTER_TAG, tag);
784 : }
785 :
786 103 : if (groups != null) {
787 103 : addFooter(msg, FOOTER_GROUPS, comma.join(groups));
788 : }
789 :
790 103 : for (Map.Entry<Account.Id, ReviewerStateInternal> e : reviewers.entrySet()) {
791 75 : addFooter(msg, e.getValue().getFooterKey());
792 75 : noteUtil.appendAccountIdIdentString(msg, e.getKey()).append('\n');
793 75 : }
794 :
795 103 : applyReviewerUpdatesToAttentionSet();
796 :
797 103 : for (Map.Entry<Address, ReviewerStateInternal> e : reviewersByEmail.entrySet()) {
798 11 : addFooter(msg, e.getValue().getByEmailFooterKey(), e.getKey().toString());
799 11 : }
800 :
801 103 : for (Table.Cell<String, Account.Id, Optional<PatchSetApproval>> c : approvals.cellSet()) {
802 68 : addLabelFooter(msg, c);
803 68 : }
804 103 : for (PatchSetApproval patchSetApproval : copiedApprovals) {
805 13 : addCopiedLabelFooter(msg, patchSetApproval);
806 13 : }
807 :
808 103 : if (submissionId != null) {
809 57 : addFooter(msg, FOOTER_SUBMISSION_ID, submissionId);
810 : }
811 :
812 103 : if (submitRecords != null) {
813 55 : for (SubmitRecord rec : submitRecords) {
814 55 : addFooter(msg, FOOTER_SUBMITTED_WITH).append(rec.status);
815 55 : if (rec.errorMessage != null) {
816 1 : msg.append(' ').append(sanitizeFooter(rec.errorMessage));
817 : }
818 55 : msg.append('\n');
819 55 : if (rec.ruleName != null) {
820 53 : addFooter(msg, FOOTER_SUBMITTED_WITH).append("Rule-Name: ").append(rec.ruleName);
821 53 : msg.append('\n');
822 : }
823 55 : if (rec.labels != null) {
824 55 : for (SubmitRecord.Label label : rec.labels) {
825 : // Label names/values are safe to append without sanitizing.
826 55 : addFooter(msg, FOOTER_SUBMITTED_WITH)
827 55 : .append(label.status)
828 55 : .append(": ")
829 55 : .append(label.label);
830 55 : if (label.appliedBy != null) {
831 48 : msg.append(": ");
832 48 : noteUtil.appendAccountIdIdentString(msg, label.appliedBy);
833 : }
834 55 : msg.append('\n');
835 55 : }
836 : }
837 55 : }
838 : }
839 :
840 103 : if (!Objects.equals(accountId, realAccountId)) {
841 4 : addFooter(msg, FOOTER_REAL_USER);
842 4 : noteUtil.appendAccountIdIdentString(msg, realAccountId).append('\n');
843 : }
844 :
845 103 : if (isPrivate != null) {
846 103 : addFooter(msg, FOOTER_PRIVATE, isPrivate);
847 : }
848 :
849 103 : if (workInProgress != null) {
850 103 : addFooter(msg, FOOTER_WORK_IN_PROGRESS, workInProgress);
851 103 : if (workInProgress) {
852 25 : clearAttentionSet("Change was marked work in progress");
853 : } else {
854 103 : addAllReviewersToAttentionSet();
855 : }
856 : }
857 :
858 103 : if (revertOf != null) {
859 15 : addFooter(msg, FOOTER_REVERT_OF, revertOf);
860 : }
861 :
862 103 : if (cherryPickOf != null) {
863 11 : if (cherryPickOf.isPresent()) {
864 11 : addFooter(msg, FOOTER_CHERRY_PICK_OF, cherryPickOf.get());
865 : } else {
866 : // Update cherryPickOf with an empty value.
867 2 : addFooter(msg, FOOTER_CHERRY_PICK_OF).append('\n');
868 : }
869 : }
870 :
871 103 : boolean hasAttentionSeUpdates = updateAttentionSet(msg);
872 103 : if (isEmptyWithoutAttentionSet() && !hasAttentionSeUpdates) {
873 25 : return NO_OP_UPDATE;
874 : }
875 :
876 103 : CommitBuilder cb = new CommitBuilder();
877 103 : cb.setMessage(msg.toString());
878 : try {
879 103 : ObjectId treeId = storeRevisionNotes(rw, ins, curr);
880 103 : if (treeId != null) {
881 64 : cb.setTreeId(treeId);
882 : }
883 0 : } catch (ConfigInvalidException e) {
884 0 : throw new StorageException(e);
885 103 : }
886 103 : return cb;
887 : }
888 :
889 : private void addLabelFooter(
890 : StringBuilder msg, Cell<String, Account.Id, Optional<PatchSetApproval>> c) {
891 68 : addFooter(msg, FOOTER_LABEL);
892 68 : String label = c.getRowKey();
893 68 : Account.Id reviewerId = c.getColumnKey();
894 : // Label names/values are safe to append without sanitizing.
895 68 : boolean isRemoval = !c.getValue().isPresent();
896 68 : if (isRemoval) {
897 11 : msg.append('-').append(label);
898 : // Since vote removals do not need to be referenced, e.g. by the copy approvals, they do not
899 : // require a UUID.
900 : } else {
901 68 : short value = c.getValue().get().value();
902 68 : msg.append(LabelVote.create(label, value).formatWithEquals());
903 68 : msg.append(", ");
904 68 : msg.append(c.getValue().get().uuid().get());
905 : }
906 68 : if (!reviewerId.equals(getAccountId())) {
907 7 : noteUtil.appendAccountIdIdentString(msg.append(' '), reviewerId);
908 : }
909 68 : msg.append('\n');
910 68 : }
911 :
912 : private void addCopiedLabelFooter(StringBuilder msg, PatchSetApproval patchSetApproval) {
913 13 : if (patchSetApproval.value() == 0) {
914 1 : addFooter(msg, FOOTER_COPIED_LABEL);
915 :
916 : // Mark the copied approval as deleted.
917 1 : msg.append('-').append(patchSetApproval.label());
918 :
919 1 : noteUtil.appendAccountIdIdentString(msg.append(' '), patchSetApproval.accountId());
920 :
921 : // In the non-copied labels, we don't need to pass the real account id since it's already
922 : // in FOOTER_REAL_USER. Here, we want to retain the original real account id.
923 1 : if (!patchSetApproval.realAccountId().equals(patchSetApproval.accountId())) {
924 0 : noteUtil.appendAccountIdIdentString(msg.append(","), patchSetApproval.realAccountId());
925 : }
926 :
927 1 : msg.append('\n');
928 1 : return;
929 : }
930 13 : addFooter(msg, FOOTER_COPIED_LABEL);
931 : // Label names/values are safe to append without sanitizing.
932 13 : msg.append(
933 13 : LabelVote.create(patchSetApproval.label(), patchSetApproval.value()).formatWithEquals());
934 : // Might be copied from the vote that was generated before UUID was introduced.
935 13 : if (patchSetApproval.uuid().isPresent()) {
936 13 : msg.append(", ");
937 13 : msg.append(patchSetApproval.uuid().get());
938 : }
939 13 : Account.Id id = patchSetApproval.accountId();
940 13 : noteUtil.appendAccountIdIdentString(msg.append(' '), id);
941 :
942 : // In the non-copied labels, we don't need to pass the real account id since it's already
943 : // in FOOTER_REAL_USER. Here, we want to retain the original real account id.
944 13 : if (!patchSetApproval.realAccountId().equals(patchSetApproval.accountId())) {
945 2 : noteUtil.appendAccountIdIdentString(msg.append(","), patchSetApproval.realAccountId());
946 : }
947 :
948 : // In the non-copied labels, we don't need to pass the tag since it's already in
949 : // FOOTER_TAG, but in this chase we want to retain the original tag, and not the current tag.
950 13 : if (patchSetApproval.tag().isPresent()) {
951 5 : msg.append(":\"" + sanitizeFooter(patchSetApproval.tag().get()) + "\"");
952 : }
953 :
954 13 : msg.append('\n');
955 13 : }
956 :
957 : private void clearAttentionSet(String reason) {
958 59 : if (getNotes().getAttentionSet() == null) {
959 0 : return;
960 : }
961 59 : AttentionSetUtil.additionsOnly(getNotes().getAttentionSet()).stream()
962 59 : .map(
963 : a ->
964 28 : AttentionSetUpdate.createForWrite(
965 28 : a.account(), AttentionSetUpdate.Operation.REMOVE, reason))
966 59 : .forEach(this::addToPlannedAttentionSetUpdates);
967 59 : }
968 :
969 : private void applyReviewerUpdatesToAttentionSet() {
970 103 : if ((workInProgress != null && workInProgress == true)
971 103 : || getNotes().getChange().isWorkInProgress()
972 : || status == Change.Status.MERGED) {
973 : // Attention set shouldn't change here for changes that are work in progress or are about to
974 : // be submitted or when the caller is a robot.
975 58 : return;
976 : }
977 :
978 103 : Set<AttentionSetUpdate> updates = new HashSet<>();
979 103 : Set<Account.Id> currentReviewers =
980 103 : getNotes().getReviewers().byState(ReviewerStateInternal.REVIEWER);
981 103 : for (Map.Entry<Account.Id, ReviewerStateInternal> reviewer : reviewers.entrySet()) {
982 70 : Account.Id reviewerId = reviewer.getKey();
983 :
984 70 : ReviewerStateInternal reviewerState = reviewer.getValue();
985 : // Only add new reviewers to the attention set. Also, don't add the owner because the owner
986 : // can only be a "dummy" reviewer for legacy reasons.
987 70 : if (reviewerState.equals(ReviewerStateInternal.REVIEWER)
988 69 : && !currentReviewers.contains(reviewerId)
989 69 : && !reviewerId.equals(getChange().getOwner())) {
990 48 : updates.add(
991 48 : AttentionSetUpdate.createForWrite(
992 : reviewerId, AttentionSetUpdate.Operation.ADD, "Reviewer was added"));
993 : }
994 70 : boolean reviewerRemoved =
995 70 : !reviewerState.equals(ReviewerStateInternal.REVIEWER)
996 70 : && currentReviewers.contains(reviewerId);
997 70 : boolean ccRemoved = reviewerState.equals(ReviewerStateInternal.REMOVED);
998 70 : if (reviewerRemoved || ccRemoved) {
999 13 : updates.add(
1000 13 : AttentionSetUpdate.createForWrite(
1001 : reviewerId, AttentionSetUpdate.Operation.REMOVE, "Reviewer/Cc was removed"));
1002 : }
1003 70 : }
1004 103 : addToPlannedAttentionSetUpdates(updates);
1005 103 : }
1006 :
1007 : private void addAllReviewersToAttentionSet() {
1008 103 : getNotes().getReviewers().byState(ReviewerStateInternal.REVIEWER).stream()
1009 103 : .map(
1010 : r ->
1011 7 : AttentionSetUpdate.createForWrite(
1012 : r, AttentionSetUpdate.Operation.ADD, "Change was marked ready for review"))
1013 103 : .forEach(this::addToPlannedAttentionSetUpdates);
1014 103 : }
1015 :
1016 : /**
1017 : * Any updates to the attention set must be done in {@link #addToPlannedAttentionSetUpdates}. This
1018 : * method is called after all the updates are finished to do the updates once and for real.
1019 : *
1020 : * <p>Changing the behaviour of this method might affect the way a ChangeUpdate is considered to
1021 : * be an "Attention Set Change Only". Make sure the {@link #isAttentionSetChangeOnly} logic is
1022 : * amended as well if needed.
1023 : *
1024 : * @return True if one or more attention set updates are appended to the {@code msg}, and false
1025 : * otherwise.
1026 : */
1027 : private boolean updateAttentionSet(StringBuilder msg) {
1028 103 : if (plannedAttentionSetUpdates == null) {
1029 103 : plannedAttentionSetUpdates = new HashMap<>();
1030 : }
1031 103 : Set<Account.Id> currentUsersInAttentionSet =
1032 103 : AttentionSetUtil.additionsOnly(getNotes().getAttentionSet()).stream()
1033 103 : .map(AttentionSetUpdate::account)
1034 103 : .collect(Collectors.toSet());
1035 :
1036 : // Current reviewers/ccs are the reviewers/ccs before the update + the new reviewers/ccs - the
1037 : // deleted reviewers/ccs.
1038 103 : Set<Account.Id> currentReviewers =
1039 103 : Stream.concat(
1040 103 : getNotes().getReviewers().all().stream(),
1041 103 : reviewers.entrySet().stream()
1042 103 : .filter(r -> r.getValue().asReviewerState() != ReviewerState.REMOVED)
1043 103 : .map(r -> r.getKey()))
1044 103 : .collect(Collectors.toSet());
1045 103 : currentReviewers.removeAll(
1046 103 : reviewers.entrySet().stream()
1047 103 : .filter(r -> r.getValue().asReviewerState() == ReviewerState.REMOVED)
1048 103 : .map(r -> r.getKey())
1049 103 : .collect(ImmutableSet.toImmutableSet()));
1050 :
1051 103 : removeInactiveUsersFromAttentionSet(currentReviewers);
1052 :
1053 103 : boolean hasUpdates = false;
1054 :
1055 103 : for (AttentionSetUpdate attentionSetUpdate : plannedAttentionSetUpdates.values()) {
1056 74 : if (attentionSetUpdate.operation() == AttentionSetUpdate.Operation.ADD
1057 52 : && currentUsersInAttentionSet.contains(attentionSetUpdate.account())) {
1058 : // Skip users that are already in the attention set: no need to re-add them.
1059 12 : continue;
1060 : }
1061 :
1062 74 : if (attentionSetUpdate.operation() == AttentionSetUpdate.Operation.REMOVE
1063 70 : && !currentUsersInAttentionSet.contains(attentionSetUpdate.account())) {
1064 : // Skip users that are not in the attention set: no need to remove them.
1065 65 : continue;
1066 : }
1067 :
1068 52 : if (attentionSetUpdate.operation() == AttentionSetUpdate.Operation.ADD
1069 52 : && serviceUserClassifier.isServiceUser(attentionSetUpdate.account())) {
1070 : // Skip adding robots to the attention set.
1071 1 : continue;
1072 : }
1073 :
1074 52 : if (attentionSetUpdate.operation() == AttentionSetUpdate.Operation.ADD
1075 52 : && approvals.rowKeySet().contains(LabelId.legacySubmit().get())) {
1076 : // On submit, we sometimes can add the person who submitted the change as a reviewer, and in
1077 : // turn it will add that person to the attention set.
1078 : // This ensures we don't add users to the attention set on submit.
1079 0 : continue;
1080 : }
1081 :
1082 : // Don't add accounts that are not active in the change to the attention set.
1083 52 : if (attentionSetUpdate.operation() == AttentionSetUpdate.Operation.ADD
1084 52 : && !isActiveOnChange(currentReviewers, attentionSetUpdate.account())) {
1085 8 : continue;
1086 : }
1087 :
1088 52 : addFooter(msg, FOOTER_ATTENTION, noteUtil.attentionSetUpdateToJson(attentionSetUpdate));
1089 52 : attentionSetUpdatesBuilder.add(attentionSetUpdate);
1090 52 : hasUpdates = true;
1091 52 : }
1092 103 : return hasUpdates;
1093 : }
1094 :
1095 : private void removeInactiveUsersFromAttentionSet(Set<Account.Id> currentReviewers) {
1096 103 : Set<Account.Id> inActiveUsersInTheAttentionSet =
1097 : // get the current attention set.
1098 103 : getNotes().getAttentionSet().stream()
1099 103 : .filter(a -> a.operation().equals(Operation.ADD))
1100 103 : .map(a -> a.account())
1101 : // remove users that are currently being removed from the attention set.
1102 103 : .filter(
1103 : a ->
1104 44 : plannedAttentionSetUpdates.getOrDefault(a, /*defaultValue= */ null) == null
1105 44 : || plannedAttentionSetUpdates.get(a).operation().equals(Operation.REMOVE))
1106 : // remove users that are still active on the change.
1107 103 : .filter(a -> !isActiveOnChange(currentReviewers, a))
1108 103 : .collect(ImmutableSet.toImmutableSet());
1109 :
1110 : // We override the flag, as we never want such users in the attention set.
1111 103 : ignoreFurtherAttentionSetUpdates = false;
1112 :
1113 103 : addToPlannedAttentionSetUpdates(
1114 103 : inActiveUsersInTheAttentionSet.stream()
1115 103 : .map(
1116 : a ->
1117 10 : AttentionSetUpdate.createForWrite(
1118 : a,
1119 : Operation.REMOVE,
1120 : /* reason= */ "Only change owner, uploader, reviewers, and cc can "
1121 : + "be in the attention set"))
1122 103 : .collect(ImmutableSet.toImmutableSet()));
1123 :
1124 103 : ignoreFurtherAttentionSetUpdates = true;
1125 103 : }
1126 :
1127 : /**
1128 : * Returns whether {@code accountId} is active on a change based on the {@code currentReviewers}.
1129 : * Activity is defined as being a part of the reviewers, an uploader, or an owner of a change.
1130 : */
1131 : private boolean isActiveOnChange(Set<Account.Id> currentReviewers, Account.Id accountId) {
1132 52 : return currentReviewers.contains(accountId)
1133 33 : || getChange().getOwner().equals(accountId)
1134 52 : || getNotes().getCurrentPatchSet().uploader().equals(accountId);
1135 : }
1136 :
1137 : /**
1138 : * When set, default attention set rules are ignored (E.g, adding reviewers -> adds to attention
1139 : * set, etc).
1140 : */
1141 : public void ignoreFurtherAttentionSetUpdates() {
1142 5 : ignoreFurtherAttentionSetUpdates = true;
1143 5 : }
1144 :
1145 : private void addPatchSetFooter(StringBuilder sb, PatchSet.Id ps) {
1146 103 : addFooter(sb, FOOTER_PATCH_SET).append(ps.get());
1147 103 : if (psState != null) {
1148 2 : sb.append(" (").append(psState.name().toLowerCase()).append(')');
1149 : }
1150 103 : sb.append('\n');
1151 103 : }
1152 :
1153 : @Override
1154 : protected Project.NameKey getProjectName() {
1155 103 : return getChange().getProject();
1156 : }
1157 :
1158 : @Override
1159 : public boolean isEmpty() {
1160 103 : return isEmptyWithoutAttentionSet() && plannedAttentionSetUpdates == null;
1161 : }
1162 :
1163 : private boolean isEmptyWithoutAttentionSet() {
1164 103 : return commitSubject == null
1165 82 : && approvals.isEmpty()
1166 59 : && copiedApprovals.isEmpty()
1167 : && changeMessage == null
1168 47 : && comments.isEmpty()
1169 47 : && reviewers.isEmpty()
1170 103 : && reviewersByEmail.isEmpty()
1171 : && changeId == null
1172 : && branch == null
1173 : && status == null
1174 : && submissionId == null
1175 : && submitRecords == null
1176 : && assignee == null
1177 : && hashtags == null
1178 : && topic == null
1179 : && commit == null
1180 : && psState == null
1181 : && groups == null
1182 : && tag == null
1183 : && psDescription == null
1184 : && !currentPatchSet
1185 : && isPrivate == null
1186 : && workInProgress == null
1187 : && revertOf == null
1188 : && cherryPickOf == null;
1189 : }
1190 :
1191 : ChangeDraftUpdate getDraftUpdate() {
1192 103 : return draftUpdate;
1193 : }
1194 :
1195 : RobotCommentUpdate getRobotCommentUpdate() {
1196 103 : return robotCommentUpdate;
1197 : }
1198 :
1199 : DeleteCommentRewriter getDeleteCommentRewriter() {
1200 103 : return deleteCommentRewriter;
1201 : }
1202 :
1203 : DeleteChangeMessageRewriter getDeleteChangeMessageRewriter() {
1204 103 : return deleteChangeMessageRewriter;
1205 : }
1206 :
1207 : public void setAllowWriteToNewRef(boolean allow) {
1208 103 : isAllowWriteToNewtRef = allow;
1209 103 : }
1210 :
1211 : @Override
1212 : public boolean allowWriteToNewRef() {
1213 103 : return isAllowWriteToNewtRef;
1214 : }
1215 :
1216 : public void setPrivate(boolean isPrivate) {
1217 103 : this.isPrivate = isPrivate;
1218 103 : }
1219 :
1220 : public void setWorkInProgress(boolean workInProgress) {
1221 103 : this.workInProgress = workInProgress;
1222 103 : }
1223 :
1224 : private static StringBuilder addFooter(StringBuilder sb, FooterKey footer) {
1225 103 : return sb.append(footer.getName()).append(": ");
1226 : }
1227 :
1228 : private static void addFooter(StringBuilder sb, FooterKey footer, Object... values) {
1229 103 : addFooter(sb, footer);
1230 103 : for (Object value : values) {
1231 103 : sb.append(sanitizeFooter(Objects.toString(value)));
1232 : }
1233 103 : sb.append('\n');
1234 103 : }
1235 :
1236 : private static boolean isIllegalTopic(String topic) {
1237 103 : return (topic != null && topic.contains("\""));
1238 : }
1239 : }
|