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

          Line data    Source code
       1             : // Copyright (C) 2013 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.restapi.change;
      16             : 
      17             : import static com.google.gerrit.server.project.ProjectCache.illegalState;
      18             : 
      19             : import com.google.common.collect.ImmutableListMultimap;
      20             : import com.google.common.collect.ImmutableSet;
      21             : import com.google.common.collect.Sets;
      22             : import com.google.gerrit.common.Nullable;
      23             : import com.google.gerrit.entities.BranchNameKey;
      24             : import com.google.gerrit.entities.Change;
      25             : import com.google.gerrit.entities.PatchSet;
      26             : import com.google.gerrit.extensions.api.changes.RebaseInput;
      27             : import com.google.gerrit.extensions.client.ListChangesOption;
      28             : import com.google.gerrit.extensions.common.ChangeInfo;
      29             : import com.google.gerrit.extensions.restapi.AuthException;
      30             : import com.google.gerrit.extensions.restapi.ResourceConflictException;
      31             : import com.google.gerrit.extensions.restapi.Response;
      32             : import com.google.gerrit.extensions.restapi.RestApiException;
      33             : import com.google.gerrit.extensions.restapi.RestModifyView;
      34             : import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
      35             : import com.google.gerrit.extensions.webui.UiAction;
      36             : import com.google.gerrit.server.ChangeUtil;
      37             : import com.google.gerrit.server.PatchSetUtil;
      38             : import com.google.gerrit.server.change.ChangeJson;
      39             : import com.google.gerrit.server.change.ChangeResource;
      40             : import com.google.gerrit.server.change.NotifyResolver;
      41             : import com.google.gerrit.server.change.RebaseChangeOp;
      42             : import com.google.gerrit.server.change.RebaseUtil;
      43             : import com.google.gerrit.server.change.RebaseUtil.Base;
      44             : import com.google.gerrit.server.change.RevisionResource;
      45             : import com.google.gerrit.server.git.CodeReviewCommit;
      46             : import com.google.gerrit.server.git.GitRepositoryManager;
      47             : import com.google.gerrit.server.permissions.ChangePermission;
      48             : import com.google.gerrit.server.permissions.PermissionBackend;
      49             : import com.google.gerrit.server.permissions.PermissionBackendException;
      50             : import com.google.gerrit.server.project.NoSuchChangeException;
      51             : import com.google.gerrit.server.project.ProjectCache;
      52             : import com.google.gerrit.server.update.BatchUpdate;
      53             : import com.google.gerrit.server.update.UpdateException;
      54             : import com.google.gerrit.server.util.time.TimeUtil;
      55             : import com.google.inject.Inject;
      56             : import com.google.inject.Singleton;
      57             : import java.io.IOException;
      58             : import java.util.Map;
      59             : import org.eclipse.jgit.lib.ObjectId;
      60             : import org.eclipse.jgit.lib.ObjectInserter;
      61             : import org.eclipse.jgit.lib.ObjectReader;
      62             : import org.eclipse.jgit.lib.Ref;
      63             : import org.eclipse.jgit.lib.Repository;
      64             : import org.eclipse.jgit.revwalk.RevCommit;
      65             : import org.eclipse.jgit.revwalk.RevWalk;
      66             : 
      67             : @Singleton
      68             : public class Rebase
      69             :     implements RestModifyView<RevisionResource, RebaseInput>, UiAction<RevisionResource> {
      70         145 :   private static final ImmutableSet<ListChangesOption> OPTIONS =
      71         145 :       Sets.immutableEnumSet(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT);
      72             : 
      73             :   private final BatchUpdate.Factory updateFactory;
      74             :   private final GitRepositoryManager repoManager;
      75             :   private final RebaseChangeOp.Factory rebaseFactory;
      76             :   private final RebaseUtil rebaseUtil;
      77             :   private final ChangeJson.Factory json;
      78             :   private final PermissionBackend permissionBackend;
      79             :   private final ProjectCache projectCache;
      80             :   private final PatchSetUtil patchSetUtil;
      81             : 
      82             :   @Inject
      83             :   public Rebase(
      84             :       BatchUpdate.Factory updateFactory,
      85             :       GitRepositoryManager repoManager,
      86             :       RebaseChangeOp.Factory rebaseFactory,
      87             :       RebaseUtil rebaseUtil,
      88             :       ChangeJson.Factory json,
      89             :       PermissionBackend permissionBackend,
      90             :       ProjectCache projectCache,
      91         145 :       PatchSetUtil patchSetUtil) {
      92         145 :     this.updateFactory = updateFactory;
      93         145 :     this.repoManager = repoManager;
      94         145 :     this.rebaseFactory = rebaseFactory;
      95         145 :     this.rebaseUtil = rebaseUtil;
      96         145 :     this.json = json;
      97         145 :     this.permissionBackend = permissionBackend;
      98         145 :     this.projectCache = projectCache;
      99         145 :     this.patchSetUtil = patchSetUtil;
     100         145 :   }
     101             : 
     102             :   @Override
     103             :   public Response<ChangeInfo> apply(RevisionResource rsrc, RebaseInput input)
     104             :       throws UpdateException, RestApiException, IOException, PermissionBackendException {
     105             :     // Not allowed to rebase if the current patch set is locked.
     106          12 :     patchSetUtil.checkPatchSetNotLocked(rsrc.getNotes());
     107             : 
     108          12 :     rsrc.permissions().check(ChangePermission.REBASE);
     109          12 :     projectCache
     110          12 :         .get(rsrc.getProject())
     111          12 :         .orElseThrow(illegalState(rsrc.getProject()))
     112          12 :         .checkStatePermitsWrite();
     113             : 
     114          12 :     Change change = rsrc.getChange();
     115          12 :     try (Repository repo = repoManager.openRepository(change.getProject());
     116          12 :         ObjectInserter oi = repo.newObjectInserter();
     117          12 :         ObjectReader reader = oi.newReader();
     118          12 :         RevWalk rw = CodeReviewCommit.newRevWalk(reader);
     119          12 :         BatchUpdate bu =
     120          12 :             updateFactory.create(change.getProject(), rsrc.getUser(), TimeUtil.now())) {
     121          12 :       if (!change.isNew()) {
     122           2 :         throw new ResourceConflictException("change is " + ChangeUtil.status(change));
     123          12 :       } else if (!hasOneParent(rw, rsrc.getPatchSet())) {
     124           0 :         throw new ResourceConflictException(
     125             :             "cannot rebase merge commits or commit with no ancestor");
     126             :       }
     127          12 :       RebaseChangeOp rebaseOp =
     128             :           rebaseFactory
     129          11 :               .create(rsrc.getNotes(), rsrc.getPatchSet(), findBaseRev(repo, rw, rsrc, input))
     130          11 :               .setForceContentMerge(true)
     131          11 :               .setAllowConflicts(input.allowConflicts)
     132          11 :               .setValidationOptions(getValidateOptionsAsMultimap(input.validationOptions))
     133          11 :               .setFireRevisionCreated(true);
     134             :       // TODO(dborowitz): Why no notification? This seems wrong; dig up blame.
     135          11 :       bu.setNotify(NotifyResolver.Result.none());
     136          11 :       bu.setRepository(repo, rw, oi);
     137          11 :       bu.addOp(change.getId(), rebaseOp);
     138          11 :       bu.execute();
     139             : 
     140          11 :       ChangeInfo changeInfo = json.create(OPTIONS).format(change.getProject(), change.getId());
     141          11 :       changeInfo.containsGitConflicts =
     142          11 :           !rebaseOp.getRebasedCommit().getFilesWithGitConflicts().isEmpty() ? true : null;
     143          11 :       return Response.ok(changeInfo);
     144             :     }
     145             :   }
     146             : 
     147             :   private ObjectId findBaseRev(
     148             :       Repository repo, RevWalk rw, RevisionResource rsrc, RebaseInput input)
     149             :       throws RestApiException, IOException, NoSuchChangeException, AuthException,
     150             :           PermissionBackendException {
     151          12 :     BranchNameKey destRefKey = rsrc.getChange().getDest();
     152          12 :     if (input == null || input.base == null) {
     153           7 :       return rebaseUtil.findBaseRevision(rsrc.getPatchSet(), destRefKey, repo, rw);
     154             :     }
     155             : 
     156           5 :     Change change = rsrc.getChange();
     157           5 :     String str = input.base.trim();
     158           5 :     if (str.equals("")) {
     159             :       // Remove existing dependency to other patch set.
     160           1 :       Ref destRef = repo.exactRef(destRefKey.branch());
     161           1 :       if (destRef == null) {
     162           0 :         throw new ResourceConflictException(
     163           0 :             "can't rebase onto tip of branch " + destRefKey.branch() + "; branch doesn't exist");
     164             :       }
     165           1 :       return destRef.getObjectId();
     166             :     }
     167             : 
     168             :     Base base;
     169             :     try {
     170           5 :       base = rebaseUtil.parseBase(rsrc, str);
     171           5 :       if (base == null) {
     172           1 :         throw new ResourceConflictException(
     173             :             "base revision is missing from the destination branch: " + str);
     174             :       }
     175           1 :     } catch (NoSuchChangeException e) {
     176           1 :       throw new UnprocessableEntityException(
     177           1 :           String.format("Base change not found: %s", input.base), e);
     178           5 :     }
     179             : 
     180           5 :     PatchSet.Id baseId = base.patchSet().id();
     181           5 :     if (change.getId().equals(baseId.changeId())) {
     182           1 :       throw new ResourceConflictException("cannot rebase change onto itself");
     183             :     }
     184             : 
     185           5 :     permissionBackend.user(rsrc.getUser()).change(base.notes()).check(ChangePermission.READ);
     186             : 
     187           5 :     Change baseChange = base.notes().getChange();
     188           5 :     if (!baseChange.getProject().equals(change.getProject())) {
     189           0 :       throw new ResourceConflictException(
     190           0 :           "base change is in wrong project: " + baseChange.getProject());
     191           5 :     } else if (!baseChange.getDest().equals(change.getDest())) {
     192           1 :       throw new ResourceConflictException(
     193           1 :           "base change is targeting wrong branch: " + baseChange.getDest());
     194           5 :     } else if (baseChange.isAbandoned()) {
     195           1 :       throw new ResourceConflictException("base change is abandoned: " + baseChange.getKey());
     196           5 :     } else if (isMergedInto(rw, rsrc.getPatchSet(), base.patchSet())) {
     197           1 :       throw new ResourceConflictException(
     198             :           "base change "
     199           1 :               + baseChange.getKey()
     200             :               + " is a descendant of the current change - recursion not allowed");
     201             :     }
     202           5 :     return base.patchSet().commitId();
     203             :   }
     204             : 
     205             :   private boolean isMergedInto(RevWalk rw, PatchSet base, PatchSet tip) throws IOException {
     206           5 :     ObjectId baseId = base.commitId();
     207           5 :     ObjectId tipId = tip.commitId();
     208           5 :     return rw.isMergedInto(rw.parseCommit(baseId), rw.parseCommit(tipId));
     209             :   }
     210             : 
     211             :   private boolean hasOneParent(RevWalk rw, PatchSet ps) throws IOException {
     212             :     // Prevent rebase of exotic changes (merge commit, no ancestor).
     213          54 :     RevCommit c = rw.parseCommit(ps.commitId());
     214          54 :     return c.getParentCount() == 1;
     215             :   }
     216             : 
     217             :   @Override
     218             :   public UiAction.Description getDescription(RevisionResource rsrc) throws IOException {
     219          57 :     UiAction.Description description =
     220             :         new UiAction.Description()
     221          57 :             .setLabel("Rebase")
     222          57 :             .setTitle(
     223             :                 "Rebase onto tip of branch or parent change. Makes you the uploader of this "
     224             :                     + "change which can affect validity of approvals.")
     225          57 :             .setVisible(false);
     226             : 
     227          57 :     Change change = rsrc.getChange();
     228          57 :     if (!(change.isNew() && rsrc.isCurrent())) {
     229          24 :       return description;
     230             :     }
     231          52 :     if (!projectCache
     232          52 :         .get(rsrc.getProject())
     233          52 :         .orElseThrow(illegalState(rsrc.getProject()))
     234          52 :         .statePermitsWrite()) {
     235           0 :       return description;
     236             :     }
     237          52 :     if (patchSetUtil.isPatchSetLocked(rsrc.getNotes())) {
     238           0 :       return description;
     239             :     }
     240             : 
     241          52 :     boolean enabled = false;
     242          52 :     try (Repository repo = repoManager.openRepository(change.getDest().project());
     243          52 :         RevWalk rw = new RevWalk(repo)) {
     244          52 :       if (hasOneParent(rw, rsrc.getPatchSet())) {
     245          49 :         enabled = rebaseUtil.canRebase(rsrc.getPatchSet(), change.getDest(), repo, rw);
     246             :       }
     247             :     }
     248             : 
     249          52 :     if (rsrc.permissions().testOrFalse(ChangePermission.REBASE)) {
     250          50 :       return description.setVisible(true).setEnabled(enabled);
     251             :     }
     252          14 :     return description;
     253             :   }
     254             : 
     255             :   private static ImmutableListMultimap<String, String> getValidateOptionsAsMultimap(
     256             :       @Nullable Map<String, String> validationOptions) {
     257          11 :     if (validationOptions == null) {
     258          11 :       return ImmutableListMultimap.of();
     259             :     }
     260             : 
     261             :     ImmutableListMultimap.Builder<String, String> validationOptionsBuilder =
     262           1 :         ImmutableListMultimap.builder();
     263           1 :     validationOptions
     264           1 :         .entrySet()
     265           1 :         .forEach(e -> validationOptionsBuilder.put(e.getKey(), e.getValue()));
     266           1 :     return validationOptionsBuilder.build();
     267             :   }
     268             : 
     269             :   public static class CurrentRevision implements RestModifyView<ChangeResource, RebaseInput> {
     270             :     private final PatchSetUtil psUtil;
     271             :     private final Rebase rebase;
     272             : 
     273             :     @Inject
     274          91 :     CurrentRevision(PatchSetUtil psUtil, Rebase rebase) {
     275          91 :       this.psUtil = psUtil;
     276          91 :       this.rebase = rebase;
     277          91 :     }
     278             : 
     279             :     @Override
     280             :     public Response<ChangeInfo> apply(ChangeResource rsrc, RebaseInput input)
     281             :         throws RestApiException, UpdateException, IOException, PermissionBackendException {
     282           4 :       PatchSet ps = psUtil.current(rsrc.getNotes());
     283           4 :       if (ps == null) {
     284           0 :         throw new ResourceConflictException("current revision is missing");
     285             :       }
     286           4 :       return Response.ok(rebase.apply(new RevisionResource(rsrc, ps), input).value());
     287             :     }
     288             :   }
     289             : }

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