Line data Source code
1 : // Copyright (C) 2013 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.server.change;
16 :
17 : import static com.google.common.base.Preconditions.checkState;
18 : import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
19 : import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
20 : import static com.google.gerrit.server.project.ProjectCache.illegalState;
21 : import static java.util.Objects.requireNonNull;
22 :
23 : import com.google.common.collect.ImmutableListMultimap;
24 : import com.google.gerrit.common.Nullable;
25 : import com.google.gerrit.entities.Change;
26 : import com.google.gerrit.entities.PatchSet;
27 : import com.google.gerrit.entities.PatchSetInfo;
28 : import com.google.gerrit.extensions.api.changes.NotifyHandling;
29 : import com.google.gerrit.extensions.client.ChangeKind;
30 : import com.google.gerrit.extensions.restapi.AuthException;
31 : import com.google.gerrit.extensions.restapi.BadRequestException;
32 : import com.google.gerrit.extensions.restapi.ResourceConflictException;
33 : import com.google.gerrit.server.ChangeMessagesUtil;
34 : import com.google.gerrit.server.ChangeUtil;
35 : import com.google.gerrit.server.PatchSetUtil;
36 : import com.google.gerrit.server.ReviewerSet;
37 : import com.google.gerrit.server.approval.ApprovalCopier;
38 : import com.google.gerrit.server.approval.ApprovalsUtil;
39 : import com.google.gerrit.server.events.CommitReceivedEvent;
40 : import com.google.gerrit.server.extensions.events.RevisionCreated;
41 : import com.google.gerrit.server.extensions.events.WorkInProgressStateChanged;
42 : import com.google.gerrit.server.git.validators.CommitValidationException;
43 : import com.google.gerrit.server.git.validators.CommitValidators;
44 : import com.google.gerrit.server.notedb.ChangeNotes;
45 : import com.google.gerrit.server.notedb.ChangeUpdate;
46 : import com.google.gerrit.server.patch.AutoMerger;
47 : import com.google.gerrit.server.patch.PatchSetInfoFactory;
48 : import com.google.gerrit.server.permissions.ChangePermission;
49 : import com.google.gerrit.server.permissions.PermissionBackend;
50 : import com.google.gerrit.server.permissions.PermissionBackendException;
51 : import com.google.gerrit.server.project.ProjectCache;
52 : import com.google.gerrit.server.ssh.NoSshInfo;
53 : import com.google.gerrit.server.update.BatchUpdateOp;
54 : import com.google.gerrit.server.update.ChangeContext;
55 : import com.google.gerrit.server.update.PostUpdateContext;
56 : import com.google.gerrit.server.update.RepoContext;
57 : import com.google.gerrit.server.validators.ValidationException;
58 : import com.google.inject.Inject;
59 : import com.google.inject.assistedinject.Assisted;
60 : import java.io.IOException;
61 : import java.util.Collections;
62 : import java.util.List;
63 : import java.util.Optional;
64 : import org.eclipse.jgit.lib.ObjectId;
65 : import org.eclipse.jgit.transport.ReceiveCommand;
66 :
67 : public class PatchSetInserter implements BatchUpdateOp {
68 : public interface Factory {
69 : PatchSetInserter create(ChangeNotes notes, PatchSet.Id psId, ObjectId commitId);
70 : }
71 :
72 : // Injected fields.
73 : private final PermissionBackend permissionBackend;
74 : private final PatchSetInfoFactory patchSetInfoFactory;
75 : private final ChangeKindCache changeKindCache;
76 : private final CommitValidators.Factory commitValidatorsFactory;
77 : private final EmailNewPatchSet.Factory emailNewPatchSetFactory;
78 : private final ProjectCache projectCache;
79 : private final RevisionCreated revisionCreated;
80 : private final ApprovalsUtil approvalsUtil;
81 : private final ChangeMessagesUtil cmUtil;
82 : private final PatchSetUtil psUtil;
83 : private final WorkInProgressStateChanged wipStateChanged;
84 : private final AutoMerger autoMerger;
85 :
86 : // Assisted-injected fields.
87 : private final PatchSet.Id psId;
88 : private final ObjectId commitId;
89 : // Read prior to running the batch update, so must only be used during
90 : // updateRepo; updateChange and later must use the notes from the
91 : // ChangeContext.
92 : private final ChangeNotes origNotes;
93 :
94 : // Fields exposed as setters.
95 : private String message;
96 : private String description;
97 : private Boolean workInProgress;
98 33 : private boolean validate = true;
99 33 : private boolean checkAddPatchSetPermission = true;
100 33 : private List<String> groups = Collections.emptyList();
101 33 : private ImmutableListMultimap<String, String> validationOptions = ImmutableListMultimap.of();
102 33 : private boolean fireRevisionCreated = true;
103 : private boolean allowClosed;
104 33 : private boolean sendEmail = true;
105 : private String topic;
106 33 : private boolean storeCopiedVotes = true;
107 :
108 : // Fields set during some phase of BatchUpdate.Op.
109 : private Change change;
110 : private PatchSet patchSet;
111 : private PatchSetInfo patchSetInfo;
112 : private ChangeKind changeKind;
113 : private String mailMessage;
114 : private ReviewerSet oldReviewers;
115 : private boolean oldWorkInProgressState;
116 : private ApprovalCopier.Result approvalCopierResult;
117 : private ObjectId preUpdateMetaId;
118 :
119 : @Inject
120 : public PatchSetInserter(
121 : PermissionBackend permissionBackend,
122 : ApprovalsUtil approvalsUtil,
123 : ChangeMessagesUtil cmUtil,
124 : PatchSetInfoFactory patchSetInfoFactory,
125 : ChangeKindCache changeKindCache,
126 : CommitValidators.Factory commitValidatorsFactory,
127 : EmailNewPatchSet.Factory emailNewPatchSetFactory,
128 : PatchSetUtil psUtil,
129 : RevisionCreated revisionCreated,
130 : ProjectCache projectCache,
131 : WorkInProgressStateChanged wipStateChanged,
132 : AutoMerger autoMerger,
133 : @Assisted ChangeNotes notes,
134 : @Assisted PatchSet.Id psId,
135 33 : @Assisted ObjectId commitId) {
136 33 : this.permissionBackend = permissionBackend;
137 33 : this.approvalsUtil = approvalsUtil;
138 33 : this.cmUtil = cmUtil;
139 33 : this.patchSetInfoFactory = patchSetInfoFactory;
140 33 : this.changeKindCache = changeKindCache;
141 33 : this.commitValidatorsFactory = commitValidatorsFactory;
142 33 : this.emailNewPatchSetFactory = emailNewPatchSetFactory;
143 33 : this.psUtil = psUtil;
144 33 : this.revisionCreated = revisionCreated;
145 33 : this.projectCache = projectCache;
146 33 : this.wipStateChanged = wipStateChanged;
147 33 : this.autoMerger = autoMerger;
148 :
149 33 : this.origNotes = notes;
150 33 : this.psId = psId;
151 33 : this.commitId = commitId.copy();
152 33 : }
153 :
154 : public PatchSet.Id getPatchSetId() {
155 33 : return psId;
156 : }
157 :
158 : public PatchSetInserter setMessage(String message) {
159 32 : this.message = message;
160 32 : return this;
161 : }
162 :
163 : public PatchSetInserter setDescription(String description) {
164 18 : this.description = description;
165 18 : return this;
166 : }
167 :
168 : public PatchSetInserter setWorkInProgress(boolean workInProgress) {
169 3 : this.workInProgress = workInProgress;
170 3 : return this;
171 : }
172 :
173 : public PatchSetInserter setValidate(boolean validate) {
174 19 : this.validate = validate;
175 19 : return this;
176 : }
177 :
178 : public PatchSetInserter setCheckAddPatchSetPermission(boolean checkAddPatchSetPermission) {
179 15 : this.checkAddPatchSetPermission = checkAddPatchSetPermission;
180 15 : return this;
181 : }
182 :
183 : public PatchSetInserter setGroups(List<String> groups) {
184 6 : requireNonNull(groups, "groups may not be null");
185 6 : this.groups = groups;
186 6 : return this;
187 : }
188 :
189 : public PatchSetInserter setValidationOptions(
190 : ImmutableListMultimap<String, String> validationOptions) {
191 15 : requireNonNull(validationOptions, "validationOptions may not be null");
192 15 : this.validationOptions = validationOptions;
193 15 : return this;
194 : }
195 :
196 : public PatchSetInserter setFireRevisionCreated(boolean fireRevisionCreated) {
197 19 : this.fireRevisionCreated = fireRevisionCreated;
198 19 : return this;
199 : }
200 :
201 : public PatchSetInserter setAllowClosed(boolean allowClosed) {
202 2 : this.allowClosed = allowClosed;
203 2 : return this;
204 : }
205 :
206 : public PatchSetInserter setSendEmail(boolean sendEmail) {
207 28 : this.sendEmail = sendEmail;
208 28 : return this;
209 : }
210 :
211 : public PatchSetInserter setTopic(String topic) {
212 3 : this.topic = topic;
213 3 : return this;
214 : }
215 :
216 : /**
217 : * We always want to store copied votes except when the change is getting submitted and a new
218 : * patch-set is created on submit (using submit strategies such as "REBASE_ALWAYS"). In such
219 : * cases, we already store the votes of the new patch-sets in SubmitStrategyOp#saveApprovals. We
220 : * should not also store the copied votes.
221 : */
222 : public PatchSetInserter setStoreCopiedVotes(boolean storeCopiedVotes) {
223 13 : this.storeCopiedVotes = storeCopiedVotes;
224 13 : return this;
225 : }
226 :
227 : public Change getChange() {
228 8 : checkState(change != null, "getChange() only valid after executing update");
229 8 : return change;
230 : }
231 :
232 : public PatchSet getPatchSet() {
233 13 : checkState(patchSet != null, "getPatchSet() only valid after executing update");
234 13 : return patchSet;
235 : }
236 :
237 : @Override
238 : public void updateRepo(RepoContext ctx)
239 : throws AuthException, ResourceConflictException, IOException, PermissionBackendException {
240 33 : validate(ctx);
241 33 : ctx.addRefUpdate(ObjectId.zeroId(), commitId, getPatchSetId().toRefName());
242 :
243 33 : changeKind =
244 33 : changeKindCache.getChangeKind(
245 33 : ctx.getProject(),
246 33 : ctx.getRevWalk(),
247 33 : ctx.getRepoView().getConfig(),
248 33 : psUtil.current(origNotes).commitId(),
249 : commitId);
250 :
251 33 : Optional<ReceiveCommand> autoMerge =
252 33 : autoMerger.createAutoMergeCommitIfNecessary(
253 33 : ctx.getRepoView(),
254 33 : ctx.getRevWalk(),
255 33 : ctx.getInserter(),
256 33 : ctx.getRevWalk().parseCommit(commitId));
257 33 : if (autoMerge.isPresent()) {
258 7 : ctx.addRefUpdate(autoMerge.get());
259 : }
260 33 : }
261 :
262 : @Override
263 : public boolean updateChange(ChangeContext ctx)
264 : throws ResourceConflictException, IOException, BadRequestException {
265 33 : preUpdateMetaId = ctx.getNotes().getMetaId();
266 33 : change = ctx.getChange();
267 33 : ChangeUpdate update = ctx.getUpdate(psId);
268 33 : update.setSubjectForCommit("Create patch set " + psId.get());
269 :
270 33 : if (!change.isNew() && !allowClosed) {
271 0 : throw new ResourceConflictException(
272 0 : String.format(
273 : "Cannot create new patch set of change %s because it is %s",
274 0 : change.getId(), ChangeUtil.status(change)));
275 : }
276 :
277 33 : List<String> newGroups = groups;
278 33 : if (newGroups.isEmpty()) {
279 33 : PatchSet prevPs = psUtil.current(ctx.getNotes());
280 33 : if (prevPs != null) {
281 33 : newGroups = prevPs.groups();
282 : }
283 : }
284 33 : patchSet =
285 33 : psUtil.insert(
286 33 : ctx.getRevWalk(), ctx.getUpdate(psId), psId, commitId, newGroups, null, description);
287 :
288 33 : if (ctx.getNotify(change.getId()).handling() != NotifyHandling.NONE) {
289 31 : oldReviewers = approvalsUtil.getReviewers(ctx.getNotes());
290 : }
291 :
292 33 : oldWorkInProgressState = change.isWorkInProgress();
293 33 : if (workInProgress != null) {
294 3 : change.setWorkInProgress(workInProgress);
295 3 : change.setReviewStarted(!workInProgress);
296 3 : update.setWorkInProgress(workInProgress);
297 : }
298 :
299 33 : patchSetInfo =
300 33 : patchSetInfoFactory.get(ctx.getRevWalk(), ctx.getRevWalk().parseCommit(commitId), psId);
301 33 : if (!allowClosed) {
302 33 : change.setStatus(Change.Status.NEW);
303 : }
304 33 : change.setCurrentPatchSet(patchSetInfo);
305 33 : if (topic != null) {
306 1 : change.setTopic(topic);
307 : try {
308 1 : update.setTopic(topic);
309 0 : } catch (ValidationException ex) {
310 0 : throw new BadRequestException(ex.getMessage());
311 1 : }
312 : }
313 :
314 33 : if (storeCopiedVotes) {
315 32 : approvalCopierResult =
316 32 : approvalsUtil.copyApprovalsToNewPatchSet(
317 32 : ctx.getNotes(), patchSet, ctx.getRevWalk(), ctx.getRepoView().getConfig(), update);
318 : }
319 :
320 33 : mailMessage = insertChangeMessage(update, ctx);
321 :
322 33 : return true;
323 : }
324 :
325 : @Nullable
326 : private String insertChangeMessage(ChangeUpdate update, ChangeContext ctx) {
327 33 : StringBuilder messageBuilder = new StringBuilder();
328 33 : if (message != null) {
329 32 : messageBuilder.append(message);
330 : }
331 :
332 33 : if (approvalCopierResult != null) {
333 32 : approvalsUtil
334 32 : .formatApprovalCopierResult(
335 : approvalCopierResult,
336 : projectCache
337 32 : .get(ctx.getProject())
338 32 : .orElseThrow(illegalState(ctx.getProject()))
339 32 : .getLabelTypes())
340 32 : .ifPresent(
341 : msg -> {
342 5 : if (message != null && !message.endsWith("\n")) {
343 5 : messageBuilder.append("\n");
344 : }
345 5 : messageBuilder.append("\n").append(msg);
346 5 : });
347 : }
348 :
349 33 : String changeMessage = messageBuilder.toString();
350 33 : if (changeMessage.isEmpty()) {
351 9 : return null;
352 : }
353 :
354 32 : return cmUtil.setChangeMessage(
355 : update,
356 32 : messageBuilder.toString(),
357 32 : ChangeMessagesUtil.uploadedPatchSetTag(change.isWorkInProgress()));
358 : }
359 :
360 : @Override
361 : public void postUpdate(PostUpdateContext ctx) {
362 33 : NotifyResolver.Result notify = ctx.getNotify(change.getId());
363 33 : if (notify.shouldNotify() && sendEmail) {
364 28 : requireNonNull(mailMessage);
365 :
366 28 : emailNewPatchSetFactory
367 28 : .create(
368 : ctx,
369 : patchSet,
370 : mailMessage,
371 28 : approvalCopierResult.outdatedApprovals(),
372 28 : oldReviewers.byState(REVIEWER),
373 28 : oldReviewers.byState(CC),
374 : changeKind,
375 : preUpdateMetaId)
376 28 : .sendAsync();
377 : }
378 :
379 33 : if (fireRevisionCreated) {
380 32 : revisionCreated.fire(
381 32 : ctx.getChangeData(change), patchSet, ctx.getAccount(), ctx.getWhen(), notify);
382 : }
383 :
384 33 : if (workInProgress != null && oldWorkInProgressState != workInProgress) {
385 3 : wipStateChanged.fire(ctx.getChangeData(change), patchSet, ctx.getAccount(), ctx.getWhen());
386 : }
387 33 : }
388 :
389 : private void validate(RepoContext ctx)
390 : throws AuthException, ResourceConflictException, IOException, PermissionBackendException {
391 : // Not allowed to create a new patch set if the current patch set is locked.
392 33 : psUtil.checkPatchSetNotLocked(origNotes);
393 :
394 33 : if (checkAddPatchSetPermission) {
395 30 : permissionBackend.user(ctx.getUser()).change(origNotes).check(ChangePermission.ADD_PATCH_SET);
396 : }
397 33 : projectCache
398 33 : .get(ctx.getProject())
399 33 : .orElseThrow(illegalState(ctx.getProject()))
400 33 : .checkStatePermitsWrite();
401 33 : if (!validate) {
402 10 : return;
403 : }
404 :
405 32 : String refName = getPatchSetId().toRefName();
406 32 : try (CommitReceivedEvent event =
407 : new CommitReceivedEvent(
408 : new ReceiveCommand(
409 32 : ObjectId.zeroId(),
410 : commitId,
411 32 : refName.substring(0, refName.lastIndexOf('/') + 1) + "new"),
412 : projectCache
413 32 : .get(origNotes.getProjectName())
414 32 : .orElseThrow(illegalState(origNotes.getProjectName()))
415 32 : .getProject(),
416 32 : origNotes.getChange().getDest().branch(),
417 : validationOptions,
418 32 : ctx.getRepoView().getConfig(),
419 32 : ctx.getRevWalk().getObjectReader(),
420 : commitId,
421 32 : ctx.getIdentifiedUser())) {
422 32 : commitValidatorsFactory
423 32 : .forGerritCommits(
424 32 : permissionBackend.user(ctx.getUser()).project(ctx.getProject()),
425 32 : origNotes.getChange().getDest(),
426 32 : ctx.getIdentifiedUser(),
427 : new NoSshInfo(),
428 32 : ctx.getRevWalk(),
429 32 : origNotes.getChange())
430 32 : .validate(event);
431 2 : } catch (CommitValidationException e) {
432 2 : throw new ResourceConflictException(e.getFullMessage());
433 32 : }
434 32 : }
435 : }
|