Line data Source code
1 : // Copyright (C) 2019 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.restapi.change;
16 :
17 : import static com.google.common.base.MoreObjects.firstNonNull;
18 : import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
19 : import static com.google.gerrit.server.permissions.ChangePermission.REVERT;
20 : import static com.google.gerrit.server.permissions.RefPermission.CREATE_CHANGE;
21 : import static com.google.gerrit.server.project.ProjectCache.illegalState;
22 : import static java.util.Objects.requireNonNull;
23 :
24 : import com.google.common.base.Strings;
25 : import com.google.common.collect.ArrayListMultimap;
26 : import com.google.common.collect.Iterables;
27 : import com.google.common.collect.Multimap;
28 : import com.google.common.flogger.FluentLogger;
29 : import com.google.gerrit.entities.BranchNameKey;
30 : import com.google.gerrit.entities.Change;
31 : import com.google.gerrit.entities.Project;
32 : import com.google.gerrit.entities.RefNames;
33 : import com.google.gerrit.exceptions.StorageException;
34 : import com.google.gerrit.extensions.api.changes.CherryPickInput;
35 : import com.google.gerrit.extensions.api.changes.NotifyHandling;
36 : import com.google.gerrit.extensions.api.changes.RevertInput;
37 : import com.google.gerrit.extensions.common.ChangeInfo;
38 : import com.google.gerrit.extensions.common.RevertSubmissionInfo;
39 : import com.google.gerrit.extensions.restapi.AuthException;
40 : import com.google.gerrit.extensions.restapi.ResourceConflictException;
41 : import com.google.gerrit.extensions.restapi.Response;
42 : import com.google.gerrit.extensions.restapi.RestApiException;
43 : import com.google.gerrit.extensions.restapi.RestModifyView;
44 : import com.google.gerrit.extensions.webui.UiAction;
45 : import com.google.gerrit.server.ChangeUtil;
46 : import com.google.gerrit.server.CurrentUser;
47 : import com.google.gerrit.server.PatchSetUtil;
48 : import com.google.gerrit.server.change.ChangeJson;
49 : import com.google.gerrit.server.change.ChangeMessages;
50 : import com.google.gerrit.server.change.ChangeResource;
51 : import com.google.gerrit.server.change.NotifyResolver;
52 : import com.google.gerrit.server.change.RevisionResource;
53 : import com.google.gerrit.server.change.WalkSorter;
54 : import com.google.gerrit.server.change.WalkSorter.PatchSetData;
55 : import com.google.gerrit.server.git.CommitUtil;
56 : import com.google.gerrit.server.git.GitRepositoryManager;
57 : import com.google.gerrit.server.notedb.ChangeNotes;
58 : import com.google.gerrit.server.notedb.Sequences;
59 : import com.google.gerrit.server.permissions.ChangePermission;
60 : import com.google.gerrit.server.permissions.PermissionBackend;
61 : import com.google.gerrit.server.permissions.PermissionBackendException;
62 : import com.google.gerrit.server.project.ContributorAgreementsChecker;
63 : import com.google.gerrit.server.project.NoSuchProjectException;
64 : import com.google.gerrit.server.project.ProjectCache;
65 : import com.google.gerrit.server.project.ProjectState;
66 : import com.google.gerrit.server.query.change.ChangeData;
67 : import com.google.gerrit.server.query.change.InternalChangeQuery;
68 : import com.google.gerrit.server.restapi.change.CherryPickChange.Result;
69 : import com.google.gerrit.server.update.BatchUpdate;
70 : import com.google.gerrit.server.update.BatchUpdateOp;
71 : import com.google.gerrit.server.update.ChangeContext;
72 : import com.google.gerrit.server.update.UpdateException;
73 : import com.google.gerrit.server.util.CommitMessageUtil;
74 : import com.google.gerrit.server.util.time.TimeUtil;
75 : import com.google.inject.Inject;
76 : import com.google.inject.Provider;
77 : import java.io.IOException;
78 : import java.text.MessageFormat;
79 : import java.time.Instant;
80 : import java.util.ArrayList;
81 : import java.util.Arrays;
82 : import java.util.Collection;
83 : import java.util.Comparator;
84 : import java.util.Iterator;
85 : import java.util.List;
86 : import java.util.Set;
87 : import java.util.regex.Matcher;
88 : import java.util.regex.Pattern;
89 : import java.util.stream.Collectors;
90 : import org.apache.commons.lang3.RandomStringUtils;
91 : import org.eclipse.jgit.errors.ConfigInvalidException;
92 : import org.eclipse.jgit.lib.ObjectId;
93 : import org.eclipse.jgit.lib.ObjectInserter;
94 : import org.eclipse.jgit.lib.ObjectReader;
95 : import org.eclipse.jgit.lib.Repository;
96 : import org.eclipse.jgit.revwalk.RevCommit;
97 : import org.eclipse.jgit.revwalk.RevWalk;
98 :
99 : public class RevertSubmission
100 : implements RestModifyView<ChangeResource, RevertInput>, UiAction<ChangeResource> {
101 91 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
102 :
103 : private final Provider<InternalChangeQuery> queryProvider;
104 : private final Provider<CurrentUser> user;
105 : private final PermissionBackend permissionBackend;
106 : private final ProjectCache projectCache;
107 : private final PatchSetUtil psUtil;
108 : private final ContributorAgreementsChecker contributorAgreements;
109 : private final CherryPickChange cherryPickChange;
110 : private final ChangeJson.Factory json;
111 : private final GitRepositoryManager repoManager;
112 : private final WalkSorter sorter;
113 : private final CommitUtil commitUtil;
114 : private final ChangeNotes.Factory changeNotesFactory;
115 : private final Sequences seq;
116 : private final NotifyResolver notifyResolver;
117 : private final BatchUpdate.Factory updateFactory;
118 : private final ChangeResource.Factory changeResourceFactory;
119 : private final GetRelated getRelated;
120 :
121 : private CherryPickInput cherryPickInput;
122 : private List<ChangeInfo> results;
123 91 : private static final Pattern patternRevertSubject = Pattern.compile("Revert \"(.+)\"");
124 91 : private static final Pattern patternRevertSubjectWithNum =
125 91 : Pattern.compile("Revert\\^(\\d+) \"(.+)\"");
126 :
127 : @Inject
128 : RevertSubmission(
129 : Provider<InternalChangeQuery> queryProvider,
130 : Provider<CurrentUser> user,
131 : PermissionBackend permissionBackend,
132 : ProjectCache projectCache,
133 : PatchSetUtil psUtil,
134 : ContributorAgreementsChecker contributorAgreements,
135 : CherryPickChange cherryPickChange,
136 : ChangeJson.Factory json,
137 : GitRepositoryManager repoManager,
138 : WalkSorter sorter,
139 : CommitUtil commitUtil,
140 : ChangeNotes.Factory changeNotesFactory,
141 : Sequences seq,
142 : NotifyResolver notifyResolver,
143 : BatchUpdate.Factory updateFactory,
144 : ChangeResource.Factory changeResourceFactory,
145 91 : GetRelated getRelated) {
146 91 : this.queryProvider = queryProvider;
147 91 : this.user = user;
148 91 : this.permissionBackend = permissionBackend;
149 91 : this.projectCache = projectCache;
150 91 : this.psUtil = psUtil;
151 91 : this.contributorAgreements = contributorAgreements;
152 91 : this.cherryPickChange = cherryPickChange;
153 91 : this.json = json;
154 91 : this.repoManager = repoManager;
155 91 : this.sorter = sorter;
156 91 : this.commitUtil = commitUtil;
157 91 : this.changeNotesFactory = changeNotesFactory;
158 91 : this.seq = seq;
159 91 : this.notifyResolver = notifyResolver;
160 91 : this.updateFactory = updateFactory;
161 91 : this.changeResourceFactory = changeResourceFactory;
162 91 : this.getRelated = getRelated;
163 91 : results = new ArrayList<>();
164 91 : cherryPickInput = null;
165 91 : }
166 :
167 : @Override
168 : public Response<RevertSubmissionInfo> apply(ChangeResource changeResource, RevertInput input)
169 : throws RestApiException, IOException, UpdateException, PermissionBackendException,
170 : NoSuchProjectException, ConfigInvalidException, StorageException {
171 :
172 3 : if (!changeResource.getChange().isMerged()) {
173 2 : throw new ResourceConflictException(
174 2 : String.format("change is %s.", ChangeUtil.status(changeResource.getChange())));
175 : }
176 :
177 2 : String submissionId = changeResource.getChange().getSubmissionId();
178 2 : if (submissionId == null) {
179 0 : throw new ResourceConflictException(
180 : "This change is merged but doesn't have a submission id,"
181 : + " meaning it was not submitted through Gerrit.");
182 : }
183 2 : List<ChangeData> changeDatas = queryProvider.get().bySubmissionId(submissionId);
184 :
185 1 : checkPermissionsForAllChanges(changeResource, changeDatas);
186 1 : input.topic = createTopic(input.topic, submissionId);
187 :
188 1 : return Response.ok(revertSubmission(changeDatas, input));
189 : }
190 :
191 : private String createTopic(String topic, String submissionId) {
192 1 : if (topic != null) {
193 1 : topic = Strings.emptyToNull(topic.trim());
194 : }
195 1 : if (topic == null) {
196 1 : return String.format(
197 1 : "revert-%s-%s", submissionId, RandomStringUtils.randomAlphabetic(10).toUpperCase());
198 : }
199 1 : return topic;
200 : }
201 :
202 : private void checkPermissionsForAllChanges(
203 : ChangeResource changeResource, List<ChangeData> changeDatas)
204 : throws IOException, AuthException, PermissionBackendException, ResourceConflictException {
205 2 : for (ChangeData changeData : changeDatas) {
206 2 : Change change = changeData.change();
207 :
208 : // Might do the permission tests multiple times, but these are necessary to ensure that the
209 : // user has permissions to revert all changes. If they lack any permission, no revert will be
210 : // done.
211 :
212 1 : contributorAgreements.check(change.getProject(), changeResource.getUser());
213 1 : permissionBackend.currentUser().ref(change.getDest()).check(CREATE_CHANGE);
214 1 : permissionBackend.currentUser().change(changeData).check(REVERT);
215 1 : permissionBackend.currentUser().change(changeData).check(ChangePermission.READ);
216 1 : projectCache
217 1 : .get(change.getProject())
218 1 : .orElseThrow(illegalState(change.getProject()))
219 1 : .checkStatePermitsWrite();
220 :
221 1 : requireNonNull(
222 1 : psUtil.get(changeData.notes(), change.currentPatchSetId()),
223 1 : String.format(
224 : "current patch set %s of change %s not found",
225 1 : change.currentPatchSetId(), change.currentPatchSetId()));
226 1 : }
227 1 : }
228 :
229 : private RevertSubmissionInfo revertSubmission(
230 : List<ChangeData> changeData, RevertInput revertInput)
231 : throws RestApiException, IOException, UpdateException, ConfigInvalidException,
232 : StorageException, PermissionBackendException {
233 :
234 1 : Multimap<BranchNameKey, ChangeData> changesPerProjectAndBranch = ArrayListMultimap.create();
235 1 : changeData.stream().forEach(c -> changesPerProjectAndBranch.put(c.change().getDest(), c));
236 1 : cherryPickInput = createCherryPickInput(revertInput);
237 1 : Instant timestamp = TimeUtil.now();
238 :
239 1 : for (BranchNameKey projectAndBranch : changesPerProjectAndBranch.keySet()) {
240 1 : cherryPickInput.base = null;
241 1 : Project.NameKey project = projectAndBranch.project();
242 1 : cherryPickInput.destination = projectAndBranch.branch();
243 1 : Collection<ChangeData> changesInProjectAndBranch =
244 1 : changesPerProjectAndBranch.get(projectAndBranch);
245 :
246 : // Sort the changes topologically.
247 1 : Iterator<PatchSetData> sortedChangesInProjectAndBranch =
248 1 : sorter.sort(changesInProjectAndBranch).iterator();
249 :
250 1 : Set<ObjectId> commitIdsInProjectAndBranch =
251 1 : changesInProjectAndBranch.stream()
252 1 : .map(c -> c.currentPatchSet().commitId())
253 1 : .collect(Collectors.toSet());
254 :
255 1 : revertAllChangesInProjectAndBranch(
256 : revertInput,
257 : project,
258 : sortedChangesInProjectAndBranch,
259 : commitIdsInProjectAndBranch,
260 : timestamp);
261 1 : }
262 1 : results.sort(Comparator.comparing(c -> c.revertOf));
263 1 : RevertSubmissionInfo revertSubmissionInfo = new RevertSubmissionInfo();
264 1 : revertSubmissionInfo.revertChanges = results;
265 1 : return revertSubmissionInfo;
266 : }
267 :
268 : private void revertAllChangesInProjectAndBranch(
269 : RevertInput revertInput,
270 : Project.NameKey project,
271 : Iterator<PatchSetData> sortedChangesInProjectAndBranch,
272 : Set<ObjectId> commitIdsInProjectAndBranch,
273 : Instant timestamp)
274 : throws IOException, RestApiException, UpdateException, ConfigInvalidException,
275 : PermissionBackendException {
276 :
277 1 : String initialMessage = revertInput.message;
278 1 : while (sortedChangesInProjectAndBranch.hasNext()) {
279 1 : ChangeNotes changeNotes = sortedChangesInProjectAndBranch.next().data().notes();
280 1 : if (cherryPickInput.base == null) {
281 : // If no base was provided, the first change will be used to find a common base.
282 1 : cherryPickInput.base = getBase(changeNotes, commitIdsInProjectAndBranch).name();
283 : }
284 :
285 1 : revertInput.message = getMessage(initialMessage, changeNotes);
286 1 : if (cherryPickInput.base.equals(changeNotes.getCurrentPatchSet().commitId().getName())) {
287 : // This is the code in case this is the first revert of this project + branch, and the
288 : // revert would be on top of the change being reverted.
289 1 : createNormalRevert(revertInput, changeNotes, timestamp);
290 : } else {
291 1 : createCherryPickedRevert(revertInput, project, changeNotes, timestamp);
292 : }
293 1 : }
294 1 : }
295 :
296 : private void createCherryPickedRevert(
297 : RevertInput revertInput, Project.NameKey project, ChangeNotes changeNotes, Instant timestamp)
298 : throws IOException, ConfigInvalidException, UpdateException, RestApiException {
299 1 : ObjectId revCommitId =
300 1 : commitUtil.createRevertCommit(revertInput.message, changeNotes, user.get(), timestamp);
301 : // TODO (paiking): As a future change, the revert should just be done directly on the
302 : // target rather than just creating a commit and then cherry-picking it.
303 1 : cherryPickInput.message = revertInput.message;
304 1 : ObjectId generatedChangeId = CommitMessageUtil.generateChangeId();
305 1 : Change.Id cherryPickRevertChangeId = Change.id(seq.nextChangeId());
306 1 : try (BatchUpdate bu = updateFactory.create(project, user.get(), TimeUtil.now())) {
307 1 : bu.setNotify(
308 1 : notifyResolver.resolve(
309 1 : firstNonNull(cherryPickInput.notify, NotifyHandling.ALL),
310 : cherryPickInput.notifyDetails));
311 1 : bu.addOp(
312 1 : changeNotes.getChange().getId(),
313 : new CreateCherryPickOp(
314 : revCommitId,
315 : generatedChangeId,
316 : cherryPickRevertChangeId,
317 : timestamp,
318 1 : revertInput.workInProgress));
319 1 : if (!revertInput.workInProgress) {
320 1 : commitUtil.addChangeRevertedNotificationOps(
321 1 : bu, changeNotes.getChangeId(), cherryPickRevertChangeId, generatedChangeId.name());
322 : }
323 1 : bu.execute();
324 : }
325 1 : }
326 :
327 : private void createNormalRevert(
328 : RevertInput revertInput, ChangeNotes changeNotes, Instant timestamp)
329 : throws IOException, RestApiException, UpdateException, ConfigInvalidException {
330 :
331 1 : Change.Id revertId =
332 1 : commitUtil.createRevertChange(changeNotes, user.get(), revertInput, timestamp);
333 1 : results.add(json.noOptions().format(changeNotes.getProjectName(), revertId));
334 1 : cherryPickInput.base =
335 : changeNotesFactory
336 1 : .createChecked(changeNotes.getProjectName(), revertId)
337 1 : .getCurrentPatchSet()
338 1 : .commitId()
339 1 : .getName();
340 1 : }
341 :
342 : private CherryPickInput createCherryPickInput(RevertInput revertInput) {
343 1 : cherryPickInput = new CherryPickInput();
344 : // To create a revert change, we create a revert commit that is then cherry-picked. The revert
345 : // change is created for the cherry-picked commit. Notifications are sent only for this change,
346 : // but not for the intermediately created revert commit.
347 1 : cherryPickInput.notify = revertInput.notify;
348 1 : if (revertInput.workInProgress) {
349 1 : cherryPickInput.notify = firstNonNull(cherryPickInput.notify, NotifyHandling.NONE);
350 : }
351 1 : cherryPickInput.notifyDetails = revertInput.notifyDetails;
352 1 : cherryPickInput.parent = 1;
353 1 : cherryPickInput.keepReviewers = true;
354 1 : cherryPickInput.topic = revertInput.topic;
355 1 : cherryPickInput.allowEmpty = true;
356 1 : return cherryPickInput;
357 : }
358 :
359 : private String getMessage(String initialMessage, ChangeNotes changeNotes) {
360 1 : String subject = changeNotes.getChange().getSubject();
361 1 : if (subject.length() > 60) {
362 1 : subject = subject.substring(0, 56) + "...";
363 : }
364 1 : if (initialMessage == null) {
365 : initialMessage =
366 1 : MessageFormat.format(
367 1 : ChangeMessages.get().revertSubmissionDefaultMessage,
368 1 : changeNotes.getCurrentPatchSet().commitId().name());
369 : }
370 :
371 : // For performance purposes: Almost all cases will end here.
372 1 : if (!subject.startsWith("Revert")) {
373 1 : return MessageFormat.format(
374 1 : ChangeMessages.get().revertSubmissionUserMessage, subject, initialMessage);
375 : }
376 :
377 1 : Matcher matcher = patternRevertSubjectWithNum.matcher(subject);
378 :
379 1 : if (matcher.matches()) {
380 1 : return MessageFormat.format(
381 1 : ChangeMessages.get().revertSubmissionOfRevertSubmissionUserMessage,
382 1 : Integer.valueOf(matcher.group(1)) + 1,
383 1 : matcher.group(2),
384 1 : changeNotes.getCurrentPatchSet().commitId().name());
385 : }
386 :
387 1 : matcher = patternRevertSubject.matcher(subject);
388 1 : if (matcher.matches()) {
389 1 : return MessageFormat.format(
390 1 : ChangeMessages.get().revertSubmissionOfRevertSubmissionUserMessage,
391 1 : 2,
392 1 : matcher.group(1),
393 1 : changeNotes.getCurrentPatchSet().commitId().name());
394 : }
395 :
396 1 : return MessageFormat.format(
397 1 : ChangeMessages.get().revertSubmissionUserMessage, subject, initialMessage);
398 : }
399 :
400 : /**
401 : * This function finds the base that the first revert in a project + branch should be based on.
402 : *
403 : * <p>If there is only one change, we will base the revert on that change. If all changes are
404 : * related, we will base on the first commit of this submission in the topological order.
405 : *
406 : * <p>If none of those special cases applies, the only case left is the case where we have at
407 : * least 2 independent changes in the same project + branch (and possibly other dependent
408 : * changes). In this case, it searches using BFS for the first commit that is either: 1. Has 2 or
409 : * more parents, and has as parents at least one commit that is part of the submission. 2. A
410 : * commit that is part of the submission. If neither of those are true, it just continues the
411 : * search by going to the parents.
412 : *
413 : * <p>If 1 is true, it means that this merge commit was created when this submission was
414 : * submitted. It also means that this merge commit is a descendant of all of the changes in this
415 : * submission and project + branch. Therefore, we return this merge commit.
416 : *
417 : * <p>If 2 is true, it will return the commit that WalkSorter has decided that it should be the
418 : * first commit reverted (e.g changeNotes, which is also the commit that is the first in the
419 : * topological sorting).
420 : *
421 : * <p>It doesn't run through the entire graph since it will stop once it finds at least one commit
422 : * that is part of the submission.
423 : *
424 : * @param changeNotes changeNotes for the change that is found by WalkSorter to be the first one
425 : * that should be reverted, the first in the topological sorting.
426 : * @param commitIds The commitIds of this project and branch.
427 : * @return the base of the first revert.
428 : */
429 : private ObjectId getBase(ChangeNotes changeNotes, Set<ObjectId> commitIds)
430 : throws StorageException, IOException, PermissionBackendException {
431 : // If there is only one change in that project and branch, just base the revert on that one
432 : // change.
433 1 : if (commitIds.size() == 1) {
434 1 : return Iterables.getOnlyElement(commitIds);
435 : }
436 : // If all changes are related, just return the first commit of this submission in the
437 : // topological sorting.
438 1 : if (getRelated.getRelated(getRevisionResource(changeNotes)).stream()
439 1 : .map(changes -> ObjectId.fromString(changes.commit.commit))
440 1 : .collect(Collectors.toSet())
441 1 : .containsAll(commitIds)) {
442 1 : return changeNotes.getCurrentPatchSet().commitId();
443 : }
444 : // There are independent changes in this submission and repository + branch.
445 1 : try (Repository git = repoManager.openRepository(changeNotes.getProjectName());
446 1 : ObjectInserter oi = git.newObjectInserter();
447 1 : ObjectReader reader = oi.newReader();
448 1 : RevWalk revWalk = new RevWalk(reader)) {
449 :
450 1 : ObjectId startCommit =
451 1 : git.getRefDatabase().findRef(changeNotes.getChange().getDest().branch()).getObjectId();
452 1 : revWalk.markStart(revWalk.parseCommit(startCommit));
453 1 : markChangesParentsUninteresting(commitIds, revWalk);
454 1 : Iterator<RevCommit> revWalkIterator = revWalk.iterator();
455 1 : while (revWalkIterator.hasNext()) {
456 1 : RevCommit revCommit = revWalkIterator.next();
457 1 : if (commitIds.contains(revCommit.getId())) {
458 0 : return changeNotes.getCurrentPatchSet().commitId();
459 : }
460 1 : if (Arrays.stream(revCommit.getParents())
461 1 : .anyMatch(parent -> commitIds.contains(parent.getId()))) {
462 : // Found a merge commit that at least one parent is in this submission. we should only
463 : // reach here if both conditions apply:
464 : // 1. There is more than one change in that project + branch in this submission.
465 : // 2. Not all changes in that project + branch are related in this submission.
466 : // Therefore, there are at least 2 unrelated changes in this project + branch that got
467 : // submitted together,
468 : // and since we found a merge commit with one of those as parents, this merge commit is
469 : // the first common descendant of all those changes.
470 1 : return revCommit.getId();
471 : }
472 1 : }
473 : // This should never happen since it can only happen if we go through the entire repository
474 : // without finding a single commit that matches any commit from the submission.
475 0 : throw new StorageException(
476 0 : String.format(
477 : "Couldn't find change %s in the repository %s",
478 0 : changeNotes.getChangeId(), changeNotes.getProjectName().get()));
479 0 : }
480 : }
481 :
482 : private RevisionResource getRevisionResource(ChangeNotes changeNotes) {
483 1 : return new RevisionResource(
484 1 : changeResourceFactory.create(changeNotes, user.get()), psUtil.current(changeNotes));
485 : }
486 :
487 : // The parents are not interesting since there is no reason to base the reverts on any of the
488 : // parents or their ancestors.
489 : private void markChangesParentsUninteresting(Set<ObjectId> commitIds, RevWalk revWalk)
490 : throws IOException {
491 1 : for (ObjectId id : commitIds) {
492 1 : RevCommit revCommit = revWalk.parseCommit(id);
493 1 : for (int i = 0; i < revCommit.getParentCount(); i++) {
494 1 : revWalk.markUninteresting(revCommit.getParent(i));
495 : }
496 1 : }
497 1 : }
498 :
499 : @Override
500 : public Description getDescription(ChangeResource rsrc) {
501 57 : Change change = rsrc.getChange();
502 57 : boolean projectStatePermitsWrite = false;
503 : try {
504 57 : projectStatePermitsWrite =
505 57 : projectCache.get(rsrc.getProject()).map(ProjectState::statePermitsWrite).orElse(false);
506 0 : } catch (StorageException e) {
507 0 : logger.atSevere().withCause(e).log(
508 0 : "Failed to check if project state permits write: %s", rsrc.getProject());
509 57 : }
510 57 : return new UiAction.Description()
511 57 : .setLabel("Revert submission")
512 57 : .setTitle(
513 : "Revert this change and all changes that have been submitted together with this change")
514 57 : .setVisible(
515 57 : and(
516 57 : and(
517 57 : change.isMerged()
518 25 : && change.getSubmissionId() != null
519 57 : && isChangePartOfSubmission(change.getSubmissionId())
520 : && projectStatePermitsWrite,
521 : permissionBackend
522 57 : .user(rsrc.getUser())
523 57 : .ref(change.getDest())
524 57 : .testCond(CREATE_CHANGE)),
525 57 : permissionBackend.user(rsrc.getUser()).change(rsrc.getNotes()).testCond(REVERT)));
526 : }
527 :
528 : /**
529 : * @param submissionId the submission id of the change.
530 : * @return True if the submission has more than one change, false otherwise.
531 : */
532 : private Boolean isChangePartOfSubmission(String submissionId) {
533 25 : return (queryProvider.get().setLimit(2).bySubmissionId(submissionId).size() > 1);
534 : }
535 :
536 : private class CreateCherryPickOp implements BatchUpdateOp {
537 : private final ObjectId revCommitId;
538 : private final ObjectId computedChangeId;
539 : private final Change.Id cherryPickRevertChangeId;
540 : private final Instant timestamp;
541 : private final boolean workInProgress;
542 :
543 : CreateCherryPickOp(
544 : ObjectId revCommitId,
545 : ObjectId computedChangeId,
546 : Change.Id cherryPickRevertChangeId,
547 : Instant timestamp,
548 1 : Boolean workInProgress) {
549 1 : this.revCommitId = revCommitId;
550 1 : this.computedChangeId = computedChangeId;
551 1 : this.cherryPickRevertChangeId = cherryPickRevertChangeId;
552 1 : this.timestamp = timestamp;
553 1 : this.workInProgress = workInProgress;
554 1 : }
555 :
556 : @Override
557 : public boolean updateChange(ChangeContext ctx) throws Exception {
558 1 : Change change = ctx.getChange();
559 1 : Result cherryPickResult =
560 1 : cherryPickChange.cherryPick(
561 : change,
562 1 : change.getProject(),
563 : revCommitId,
564 : cherryPickInput,
565 1 : BranchNameKey.create(
566 1 : change.getProject(), RefNames.fullName(cherryPickInput.destination)),
567 : timestamp,
568 1 : change.getId(),
569 : computedChangeId,
570 : cherryPickRevertChangeId,
571 1 : workInProgress);
572 : // save the commit as base for next cherryPick of that branch
573 1 : cherryPickInput.base =
574 : changeNotesFactory
575 1 : .createChecked(ctx.getProject(), cherryPickResult.changeId())
576 1 : .getCurrentPatchSet()
577 1 : .commitId()
578 1 : .getName();
579 1 : results.add(json.noOptions().format(change.getProject(), cherryPickResult.changeId()));
580 1 : return true;
581 : }
582 : }
583 : }
|