LCOV - code coverage report
Current view: top level - server/restapi/project - DeleteRef.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 94 109 86.2 %
Date: 2022-11-19 15:00:39 Functions: 9 9 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2016 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.project;
      16             : 
      17             : import static com.google.common.collect.ImmutableSet.toImmutableSet;
      18             : import static com.google.gerrit.entities.RefNames.isConfigRef;
      19             : import static java.lang.String.format;
      20             : import static org.eclipse.jgit.lib.Constants.R_REFS;
      21             : import static org.eclipse.jgit.lib.Constants.R_TAGS;
      22             : import static org.eclipse.jgit.transport.ReceiveCommand.Type.DELETE;
      23             : 
      24             : import com.google.common.collect.ImmutableListMultimap;
      25             : import com.google.common.collect.ImmutableSet;
      26             : import com.google.common.collect.Iterables;
      27             : import com.google.common.flogger.FluentLogger;
      28             : import com.google.gerrit.common.Nullable;
      29             : import com.google.gerrit.entities.BranchNameKey;
      30             : import com.google.gerrit.entities.Project;
      31             : import com.google.gerrit.exceptions.StorageException;
      32             : import com.google.gerrit.extensions.restapi.AuthException;
      33             : import com.google.gerrit.extensions.restapi.ResourceConflictException;
      34             : import com.google.gerrit.git.LockFailureException;
      35             : import com.google.gerrit.server.IdentifiedUser;
      36             : import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
      37             : import com.google.gerrit.server.git.GitRepositoryManager;
      38             : import com.google.gerrit.server.permissions.PermissionBackend;
      39             : import com.google.gerrit.server.permissions.PermissionBackendException;
      40             : import com.google.gerrit.server.permissions.RefPermission;
      41             : import com.google.gerrit.server.project.ProjectState;
      42             : import com.google.gerrit.server.project.RefValidationHelper;
      43             : import com.google.gerrit.server.query.change.InternalChangeQuery;
      44             : import com.google.inject.Inject;
      45             : import com.google.inject.Provider;
      46             : import com.google.inject.Singleton;
      47             : import java.io.IOException;
      48             : import org.eclipse.jgit.lib.BatchRefUpdate;
      49             : import org.eclipse.jgit.lib.NullProgressMonitor;
      50             : import org.eclipse.jgit.lib.ObjectId;
      51             : import org.eclipse.jgit.lib.Ref;
      52             : import org.eclipse.jgit.lib.RefUpdate;
      53             : import org.eclipse.jgit.lib.Repository;
      54             : import org.eclipse.jgit.revwalk.RevWalk;
      55             : import org.eclipse.jgit.transport.ReceiveCommand;
      56             : import org.eclipse.jgit.transport.ReceiveCommand.Result;
      57             : 
      58             : @Singleton
      59             : public class DeleteRef {
      60         146 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      61             : 
      62             :   private final Provider<IdentifiedUser> identifiedUser;
      63             :   private final PermissionBackend permissionBackend;
      64             :   private final GitRepositoryManager repoManager;
      65             :   private final GitReferenceUpdated referenceUpdated;
      66             :   private final RefValidationHelper refDeletionValidator;
      67             :   private final Provider<InternalChangeQuery> queryProvider;
      68             : 
      69             :   @Inject
      70             :   DeleteRef(
      71             :       Provider<IdentifiedUser> identifiedUser,
      72             :       PermissionBackend permissionBackend,
      73             :       GitRepositoryManager repoManager,
      74             :       GitReferenceUpdated referenceUpdated,
      75             :       RefValidationHelper.Factory refDeletionValidatorFactory,
      76         146 :       Provider<InternalChangeQuery> queryProvider) {
      77         146 :     this.identifiedUser = identifiedUser;
      78         146 :     this.permissionBackend = permissionBackend;
      79         146 :     this.repoManager = repoManager;
      80         146 :     this.referenceUpdated = referenceUpdated;
      81         146 :     this.refDeletionValidator = refDeletionValidatorFactory.create(DELETE);
      82         146 :     this.queryProvider = queryProvider;
      83         146 :   }
      84             : 
      85             :   /**
      86             :    * Deletes a single ref from the repository.
      87             :    *
      88             :    * @param projectState the {@code ProjectState} of the project containing the target ref.
      89             :    * @param ref the ref to be deleted.
      90             :    */
      91             :   public void deleteSingleRef(ProjectState projectState, String ref)
      92             :       throws IOException, ResourceConflictException, AuthException, PermissionBackendException {
      93           2 :     deleteSingleRef(projectState, ref, null);
      94           2 :   }
      95             : 
      96             :   /**
      97             :    * Deletes a single ref from the repository.
      98             :    *
      99             :    * @param projectState the {@code ProjectState} of the project containing the target ref.
     100             :    * @param ref the ref to be deleted.
     101             :    * @param prefix the prefix of the ref.
     102             :    */
     103             :   public void deleteSingleRef(ProjectState projectState, String ref, @Nullable String prefix)
     104             :       throws IOException, ResourceConflictException, AuthException, PermissionBackendException {
     105           7 :     if (prefix != null && !ref.startsWith(R_REFS)) {
     106           0 :       ref = prefix + ref;
     107             :     }
     108             : 
     109           7 :     projectState.checkStatePermitsWrite();
     110           7 :     permissionBackend
     111           7 :         .currentUser()
     112           7 :         .project(projectState.getNameKey())
     113           7 :         .ref(ref)
     114           6 :         .check(RefPermission.DELETE);
     115             : 
     116           6 :     try (Repository repository = repoManager.openRepository(projectState.getNameKey())) {
     117             :       RefUpdate.Result result;
     118           6 :       RefUpdate u = repository.updateRef(ref);
     119           6 :       u.setExpectedOldObjectId(repository.exactRef(ref).getObjectId());
     120           6 :       u.setNewObjectId(ObjectId.zeroId());
     121           6 :       u.setForceUpdate(true);
     122           6 :       refDeletionValidator.validateRefOperation(
     123           6 :           projectState.getName(),
     124           6 :           identifiedUser.get(),
     125             :           u,
     126           6 :           /* pushOptions */ ImmutableListMultimap.of());
     127           5 :       result = u.delete();
     128             : 
     129           5 :       switch (result) {
     130             :         case NEW:
     131             :         case NO_CHANGE:
     132             :         case FAST_FORWARD:
     133             :         case FORCED:
     134           5 :           referenceUpdated.fire(
     135           5 :               projectState.getNameKey(),
     136             :               u,
     137             :               ReceiveCommand.Type.DELETE,
     138           5 :               identifiedUser.get().state());
     139           5 :           break;
     140             : 
     141             :         case REJECTED_CURRENT_BRANCH:
     142           0 :           logger.atFine().log("Cannot delete current branch %s: %s", ref, result.name());
     143           0 :           throw new ResourceConflictException("cannot delete current branch");
     144             : 
     145             :         case LOCK_FAILURE:
     146           0 :           throw new LockFailureException(String.format("Cannot delete %s", ref), u);
     147             :         case IO_FAILURE:
     148             :         case NOT_ATTEMPTED:
     149             :         case REJECTED:
     150             :         case RENAMED:
     151             :         case REJECTED_MISSING_OBJECT:
     152             :         case REJECTED_OTHER_REASON:
     153             :         default:
     154           0 :           throw new StorageException(String.format("Cannot delete %s: %s", ref, result.name()));
     155             :       }
     156             :     }
     157           5 :   }
     158             : 
     159             :   /**
     160             :    * Deletes a set of refs from the repository.
     161             :    *
     162             :    * @param projectState the {@code ProjectState} of the project whose refs are to be deleted.
     163             :    * @param refsToDelete the refs to be deleted.
     164             :    * @param prefix the prefix to add to abbreviated refs, eg. "refs/heads/".
     165             :    */
     166             :   public void deleteMultipleRefs(
     167             :       ProjectState projectState, ImmutableSet<String> refsToDelete, String prefix)
     168             :       throws IOException, ResourceConflictException, PermissionBackendException, AuthException {
     169           2 :     if (refsToDelete.isEmpty()) {
     170           0 :       return;
     171             :     }
     172             : 
     173           2 :     if (refsToDelete.size() == 1) {
     174           0 :       deleteSingleRef(projectState, Iterables.getOnlyElement(refsToDelete), prefix);
     175           0 :       return;
     176             :     }
     177             : 
     178           2 :     try (Repository repository = repoManager.openRepository(projectState.getNameKey())) {
     179           2 :       BatchRefUpdate batchUpdate = repository.getRefDatabase().newBatchUpdate();
     180           2 :       batchUpdate.setAtomic(false);
     181             :       ImmutableSet<String> refs =
     182           2 :           prefix == null
     183           0 :               ? refsToDelete
     184           2 :               : refsToDelete.stream()
     185           2 :                   .map(ref -> ref.startsWith(R_REFS) ? ref : prefix + ref)
     186           2 :                   .collect(toImmutableSet());
     187           2 :       for (String ref : refs) {
     188           2 :         batchUpdate.addCommand(createDeleteCommand(projectState, repository, ref));
     189           2 :       }
     190           2 :       try (RevWalk rw = new RevWalk(repository)) {
     191           2 :         batchUpdate.execute(rw, NullProgressMonitor.INSTANCE);
     192             :       }
     193           2 :       StringBuilder errorMessages = new StringBuilder();
     194           2 :       for (ReceiveCommand command : batchUpdate.getCommands()) {
     195           2 :         if (command.getResult() == Result.OK) {
     196           2 :           postDeletion(projectState.getNameKey(), command);
     197             :         } else {
     198           2 :           appendAndLogErrorMessage(errorMessages, command);
     199             :         }
     200           2 :       }
     201           2 :       if (errorMessages.length() > 0) {
     202           2 :         throw new ResourceConflictException(errorMessages.toString());
     203             :       }
     204             :     }
     205           2 :   }
     206             : 
     207             :   private ReceiveCommand createDeleteCommand(
     208             :       ProjectState projectState, Repository r, String refName)
     209             :       throws IOException, ResourceConflictException, PermissionBackendException {
     210           2 :     Ref ref = r.getRefDatabase().exactRef(refName);
     211             :     ReceiveCommand command;
     212           2 :     if (ref == null) {
     213           2 :       command = new ReceiveCommand(ObjectId.zeroId(), ObjectId.zeroId(), refName);
     214           2 :       command.setResult(
     215             :           Result.REJECTED_OTHER_REASON,
     216             :           "it doesn't exist or you do not have permission to delete it");
     217           2 :       return command;
     218             :     }
     219           2 :     command = new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), ref.getName());
     220             : 
     221           2 :     if (isConfigRef(refName)) {
     222             :       // Never allow to delete the meta config branch.
     223           0 :       command.setResult(Result.REJECTED_OTHER_REASON, "not allowed to delete branch " + refName);
     224             :     } else {
     225             :       try {
     226           2 :         permissionBackend
     227           2 :             .currentUser()
     228           2 :             .project(projectState.getNameKey())
     229           2 :             .ref(refName)
     230           2 :             .check(RefPermission.DELETE);
     231           2 :       } catch (AuthException denied) {
     232           2 :         command.setResult(
     233             :             Result.REJECTED_OTHER_REASON,
     234             :             "it doesn't exist or you do not have permission to delete it");
     235           2 :       }
     236             :     }
     237             : 
     238           2 :     if (!projectState.statePermitsWrite()) {
     239           0 :       command.setResult(Result.REJECTED_OTHER_REASON, "project state does not permit write");
     240             :     }
     241             : 
     242           2 :     if (!refName.startsWith(R_TAGS)) {
     243           1 :       BranchNameKey branchKey = BranchNameKey.create(projectState.getNameKey(), ref.getName());
     244           1 :       if (!queryProvider.get().setLimit(1).byBranchOpen(branchKey).isEmpty()) {
     245           0 :         command.setResult(Result.REJECTED_OTHER_REASON, "it has open changes");
     246             :       }
     247             :     }
     248             : 
     249           2 :     RefUpdate u = r.updateRef(refName);
     250           2 :     u.setForceUpdate(true);
     251           2 :     u.setExpectedOldObjectId(r.exactRef(refName).getObjectId());
     252           2 :     u.setNewObjectId(ObjectId.zeroId());
     253           2 :     refDeletionValidator.validateRefOperation(
     254           2 :         projectState.getName(),
     255           2 :         identifiedUser.get(),
     256             :         u,
     257           2 :         /* pushOptions */ ImmutableListMultimap.of());
     258           2 :     return command;
     259             :   }
     260             : 
     261             :   private void appendAndLogErrorMessage(StringBuilder errorMessages, ReceiveCommand cmd) {
     262             :     String msg;
     263           2 :     switch (cmd.getResult()) {
     264             :       case REJECTED_CURRENT_BRANCH:
     265           0 :         msg = format("Cannot delete %s: it is the current branch", cmd.getRefName());
     266           0 :         break;
     267             :       case REJECTED_OTHER_REASON:
     268           2 :         msg = format("Cannot delete %s: %s", cmd.getRefName(), cmd.getMessage());
     269           2 :         break;
     270             :       case LOCK_FAILURE:
     271             :       case NOT_ATTEMPTED:
     272             :       case OK:
     273             :       case REJECTED_MISSING_OBJECT:
     274             :       case REJECTED_NOCREATE:
     275             :       case REJECTED_NODELETE:
     276             :       case REJECTED_NONFASTFORWARD:
     277             :       default:
     278           0 :         msg = format("Cannot delete %s: %s", cmd.getRefName(), cmd.getResult());
     279             :         break;
     280             :     }
     281           2 :     logger.atSevere().log("%s", msg);
     282           2 :     errorMessages.append(msg);
     283           2 :     errorMessages.append("\n");
     284           2 :   }
     285             : 
     286             :   private void postDeletion(Project.NameKey project, ReceiveCommand cmd) {
     287           2 :     referenceUpdated.fire(project, cmd, identifiedUser.get().state());
     288           2 :   }
     289             : }

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