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

          Line data    Source code
       1             : // Copyright (C) 2015 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.extensions.conditions.BooleanCondition.and;
      18             : import static com.google.gerrit.server.permissions.ChangePermission.ABANDON;
      19             : import static com.google.gerrit.server.permissions.RefPermission.CREATE_CHANGE;
      20             : import static com.google.gerrit.server.project.ProjectCache.illegalState;
      21             : import static com.google.gerrit.server.query.change.ChangeData.asChanges;
      22             : 
      23             : import com.google.common.base.Strings;
      24             : import com.google.gerrit.common.Nullable;
      25             : import com.google.gerrit.entities.BranchNameKey;
      26             : import com.google.gerrit.entities.Change;
      27             : import com.google.gerrit.entities.LabelType;
      28             : import com.google.gerrit.entities.PatchSet;
      29             : import com.google.gerrit.entities.PatchSetApproval;
      30             : import com.google.gerrit.entities.Project;
      31             : import com.google.gerrit.entities.RefNames;
      32             : import com.google.gerrit.extensions.api.changes.MoveInput;
      33             : import com.google.gerrit.extensions.common.ChangeInfo;
      34             : import com.google.gerrit.extensions.restapi.AuthException;
      35             : import com.google.gerrit.extensions.restapi.BadRequestException;
      36             : import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
      37             : import com.google.gerrit.extensions.restapi.ResourceConflictException;
      38             : import com.google.gerrit.extensions.restapi.Response;
      39             : import com.google.gerrit.extensions.restapi.RestApiException;
      40             : import com.google.gerrit.extensions.restapi.RestModifyView;
      41             : import com.google.gerrit.extensions.webui.UiAction;
      42             : import com.google.gerrit.server.ChangeMessagesUtil;
      43             : import com.google.gerrit.server.ChangeUtil;
      44             : import com.google.gerrit.server.IdentifiedUser;
      45             : import com.google.gerrit.server.PatchSetUtil;
      46             : import com.google.gerrit.server.approval.ApprovalsUtil;
      47             : import com.google.gerrit.server.change.ChangeJson;
      48             : import com.google.gerrit.server.change.ChangeResource;
      49             : import com.google.gerrit.server.config.GerritServerConfig;
      50             : import com.google.gerrit.server.git.GitRepositoryManager;
      51             : import com.google.gerrit.server.notedb.ChangeUpdate;
      52             : import com.google.gerrit.server.permissions.GlobalPermission;
      53             : import com.google.gerrit.server.permissions.PermissionBackend;
      54             : import com.google.gerrit.server.permissions.PermissionBackendException;
      55             : import com.google.gerrit.server.project.ProjectCache;
      56             : import com.google.gerrit.server.project.ProjectState;
      57             : import com.google.gerrit.server.query.change.InternalChangeQuery;
      58             : import com.google.gerrit.server.update.BatchUpdate;
      59             : import com.google.gerrit.server.update.BatchUpdateOp;
      60             : import com.google.gerrit.server.update.ChangeContext;
      61             : import com.google.gerrit.server.update.UpdateException;
      62             : import com.google.gerrit.server.util.time.TimeUtil;
      63             : import com.google.inject.Inject;
      64             : import com.google.inject.Provider;
      65             : import com.google.inject.Singleton;
      66             : import java.io.IOException;
      67             : import java.util.Optional;
      68             : import org.eclipse.jgit.lib.Config;
      69             : import org.eclipse.jgit.lib.ObjectId;
      70             : import org.eclipse.jgit.lib.Repository;
      71             : import org.eclipse.jgit.revwalk.RevCommit;
      72             : import org.eclipse.jgit.revwalk.RevWalk;
      73             : 
      74             : @Singleton
      75             : public class Move implements RestModifyView<ChangeResource, MoveInput>, UiAction<ChangeResource> {
      76             :   private final PermissionBackend permissionBackend;
      77             :   private final BatchUpdate.Factory updateFactory;
      78             :   private final ChangeJson.Factory json;
      79             :   private final GitRepositoryManager repoManager;
      80             :   private final Provider<InternalChangeQuery> queryProvider;
      81             :   private final ChangeMessagesUtil cmUtil;
      82             :   private final PatchSetUtil psUtil;
      83             :   private final ApprovalsUtil approvalsUtil;
      84             :   private final ProjectCache projectCache;
      85             :   private final boolean moveEnabled;
      86             : 
      87             :   @Inject
      88             :   Move(
      89             :       PermissionBackend permissionBackend,
      90             :       BatchUpdate.Factory updateFactory,
      91             :       ChangeJson.Factory json,
      92             :       GitRepositoryManager repoManager,
      93             :       Provider<InternalChangeQuery> queryProvider,
      94             :       ChangeMessagesUtil cmUtil,
      95             :       PatchSetUtil psUtil,
      96             :       ApprovalsUtil approvalsUtil,
      97             :       ProjectCache projectCache,
      98         145 :       @GerritServerConfig Config gerritConfig) {
      99         145 :     this.permissionBackend = permissionBackend;
     100         145 :     this.updateFactory = updateFactory;
     101         145 :     this.json = json;
     102         145 :     this.repoManager = repoManager;
     103         145 :     this.queryProvider = queryProvider;
     104         145 :     this.cmUtil = cmUtil;
     105         145 :     this.psUtil = psUtil;
     106         145 :     this.approvalsUtil = approvalsUtil;
     107         145 :     this.projectCache = projectCache;
     108         145 :     this.moveEnabled = gerritConfig.getBoolean("change", null, "move", true);
     109         145 :   }
     110             : 
     111             :   @Override
     112             :   public Response<ChangeInfo> apply(ChangeResource rsrc, MoveInput input)
     113             :       throws RestApiException, UpdateException, PermissionBackendException, IOException {
     114           3 :     if (!moveEnabled) {
     115             :       // This will be removed with the above config once we reach consensus for the move change
     116             :       // behavior. See: https://bugs.chromium.org/p/gerrit/issues/detail?id=9877
     117           1 :       throw new MethodNotAllowedException("move changes endpoint is disabled");
     118             :     }
     119             : 
     120           3 :     Change change = rsrc.getChange();
     121           3 :     Project.NameKey project = rsrc.getProject();
     122           3 :     IdentifiedUser caller = rsrc.getUser().asIdentifiedUser();
     123           3 :     if (input.destinationBranch == null) {
     124           2 :       throw new BadRequestException("destination branch is required");
     125             :     }
     126           2 :     input.destinationBranch = RefNames.fullName(input.destinationBranch);
     127             : 
     128           2 :     if (!change.isNew()) {
     129           1 :       throw new ResourceConflictException("Change is " + ChangeUtil.status(change));
     130             :     }
     131             : 
     132           2 :     BranchNameKey newDest = BranchNameKey.create(project, input.destinationBranch);
     133           2 :     if (change.getDest().equals(newDest)) {
     134           1 :       throw new ResourceConflictException("Change is already destined for the specified branch");
     135             :     }
     136             : 
     137             :     // Not allowed to move if the current patch set is locked.
     138           2 :     psUtil.checkPatchSetNotLocked(rsrc.getNotes());
     139             : 
     140             :     // Keeping all votes can be confusing in the context of the destination branch, see the
     141             :     // discussion in
     142             :     // https://gerrit-review.googlesource.com/c/gerrit/+/129171
     143             :     // Only administrators are allowed to keep all labels at their own risk.
     144           2 :     if (input.keepAllVotes
     145           1 :         && !permissionBackend.user(caller).test(GlobalPermission.ADMINISTRATE_SERVER)) {
     146           1 :       throw new AuthException("move is not permitted with keepAllVotes option");
     147             :     }
     148             : 
     149             :     // Move requires abandoning this change, and creating a new change.
     150             :     try {
     151           2 :       rsrc.permissions().check(ABANDON);
     152           2 :       permissionBackend.user(caller).ref(newDest).check(CREATE_CHANGE);
     153           1 :     } catch (AuthException denied) {
     154           1 :       throw new AuthException("move not permitted", denied);
     155           2 :     }
     156           2 :     projectCache.get(project).orElseThrow(illegalState(project)).checkStatePermitsWrite();
     157             : 
     158           2 :     Op op = new Op(input);
     159           2 :     try (BatchUpdate u = updateFactory.create(project, caller, TimeUtil.now())) {
     160           2 :       u.addOp(change.getId(), op);
     161           2 :       u.execute();
     162             :     }
     163           2 :     return Response.ok(json.noOptions().format(op.getChange()));
     164             :   }
     165             : 
     166             :   private class Op implements BatchUpdateOp {
     167             :     private final MoveInput input;
     168             : 
     169             :     private Change change;
     170             :     private BranchNameKey newDestKey;
     171             : 
     172           2 :     Op(MoveInput input) {
     173           2 :       this.input = input;
     174           2 :     }
     175             : 
     176             :     @Nullable
     177             :     public Change getChange() {
     178           2 :       return change;
     179             :     }
     180             : 
     181             :     @Override
     182             :     public boolean updateChange(ChangeContext ctx) throws ResourceConflictException, IOException {
     183           2 :       change = ctx.getChange();
     184           2 :       if (!change.isNew()) {
     185           0 :         throw new ResourceConflictException("Change is " + ChangeUtil.status(change));
     186             :       }
     187             : 
     188           2 :       Project.NameKey projectKey = change.getProject();
     189           2 :       newDestKey = BranchNameKey.create(projectKey, input.destinationBranch);
     190           2 :       BranchNameKey changePrevDest = change.getDest();
     191           2 :       if (changePrevDest.equals(newDestKey)) {
     192           0 :         throw new ResourceConflictException("Change is already destined for the specified branch");
     193             :       }
     194             : 
     195           2 :       final PatchSet.Id patchSetId = change.currentPatchSetId();
     196           2 :       try (Repository repo = repoManager.openRepository(projectKey);
     197           2 :           RevWalk revWalk = new RevWalk(repo)) {
     198           2 :         RevCommit currPatchsetRevCommit =
     199           2 :             revWalk.parseCommit(psUtil.current(ctx.getNotes()).commitId());
     200             : 
     201           2 :         ObjectId refId = repo.resolve(input.destinationBranch);
     202             :         // Check if destination ref exists in project repo
     203           2 :         if (refId == null) {
     204           1 :           throw new ResourceConflictException(
     205             :               "Destination " + input.destinationBranch + " not found in the project");
     206             :         }
     207           2 :         RevCommit refCommit = revWalk.parseCommit(refId);
     208           2 :         if (revWalk.isMergedInto(currPatchsetRevCommit, refCommit)) {
     209           1 :           throw new ResourceConflictException(
     210             :               "Current patchset revision is reachable from tip of " + input.destinationBranch);
     211             :         }
     212             :       }
     213             : 
     214           2 :       Change.Key changeKey = change.getKey();
     215           2 :       if (!asChanges(queryProvider.get().byBranchKey(newDestKey, changeKey)).isEmpty()) {
     216           1 :         throw new ResourceConflictException(
     217             :             "Destination "
     218           1 :                 + newDestKey.shortName()
     219             :                 + " has a different change with same change key "
     220             :                 + changeKey);
     221             :       }
     222             : 
     223           2 :       if (!change.currentPatchSetId().equals(patchSetId)) {
     224           0 :         throw new ResourceConflictException("Patch set is not current");
     225             :       }
     226             : 
     227           2 :       PatchSet.Id psId = change.currentPatchSetId();
     228           2 :       ChangeUpdate update = ctx.getUpdate(psId);
     229           2 :       update.setBranch(newDestKey.branch());
     230           2 :       change.setDest(newDestKey);
     231             : 
     232           2 :       if (!input.keepAllVotes) {
     233           2 :         updateApprovals(ctx, update, psId, projectKey);
     234             :       }
     235             : 
     236           2 :       StringBuilder msgBuf = new StringBuilder();
     237           2 :       msgBuf.append("Change destination moved from ");
     238           2 :       msgBuf.append(changePrevDest.shortName());
     239           2 :       msgBuf.append(" to ");
     240           2 :       msgBuf.append(newDestKey.shortName());
     241           2 :       if (!Strings.isNullOrEmpty(input.message)) {
     242           1 :         msgBuf.append("\n\n");
     243           1 :         msgBuf.append(input.message);
     244             :       }
     245           2 :       cmUtil.setChangeMessage(ctx, msgBuf.toString(), ChangeMessagesUtil.TAG_MOVE);
     246             : 
     247           2 :       return true;
     248             :     }
     249             : 
     250             :     /**
     251             :      * We have a long discussion about how to deal with its votes after moving a change from one
     252             :      * branch to another. In the end, we think only keeping the veto votes is the best way since
     253             :      * it's simple for us and less confusing for our users. See the discussion in the following
     254             :      * proposal: https://gerrit-review.googlesource.com/c/gerrit/+/129171
     255             :      */
     256             :     private void updateApprovals(
     257             :         ChangeContext ctx, ChangeUpdate update, PatchSet.Id psId, Project.NameKey project) {
     258           2 :       for (PatchSetApproval psa : approvalsUtil.byPatchSet(ctx.getNotes(), psId)) {
     259           2 :         ProjectState projectState = projectCache.get(project).orElseThrow(illegalState(project));
     260           2 :         Optional<LabelType> type =
     261           2 :             projectState.getLabelTypes(ctx.getNotes()).byLabel(psa.labelId());
     262             :         // Only keep veto votes, defined as votes where:
     263             :         // 1- the label function allows minimum values to block submission.
     264             :         // 2- the vote holds the minimum value.
     265           2 :         if (!type.isPresent()
     266           2 :             || (type.get().isMaxNegative(psa) && type.get().getFunction().isBlock())) {
     267           1 :           continue;
     268             :         }
     269             : 
     270             :         // Remove votes from NoteDb.
     271           2 :         update.removeApprovalFor(psa.accountId(), psa.label());
     272           2 :       }
     273           2 :     }
     274             :   }
     275             : 
     276             :   @Override
     277             :   public UiAction.Description getDescription(ChangeResource rsrc) throws IOException {
     278          57 :     UiAction.Description description =
     279             :         new UiAction.Description()
     280          57 :             .setLabel("Move Change")
     281          57 :             .setTitle("Move change to a different branch")
     282          57 :             .setVisible(false);
     283             : 
     284          57 :     Change change = rsrc.getChange();
     285          57 :     if (!change.isNew()) {
     286          25 :       return description;
     287             :     }
     288          52 :     if (!projectCache
     289          52 :         .get(rsrc.getProject())
     290          52 :         .orElseThrow(illegalState(rsrc.getProject()))
     291          52 :         .statePermitsWrite()) {
     292           0 :       return description;
     293             :     }
     294          52 :     if (psUtil.isPatchSetLocked(rsrc.getNotes())) {
     295           0 :       return description;
     296             :     }
     297          52 :     return description.setVisible(
     298          52 :         and(
     299          52 :             permissionBackend.user(rsrc.getUser()).ref(change.getDest()).testCond(CREATE_CHANGE),
     300          52 :             rsrc.permissions().testCond(ABANDON)));
     301             :   }
     302             : }

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