diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java
index 58261b4d15..8d6d23c580 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java
@@ -47,6 +47,7 @@ import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
@@ -57,6 +58,7 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.net.URI;
+import java.util.Optional;
import static sonia.scm.AlreadyExistsException.alreadyExists;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
@@ -308,4 +310,50 @@ public class BranchRootResource {
return Response.status(Response.Status.BAD_REQUEST).build();
}
}
+
+ /**
+ * Deletes a branch.
+ *
+ * Note: This method requires "repository" privilege.
+ *
+ * @param branch the name of the branch to delete.
+ */
+ @DELETE
+ @Path("{branch}")
+ @Operation(summary = "Delete branch", description = "Deletes the given branch.", tags = "Repository")
+ @ApiResponse(responseCode = "204", description = "delete success or nothing to delete")
+ @ApiResponse(responseCode = "400", description = "the default branch cannot be deleted")
+ @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
+ @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to modify the repository")
+ @ApiResponse(
+ responseCode = "500",
+ description = "internal server error",
+ content = @Content(
+ mediaType = VndMediaType.ERROR_TYPE,
+ schema = @Schema(implementation = ErrorDto.class)
+ )
+ )
+ public Response delete(@PathParam("namespace") String namespace,
+ @PathParam("name") String name,
+ @PathParam("branch") String branch) {
+ try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
+ RepositoryPermissions.modify(repositoryService.getRepository()).check();
+
+ Optional branchToBeDeleted = repositoryService.getBranchesCommand().getBranches().getBranches().stream()
+ .filter(b -> b.getName().equalsIgnoreCase(branch))
+ .findFirst();
+
+ if (branchToBeDeleted.isPresent()) {
+ if (branchToBeDeleted.get().isDefaultBranch()) {
+ return Response.status(400).build();
+ } else {
+ repositoryService.getBranchCommand().delete(branch);
+ }
+ }
+ } catch (IOException e) {
+ return Response.serverError().build();
+ }
+ return Response.noContent().build();
+ }
+
}
diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java
index 4178b0664f..7022422cea 100644
--- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java
@@ -24,8 +24,8 @@
package sonia.scm.api.v2.resources;
-import com.google.inject.util.Providers;
import lombok.extern.slf4j.Slf4j;
+import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.support.SubjectThreadState;
import org.apache.shiro.util.ThreadContext;
@@ -56,11 +56,12 @@ import sonia.scm.web.RestDispatcher;
import sonia.scm.web.VndMediaType;
import javax.ws.rs.core.MediaType;
+import java.io.IOException;
import java.net.URI;
+import java.net.URISyntaxException;
import java.time.Instant;
import java.util.Date;
import java.util.List;
-import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
@@ -68,6 +69,7 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -271,6 +273,62 @@ public class BranchRootResourceTest extends RepositoryTestBase {
verify(branchCommandBuilder, never()).branch(anyString());
}
+ @Test
+ public void shouldNotDeleteBranchIfNotPermitted() throws IOException, URISyntaxException {
+ doThrow(AuthorizationException.class).when(subject).checkPermission("repository:modify:repoId");
+ when(branchesCommandBuilder.getBranches()).thenReturn(new Branches(Branch.normalBranch("suspicious", "0")));
+
+ MockHttpRequest request = MockHttpRequest
+ .delete("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/branches/suspicious");
+ MockHttpResponse response = new MockHttpResponse();
+
+ dispatcher.invoke(request, response);
+
+ assertEquals(403, response.getStatus());
+ verify(branchCommandBuilder, never()).delete("suspicious");
+ }
+
+ @Test
+ public void shouldNotDeleteDefaultBranch() throws IOException, URISyntaxException {
+ when(branchesCommandBuilder.getBranches()).thenReturn(new Branches(Branch.defaultBranch("main", "0")));
+
+ MockHttpRequest request = MockHttpRequest
+ .delete("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/branches/main");
+ MockHttpResponse response = new MockHttpResponse();
+
+ dispatcher.invoke(request, response);
+
+ assertEquals(400, response.getStatus());
+ }
+
+ @Test
+ public void shouldDeleteBranch() throws IOException, URISyntaxException {
+ when(branchesCommandBuilder.getBranches()).thenReturn(new Branches(Branch.normalBranch("suspicious", "0")));
+
+ MockHttpRequest request = MockHttpRequest
+ .delete("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/branches/suspicious");
+ MockHttpResponse response = new MockHttpResponse();
+
+ dispatcher.invoke(request, response);
+
+ assertEquals(204, response.getStatus());
+ verify(branchCommandBuilder).delete("suspicious");
+ }
+
+ @Test
+ public void shouldAnswer204IfNothingWasDeleted() throws IOException, URISyntaxException {
+ when(branchesCommandBuilder.getBranches()).thenReturn(new Branches());
+
+ MockHttpRequest request = MockHttpRequest
+ .delete("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/branches/suspicious");
+ MockHttpResponse response = new MockHttpResponse();
+
+ dispatcher.invoke(request, response);
+
+ assertEquals(204, response.getStatus());
+ verify(branchCommandBuilder, never()).delete(anyString());
+ }
+
private Branch createBranch(String existing_branch) {
return Branch.normalBranch(existing_branch, REVISION);
}