Line data Source code
1 : // Copyright (C) 2017 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.server.change;
16 :
17 : import static com.google.gerrit.server.project.ProjectCache.illegalState;
18 :
19 : import com.google.common.collect.Iterables;
20 : import com.google.common.flogger.FluentLogger;
21 : import com.google.gerrit.entities.Account;
22 : import com.google.gerrit.entities.Change;
23 : import com.google.gerrit.entities.LabelType;
24 : import com.google.gerrit.entities.LabelTypes;
25 : import com.google.gerrit.entities.PatchSetApproval;
26 : import com.google.gerrit.entities.Project;
27 : import com.google.gerrit.exceptions.EmailException;
28 : import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
29 : import com.google.gerrit.extensions.api.changes.NotifyHandling;
30 : import com.google.gerrit.extensions.restapi.AuthException;
31 : import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
32 : import com.google.gerrit.server.ChangeMessagesUtil;
33 : import com.google.gerrit.server.IdentifiedUser;
34 : import com.google.gerrit.server.PatchSetUtil;
35 : import com.google.gerrit.server.account.AccountCache;
36 : import com.google.gerrit.server.account.AccountState;
37 : import com.google.gerrit.server.approval.ApprovalsUtil;
38 : import com.google.gerrit.server.extensions.events.ReviewerDeleted;
39 : import com.google.gerrit.server.mail.send.DeleteReviewerSender;
40 : import com.google.gerrit.server.mail.send.MessageIdGenerator;
41 : import com.google.gerrit.server.notedb.ChangeUpdate;
42 : import com.google.gerrit.server.notedb.ReviewerStateInternal;
43 : import com.google.gerrit.server.permissions.PermissionBackendException;
44 : import com.google.gerrit.server.project.ProjectCache;
45 : import com.google.gerrit.server.project.RemoveReviewerControl;
46 : import com.google.gerrit.server.update.ChangeContext;
47 : import com.google.gerrit.server.update.PostUpdateContext;
48 : import com.google.gerrit.server.update.RepoView;
49 : import com.google.gerrit.server.util.AccountTemplateUtil;
50 : import com.google.inject.Inject;
51 : import com.google.inject.Provider;
52 : import com.google.inject.assistedinject.Assisted;
53 : import java.io.IOException;
54 : import java.sql.Timestamp;
55 : import java.util.Collections;
56 : import java.util.HashMap;
57 : import java.util.Map;
58 :
59 : public class DeleteReviewerOp extends ReviewerOp {
60 16 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
61 :
62 : public interface Factory {
63 : DeleteReviewerOp create(Account reviewerAccount, DeleteReviewerInput input);
64 : }
65 :
66 : private final ApprovalsUtil approvalsUtil;
67 : private final PatchSetUtil psUtil;
68 : private final ChangeMessagesUtil cmUtil;
69 : private final ReviewerDeleted reviewerDeleted;
70 : private final Provider<IdentifiedUser> user;
71 : private final DeleteReviewerSender.Factory deleteReviewerSenderFactory;
72 : private final RemoveReviewerControl removeReviewerControl;
73 : private final ProjectCache projectCache;
74 : private final MessageIdGenerator messageIdGenerator;
75 : private final AccountCache accountCache;
76 :
77 : private final Account reviewer;
78 : private final DeleteReviewerInput input;
79 :
80 : String mailMessage;
81 : Change currChange;
82 16 : Map<String, Short> newApprovals = new HashMap<>();
83 16 : Map<String, Short> oldApprovals = new HashMap<>();
84 :
85 : @Inject
86 : DeleteReviewerOp(
87 : ApprovalsUtil approvalsUtil,
88 : PatchSetUtil psUtil,
89 : ChangeMessagesUtil cmUtil,
90 : ReviewerDeleted reviewerDeleted,
91 : Provider<IdentifiedUser> user,
92 : DeleteReviewerSender.Factory deleteReviewerSenderFactory,
93 : RemoveReviewerControl removeReviewerControl,
94 : ProjectCache projectCache,
95 : MessageIdGenerator messageIdGenerator,
96 : AccountCache accountCache,
97 : @Assisted Account reviewerAccount,
98 16 : @Assisted DeleteReviewerInput input) {
99 16 : this.approvalsUtil = approvalsUtil;
100 16 : this.psUtil = psUtil;
101 16 : this.cmUtil = cmUtil;
102 16 : this.reviewerDeleted = reviewerDeleted;
103 16 : this.user = user;
104 16 : this.deleteReviewerSenderFactory = deleteReviewerSenderFactory;
105 16 : this.removeReviewerControl = removeReviewerControl;
106 16 : this.projectCache = projectCache;
107 16 : this.messageIdGenerator = messageIdGenerator;
108 16 : this.accountCache = accountCache;
109 16 : this.reviewer = reviewerAccount;
110 16 : this.input = input;
111 16 : }
112 :
113 : @Override
114 : public boolean updateChange(ChangeContext ctx)
115 : throws AuthException, ResourceNotFoundException, PermissionBackendException, IOException {
116 16 : Account.Id reviewerId = reviewer.id();
117 : // Check of removing this reviewer (even if there is no vote processed by the loop below) is OK
118 16 : removeReviewerControl.checkRemoveReviewer(ctx.getNotes(), ctx.getUser(), reviewerId);
119 :
120 16 : if (!approvalsUtil.getReviewers(ctx.getNotes()).all().contains(reviewerId)) {
121 1 : throw new ResourceNotFoundException(
122 1 : String.format(
123 : "Reviewer %s doesn't exist in the change, hence can't delete it",
124 1 : reviewer.getName()));
125 : }
126 16 : currChange = ctx.getChange();
127 16 : setPatchSet(psUtil.current(ctx.getNotes()));
128 :
129 16 : LabelTypes labelTypes =
130 : projectCache
131 16 : .get(ctx.getProject())
132 16 : .orElseThrow(illegalState(ctx.getProject()))
133 16 : .getLabelTypes(ctx.getNotes());
134 : // removing a reviewer will remove all her votes
135 16 : for (LabelType lt : labelTypes.getLabelTypes()) {
136 16 : newApprovals.put(lt.getName(), (short) 0);
137 16 : }
138 : String ccOrReviewer =
139 : approvalsUtil
140 16 : .getReviewers(ctx.getNotes())
141 16 : .byState(ReviewerStateInternal.CC)
142 16 : .contains(reviewerId)
143 8 : ? "cc"
144 16 : : "reviewer";
145 16 : StringBuilder msg = new StringBuilder();
146 16 : msg.append(
147 16 : String.format(
148 16 : "Removed %s %s", ccOrReviewer, AccountTemplateUtil.getAccountTemplate(reviewer.id())));
149 16 : StringBuilder removedVotesMsg = new StringBuilder();
150 16 : removedVotesMsg.append(" with the following votes:\n\n");
151 16 : boolean votesRemoved = false;
152 16 : for (PatchSetApproval a : approvals(ctx, reviewerId)) {
153 : // Check if removing this vote is OK
154 4 : removeReviewerControl.checkRemoveReviewer(ctx.getNotes(), ctx.getUser(), a);
155 4 : if (a.patchSetId().equals(patchSet.id()) && a.value() != 0) {
156 4 : oldApprovals.put(a.label(), a.value());
157 4 : removedVotesMsg
158 4 : .append("* ")
159 4 : .append(a.label())
160 4 : .append(formatLabelValue(a.value()))
161 4 : .append(" by ")
162 4 : .append(AccountTemplateUtil.getAccountTemplate(a.accountId()))
163 4 : .append("\n");
164 4 : votesRemoved = true;
165 : }
166 4 : }
167 :
168 16 : if (votesRemoved) {
169 4 : msg.append(removedVotesMsg);
170 : } else {
171 15 : msg.append(".");
172 : }
173 16 : ChangeUpdate update = ctx.getUpdate(patchSet.id());
174 16 : update.removeReviewer(reviewerId);
175 :
176 16 : mailMessage =
177 16 : cmUtil.setChangeMessage(ctx, msg.toString(), ChangeMessagesUtil.TAG_DELETE_REVIEWER);
178 16 : return true;
179 : }
180 :
181 : @Override
182 : public void postUpdate(PostUpdateContext ctx) {
183 16 : opResult = Result.builder().setDeletedReviewer(reviewer.id()).build();
184 :
185 16 : NotifyResolver.Result notify = ctx.getNotify(currChange.getId());
186 16 : if (sendEmail) {
187 15 : if (input.notify == null
188 15 : && currChange.isWorkInProgress()
189 6 : && !oldApprovals.isEmpty()
190 1 : && notify.handling().compareTo(NotifyHandling.OWNER) < 0) {
191 : // Override NotifyHandling from the context to notify owner if votes were removed on a WIP
192 : // change.
193 1 : notify = notify.withHandling(NotifyHandling.OWNER);
194 : }
195 : try {
196 15 : if (notify.shouldNotify()) {
197 11 : emailReviewers(
198 11 : ctx.getProject(),
199 : currChange,
200 : mailMessage,
201 11 : Timestamp.from(ctx.getWhen()),
202 : notify,
203 11 : ctx.getRepoView());
204 : }
205 0 : } catch (Exception err) {
206 0 : logger.atSevere().withCause(err).log(
207 0 : "Cannot email update for change %s", currChange.getId());
208 15 : }
209 : }
210 :
211 16 : NotifyHandling notifyHandling = notify.handling();
212 16 : eventSender =
213 : () ->
214 16 : reviewerDeleted.fire(
215 16 : ctx.getChangeData(currChange),
216 : patchSet,
217 16 : accountCache.get(reviewer.id()).orElse(AccountState.forAccount(reviewer)),
218 16 : ctx.getAccount(),
219 : mailMessage,
220 : newApprovals,
221 : oldApprovals,
222 : notifyHandling,
223 16 : ctx.getWhen());
224 16 : if (sendEvent) {
225 15 : sendEvent();
226 : }
227 16 : }
228 :
229 : private Iterable<PatchSetApproval> approvals(ChangeContext ctx, Account.Id accountId) {
230 : Iterable<PatchSetApproval> approvals;
231 16 : approvals = ctx.getNotes().getApprovals().all().values();
232 16 : return Iterables.filter(approvals, psa -> accountId.equals(psa.accountId()));
233 : }
234 :
235 : private String formatLabelValue(short value) {
236 4 : if (value > 0) {
237 4 : return "+" + value;
238 : }
239 1 : return Short.toString(value);
240 : }
241 :
242 : private void emailReviewers(
243 : Project.NameKey projectName,
244 : Change change,
245 : String mailMessage,
246 : Timestamp timestamp,
247 : NotifyResolver.Result notify,
248 : RepoView repoView)
249 : throws EmailException {
250 11 : Account.Id userId = user.get().getAccountId();
251 11 : if (userId.equals(reviewer.id())) {
252 : // The user knows they removed themselves, don't bother emailing them.
253 5 : return;
254 : }
255 10 : DeleteReviewerSender emailSender =
256 10 : deleteReviewerSenderFactory.create(projectName, change.getId());
257 10 : emailSender.setFrom(userId);
258 10 : emailSender.addReviewers(Collections.singleton(reviewer.id()));
259 10 : emailSender.setChangeMessage(mailMessage, timestamp.toInstant());
260 10 : emailSender.setNotify(notify);
261 10 : emailSender.setMessageId(
262 10 : messageIdGenerator.fromChangeUpdate(repoView, change.currentPatchSetId()));
263 10 : emailSender.send();
264 10 : }
265 : }
|