LCOV - code coverage report
Current view: top level - server/notedb - ChangeUpdate.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 517 531 97.4 %
Date: 2022-11-19 15:00:39 Functions: 97 98 99.0 %

          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             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750