blob: 9f05b97bf8e04288d8f7eba9eb934dacd197b189 [file] [log] [blame]
// 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_REFS;
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, AuthException,
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, AuthException, PermissionBackendException {
String ref = refsToDelete.get(0);
if (prefix != null && !ref.startsWith(R_REFS)) {
ref = prefix + ref;
}
permissionBackend
.user(identifiedUser)
.project(resource.getNameKey())
.ref(ref)
.check(RefPermission.DELETE);
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:
case REJECTED_MISSING_OBJECT:
case REJECTED_OTHER_REASON:
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(R_REFS) ? 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());
}
}