All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.notedb.ChangeUpdate Maven / Gradle / Ivy

There is a newer version: 3.10.1
Show newest version
// Copyright (C) 2013 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.gerrit.server.notedb;

import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.entities.RefNames.changeMetaRef;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_ASSIGNEE;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_ATTENTION;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_BRANCH;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CHANGE_ID;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CHERRY_PICK_OF;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_COMMIT;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CURRENT;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_GROUPS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_HASHTAGS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_LABEL;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET_DESCRIPTION;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PRIVATE;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REAL_USER;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REVERT_OF;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBJECT;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMISSION_ID;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMITTED_WITH;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TAG;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TOPIC;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_WORK_IN_PROGRESS;
import static com.google.gerrit.server.notedb.NoteDbUtil.sanitizeFooter;
import static com.google.gerrit.server.project.ProjectCache.illegalState;
import static java.util.Comparator.naturalOrder;
import static java.util.Objects.requireNonNull;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Table;
import com.google.common.collect.TreeBasedTable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Address;
import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Comment;
import com.google.gerrit.entities.HumanComment;
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RobotComment;
import com.google.gerrit.entities.SubmissionId;
import com.google.gerrit.entities.SubmitRecord;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.account.ServiceUserClassifier;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.util.AttentionSetUtil;
import com.google.gerrit.server.util.LabelVote;
import com.google.gerrit.server.validators.ValidationException;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.FooterKey;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;

/**
 * A delta to apply to a change.
 *
 * 

This delta will become two unique commits: one in the AllUsers repo that will contain the * draft comments on this change and one in the notes branch that will contain approvals, reviewers, * change status, subject, submit records, the change message, and published comments. There are * limitations on the set of modifications that can be handled in a single update. In particular, * there is a single author and timestamp for each update. * *

This class is not thread-safe. */ public class ChangeUpdate extends AbstractChangeUpdate { public interface Factory { ChangeUpdate create(ChangeNotes notes, CurrentUser user, Date when); @VisibleForTesting ChangeUpdate create( ChangeNotes notes, CurrentUser user, Date when, Comparator labelNameComparator); } private final NoteDbUpdateManager.Factory updateManagerFactory; private final ChangeDraftUpdate.Factory draftUpdateFactory; private final RobotCommentUpdate.Factory robotCommentUpdateFactory; private final DeleteCommentRewriter.Factory deleteCommentRewriterFactory; private final ServiceUserClassifier serviceUserClassifier; private final Table> approvals; private final Map reviewers = new LinkedHashMap<>(); private final Map reviewersByEmail = new LinkedHashMap<>(); private final List comments = new ArrayList<>(); private String commitSubject; private String subject; private String changeId; private String branch; private Change.Status status; private List submitRecords; private String submissionId; private String topic; private String commit; private Map plannedAttentionSetUpdates; private boolean ignoreFurtherAttentionSetUpdates; private Optional assignee; private Set hashtags; private String changeMessage; private String tag; private PatchSetState psState; private Iterable groups; private String pushCert; private boolean isAllowWriteToNewtRef; private String psDescription; private boolean currentPatchSet; private Boolean isPrivate; private Boolean workInProgress; private Integer revertOf; private String cherryPickOf; private ChangeDraftUpdate draftUpdate; private RobotCommentUpdate robotCommentUpdate; private DeleteCommentRewriter deleteCommentRewriter; private DeleteChangeMessageRewriter deleteChangeMessageRewriter; @AssistedInject private ChangeUpdate( @GerritPersonIdent PersonIdent serverIdent, NoteDbUpdateManager.Factory updateManagerFactory, ChangeDraftUpdate.Factory draftUpdateFactory, RobotCommentUpdate.Factory robotCommentUpdateFactory, DeleteCommentRewriter.Factory deleteCommentRewriterFactory, ProjectCache projectCache, ServiceUserClassifier serviceUserClassifier, @Assisted ChangeNotes notes, @Assisted CurrentUser user, @Assisted Date when, ChangeNoteUtil noteUtil) { this( serverIdent, updateManagerFactory, draftUpdateFactory, robotCommentUpdateFactory, deleteCommentRewriterFactory, serviceUserClassifier, notes, user, when, projectCache .get(notes.getProjectName()) .orElseThrow(illegalState(notes.getProjectName())) .getLabelTypes() .nameComparator(), noteUtil); } private static Table> approvals( Comparator nameComparator) { return TreeBasedTable.create(nameComparator, naturalOrder()); } @AssistedInject private ChangeUpdate( @GerritPersonIdent PersonIdent serverIdent, NoteDbUpdateManager.Factory updateManagerFactory, ChangeDraftUpdate.Factory draftUpdateFactory, RobotCommentUpdate.Factory robotCommentUpdateFactory, DeleteCommentRewriter.Factory deleteCommentRewriterFactory, ServiceUserClassifier serviceUserClassifier, @Assisted ChangeNotes notes, @Assisted CurrentUser user, @Assisted Date when, @Assisted Comparator labelNameComparator, ChangeNoteUtil noteUtil) { super(notes, user, serverIdent, noteUtil, when); this.updateManagerFactory = updateManagerFactory; this.draftUpdateFactory = draftUpdateFactory; this.robotCommentUpdateFactory = robotCommentUpdateFactory; this.deleteCommentRewriterFactory = deleteCommentRewriterFactory; this.serviceUserClassifier = serviceUserClassifier; this.approvals = approvals(labelNameComparator); } public ObjectId commit() throws IOException { try (NoteDbUpdateManager updateManager = updateManagerFactory.create(getProjectName())) { updateManager.add(this); updateManager.execute(); } return getResult(); } public void setChangeId(String changeId) { String old = getChange().getKey().get(); checkArgument( old.equals(changeId), "The Change-Id was already set to %s, so we cannot set this Change-Id: %s", old, changeId); this.changeId = changeId; } public void setBranch(String branch) { this.branch = branch; } public void setStatus(Change.Status status) { checkArgument(status != Change.Status.MERGED, "use merge(RequestId, Iterable)"); this.status = status; } public void fixStatusToMerged(SubmissionId submissionId) { checkArgument(submissionId != null, "submission id must be set for merged changes"); this.status = Change.Status.MERGED; this.submissionId = submissionId.toString(); } public void putApproval(String label, short value) { putApprovalFor(getAccountId(), label, value); } public void putApprovalFor(Account.Id reviewer, String label, short value) { approvals.put(label, reviewer, Optional.of(value)); } void removeApproval(String label) { removeApprovalFor(getAccountId(), label); } public void removeApprovalFor(Account.Id reviewer, String label) { approvals.put(label, reviewer, Optional.empty()); } public void merge(SubmissionId submissionId, Iterable submitRecords) { this.status = Change.Status.MERGED; this.submissionId = submissionId.toString(); this.submitRecords = ImmutableList.copyOf(submitRecords); checkArgument(!this.submitRecords.isEmpty(), "no submit records specified at submit time"); } public void setSubjectForCommit(String commitSubject) { this.commitSubject = commitSubject; } public void setSubject(String subject) { this.subject = subject; } @VisibleForTesting ObjectId getCommit() { return ObjectId.fromString(commit); } public void setChangeMessage(String changeMessage) { this.changeMessage = changeMessage; } public void setTag(String tag) { this.tag = tag; } public void setPsDescription(String psDescription) { this.psDescription = psDescription; } public void putComment(HumanComment.Status status, HumanComment c) { verifyComment(c); createDraftUpdateIfNull(); if (status == HumanComment.Status.DRAFT) { draftUpdate.putComment(c); } else { comments.add(c); draftUpdate.markCommentPublished(c); } } public void putRobotComment(RobotComment c) { verifyComment(c); createRobotCommentUpdateIfNull(); robotCommentUpdate.putComment(c); } public void deleteComment(HumanComment c) { verifyComment(c); createDraftUpdateIfNull().deleteComment(c); } public void deleteCommentByRewritingHistory(String uuid, String newMessage) { deleteCommentRewriter = deleteCommentRewriterFactory.create(getChange().getId(), uuid, newMessage); } public void deleteChangeMessageByRewritingHistory(String targetMessageId, String newMessage) { deleteChangeMessageRewriter = new DeleteChangeMessageRewriter(getChange().getId(), targetMessageId, newMessage); } @VisibleForTesting ChangeDraftUpdate createDraftUpdateIfNull() { if (draftUpdate == null) { ChangeNotes notes = getNotes(); if (notes != null) { draftUpdate = draftUpdateFactory.create(notes, accountId, realAccountId, authorIdent, when); } else { // tests will always take the notes != null path above. draftUpdate = draftUpdateFactory.create(getChange(), accountId, realAccountId, authorIdent, when); } } return draftUpdate; } private void createRobotCommentUpdateIfNull() { if (robotCommentUpdate == null) { ChangeNotes notes = getNotes(); if (notes != null) { robotCommentUpdate = robotCommentUpdateFactory.create(notes, accountId, realAccountId, authorIdent, when); } else { robotCommentUpdate = robotCommentUpdateFactory.create( getChange(), accountId, realAccountId, authorIdent, when); } } } public void setTopic(String topic) throws ValidationException { if (isIllegalTopic(topic)) { throw new ValidationException("topic can't contain quotation marks."); } this.topic = Strings.nullToEmpty(topic); } public void setCommit(RevWalk rw, ObjectId id) throws IOException { setCommit(rw, id, null); } public void setCommit(RevWalk rw, ObjectId id, String pushCert) throws IOException { RevCommit commit = rw.parseCommit(id); rw.parseBody(commit); this.commit = commit.name(); subject = commit.getShortMessage(); this.pushCert = pushCert; } public void setHashtags(Set hashtags) { this.hashtags = hashtags; } /** * All updates must have a timestamp of null since we use the commit's timestamp. There also must * not be multiple updates for a single user. Only the first update takes place because of the * different priorities: e.g, if we want to add someone to the attention set but also want to * remove someone from the attention set, we should ensure to add/remove that user based on the * priority of the addition and removal. If most importantly we want to remove the user, then we * must first create the removal, and the addition will not take effect. */ public void addToPlannedAttentionSetUpdates(Set updates) { if (updates == null || updates.isEmpty() || ignoreFurtherAttentionSetUpdates) { // No updates to do. Robots don't change attention set. return; } checkArgument( updates.stream().noneMatch(a -> a.timestamp() != null), "must not specify timestamp for write"); checkArgument( updates.stream().map(AttentionSetUpdate::account).distinct().count() == updates.size(), "must not specify multiple updates for single user"); if (plannedAttentionSetUpdates == null) { plannedAttentionSetUpdates = new HashMap<>(); } Set currentAccountUpdates = plannedAttentionSetUpdates.values().stream() .map(AttentionSetUpdate::account) .collect(Collectors.toSet()); updates.stream() .filter(u -> !currentAccountUpdates.contains(u.account())) .forEach(u -> plannedAttentionSetUpdates.putIfAbsent(u.account(), u)); } public void addToPlannedAttentionSetUpdates(AttentionSetUpdate update) { addToPlannedAttentionSetUpdates(ImmutableSet.of(update)); } public void setAssignee(Account.Id assignee) { checkArgument(assignee != null, "use removeAssignee"); this.assignee = Optional.of(assignee); } public void removeAssignee() { this.assignee = Optional.empty(); } public Map getReviewers() { return reviewers; } public void putReviewer(Account.Id reviewer, ReviewerStateInternal type) { checkArgument(type != ReviewerStateInternal.REMOVED, "invalid ReviewerType"); reviewers.put(reviewer, type); } public void removeReviewer(Account.Id reviewer) { reviewers.put(reviewer, ReviewerStateInternal.REMOVED); } public void putReviewerByEmail(Address reviewer, ReviewerStateInternal type) { checkArgument(type != ReviewerStateInternal.REMOVED, "invalid ReviewerType"); reviewersByEmail.put(reviewer, type); } public void removeReviewerByEmail(Address reviewer) { reviewersByEmail.put(reviewer, ReviewerStateInternal.REMOVED); } public void setPatchSetState(PatchSetState psState) { this.psState = psState; } public void setCurrentPatchSet() { this.currentPatchSet = true; } public void setGroups(List groups) { requireNonNull(groups, "groups may not be null"); this.groups = groups; } public void setRevertOf(int revertOf) { int ownId = getId().get(); checkArgument(ownId != revertOf, "A change cannot revert itself"); this.revertOf = revertOf; rootOnly = true; } public void setCherryPickOf(String cherryPickOf) { this.cherryPickOf = cherryPickOf; } /** @return the tree id for the updated tree */ private ObjectId storeRevisionNotes(RevWalk rw, ObjectInserter inserter, ObjectId curr) throws ConfigInvalidException, IOException { if (comments.isEmpty() && pushCert == null) { return null; } RevisionNoteMap rnm = getRevisionNoteMap(rw, curr); RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(rnm); for (HumanComment c : comments) { c.tag = tag; cache.get(c.getCommitId()).putComment(c); } if (pushCert != null) { checkState(commit != null); cache.get(ObjectId.fromString(commit)).setPushCertificate(pushCert); } Map builders = cache.getBuilders(); checkComments(rnm.revisionNotes, builders); for (Map.Entry e : builders.entrySet()) { ObjectId data = inserter.insert(OBJ_BLOB, e.getValue().build(noteUtil.getChangeNoteJson())); rnm.noteMap.set(e.getKey(), data); } return rnm.noteMap.writeTree(inserter); } private RevisionNoteMap getRevisionNoteMap(RevWalk rw, ObjectId curr) throws ConfigInvalidException, IOException { if (curr.equals(ObjectId.zeroId())) { return RevisionNoteMap.emptyMap(); } // The old ChangeNotes may have already parsed the revision notes. We can reuse them as long as // the ref hasn't advanced. ChangeNotes notes = getNotes(); if (notes != null && notes.revisionNoteMap != null) { ObjectId idFromNotes = firstNonNull(notes.load().getRevision(), ObjectId.zeroId()); if (idFromNotes.equals(curr)) { return notes.revisionNoteMap; } } NoteMap noteMap = NoteMap.read(rw.getObjectReader(), rw.parseCommit(curr)); // Even though reading from changes might not be enabled, we need to // parse any existing revision notes so we can merge them. return RevisionNoteMap.parse( noteUtil.getChangeNoteJson(), rw.getObjectReader(), noteMap, HumanComment.Status.PUBLISHED); } private void checkComments( Map existingNotes, Map toUpdate) { // Prohibit various kinds of illegal operations on comments. Set existing = new HashSet<>(); for (ChangeRevisionNote rn : existingNotes.values()) { for (Comment c : rn.getEntities()) { existing.add(c.key); if (draftUpdate != null) { // Take advantage of an existing update on All-Users to prune any // published comments from drafts. NoteDbUpdateManager takes care of // ensuring that this update is applied before its dependent draft // update. // // Deleting aggressively in this way, combined with filtering out // duplicate published/draft comments in ChangeNotes#getDraftComments, // makes up for the fact that updates between the change repo and // All-Users are not atomic. // // TODO(dborowitz): We might want to distinguish between deleted // drafts that we're fixing up after the fact by putting them in a // separate commit. But note that we don't care much about the commit // graph of the draft ref, particularly because the ref is completely // deleted when all drafts are gone. draftUpdate.deleteComment(c.getCommitId(), c.key); } } } for (RevisionNoteBuilder b : toUpdate.values()) { for (Comment c : b.put.values()) { if (existing.contains(c.key)) { throw new StorageException("Cannot update existing published comment: " + c); } } } } @Override protected String getRefName() { return changeMetaRef(getId()); } @Override protected boolean bypassMaxUpdates() { // Allow abandoning or submitting a change even if it would exceed the max update count. return status != null && status.isClosed(); } @Override protected CommitBuilder applyImpl(RevWalk rw, ObjectInserter ins, ObjectId curr) throws IOException { checkState( deleteCommentRewriter == null && deleteChangeMessageRewriter == null, "cannot update and rewrite ref in one BatchUpdate"); int ps = psId != null ? psId.get() : getChange().currentPatchSetId().get(); StringBuilder msg = new StringBuilder(); if (commitSubject != null) { msg.append(commitSubject); } else { msg.append("Update patch set ").append(ps); } msg.append("\n\n"); if (changeMessage != null) { msg.append(changeMessage); msg.append("\n\n"); } addPatchSetFooter(msg, ps); if (currentPatchSet) { addFooter(msg, FOOTER_CURRENT, Boolean.TRUE); } if (psDescription != null) { addFooter(msg, FOOTER_PATCH_SET_DESCRIPTION, psDescription); } if (changeId != null) { addFooter(msg, FOOTER_CHANGE_ID, changeId); } if (subject != null) { addFooter(msg, FOOTER_SUBJECT, subject); } if (branch != null) { addFooter(msg, FOOTER_BRANCH, branch); } if (status != null) { addFooter(msg, FOOTER_STATUS, status.name().toLowerCase()); if (status.equals(Change.Status.ABANDONED)) { clearAttentionSet("Change was abandoned"); } if (status.equals(Change.Status.MERGED)) { clearAttentionSet("Change was submitted"); } } if (topic != null) { addFooter(msg, FOOTER_TOPIC, topic); } if (commit != null) { addFooter(msg, FOOTER_COMMIT, commit); } if (assignee != null) { if (assignee.isPresent()) { addFooter(msg, FOOTER_ASSIGNEE); noteUtil.appendAccountIdIdentString(msg, assignee.get()).append('\n'); } else { addFooter(msg, FOOTER_ASSIGNEE).append('\n'); } } Joiner comma = Joiner.on(','); if (hashtags != null) { addFooter(msg, FOOTER_HASHTAGS, comma.join(hashtags)); } if (tag != null) { addFooter(msg, FOOTER_TAG, tag); } if (groups != null) { addFooter(msg, FOOTER_GROUPS, comma.join(groups)); } for (Map.Entry e : reviewers.entrySet()) { addFooter(msg, e.getValue().getFooterKey()); noteUtil.appendAccountIdIdentString(msg, e.getKey()).append('\n'); } applyReviewerUpdatesToAttentionSet(); for (Map.Entry e : reviewersByEmail.entrySet()) { addFooter(msg, e.getValue().getByEmailFooterKey(), e.getKey().toString()); } for (Table.Cell> c : approvals.cellSet()) { addFooter(msg, FOOTER_LABEL); // Label names/values are safe to append without sanitizing. if (!c.getValue().isPresent()) { msg.append('-').append(c.getRowKey()); } else { msg.append(LabelVote.create(c.getRowKey(), c.getValue().get()).formatWithEquals()); } Account.Id id = c.getColumnKey(); if (!id.equals(getAccountId())) { noteUtil.appendAccountIdIdentString(msg.append(' '), id); } msg.append('\n'); } if (submissionId != null) { addFooter(msg, FOOTER_SUBMISSION_ID, submissionId); } if (submitRecords != null) { for (SubmitRecord rec : submitRecords) { addFooter(msg, FOOTER_SUBMITTED_WITH).append(rec.status); if (rec.errorMessage != null) { msg.append(' ').append(sanitizeFooter(rec.errorMessage)); } msg.append('\n'); if (rec.labels != null) { for (SubmitRecord.Label label : rec.labels) { // Label names/values are safe to append without sanitizing. addFooter(msg, FOOTER_SUBMITTED_WITH) .append(label.status) .append(": ") .append(label.label); if (label.appliedBy != null) { msg.append(": "); noteUtil.appendAccountIdIdentString(msg, label.appliedBy); } msg.append('\n'); } } // TODO(maximeg) We might want to list plugins that validated this submission. } } if (!Objects.equals(accountId, realAccountId)) { addFooter(msg, FOOTER_REAL_USER); noteUtil.appendAccountIdIdentString(msg, realAccountId).append('\n'); } if (isPrivate != null) { addFooter(msg, FOOTER_PRIVATE, isPrivate); } if (workInProgress != null) { addFooter(msg, FOOTER_WORK_IN_PROGRESS, workInProgress); if (workInProgress) { clearAttentionSet("Change was marked work in progress"); } else { addAllReviewersToAttentionSet(); } } if (revertOf != null) { addFooter(msg, FOOTER_REVERT_OF, revertOf); } if (cherryPickOf != null) { addFooter(msg, FOOTER_CHERRY_PICK_OF, cherryPickOf); } if (plannedAttentionSetUpdates != null) { updateAttentionSet(msg); } CommitBuilder cb = new CommitBuilder(); cb.setMessage(msg.toString()); try { ObjectId treeId = storeRevisionNotes(rw, ins, curr); if (treeId != null) { cb.setTreeId(treeId); } } catch (ConfigInvalidException e) { throw new StorageException(e); } return cb; } private void clearAttentionSet(String reason) { if (getNotes().getAttentionSet() == null) { return; } AttentionSetUtil.additionsOnly(getNotes().getAttentionSet()).stream() .map( a -> AttentionSetUpdate.createForWrite( a.account(), AttentionSetUpdate.Operation.REMOVE, reason)) .forEach(this::addToPlannedAttentionSetUpdates); } private void applyReviewerUpdatesToAttentionSet() { if ((workInProgress != null && workInProgress == true) || getNotes().getChange().isWorkInProgress() || status == Change.Status.MERGED) { // Attention set shouldn't change here for changes that are work in progress or are about to // be submitted or when the caller is a robot. return; } Set currentReviewers = getNotes().getReviewers().byState(ReviewerStateInternal.REVIEWER); Set updates = new HashSet<>(); for (Map.Entry reviewer : reviewers.entrySet()) { // Only add new reviewers to the attention set. if (reviewer.getValue().equals(ReviewerStateInternal.REVIEWER) && !currentReviewers.contains(reviewer.getKey())) { updates.add( AttentionSetUpdate.createForWrite( reviewer.getKey(), AttentionSetUpdate.Operation.ADD, "Reviewer was added")); } boolean reviewerRemoved = !reviewer.getValue().equals(ReviewerStateInternal.REVIEWER) && currentReviewers.contains(reviewer.getKey()); boolean ccRemoved = reviewer.getValue().equals(ReviewerStateInternal.REMOVED); if (reviewerRemoved || ccRemoved) { updates.add( AttentionSetUpdate.createForWrite( reviewer.getKey(), AttentionSetUpdate.Operation.REMOVE, "Reviewer/Cc was removed")); } } addToPlannedAttentionSetUpdates(updates); } private void addAllReviewersToAttentionSet() { getNotes().getReviewers().byState(ReviewerStateInternal.REVIEWER).stream() .map( r -> AttentionSetUpdate.createForWrite( r, AttentionSetUpdate.Operation.ADD, "Change was marked ready for review")) .forEach(this::addToPlannedAttentionSetUpdates); } /** * Any updates to the attention set must be done in {@link #addToPlannedAttentionSetUpdates}. This * method is called after all the updates are finished to do the updates once and for real. */ private void updateAttentionSet(StringBuilder msg) { if (plannedAttentionSetUpdates == null) { return; } Set currentUsersInAttentionSet = AttentionSetUtil.additionsOnly(getNotes().getAttentionSet()).stream() .map(AttentionSetUpdate::account) .collect(Collectors.toSet()); for (AttentionSetUpdate attentionSetUpdate : plannedAttentionSetUpdates.values()) { if (attentionSetUpdate.operation() == AttentionSetUpdate.Operation.ADD && currentUsersInAttentionSet.contains(attentionSetUpdate.account())) { // Skip users that are already in the attention set: no need to re-add them. continue; } if (attentionSetUpdate.operation() == AttentionSetUpdate.Operation.REMOVE && !currentUsersInAttentionSet.contains(attentionSetUpdate.account())) { // Skip users that are not in the attention set: no need to remove them. continue; } if (attentionSetUpdate.operation() == AttentionSetUpdate.Operation.ADD && serviceUserClassifier.isServiceUser(attentionSetUpdate.account())) { // Skip adding robots to the attention set. continue; } if (attentionSetUpdate.operation() == AttentionSetUpdate.Operation.ADD && approvals.rowKeySet().contains(LabelId.legacySubmit().get())) { // On submit, we sometimes can add the person who submitted the change as a reviewer, and in // turn it will add that person to the attention set. // This ensures we don't add users to the attention set on submit. continue; } addFooter(msg, FOOTER_ATTENTION, noteUtil.attentionSetUpdateToJson(attentionSetUpdate)); } } /** * When set, default attention set rules are ignored (E.g, adding reviewers -> adds to attention * set, etc). */ public void ignoreFurtherAttentionSetUpdates() { ignoreFurtherAttentionSetUpdates = true; } private void addPatchSetFooter(StringBuilder sb, int ps) { addFooter(sb, FOOTER_PATCH_SET).append(ps); if (psState != null) { sb.append(" (").append(psState.name().toLowerCase()).append(')'); } sb.append('\n'); } @Override protected Project.NameKey getProjectName() { return getChange().getProject(); } @Override public boolean isEmpty() { return commitSubject == null && approvals.isEmpty() && changeMessage == null && comments.isEmpty() && reviewers.isEmpty() && reviewersByEmail.isEmpty() && changeId == null && branch == null && status == null && submissionId == null && submitRecords == null && plannedAttentionSetUpdates == null && assignee == null && hashtags == null && topic == null && commit == null && psState == null && groups == null && tag == null && psDescription == null && !currentPatchSet && isPrivate == null && workInProgress == null && revertOf == null && cherryPickOf == null; } ChangeDraftUpdate getDraftUpdate() { return draftUpdate; } RobotCommentUpdate getRobotCommentUpdate() { return robotCommentUpdate; } DeleteCommentRewriter getDeleteCommentRewriter() { return deleteCommentRewriter; } DeleteChangeMessageRewriter getDeleteChangeMessageRewriter() { return deleteChangeMessageRewriter; } public void setAllowWriteToNewRef(boolean allow) { isAllowWriteToNewtRef = allow; } @Override public boolean allowWriteToNewRef() { return isAllowWriteToNewtRef; } public void setPrivate(boolean isPrivate) { this.isPrivate = isPrivate; } public void setWorkInProgress(boolean workInProgress) { this.workInProgress = workInProgress; } private static StringBuilder addFooter(StringBuilder sb, FooterKey footer) { return sb.append(footer.getName()).append(": "); } private static void addFooter(StringBuilder sb, FooterKey footer, Object... values) { addFooter(sb, footer); for (Object value : values) { sb.append(sanitizeFooter(Objects.toString(value))); } sb.append('\n'); } private static boolean isIllegalTopic(String topic) { return (topic != null && topic.contains("\"")); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy