LCOV - code coverage report
Current view: top level - server/submit - RebaseSubmitStrategy.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 132 146 90.4 %
Date: 2022-11-19 15:00:39 Functions: 11 13 84.6 %

          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.submit;
      16             : 
      17             : import static com.google.common.base.Preconditions.checkState;
      18             : import static com.google.common.collect.ImmutableList.toImmutableList;
      19             : import static com.google.gerrit.server.submit.CommitMergeStatus.EMPTY_COMMIT;
      20             : import static com.google.gerrit.server.submit.CommitMergeStatus.SKIPPED_IDENTICAL_TREE;
      21             : 
      22             : import com.google.common.collect.ImmutableList;
      23             : import com.google.gerrit.common.Nullable;
      24             : import com.google.gerrit.entities.BooleanProjectConfig;
      25             : import com.google.gerrit.entities.PatchSet;
      26             : import com.google.gerrit.exceptions.StorageException;
      27             : import com.google.gerrit.extensions.restapi.BadRequestException;
      28             : import com.google.gerrit.extensions.restapi.MergeConflictException;
      29             : import com.google.gerrit.extensions.restapi.ResourceConflictException;
      30             : import com.google.gerrit.extensions.restapi.RestApiException;
      31             : import com.google.gerrit.server.ChangeUtil;
      32             : import com.google.gerrit.server.change.RebaseChangeOp;
      33             : import com.google.gerrit.server.git.CodeReviewCommit;
      34             : import com.google.gerrit.server.git.MergeTip;
      35             : import com.google.gerrit.server.permissions.PermissionBackendException;
      36             : import com.google.gerrit.server.project.InvalidChangeOperationException;
      37             : import com.google.gerrit.server.project.NoSuchChangeException;
      38             : import com.google.gerrit.server.update.ChangeContext;
      39             : import com.google.gerrit.server.update.PostUpdateContext;
      40             : import com.google.gerrit.server.update.RepoContext;
      41             : import java.io.IOException;
      42             : import java.util.Collection;
      43             : import java.util.List;
      44             : import org.eclipse.jgit.lib.ObjectId;
      45             : import org.eclipse.jgit.lib.PersonIdent;
      46             : import org.eclipse.jgit.lib.Repository;
      47             : import org.eclipse.jgit.revwalk.RevCommit;
      48             : 
      49             : /** This strategy covers RebaseAlways and RebaseIfNecessary ones. */
      50             : public class RebaseSubmitStrategy extends SubmitStrategy {
      51             :   private final boolean rebaseAlways;
      52             : 
      53             :   RebaseSubmitStrategy(SubmitStrategy.Arguments args, boolean rebaseAlways) {
      54           5 :     super(args);
      55           5 :     this.rebaseAlways = rebaseAlways;
      56           5 :   }
      57             : 
      58             :   @Override
      59             :   public ImmutableList<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
      60             :     List<CodeReviewCommit> sorted;
      61             :     try {
      62           5 :       sorted = args.rebaseSorter.sort(toMerge);
      63           0 :     } catch (IOException | StorageException e) {
      64           0 :       throw new StorageException("Commit sorting failed", e);
      65           5 :     }
      66             : 
      67             :     // We cannot rebase merge commits. This is why we integrate merge changes into the target branch
      68             :     // the same way as if MERGE_IF_NECESSARY was the submit strategy. This means if needed we create
      69             :     // a merge commit that integrates the merge change into the target branch.
      70             :     // If we integrate a change series that consists out of a normal change and a merge change,
      71             :     // where the merge change depends on the normal change, we must skip rebasing the normal change,
      72             :     // because it already gets integrated by merging the merge change. If the rebasing of the normal
      73             :     // change is not skipped, it would appear twice in the history after the submit is done (once
      74             :     // through its rebased commit, and once through its original commit which is a parent of the
      75             :     // merge change that was merged into the target branch. To skip the rebasing of the normal
      76             :     // change, we call MergeUtil#reduceToMinimalMerge, as it excludes commits which will be
      77             :     // implicitly integrated by merging the series. Then we use the MergeIfNecessaryOp to integrate
      78             :     // the whole series.
      79             :     // If on the other hand, we integrate a change series that consists out of a merge change and a
      80             :     // normal change, where the normal change depends on the merge change, we can first integrate
      81             :     // the merge change by a merge and then integrate the normal change by a rebase. In this case we
      82             :     // do not want to call MergeUtil#reduceToMinimalMerge as we are not intending to integrate the
      83             :     // whole series by a merge, but rather do the integration of the commits one by one.
      84           5 :     boolean foundNonMerge = false;
      85           5 :     for (CodeReviewCommit c : sorted) {
      86           5 :       if (c.getParentCount() > 1) {
      87           2 :         if (!foundNonMerge) {
      88             :           // found a merge change, but it doesn't depend on a normal change, this means we are not
      89             :           // required to merge the whole series at once
      90           2 :           continue;
      91             :         }
      92             :         // found a merge commit that depends on a normal change, this means we are required to merge
      93             :         // the whole series at once
      94           2 :         sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, sorted);
      95           2 :         return sorted.stream().map(n -> new MergeIfNecessaryOp(n)).collect(toImmutableList());
      96             :       }
      97           5 :       foundNonMerge = true;
      98           5 :     }
      99             : 
     100           5 :     ImmutableList.Builder<SubmitStrategyOp> ops =
     101           5 :         ImmutableList.builderWithExpectedSize(sorted.size());
     102           5 :     boolean first = true;
     103           5 :     while (!sorted.isEmpty()) {
     104           5 :       CodeReviewCommit n = sorted.remove(0);
     105           5 :       if (first && args.mergeTip.getInitialTip() == null) {
     106             :         // TODO(tandrii): Cherry-Pick strategy does this too, but it's wrong
     107             :         // and can be fixed.
     108           3 :         ops.add(new FastForwardOp(args, n));
     109           5 :       } else if (n.getParentCount() == 0) {
     110           0 :         ops.add(new RebaseRootOp(n));
     111           5 :       } else if (n.getParentCount() == 1) {
     112           5 :         ops.add(new RebaseOneOp(n));
     113             :       } else {
     114           2 :         ops.add(new MergeIfNecessaryOp(n));
     115             :       }
     116           5 :       first = false;
     117           5 :     }
     118           5 :     return ops.build();
     119             :   }
     120             : 
     121             :   private class RebaseRootOp extends SubmitStrategyOp {
     122           0 :     private RebaseRootOp(CodeReviewCommit toMerge) {
     123           0 :       super(RebaseSubmitStrategy.this.args, toMerge);
     124           0 :     }
     125             : 
     126             :     @Override
     127             :     public void updateRepoImpl(RepoContext ctx) {
     128             :       // Refuse to merge a root commit into an existing branch, we cannot obtain
     129             :       // a delta for the cherry-pick to apply.
     130           0 :       toMerge.setStatusCode(CommitMergeStatus.CANNOT_REBASE_ROOT);
     131           0 :     }
     132             :   }
     133             : 
     134             :   private class RebaseOneOp extends SubmitStrategyOp {
     135             :     private RebaseChangeOp rebaseOp;
     136             :     private CodeReviewCommit newCommit;
     137             :     private PatchSet.Id newPatchSetId;
     138             : 
     139           5 :     private RebaseOneOp(CodeReviewCommit toMerge) {
     140           5 :       super(RebaseSubmitStrategy.this.args, toMerge);
     141           5 :     }
     142             : 
     143             :     @Override
     144             :     public void updateRepoImpl(RepoContext ctx)
     145             :         throws InvalidChangeOperationException, RestApiException, IOException,
     146             :             PermissionBackendException {
     147           5 :       if (args.mergeUtil.canFastForward(
     148           5 :           args.mergeSorter, args.mergeTip.getCurrentTip(), args.rw, toMerge)) {
     149           5 :         if (!rebaseAlways) {
     150           4 :           if (args.project.is(BooleanProjectConfig.REJECT_EMPTY_COMMIT)
     151           1 :               && toMerge.getTree().equals(toMerge.getParent(0).getTree())) {
     152           1 :             toMerge.setStatusCode(EMPTY_COMMIT);
     153           1 :             return;
     154             :           }
     155             : 
     156           4 :           args.mergeTip.moveTipTo(amendGitlink(toMerge), toMerge);
     157           4 :           toMerge.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
     158           4 :           acceptMergeTip(args.mergeTip);
     159           4 :           return;
     160             :         }
     161             :         // RebaseAlways means we modify commit message.
     162           4 :         args.rw.parseBody(toMerge);
     163           4 :         newPatchSetId =
     164           4 :             ChangeUtil.nextPatchSetIdFromChangeRefs(
     165           4 :                 ctx.getRepoView().getRefs(getId().toRefPrefix()).keySet(),
     166           4 :                 toMerge.change().currentPatchSetId());
     167           4 :         RevCommit mergeTip = args.mergeTip.getCurrentTip();
     168           4 :         args.rw.parseBody(mergeTip);
     169           4 :         String cherryPickCmtMsg = args.mergeUtil.createCommitMessageOnSubmit(toMerge, mergeTip);
     170           4 :         PersonIdent committer = ctx.newCommitterIdent(args.caller);
     171             :         try {
     172           4 :           newCommit =
     173           4 :               args.mergeUtil.createCherryPickFromCommit(
     174           4 :                   ctx.getInserter(),
     175           4 :                   ctx.getRepoView().getConfig(),
     176           4 :                   args.mergeTip.getCurrentTip(),
     177             :                   toMerge,
     178             :                   committer,
     179             :                   cherryPickCmtMsg,
     180             :                   args.rw,
     181             :                   0,
     182             :                   true,
     183             :                   false);
     184           0 :         } catch (MergeConflictException mce) {
     185             :           // Unlike in Cherry-pick case, this should never happen.
     186           0 :           toMerge.setStatusCode(CommitMergeStatus.REBASE_MERGE_CONFLICT);
     187           0 :           throw new IllegalStateException(
     188             :               "MergeConflictException on message edit must not happen", mce);
     189           0 :         } catch (MergeIdenticalTreeException mie) {
     190             :           // this should not happen
     191           0 :           toMerge.setStatusCode(SKIPPED_IDENTICAL_TREE);
     192           0 :           return;
     193           4 :         }
     194           4 :         ctx.addRefUpdate(ObjectId.zeroId(), newCommit, newPatchSetId.toRefName());
     195           4 :       } else {
     196             :         // Stale read of patch set is ok; see comments in RebaseChangeOp.
     197           4 :         PatchSet origPs = args.psUtil.get(toMerge.getNotes(), toMerge.getPatchsetId());
     198           4 :         rebaseOp =
     199             :             args.rebaseFactory
     200           4 :                 .create(toMerge.notes(), origPs, args.mergeTip.getCurrentTip())
     201           4 :                 .setFireRevisionCreated(false)
     202             :                 // Bypass approval copier since SubmitStrategyOp copy all approvals
     203             :                 // later anyway.
     204           4 :                 .setValidate(false)
     205           4 :                 .setCheckAddPatchSetPermission(false)
     206             :                 // RebaseAlways should set always modify commit message like
     207             :                 // Cherry-Pick strategy.
     208           4 :                 .setDetailedCommitMessage(rebaseAlways)
     209             :                 // Do not post message after inserting new patchset because there
     210             :                 // will be one about change being merged already.
     211           4 :                 .setPostMessage(false)
     212           4 :                 .setSendEmail(false)
     213           4 :                 .setMatchAuthorToCommitterDate(
     214           4 :                     args.project.is(BooleanProjectConfig.MATCH_AUTHOR_TO_COMMITTER_DATE))
     215             :                 // The votes are automatically copied and they don't count as copied votes. See
     216             :                 // method's javadoc.
     217           4 :                 .setStoreCopiedVotes(/* storeCopiedVotes = */ false);
     218             :         try {
     219           4 :           rebaseOp.updateRepo(ctx);
     220           2 :         } catch (MergeConflictException | NoSuchChangeException e) {
     221           2 :           toMerge.setStatusCode(CommitMergeStatus.REBASE_MERGE_CONFLICT);
     222           2 :           throw new IntegrationConflictException(
     223           2 :               "Cannot rebase " + toMerge.name() + ": " + e.getMessage(), e);
     224           4 :         }
     225           4 :         newCommit = args.rw.parseCommit(rebaseOp.getRebasedCommit());
     226           4 :         newPatchSetId = rebaseOp.getPatchSetId();
     227             :       }
     228           5 :       if (args.project.is(BooleanProjectConfig.REJECT_EMPTY_COMMIT)
     229           2 :           && newCommit.getTree().equals(newCommit.getParent(0).getTree())) {
     230           2 :         toMerge.setStatusCode(EMPTY_COMMIT);
     231           2 :         return;
     232             :       }
     233           5 :       newCommit = amendGitlink(newCommit);
     234           5 :       newCommit.copyFrom(toMerge);
     235           5 :       newCommit.setPatchsetId(newPatchSetId);
     236           5 :       newCommit.setStatusCode(CommitMergeStatus.CLEAN_REBASE);
     237           5 :       args.mergeTip.moveTipTo(newCommit, newCommit);
     238           5 :       args.commitStatus.put(args.mergeTip.getCurrentTip());
     239           5 :       acceptMergeTip(args.mergeTip);
     240           5 :     }
     241             : 
     242             :     @Nullable
     243             :     @Override
     244             :     public PatchSet updateChangeImpl(ChangeContext ctx)
     245             :         throws NoSuchChangeException, ResourceConflictException, IOException, BadRequestException {
     246           5 :       if (newCommit == null) {
     247           4 :         checkState(!rebaseAlways, "RebaseAlways must never fast forward");
     248             :         // otherwise, took the fast-forward option, nothing to do.
     249           4 :         return null;
     250             :       }
     251             : 
     252             :       PatchSet newPs;
     253           5 :       if (rebaseOp != null) {
     254           4 :         rebaseOp.updateChange(ctx);
     255           4 :         newPs = rebaseOp.getPatchSet();
     256             :       } else {
     257             :         // CherryPick
     258           4 :         PatchSet prevPs = args.psUtil.current(ctx.getNotes());
     259           4 :         newPs =
     260           4 :             args.psUtil.insert(
     261           4 :                 ctx.getRevWalk(),
     262           4 :                 ctx.getUpdate(newPatchSetId),
     263             :                 newPatchSetId,
     264             :                 newCommit,
     265           4 :                 prevPs != null ? prevPs.groups() : ImmutableList.of(),
     266             :                 null,
     267             :                 null);
     268             :       }
     269           5 :       ctx.getChange()
     270           5 :           .setCurrentPatchSet(
     271           5 :               args.patchSetInfoFactory.get(ctx.getRevWalk(), newCommit, newPatchSetId));
     272           5 :       newCommit.setNotes(ctx.getNotes());
     273           5 :       return newPs;
     274             :     }
     275             : 
     276             :     @Override
     277             :     public void postUpdateImpl(PostUpdateContext ctx) {
     278           5 :       if (rebaseOp != null) {
     279           4 :         rebaseOp.postUpdate(ctx);
     280             :       }
     281           5 :     }
     282             :   }
     283             : 
     284             :   private class MergeIfNecessaryOp extends SubmitStrategyOp {
     285           2 :     private MergeIfNecessaryOp(CodeReviewCommit toMerge) {
     286           2 :       super(RebaseSubmitStrategy.this.args, toMerge);
     287           2 :     }
     288             : 
     289             :     @Override
     290             :     public void updateRepoImpl(RepoContext ctx) throws IntegrationConflictException, IOException {
     291             :       // There are multiple parents, so this is a merge commit. We don't want
     292             :       // to rebase the merge as clients can't easily rebase their history with
     293             :       // that merge present and replaced by an equivalent merge with a different
     294             :       // first parent. So instead behave as though MERGE_IF_NECESSARY was
     295             :       // configured.
     296             :       // TODO(tandrii): this is not in spirit of RebaseAlways strategy because
     297             :       // the commit messages can not be modified in the process. It's also
     298             :       // possible to implement rebasing of merge commits. E.g., the Cherry Pick
     299             :       // REST endpoint already supports cherry-picking of merge commits.
     300             :       // For now, users of RebaseAlways strategy for whom changed commit footers
     301             :       // are important would be well advised to prohibit uploading patches with
     302             :       // merge commits.
     303           2 :       MergeTip mergeTip = args.mergeTip;
     304           2 :       if (args.rw.isMergedInto(mergeTip.getCurrentTip(), toMerge)
     305           2 :           && !args.subscriptionGraph.hasSubscription(args.destBranch)) {
     306           2 :         mergeTip.moveTipTo(toMerge, toMerge);
     307             :       } else {
     308           2 :         PersonIdent caller = ctx.newCommitterIdent();
     309           2 :         CodeReviewCommit newTip =
     310           2 :             args.mergeUtil.mergeOneCommit(
     311             :                 caller,
     312             :                 caller,
     313             :                 args.rw,
     314           2 :                 ctx.getInserter(),
     315           2 :                 ctx.getRepoView().getConfig(),
     316             :                 args.destBranch,
     317           2 :                 mergeTip.getCurrentTip(),
     318             :                 toMerge);
     319           2 :         mergeTip.moveTipTo(amendGitlink(newTip), toMerge);
     320             :       }
     321           2 :       args.mergeUtil.markCleanMerges(
     322           2 :           args.rw, args.canMergeFlag, mergeTip.getCurrentTip(), args.alreadyAccepted);
     323           2 :       acceptMergeTip(mergeTip);
     324           2 :     }
     325             :   }
     326             : 
     327             :   private void acceptMergeTip(MergeTip mergeTip) {
     328           5 :     args.alreadyAccepted.add(mergeTip.getCurrentTip());
     329           5 :   }
     330             : 
     331             :   static boolean dryRun(
     332             :       SubmitDryRun.Arguments args,
     333             :       Repository repo,
     334             :       CodeReviewCommit mergeTip,
     335             :       CodeReviewCommit toMerge) {
     336             :     // Test for merge instead of cherry pick to avoid false negatives
     337             :     // on commit chains.
     338           3 :     return args.mergeUtil.canMerge(args.mergeSorter, repo, mergeTip, toMerge);
     339             :   }
     340             : }

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