LCOV - code coverage report
Current view: top level - server/restapi/change - RevertSubmission.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 223 232 96.1 %
Date: 2022-11-19 15:00:39 Functions: 23 23 100.0 %

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

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