| // Copyright (C) 2016 The Android Open Source Project | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package com.google.gerrit.server.restapi.project; | |
| import static com.google.common.collect.ImmutableSet.toImmutableSet; | |
| import static com.google.gerrit.entities.RefNames.isConfigRef; | |
| import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.BRANCH_MODIFICATION; | |
| import static java.lang.String.format; | |
| import static org.eclipse.jgit.lib.Constants.R_REFS; | |
| import static org.eclipse.jgit.lib.Constants.R_TAGS; | |
| import static org.eclipse.jgit.transport.ReceiveCommand.Type.DELETE; | |
| import com.google.common.collect.ImmutableListMultimap; | |
| import com.google.common.collect.ImmutableSet; | |
| import com.google.common.collect.Iterables; | |
| import com.google.common.flogger.FluentLogger; | |
| import com.google.gerrit.common.Nullable; | |
| import com.google.gerrit.entities.BranchNameKey; | |
| import com.google.gerrit.entities.Project; | |
| import com.google.gerrit.exceptions.StorageException; | |
| import com.google.gerrit.extensions.restapi.AuthException; | |
| import com.google.gerrit.extensions.restapi.ResourceConflictException; | |
| import com.google.gerrit.git.LockFailureException; | |
| import com.google.gerrit.server.IdentifiedUser; | |
| import com.google.gerrit.server.extensions.events.GitReferenceUpdated; | |
| import com.google.gerrit.server.git.GitRepositoryManager; | |
| import com.google.gerrit.server.permissions.PermissionBackend; | |
| import com.google.gerrit.server.permissions.PermissionBackendException; | |
| import com.google.gerrit.server.permissions.RefPermission; | |
| import com.google.gerrit.server.project.ProjectState; | |
| import com.google.gerrit.server.project.RefValidationHelper; | |
| import com.google.gerrit.server.query.change.InternalChangeQuery; | |
| import com.google.gerrit.server.update.context.RefUpdateContext; | |
| import com.google.inject.Inject; | |
| import com.google.inject.Provider; | |
| import com.google.inject.Singleton; | |
| import java.io.IOException; | |
| import org.eclipse.jgit.lib.BatchRefUpdate; | |
| import org.eclipse.jgit.lib.NullProgressMonitor; | |
| import org.eclipse.jgit.lib.ObjectId; | |
| import org.eclipse.jgit.lib.Ref; | |
| import org.eclipse.jgit.lib.RefUpdate; | |
| import org.eclipse.jgit.lib.Repository; | |
| import org.eclipse.jgit.revwalk.RevWalk; | |
| import org.eclipse.jgit.transport.ReceiveCommand; | |
| import org.eclipse.jgit.transport.ReceiveCommand.Result; | |
| @Singleton | |
| public class DeleteRef { | |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); | |
| private final Provider<IdentifiedUser> identifiedUser; | |
| private final PermissionBackend permissionBackend; | |
| private final GitRepositoryManager repoManager; | |
| private final GitReferenceUpdated referenceUpdated; | |
| private final RefValidationHelper refDeletionValidator; | |
| private final Provider<InternalChangeQuery> queryProvider; | |
| @Inject | |
| DeleteRef( | |
| Provider<IdentifiedUser> identifiedUser, | |
| PermissionBackend permissionBackend, | |
| GitRepositoryManager repoManager, | |
| GitReferenceUpdated referenceUpdated, | |
| RefValidationHelper.Factory refDeletionValidatorFactory, | |
| Provider<InternalChangeQuery> queryProvider) { | |
| this.identifiedUser = identifiedUser; | |
| this.permissionBackend = permissionBackend; | |
| this.repoManager = repoManager; | |
| this.referenceUpdated = referenceUpdated; | |
| this.refDeletionValidator = refDeletionValidatorFactory.create(DELETE); | |
| this.queryProvider = queryProvider; | |
| } | |
| /** | |
| * Deletes a single ref from the repository. | |
| * | |
| * @param projectState the {@code ProjectState} of the project containing the target ref. | |
| * @param ref the ref to be deleted. | |
| */ | |
| public void deleteSingleRef(ProjectState projectState, String ref) | |
| throws IOException, ResourceConflictException, AuthException, PermissionBackendException { | |
| deleteSingleRef(projectState, ref, null); | |
| } | |
| /** | |
| * Deletes a single ref from the repository. | |
| * | |
| * @param projectState the {@code ProjectState} of the project containing the target ref. | |
| * @param ref the ref to be deleted. | |
| * @param prefix the prefix of the ref. | |
| */ | |
| public void deleteSingleRef(ProjectState projectState, String ref, @Nullable String prefix) | |
| throws IOException, ResourceConflictException, AuthException, PermissionBackendException { | |
| try (RefUpdateContext ctx = RefUpdateContext.open(BRANCH_MODIFICATION)) { | |
| if (prefix != null && !ref.startsWith(R_REFS)) { | |
| ref = prefix + ref; | |
| } | |
| projectState.checkStatePermitsWrite(); | |
| permissionBackend | |
| .currentUser() | |
| .project(projectState.getNameKey()) | |
| .ref(ref) | |
| .check(RefPermission.DELETE); | |
| try (Repository repository = repoManager.openRepository(projectState.getNameKey())) { | |
| RefUpdate.Result result; | |
| RefUpdate u = repository.updateRef(ref); | |
| u.setExpectedOldObjectId(repository.exactRef(ref).getObjectId()); | |
| u.setNewObjectId(ObjectId.zeroId()); | |
| u.setForceUpdate(true); | |
| refDeletionValidator.validateRefOperation( | |
| projectState.getName(), | |
| identifiedUser.get(), | |
| u, | |
| /* pushOptions */ ImmutableListMultimap.of()); | |
| result = u.delete(); | |
| switch (result) { | |
| case NEW: | |
| case NO_CHANGE: | |
| case FAST_FORWARD: | |
| case FORCED: | |
| referenceUpdated.fire( | |
| projectState.getNameKey(), | |
| u, | |
| ReceiveCommand.Type.DELETE, | |
| identifiedUser.get().state()); | |
| break; | |
| case REJECTED_CURRENT_BRANCH: | |
| logger.atFine().log("Cannot delete current branch %s: %s", ref, result.name()); | |
| throw new ResourceConflictException("cannot delete current branch"); | |
| case LOCK_FAILURE: | |
| throw new LockFailureException(String.format("Cannot delete %s", ref), u); | |
| case IO_FAILURE: | |
| case NOT_ATTEMPTED: | |
| case REJECTED: | |
| case RENAMED: | |
| case REJECTED_MISSING_OBJECT: | |
| case REJECTED_OTHER_REASON: | |
| default: | |
| throw new StorageException(String.format("Cannot delete %s: %s", ref, result.name())); | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * Deletes a set of refs from the repository. | |
| * | |
| * @param projectState the {@code ProjectState} of the project whose refs are to be deleted. | |
| * @param refsToDelete the refs to be deleted. | |
| * @param prefix the prefix to add to abbreviated refs, eg. "refs/heads/". | |
| */ | |
| public void deleteMultipleRefs( | |
| ProjectState projectState, ImmutableSet<String> refsToDelete, String prefix) | |
| throws IOException, ResourceConflictException, PermissionBackendException, AuthException { | |
| if (refsToDelete.isEmpty()) { | |
| return; | |
| } | |
| if (refsToDelete.size() == 1) { | |
| deleteSingleRef(projectState, Iterables.getOnlyElement(refsToDelete), prefix); | |
| return; | |
| } | |
| try (Repository repository = repoManager.openRepository(projectState.getNameKey())) { | |
| BatchRefUpdate batchUpdate = repository.getRefDatabase().newBatchUpdate(); | |
| batchUpdate.setAtomic(false); | |
| ImmutableSet<String> refs = | |
| prefix == null | |
| ? refsToDelete | |
| : refsToDelete.stream() | |
| .map(ref -> ref.startsWith(R_REFS) ? ref : prefix + ref) | |
| .collect(toImmutableSet()); | |
| for (String ref : refs) { | |
| batchUpdate.addCommand(createDeleteCommand(projectState, repository, ref)); | |
| } | |
| try (RevWalk rw = new RevWalk(repository)) { | |
| batchUpdate.execute(rw, NullProgressMonitor.INSTANCE); | |
| } | |
| StringBuilder errorMessages = new StringBuilder(); | |
| for (ReceiveCommand command : batchUpdate.getCommands()) { | |
| if (command.getResult() == Result.OK) { | |
| postDeletion(projectState.getNameKey(), command); | |
| } else { | |
| appendAndLogErrorMessage(errorMessages, command); | |
| } | |
| } | |
| if (errorMessages.length() > 0) { | |
| throw new ResourceConflictException(errorMessages.toString()); | |
| } | |
| } | |
| } | |
| private ReceiveCommand createDeleteCommand( | |
| ProjectState projectState, Repository r, String refName) | |
| throws IOException, ResourceConflictException, PermissionBackendException { | |
| Ref ref = r.getRefDatabase().exactRef(refName); | |
| ReceiveCommand command; | |
| if (ref == null) { | |
| command = new ReceiveCommand(ObjectId.zeroId(), ObjectId.zeroId(), refName); | |
| command.setResult( | |
| Result.REJECTED_OTHER_REASON, | |
| "it doesn't exist or you do not have permission to delete it"); | |
| return command; | |
| } | |
| command = new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), ref.getName()); | |
| if (isConfigRef(refName)) { | |
| // Never allow to delete the meta config branch. | |
| command.setResult(Result.REJECTED_OTHER_REASON, "not allowed to delete branch " + refName); | |
| } else { | |
| try { | |
| permissionBackend | |
| .currentUser() | |
| .project(projectState.getNameKey()) | |
| .ref(refName) | |
| .check(RefPermission.DELETE); | |
| } catch (AuthException denied) { | |
| command.setResult( | |
| Result.REJECTED_OTHER_REASON, | |
| "it doesn't exist or you do not have permission to delete it"); | |
| } | |
| } | |
| if (!projectState.statePermitsWrite()) { | |
| command.setResult(Result.REJECTED_OTHER_REASON, "project state does not permit write"); | |
| } | |
| if (!refName.startsWith(R_TAGS)) { | |
| BranchNameKey branchKey = BranchNameKey.create(projectState.getNameKey(), ref.getName()); | |
| if (!queryProvider.get().setLimit(1).byBranchOpen(branchKey).isEmpty()) { | |
| command.setResult(Result.REJECTED_OTHER_REASON, "it has open changes"); | |
| } | |
| } | |
| RefUpdate u = r.updateRef(refName); | |
| u.setForceUpdate(true); | |
| u.setExpectedOldObjectId(r.exactRef(refName).getObjectId()); | |
| u.setNewObjectId(ObjectId.zeroId()); | |
| refDeletionValidator.validateRefOperation( | |
| projectState.getName(), | |
| identifiedUser.get(), | |
| u, | |
| /* pushOptions */ ImmutableListMultimap.of()); | |
| return command; | |
| } | |
| private void appendAndLogErrorMessage(StringBuilder errorMessages, ReceiveCommand cmd) { | |
| String msg; | |
| switch (cmd.getResult()) { | |
| case REJECTED_CURRENT_BRANCH: | |
| msg = format("Cannot delete %s: it is the current branch", cmd.getRefName()); | |
| break; | |
| case REJECTED_OTHER_REASON: | |
| msg = format("Cannot delete %s: %s", cmd.getRefName(), cmd.getMessage()); | |
| break; | |
| case LOCK_FAILURE: | |
| case NOT_ATTEMPTED: | |
| case OK: | |
| case REJECTED_MISSING_OBJECT: | |
| case REJECTED_NOCREATE: | |
| case REJECTED_NODELETE: | |
| case REJECTED_NONFASTFORWARD: | |
| default: | |
| msg = format("Cannot delete %s: %s", cmd.getRefName(), cmd.getResult()); | |
| break; | |
| } | |
| logger.atSevere().log("%s", msg); | |
| errorMessages.append(msg); | |
| errorMessages.append("\n"); | |
| } | |
| private void postDeletion(Project.NameKey project, ReceiveCommand cmd) { | |
| referenceUpdated.fire(project, cmd, identifiedUser.get().state()); | |
| } | |
| } |