LCOV - code coverage report
Current view: top level - server/restapi/change - CherryPickChange.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 181 189 95.8 %
Date: 2022-11-19 15:00:39 Functions: 16 16 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2012 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.server.project.ProjectCache.noSuchProject;
      19             : 
      20             : import com.google.auto.value.AutoValue;
      21             : import com.google.common.base.Strings;
      22             : import com.google.common.collect.ImmutableListMultimap;
      23             : import com.google.common.collect.ImmutableSet;
      24             : import com.google.gerrit.common.Nullable;
      25             : import com.google.gerrit.entities.Account;
      26             : import com.google.gerrit.entities.BranchNameKey;
      27             : import com.google.gerrit.entities.Change;
      28             : import com.google.gerrit.entities.PatchSet;
      29             : import com.google.gerrit.entities.Project;
      30             : import com.google.gerrit.extensions.api.changes.CherryPickInput;
      31             : import com.google.gerrit.extensions.api.changes.NotifyHandling;
      32             : import com.google.gerrit.extensions.restapi.BadRequestException;
      33             : import com.google.gerrit.extensions.restapi.MergeConflictException;
      34             : import com.google.gerrit.extensions.restapi.RestApiException;
      35             : import com.google.gerrit.server.ChangeUtil;
      36             : import com.google.gerrit.server.GerritPersonIdent;
      37             : import com.google.gerrit.server.IdentifiedUser;
      38             : import com.google.gerrit.server.ReviewerSet;
      39             : import com.google.gerrit.server.approval.ApprovalsUtil;
      40             : import com.google.gerrit.server.change.ChangeInserter;
      41             : import com.google.gerrit.server.change.NotifyResolver;
      42             : import com.google.gerrit.server.change.PatchSetInserter;
      43             : import com.google.gerrit.server.change.ResetCherryPickOp;
      44             : import com.google.gerrit.server.change.SetCherryPickOp;
      45             : import com.google.gerrit.server.git.CodeReviewCommit;
      46             : import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
      47             : import com.google.gerrit.server.git.CommitUtil;
      48             : import com.google.gerrit.server.git.GitRepositoryManager;
      49             : import com.google.gerrit.server.git.GroupCollector;
      50             : import com.google.gerrit.server.git.MergeUtil;
      51             : import com.google.gerrit.server.git.MergeUtilFactory;
      52             : import com.google.gerrit.server.notedb.ChangeNotes;
      53             : import com.google.gerrit.server.notedb.ReviewerStateInternal;
      54             : import com.google.gerrit.server.notedb.Sequences;
      55             : import com.google.gerrit.server.project.InvalidChangeOperationException;
      56             : import com.google.gerrit.server.project.NoSuchProjectException;
      57             : import com.google.gerrit.server.project.ProjectCache;
      58             : import com.google.gerrit.server.project.ProjectState;
      59             : import com.google.gerrit.server.query.change.ChangeData;
      60             : import com.google.gerrit.server.query.change.InternalChangeQuery;
      61             : import com.google.gerrit.server.submit.IntegrationConflictException;
      62             : import com.google.gerrit.server.submit.MergeIdenticalTreeException;
      63             : import com.google.gerrit.server.update.BatchUpdate;
      64             : import com.google.gerrit.server.update.UpdateException;
      65             : import com.google.gerrit.server.util.CommitMessageUtil;
      66             : import com.google.gerrit.server.util.time.TimeUtil;
      67             : import com.google.inject.Inject;
      68             : import com.google.inject.Provider;
      69             : import com.google.inject.Singleton;
      70             : import java.io.IOException;
      71             : import java.time.Instant;
      72             : import java.time.ZoneId;
      73             : import java.util.HashSet;
      74             : import java.util.List;
      75             : import java.util.Map;
      76             : import java.util.Set;
      77             : import org.eclipse.jgit.errors.ConfigInvalidException;
      78             : import org.eclipse.jgit.lib.ObjectId;
      79             : import org.eclipse.jgit.lib.ObjectInserter;
      80             : import org.eclipse.jgit.lib.ObjectReader;
      81             : import org.eclipse.jgit.lib.PersonIdent;
      82             : import org.eclipse.jgit.lib.Ref;
      83             : import org.eclipse.jgit.lib.Repository;
      84             : import org.eclipse.jgit.revwalk.RevCommit;
      85             : import org.eclipse.jgit.util.ChangeIdUtil;
      86             : 
      87             : @Singleton
      88             : public class CherryPickChange {
      89             :   @AutoValue
      90           8 :   abstract static class Result {
      91             :     static Result create(Change.Id changeId, ImmutableSet<String> filesWithGitConflicts) {
      92           8 :       return new AutoValue_CherryPickChange_Result(changeId, filesWithGitConflicts);
      93             :     }
      94             : 
      95             :     abstract Change.Id changeId();
      96             : 
      97             :     abstract ImmutableSet<String> filesWithGitConflicts();
      98             :   }
      99             : 
     100             :   private final Sequences seq;
     101             :   private final Provider<InternalChangeQuery> queryProvider;
     102             :   private final GitRepositoryManager gitManager;
     103             :   private final ZoneId serverZoneId;
     104             :   private final Provider<IdentifiedUser> user;
     105             :   private final ChangeInserter.Factory changeInserterFactory;
     106             :   private final PatchSetInserter.Factory patchSetInserterFactory;
     107             :   private final SetCherryPickOp.Factory setCherryPickOfFactory;
     108             :   private final MergeUtilFactory mergeUtilFactory;
     109             :   private final ChangeNotes.Factory changeNotesFactory;
     110             :   private final ProjectCache projectCache;
     111             :   private final ApprovalsUtil approvalsUtil;
     112             :   private final NotifyResolver notifyResolver;
     113             :   private final BatchUpdate.Factory batchUpdateFactory;
     114             : 
     115             :   @Inject
     116             :   CherryPickChange(
     117             :       Sequences seq,
     118             :       Provider<InternalChangeQuery> queryProvider,
     119             :       @GerritPersonIdent PersonIdent myIdent,
     120             :       GitRepositoryManager gitManager,
     121             :       Provider<IdentifiedUser> user,
     122             :       ChangeInserter.Factory changeInserterFactory,
     123             :       PatchSetInserter.Factory patchSetInserterFactory,
     124             :       SetCherryPickOp.Factory setCherryPickOfFactory,
     125             :       MergeUtilFactory mergeUtilFactory,
     126             :       ChangeNotes.Factory changeNotesFactory,
     127             :       ProjectCache projectCache,
     128             :       ApprovalsUtil approvalsUtil,
     129             :       NotifyResolver notifyResolver,
     130         145 :       BatchUpdate.Factory batchUpdateFactory) {
     131         145 :     this.seq = seq;
     132         145 :     this.queryProvider = queryProvider;
     133         145 :     this.gitManager = gitManager;
     134         145 :     this.serverZoneId = myIdent.getZoneId();
     135         145 :     this.user = user;
     136         145 :     this.changeInserterFactory = changeInserterFactory;
     137         145 :     this.patchSetInserterFactory = patchSetInserterFactory;
     138         145 :     this.setCherryPickOfFactory = setCherryPickOfFactory;
     139         145 :     this.mergeUtilFactory = mergeUtilFactory;
     140         145 :     this.changeNotesFactory = changeNotesFactory;
     141         145 :     this.projectCache = projectCache;
     142         145 :     this.approvalsUtil = approvalsUtil;
     143         145 :     this.notifyResolver = notifyResolver;
     144         145 :     this.batchUpdateFactory = batchUpdateFactory;
     145         145 :   }
     146             : 
     147             :   /**
     148             :    * This function is used for cherry picking a change.
     149             :    *
     150             :    * @param change Change to cherry pick.
     151             :    * @param patch The patch of that change.
     152             :    * @param input Input object for different configurations of cherry pick.
     153             :    * @param dest Destination branch for the cherry pick.
     154             :    * @return Result object that describes the cherry pick.
     155             :    * @throws IOException Unable to open repository or read from the database.
     156             :    * @throws InvalidChangeOperationException Parent or branch don't exist, or two changes with same
     157             :    *     key exist in the branch.
     158             :    * @throws UpdateException Problem updating the database using batchUpdateFactory.
     159             :    * @throws RestApiException Error such as invalid SHA1
     160             :    * @throws ConfigInvalidException Can't find account to notify.
     161             :    * @throws NoSuchProjectException Can't find project state.
     162             :    */
     163             :   public Result cherryPick(Change change, PatchSet patch, CherryPickInput input, BranchNameKey dest)
     164             :       throws IOException, InvalidChangeOperationException, UpdateException, RestApiException,
     165             :           ConfigInvalidException, NoSuchProjectException {
     166           6 :     return cherryPick(
     167             :         change,
     168           6 :         change.getProject(),
     169           6 :         patch.commitId(),
     170             :         input,
     171             :         dest,
     172           6 :         TimeUtil.now(),
     173             :         null,
     174             :         null,
     175             :         null,
     176             :         null);
     177             :   }
     178             : 
     179             :   /**
     180             :    * This function is called directly to cherry pick a commit. Also, it is used to cherry pick a
     181             :    * change as well as long as sourceChange is not null.
     182             :    *
     183             :    * @param sourceChange Change to cherry pick. Can be null, and then the function will only cherry
     184             :    *     pick a commit.
     185             :    * @param project Project name
     186             :    * @param sourceCommit Id of the commit to be cherry picked.
     187             :    * @param input Input object for different configurations of cherry pick.
     188             :    * @param dest Destination branch for the cherry pick.
     189             :    * @return Result object that describes the cherry pick.
     190             :    * @throws IOException Unable to open repository or read from the database.
     191             :    * @throws InvalidChangeOperationException Parent or branch don't exist, or two changes with same
     192             :    *     key exist in the branch.
     193             :    * @throws UpdateException Problem updating the database using batchUpdateFactory.
     194             :    * @throws RestApiException Error such as invalid SHA1
     195             :    * @throws ConfigInvalidException Can't find account to notify.
     196             :    * @throws NoSuchProjectException Can't find project state.
     197             :    */
     198             :   public Result cherryPick(
     199             :       @Nullable Change sourceChange,
     200             :       Project.NameKey project,
     201             :       ObjectId sourceCommit,
     202             :       CherryPickInput input,
     203             :       BranchNameKey dest)
     204             :       throws IOException, InvalidChangeOperationException, UpdateException, RestApiException,
     205             :           ConfigInvalidException, NoSuchProjectException {
     206           2 :     return cherryPick(
     207           2 :         sourceChange, project, sourceCommit, input, dest, TimeUtil.now(), null, null, null, null);
     208             :   }
     209             : 
     210             :   /**
     211             :    * This function can be called directly to cherry-pick a change (or commit if sourceChange is
     212             :    * null) with a few other parameters that are especially useful for cherry-picking a commit that
     213             :    * is the revert-of another change.
     214             :    *
     215             :    * @param sourceChange Change to cherry pick. Can be null, and then the function will only cherry
     216             :    *     pick a commit.
     217             :    * @param project Project name
     218             :    * @param sourceCommit Id of the commit to be cherry picked.
     219             :    * @param input Input object for different configurations of cherry pick.
     220             :    * @param dest Destination branch for the cherry pick.
     221             :    * @param timestamp the current timestamp.
     222             :    * @param revertedChange The id of the change that is reverted. This is used for the "revertOf"
     223             :    *     field to mark the created cherry pick change as "revertOf" the original change that was
     224             :    *     reverted.
     225             :    * @param changeIdForNewChange The Change-Id that the new change of the cherry pick will have.
     226             :    * @param idForNewChange The ID that the new change of the cherry pick will have. If provided and
     227             :    *     the cherry-pick doesn't result in creating a new change, then
     228             :    *     InvalidChangeOperationException is thrown.
     229             :    * @return Result object that describes the cherry pick.
     230             :    * @throws IOException Unable to open repository or read from the database.
     231             :    * @throws InvalidChangeOperationException Parent or branch don't exist, or two changes with same
     232             :    *     key exist in the branch. Also thrown when idForNewChange is not null but cherry-pick only
     233             :    *     creates a new patchset rather than a new change.
     234             :    * @throws UpdateException Problem updating the database using batchUpdateFactory.
     235             :    * @throws RestApiException Error such as invalid SHA1
     236             :    * @throws ConfigInvalidException Can't find account to notify.
     237             :    * @throws NoSuchProjectException Can't find project state.
     238             :    */
     239             :   public Result cherryPick(
     240             :       @Nullable Change sourceChange,
     241             :       Project.NameKey project,
     242             :       ObjectId sourceCommit,
     243             :       CherryPickInput input,
     244             :       BranchNameKey dest,
     245             :       Instant timestamp,
     246             :       @Nullable Change.Id revertedChange,
     247             :       @Nullable ObjectId changeIdForNewChange,
     248             :       @Nullable Change.Id idForNewChange,
     249             :       @Nullable Boolean workInProgress)
     250             :       throws IOException, InvalidChangeOperationException, UpdateException, RestApiException,
     251             :           ConfigInvalidException, NoSuchProjectException {
     252           8 :     IdentifiedUser identifiedUser = user.get();
     253           8 :     try (Repository git = gitManager.openRepository(project);
     254             :         // This inserter and revwalk *must* be passed to any BatchUpdates
     255             :         // created later on, to ensure the cherry-picked commit is flushed
     256             :         // before patch sets are updated.
     257           8 :         ObjectInserter oi = git.newObjectInserter();
     258           8 :         ObjectReader reader = oi.newReader();
     259           8 :         CodeReviewRevWalk revWalk = CodeReviewCommit.newRevWalk(reader)) {
     260           8 :       Ref destRef = git.getRefDatabase().exactRef(dest.branch());
     261           8 :       if (destRef == null) {
     262           1 :         throw new InvalidChangeOperationException(
     263           1 :             String.format("Branch %s does not exist.", dest.branch()));
     264             :       }
     265             : 
     266           8 :       RevCommit baseCommit =
     267           8 :           CommitUtil.getBaseCommit(
     268           8 :               project.get(), queryProvider.get(), revWalk, destRef, input.base);
     269             : 
     270           8 :       CodeReviewCommit commitToCherryPick = revWalk.parseCommit(sourceCommit);
     271             : 
     272           8 :       if (input.parent <= 0 || input.parent > commitToCherryPick.getParentCount()) {
     273           1 :         throw new InvalidChangeOperationException(
     274           1 :             String.format(
     275             :                 "Cherry Pick: Parent %s does not exist. Please specify a parent in"
     276             :                     + " range [1, %s].",
     277           1 :                 input.parent, commitToCherryPick.getParentCount()));
     278             :       }
     279             : 
     280             :       // If the commit message is not set, the commit message of the source commit will be used.
     281           8 :       String commitMessage = Strings.nullToEmpty(input.message);
     282           8 :       commitMessage = commitMessage.isEmpty() ? commitToCherryPick.getFullMessage() : commitMessage;
     283             : 
     284           8 :       String destChangeId = getDestinationChangeId(commitMessage, changeIdForNewChange);
     285             : 
     286           8 :       ChangeData destChange = null;
     287           8 :       if (destChangeId != null) {
     288             :         // If "idForNewChange" is not null we must fail, since we are not expecting an already
     289             :         // existing change.
     290           5 :         destChange = getDestChangeWithVerification(destChangeId, dest, idForNewChange != null);
     291             :       }
     292             : 
     293           8 :       if (changeIdForNewChange != null) {
     294             :         // If Change-Id was explicitly provided for the new change, override the value in commit
     295             :         // message.
     296           1 :         commitMessage = ChangeIdUtil.insertId(commitMessage, changeIdForNewChange, true);
     297           7 :       } else if (destChangeId == null) {
     298             :         // If commit message did not specify Change-Id, generate a new one and insert to the
     299             :         // message.
     300           6 :         commitMessage =
     301           6 :             ChangeIdUtil.insertId(commitMessage, CommitMessageUtil.generateChangeId(), true);
     302             :       }
     303           8 :       commitMessage = CommitMessageUtil.checkAndSanitizeCommitMessage(commitMessage);
     304             : 
     305             :       CodeReviewCommit cherryPickCommit;
     306           8 :       ProjectState projectState =
     307           8 :           projectCache.get(dest.project()).orElseThrow(noSuchProject(dest.project()));
     308           8 :       PersonIdent committerIdent = identifiedUser.newCommitterIdent(timestamp, serverZoneId);
     309             : 
     310             :       try {
     311             :         MergeUtil mergeUtil;
     312           8 :         if (input.allowConflicts) {
     313             :           // allowConflicts requires to use content merge
     314           3 :           mergeUtil = mergeUtilFactory.create(projectState, true);
     315             :         } else {
     316             :           // use content merge only if it's configured on the project
     317           7 :           mergeUtil = mergeUtilFactory.create(projectState);
     318             :         }
     319           8 :         cherryPickCommit =
     320           8 :             mergeUtil.createCherryPickFromCommit(
     321             :                 oi,
     322           8 :                 git.getConfig(),
     323             :                 baseCommit,
     324             :                 commitToCherryPick,
     325             :                 committerIdent,
     326             :                 commitMessage,
     327             :                 revWalk,
     328           8 :                 input.parent - 1,
     329             :                 input.allowEmpty,
     330             :                 input.allowConflicts);
     331           8 :         oi.flush();
     332           1 :       } catch (MergeIdenticalTreeException | MergeConflictException e) {
     333           1 :         throw new IntegrationConflictException("Cherry pick failed: " + e.getMessage(), e);
     334           8 :       }
     335             : 
     336           8 :       try (BatchUpdate bu = batchUpdateFactory.create(project, identifiedUser, timestamp)) {
     337           8 :         bu.setRepository(git, revWalk, oi);
     338           8 :         bu.setNotify(resolveNotify(input));
     339             :         Change.Id changeId;
     340           8 :         String newTopic = null;
     341           8 :         if (input.topic != null) {
     342           3 :           newTopic = Strings.emptyToNull(input.topic.trim());
     343             :         }
     344           8 :         if (newTopic == null
     345             :             && sourceChange != null
     346           6 :             && !Strings.isNullOrEmpty(sourceChange.getTopic())) {
     347           1 :           newTopic = sourceChange.getTopic() + "-" + dest.shortName();
     348             :         }
     349           8 :         if (destChange != null) {
     350             :           // The change key exists on the destination branch. The cherry pick
     351             :           // will be added as a new patch set.
     352           3 :           changeId =
     353           3 :               insertPatchSet(
     354             :                   bu,
     355             :                   git,
     356           3 :                   destChange.notes(),
     357             :                   cherryPickCommit,
     358             :                   sourceChange,
     359             :                   newTopic,
     360             :                   input,
     361             :                   workInProgress);
     362             :         } else {
     363             :           // Change key not found on destination branch. We can create a new
     364             :           // change.
     365           7 :           changeId =
     366           7 :               createNewChange(
     367             :                   bu,
     368             :                   cherryPickCommit,
     369           7 :                   dest.branch(),
     370             :                   newTopic,
     371             :                   project,
     372             :                   sourceChange,
     373             :                   sourceCommit,
     374             :                   input,
     375             :                   revertedChange,
     376             :                   idForNewChange,
     377             :                   workInProgress);
     378             :         }
     379           8 :         bu.execute();
     380           8 :         return Result.create(changeId, cherryPickCommit.getFilesWithGitConflicts());
     381             :       }
     382             :     }
     383             :   }
     384             : 
     385             :   private Change.Id insertPatchSet(
     386             :       BatchUpdate bu,
     387             :       Repository git,
     388             :       ChangeNotes destNotes,
     389             :       CodeReviewCommit cherryPickCommit,
     390             :       @Nullable Change sourceChange,
     391             :       String topic,
     392             :       CherryPickInput input,
     393             :       @Nullable Boolean workInProgress)
     394             :       throws IOException {
     395           3 :     Change destChange = destNotes.getChange();
     396           3 :     PatchSet.Id psId = ChangeUtil.nextPatchSetId(git, destChange.currentPatchSetId());
     397           3 :     PatchSetInserter inserter = patchSetInserterFactory.create(destNotes, psId, cherryPickCommit);
     398           3 :     inserter.setMessage("Uploaded patch set " + inserter.getPatchSetId().get() + ".");
     399           3 :     inserter.setTopic(topic);
     400           3 :     if (workInProgress != null) {
     401           0 :       inserter.setWorkInProgress(workInProgress);
     402             :     }
     403           3 :     if (shouldSetToReady(cherryPickCommit, destNotes, workInProgress)) {
     404           1 :       inserter.setWorkInProgress(false);
     405             :     }
     406           3 :     inserter.setValidationOptions(getValidateOptionsAsMultimap(input.validationOptions));
     407           3 :     bu.addOp(destChange.getId(), inserter);
     408           3 :     PatchSet.Id sourcePatchSetId = sourceChange == null ? null : sourceChange.currentPatchSetId();
     409             :     // If sourceChange is not provided, reset cherryPickOf to avoid stale value.
     410           3 :     if (sourcePatchSetId == null) {
     411           1 :       bu.addOp(destChange.getId(), new ResetCherryPickOp());
     412           3 :     } else if (destChange.getCherryPickOf() == null
     413           1 :         || !destChange.getCherryPickOf().equals(sourcePatchSetId)) {
     414           3 :       SetCherryPickOp cherryPickOfUpdater = setCherryPickOfFactory.create(sourcePatchSetId);
     415           3 :       bu.addOp(destChange.getId(), cherryPickOfUpdater);
     416             :     }
     417           3 :     return destChange.getId();
     418             :   }
     419             : 
     420             :   /**
     421             :    * We should set the change to be "ready for review" if: 1. workInProgress is not already set on
     422             :    * this request. 2. The patch-set doesn't have any git conflict markers. 3. The change used to be
     423             :    * work in progress (because of a previous patch-set).
     424             :    */
     425             :   private boolean shouldSetToReady(
     426             :       CodeReviewCommit cherryPickCommit,
     427             :       ChangeNotes destChangeNotes,
     428             :       @Nullable Boolean workInProgress) {
     429           3 :     return workInProgress == null
     430           3 :         && cherryPickCommit.getFilesWithGitConflicts().isEmpty()
     431           3 :         && destChangeNotes.getChange().isWorkInProgress();
     432             :   }
     433             : 
     434             :   private Change.Id createNewChange(
     435             :       BatchUpdate bu,
     436             :       CodeReviewCommit cherryPickCommit,
     437             :       String refName,
     438             :       String topic,
     439             :       Project.NameKey project,
     440             :       @Nullable Change sourceChange,
     441             :       @Nullable ObjectId sourceCommit,
     442             :       CherryPickInput input,
     443             :       @Nullable Change.Id revertOf,
     444             :       @Nullable Change.Id idForNewChange,
     445             :       @Nullable Boolean workInProgress)
     446             :       throws IOException, InvalidChangeOperationException {
     447           7 :     Change.Id changeId = idForNewChange != null ? idForNewChange : Change.id(seq.nextChangeId());
     448           7 :     ChangeInserter ins = changeInserterFactory.create(changeId, cherryPickCommit, refName);
     449           7 :     ins.setRevertOf(revertOf);
     450           7 :     if (workInProgress != null) {
     451           1 :       ins.setWorkInProgress(workInProgress);
     452             :     } else {
     453           6 :       ins.setWorkInProgress(
     454           6 :           (sourceChange != null && sourceChange.isWorkInProgress())
     455           6 :               || !cherryPickCommit.getFilesWithGitConflicts().isEmpty());
     456             :     }
     457           7 :     ins.setValidationOptions(getValidateOptionsAsMultimap(input.validationOptions));
     458           7 :     BranchNameKey sourceBranch = sourceChange == null ? null : sourceChange.getDest();
     459           7 :     PatchSet.Id sourcePatchSetId = sourceChange == null ? null : sourceChange.currentPatchSetId();
     460           7 :     ins.setMessage(
     461           7 :             revertOf == null
     462           6 :                 ? messageForDestinationChange(
     463           6 :                     ins.getPatchSetId(), sourceBranch, sourceCommit, cherryPickCommit)
     464           1 :                 : "Uploaded patch set 1.") // For revert commits, the message should not include
     465             :         // cherry-pick information.
     466           7 :         .setTopic(topic);
     467           7 :     if (revertOf == null) {
     468           6 :       ins.setCherryPickOf(sourcePatchSetId);
     469             :     }
     470           7 :     if (input.keepReviewers && sourceChange != null) {
     471           2 :       ReviewerSet reviewerSet =
     472           2 :           approvalsUtil.getReviewers(changeNotesFactory.createChecked(sourceChange));
     473           2 :       Set<Account.Id> reviewers =
     474           2 :           new HashSet<>(reviewerSet.byState(ReviewerStateInternal.REVIEWER));
     475           2 :       reviewers.add(sourceChange.getOwner());
     476           2 :       reviewers.remove(user.get().getAccountId());
     477           2 :       Set<Account.Id> ccs = new HashSet<>(reviewerSet.byState(ReviewerStateInternal.CC));
     478           2 :       ccs.remove(user.get().getAccountId());
     479           2 :       ins.setReviewersAndCcsIgnoreVisibility(reviewers, ccs);
     480             :     }
     481             :     // If there is a base, and the base is not merged, the groups will be overridden by the base's
     482             :     // groups.
     483           7 :     ins.setGroups(GroupCollector.getDefaultGroups(cherryPickCommit.getId()));
     484           7 :     if (input.base != null) {
     485           3 :       List<ChangeData> changes =
     486           3 :           queryProvider.get().setLimit(2).byBranchCommitOpen(project.get(), refName, input.base);
     487           3 :       if (changes.size() > 1) {
     488           0 :         throw new InvalidChangeOperationException(
     489             :             "Several changes with key "
     490             :                 + input.base
     491             :                 + " reside on the same branch. "
     492             :                 + "Cannot cherry-pick on target branch.");
     493             :       }
     494           3 :       if (changes.size() == 1) {
     495           3 :         Change change = changes.get(0).change();
     496           3 :         ins.setGroups(changeNotesFactory.createChecked(change).getCurrentPatchSet().groups());
     497             :       }
     498             :     }
     499           7 :     bu.insertChange(ins);
     500           7 :     return changeId;
     501             :   }
     502             : 
     503             :   private static ImmutableListMultimap<String, String> getValidateOptionsAsMultimap(
     504             :       @Nullable Map<String, String> validationOptions) {
     505           8 :     if (validationOptions == null) {
     506           8 :       return ImmutableListMultimap.of();
     507             :     }
     508             : 
     509             :     ImmutableListMultimap.Builder<String, String> validationOptionsBuilder =
     510           1 :         ImmutableListMultimap.builder();
     511           1 :     validationOptions
     512           1 :         .entrySet()
     513           1 :         .forEach(e -> validationOptionsBuilder.put(e.getKey(), e.getValue()));
     514           1 :     return validationOptionsBuilder.build();
     515             :   }
     516             : 
     517             :   private NotifyResolver.Result resolveNotify(CherryPickInput input)
     518             :       throws BadRequestException, ConfigInvalidException, IOException {
     519           8 :     return notifyResolver.resolve(
     520           8 :         firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails);
     521             :   }
     522             : 
     523             :   private String messageForDestinationChange(
     524             :       PatchSet.Id patchSetId,
     525             :       BranchNameKey sourceBranch,
     526             :       ObjectId sourceCommit,
     527             :       CodeReviewCommit cherryPickCommit) {
     528           6 :     StringBuilder stringBuilder = new StringBuilder("Patch Set ").append(patchSetId.get());
     529           6 :     if (sourceBranch != null) {
     530           4 :       stringBuilder.append(": Cherry Picked from branch ").append(sourceBranch.shortName());
     531             :     } else {
     532           2 :       stringBuilder.append(": Cherry Picked from commit ").append(sourceCommit.getName());
     533             :     }
     534           6 :     stringBuilder.append(".");
     535             : 
     536           6 :     if (!cherryPickCommit.getFilesWithGitConflicts().isEmpty()) {
     537           2 :       stringBuilder.append("\n\nThe following files contain Git conflicts:");
     538           2 :       cherryPickCommit.getFilesWithGitConflicts().stream()
     539           2 :           .sorted()
     540           2 :           .forEach(filePath -> stringBuilder.append("\n* ").append(filePath));
     541             :     }
     542             : 
     543           6 :     return stringBuilder.toString();
     544             :   }
     545             : 
     546             :   /**
     547             :    * Returns the Change-Id of destination change (as intended by the caller of cherry-pick
     548             :    * operation).
     549             :    *
     550             :    * <p>The Change-Id can be provided in one of the following ways:
     551             :    *
     552             :    * <ul>
     553             :    *   <li>Explicitly provided for the new change.
     554             :    *   <li>Provided in the input commit message.
     555             :    *   <li>Taken from the source commit if commit message was not set.
     556             :    * </ul>
     557             :    *
     558             :    * Otherwise should be generated.
     559             :    *
     560             :    * @param commitMessage the commit message, as intended by the caller of cherry-pick operation.
     561             :    * @param changeIdForNewChange the explicitly provided Change-Id for the new change.
     562             :    * @return The Change-Id of destination change, {@code null} if Change-Id was not provided by the
     563             :    *     caller of cherry-pick operation and should be generated.
     564             :    */
     565             :   @Nullable
     566             :   private String getDestinationChangeId(
     567             :       String commitMessage, @Nullable ObjectId changeIdForNewChange) {
     568           8 :     if (changeIdForNewChange != null) {
     569           1 :       return CommitMessageUtil.getChangeIdFromObjectId(changeIdForNewChange);
     570             :     }
     571           7 :     return CommitMessageUtil.getChangeIdFromCommitMessageFooter(commitMessage).orElse(null);
     572             :   }
     573             : 
     574             :   /**
     575             :    * Returns the change from the destination branch, if it exists and is valid for the cherry-pick.
     576             :    *
     577             :    * @param destChangeId the Change-ID of the change in the destination branch.
     578             :    * @param destBranch the branch to search by the Change-ID.
     579             :    * @param verifyIsMissing if {@code true}, verifies that the change should be missing in the
     580             :    *     destination branch.
     581             :    * @return the verified change or {@code null} if the change was not found.
     582             :    * @throws InvalidChangeOperationException if the change was found but failed validation
     583             :    */
     584             :   @Nullable
     585             :   private ChangeData getDestChangeWithVerification(
     586             :       String destChangeId, BranchNameKey destBranch, boolean verifyIsMissing)
     587             :       throws InvalidChangeOperationException {
     588           5 :     List<ChangeData> destChanges =
     589           5 :         queryProvider.get().setLimit(2).byBranchKey(destBranch, Change.key(destChangeId));
     590           5 :     if (destChanges.size() > 1) {
     591           0 :       throw new InvalidChangeOperationException(
     592             :           "Several changes with key "
     593             :               + destChangeId
     594             :               + " reside on the same branch. "
     595             :               + "Cannot create a new patch set.");
     596             :     }
     597           5 :     if (destChanges.size() == 1 && verifyIsMissing) {
     598           0 :       throw new InvalidChangeOperationException(
     599           0 :           String.format(
     600             :               "Expected that cherry-pick with Change-Id %s to branch %s "
     601             :                   + "in project %s creates a new change, but found existing change %d",
     602             :               destChangeId,
     603           0 :               destBranch.branch(),
     604           0 :               destBranch.project().get(),
     605           0 :               destChanges.get(0).getId().get()));
     606             :     }
     607           5 :     ChangeData destChange = destChanges.size() == 1 ? destChanges.get(0) : null;
     608             : 
     609           5 :     if (destChange != null && destChange.change().isClosed()) {
     610           2 :       throw new InvalidChangeOperationException(
     611           2 :           String.format(
     612             :               "Cherry-pick with Change-Id %s could not update the existing change %d "
     613             :                   + "in destination branch %s of project %s, because the change was closed (%s)",
     614             :               destChangeId,
     615           2 :               destChange.getId().get(),
     616           2 :               destBranch.branch(),
     617           2 :               destBranch.project(),
     618           2 :               destChange.change().getStatus().name()));
     619             :     }
     620           5 :     return destChange;
     621             :   }
     622             : }

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