// 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.project; import static java.lang.String.format; import static java.util.stream.Collectors.toList; import static org.eclipse.jgit.lib.Constants.R_TAGS; import static org.eclipse.jgit.transport.ReceiveCommand.Type.DELETE; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.reviewdb.client.Branch; 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.query.change.InternalChangeQuery; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; import java.io.IOException; import java.util.ArrayList; import java.util.List; import org.eclipse.jgit.errors.LockFailedException; 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; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class DeleteRef { private static final Logger log = LoggerFactory.getLogger(DeleteRef.class); private static final int MAX_LOCK_FAILURE_CALLS = 10; private static final long SLEEP_ON_LOCK_FAILURE_MS = 15; 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; private final ProjectResource resource; private final List<String> refsToDelete; private String prefix; public interface Factory { DeleteRef create(ProjectResource r); } @Inject DeleteRef( Provider<IdentifiedUser> identifiedUser, PermissionBackend permissionBackend, GitRepositoryManager repoManager, GitReferenceUpdated referenceUpdated, RefValidationHelper.Factory refDeletionValidatorFactory, Provider<InternalChangeQuery> queryProvider, @Assisted ProjectResource resource) { this.identifiedUser = identifiedUser; this.permissionBackend = permissionBackend; this.repoManager = repoManager; this.referenceUpdated = referenceUpdated; this.refDeletionValidator = refDeletionValidatorFactory.create(DELETE); this.queryProvider = queryProvider; this.resource = resource; this.refsToDelete = new ArrayList<>(); } public DeleteRef ref(String ref) { this.refsToDelete.add(ref); return this; } public DeleteRef refs(List<String> refs) { this.refsToDelete.addAll(refs); return this; } public DeleteRef prefix(String prefix) { this.prefix = prefix; return this; } public void delete() throws OrmException, IOException, ResourceConflictException, PermissionBackendException { if (!refsToDelete.isEmpty()) { try (Repository r = repoManager.openRepository(resource.getNameKey())) { if (refsToDelete.size() == 1) { deleteSingleRef(r); } else { deleteMultipleRefs(r); } } } } private void deleteSingleRef(Repository r) throws IOException, ResourceConflictException { String ref = refsToDelete.get(0); if (prefix != null && !ref.startsWith(prefix)) { ref = prefix + ref; } RefUpdate.Result result; RefUpdate u = r.updateRef(ref); u.setExpectedOldObjectId(r.exactRef(ref).getObjectId()); u.setNewObjectId(ObjectId.zeroId()); u.setForceUpdate(true); refDeletionValidator.validateRefOperation(resource.getName(), identifiedUser.get(), u); int remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS; for (; ; ) { try { result = u.delete(); } catch (LockFailedException e) { result = RefUpdate.Result.LOCK_FAILURE; } catch (IOException e) { log.error("Cannot delete " + ref, e); throw e; } if (result == RefUpdate.Result.LOCK_FAILURE && --remainingLockFailureCalls > 0) { try { Thread.sleep(SLEEP_ON_LOCK_FAILURE_MS); } catch (InterruptedException ie) { // ignore } } else { break; } } switch (result) { case NEW: case NO_CHANGE: case FAST_FORWARD: case FORCED: referenceUpdated.fire( resource.getNameKey(), u, ReceiveCommand.Type.DELETE, identifiedUser.get().getAccount()); break; case REJECTED_CURRENT_BRANCH: log.error("Cannot delete " + ref + ": " + result.name()); throw new ResourceConflictException("cannot delete current branch"); case IO_FAILURE: case LOCK_FAILURE: case NOT_ATTEMPTED: case REJECTED: case RENAMED: default: log.error("Cannot delete " + ref + ": " + result.name()); throw new ResourceConflictException("cannot delete: " + result.name()); } } private void deleteMultipleRefs(Repository r) throws OrmException, IOException, ResourceConflictException, PermissionBackendException { BatchRefUpdate batchUpdate = r.getRefDatabase().newBatchUpdate(); batchUpdate.setAtomic(false); List<String> refs = prefix == null ? refsToDelete : refsToDelete .stream() .map(ref -> ref.startsWith(prefix) ? ref : prefix + ref) .collect(toList()); for (String ref : refs) { batchUpdate.addCommand(createDeleteCommand(resource, r, ref)); } try (RevWalk rw = new RevWalk(r)) { batchUpdate.execute(rw, NullProgressMonitor.INSTANCE); } StringBuilder errorMessages = new StringBuilder(); for (ReceiveCommand command : batchUpdate.getCommands()) { if (command.getResult() == Result.OK) { postDeletion(resource, command); } else { appendAndLogErrorMessage(errorMessages, command); } } if (errorMessages.length() > 0) { throw new ResourceConflictException(errorMessages.toString()); } } private ReceiveCommand createDeleteCommand(ProjectResource project, Repository r, String refName) throws OrmException, IOException, ResourceConflictException, PermissionBackendException { Ref ref = r.getRefDatabase().getRef(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()); try { permissionBackend .user(identifiedUser) .project(project.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 (!refName.startsWith(R_TAGS)) { Branch.NameKey branchKey = new Branch.NameKey(project.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(project.getName(), identifiedUser.get(), u); return command; } private void appendAndLogErrorMessage(StringBuilder errorMessages, ReceiveCommand cmd) { String msg = null; 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; } log.error(msg); errorMessages.append(msg); errorMessages.append("\n"); } private void postDeletion(ProjectResource project, ReceiveCommand cmd) { referenceUpdated.fire(project.getNameKey(), cmd, identifiedUser.get().getAccount()); } }