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); }