From 2fab7717403bc3d204459100e565c381bf6ed58a Mon Sep 17 00:00:00 2001
From: Eduard Heimbuch
Date: Tue, 10 Nov 2020 15:35:32 +0100
Subject: [PATCH 1/8] create endpoint for branch deletion
---
.../api/v2/resources/BranchRootResource.java | 48 ++++++++++++++
.../v2/resources/BranchRootResourceTest.java | 62 ++++++++++++++++++-
2 files changed, 108 insertions(+), 2 deletions(-)
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);
}
From bb82c18e2ba9ff344497f8279566b700ac2e8bca Mon Sep 17 00:00:00 2001
From: Eduard Heimbuch
Date: Wed, 11 Nov 2020 14:09:15 +0100
Subject: [PATCH 2/8] add delete link to branchDto
---
.../repos/branches/components/BranchRow.tsx | 28 +++++++--
.../repos/branches/components/BranchTable.tsx | 44 +++++++-------
.../branches/containers/BranchesOverview.tsx | 7 ++-
.../BranchChangesetCollectionToDtoMapper.java | 2 +-
.../BranchCollectionToDtoMapper.java | 6 +-
.../api/v2/resources/BranchRootResource.java | 4 +-
.../v2/resources/BranchToBranchDtoMapper.java | 15 +++--
.../ChangesetCollectionToDtoMapperBase.java | 2 +-
.../resources/DefaultBranchLinkProvider.java | 2 +-
.../DefaultChangesetToChangesetDtoMapper.java | 2 +-
.../scm/api/v2/resources/ResourceLinks.java | 13 +++--
.../BranchToBranchDtoMapperTest.java | 57 ++++++++++++++++++-
.../api/v2/resources/ResourceLinksTest.java | 4 +-
13 files changed, 135 insertions(+), 51 deletions(-)
diff --git a/scm-ui/ui-webapp/src/repos/branches/components/BranchRow.tsx b/scm-ui/ui-webapp/src/repos/branches/components/BranchRow.tsx
index baddb90afd..6237bbe077 100644
--- a/scm-ui/ui-webapp/src/repos/branches/components/BranchRow.tsx
+++ b/scm-ui/ui-webapp/src/repos/branches/components/BranchRow.tsx
@@ -22,25 +22,43 @@
* SOFTWARE.
*/
import React, { FC } from "react";
-import { Link } from "react-router-dom";
-import { Branch } from "@scm-manager/ui-types";
+import { Link as ReactLink } from "react-router-dom";
+import { Branch, Link } from "@scm-manager/ui-types";
import DefaultBranchTag from "./DefaultBranchTag";
+import { Icon } from "@scm-manager/ui-components";
+import { useTranslation } from "react-i18next";
type Props = {
baseUrl: string;
branch: Branch;
+ onDelete: (url: string) => void;
};
-const BranchRow: FC = ({ baseUrl, branch }) => {
+const BranchRow: FC = ({ baseUrl, branch, onDelete }) => {
const to = `${baseUrl}/${encodeURIComponent(branch.name)}/info`;
+ const [t] = useTranslation("repos");
+
+ let deleteButton;
+ if ((branch?._links?.delete as Link)?.href) {
+ const url = (branch._links.delete as Link).href;
+ deleteButton = (
+ onDelete(url)}>
+
+
+
+
+ );
+ }
+
return (
-
+
{branch.name}
-
+
+ {deleteButton}
);
};
diff --git a/scm-ui/ui-webapp/src/repos/branches/components/BranchTable.tsx b/scm-ui/ui-webapp/src/repos/branches/components/BranchTable.tsx
index db23ca7ad0..05589cffdd 100644
--- a/scm-ui/ui-webapp/src/repos/branches/components/BranchTable.tsx
+++ b/scm-ui/ui-webapp/src/repos/branches/components/BranchTable.tsx
@@ -21,41 +21,39 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-import React from "react";
-import { WithTranslation, withTranslation } from "react-i18next";
+import React, { FC } from "react";
+import { useTranslation } from "react-i18next";
import BranchRow from "./BranchRow";
import { Branch } from "@scm-manager/ui-types";
-type Props = WithTranslation & {
+type Props = {
baseUrl: string;
branches: Branch[];
+ onDelete: (url: string) => void;
};
-class BranchTable extends React.Component {
- render() {
- const { t } = this.props;
- return (
-
-
-
- {t("branches.table.branches")}
-
-
- {this.renderRow()}
-
- );
- }
+const BranchTable: FC = ({ baseUrl, branches, onDelete }) => {
+ const [t] = useTranslation("repos");
- renderRow() {
- const { baseUrl, branches } = this.props;
+ const renderRow = () => {
let rowContent = null;
if (branches) {
rowContent = branches.map((branch, index) => {
- return ;
+ return ;
});
}
return rowContent;
- }
-}
+ };
+ return (
+
+
+
+ {t("branches.table.branches")}
+
+
+ {renderRow()}
+
+ );
+};
-export default withTranslation("repos")(BranchTable);
+export default BranchTable;
diff --git a/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx b/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx
index 0e2a01622b..e18593a701 100644
--- a/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx
+++ b/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx
@@ -37,6 +37,7 @@ import {
} from "../modules/branches";
import { orderBranches } from "../util/orderBranches";
import BranchTable from "../components/BranchTable";
+import { apiClient } from "@scm-manager/ui-components/src";
type Props = WithTranslation & {
repository: Repository;
@@ -80,11 +81,15 @@ class BranchesOverview extends React.Component {
);
}
+ onDelete(url: string) {
+ apiClient.delete(url).catch(error => this.setState({ error }));
+ }
+
renderBranchesTable() {
const { baseUrl, branches, t } = this.props;
if (branches && branches.length > 0) {
orderBranches(branches);
- return ;
+ return ;
}
return {t("branches.overview.noBranches")} ;
}
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchChangesetCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchChangesetCollectionToDtoMapper.java
index 6cb1b1049b..96de0ce2dc 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchChangesetCollectionToDtoMapper.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchChangesetCollectionToDtoMapper.java
@@ -45,6 +45,6 @@ public class BranchChangesetCollectionToDtoMapper extends ChangesetCollectionToD
}
private String createSelfLink(Repository repository, String branch) {
- return resourceLinks.branch().history(repository.getNamespaceAndName(), branch);
+ return resourceLinks.branch().history(repository.getNamespace(), repository.getName(), branch);
}
}
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionToDtoMapper.java
index f640331381..f102d27c4f 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionToDtoMapper.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionToDtoMapper.java
@@ -55,11 +55,11 @@ public class BranchCollectionToDtoMapper {
public HalRepresentation map(Repository repository, Collection branches) {
return new HalRepresentation(
createLinks(repository),
- embedDtos(getBranchDtoList(repository.getNamespace(), repository.getName(), branches)));
+ embedDtos(getBranchDtoList(repository, branches)));
}
- public List getBranchDtoList(String namespace, String name, Collection branches) {
- return branches.stream().map(branch -> branchToDtoMapper.map(branch, new NamespaceAndName(namespace, name))).collect(toList());
+ public List getBranchDtoList(Repository repository, Collection branches) {
+ return branches.stream().map(branch -> branchToDtoMapper.map(branch, repository)).collect(toList());
}
private Links createLinks(Repository repository) {
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 8d6d23c580..a5ce4d951e 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
@@ -134,7 +134,7 @@ public class BranchRootResource {
.stream()
.filter(branch -> branchName.equals(branch.getName()))
.findFirst()
- .map(branch -> branchToDtoMapper.map(branch, namespaceAndName))
+ .map(branch -> branchToDtoMapper.map(branch, repositoryService.getRepository()))
.map(Response::ok)
.orElseThrow(() -> notFound(entity("branch", branchName).in(namespaceAndName)))
.build();
@@ -249,7 +249,7 @@ public class BranchRootResource {
branchCommand.from(parentName);
}
Branch newBranch = branchCommand.branch(branchName);
- return Response.created(URI.create(resourceLinks.branch().self(namespaceAndName, newBranch.getName()))).build();
+ return Response.created(URI.create(resourceLinks.branch().self(namespace, name, newBranch.getName()))).build();
}
}
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java
index f8088a79d5..f82c434b3d 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java
@@ -32,6 +32,8 @@ import org.mapstruct.Mapping;
import org.mapstruct.ObjectFactory;
import sonia.scm.repository.Branch;
import sonia.scm.repository.NamespaceAndName;
+import sonia.scm.repository.Repository;
+import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.web.EdisonHalAppender;
import javax.inject.Inject;
@@ -46,16 +48,21 @@ public abstract class BranchToBranchDtoMapper extends HalAppenderMapper {
private ResourceLinks resourceLinks;
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
- public abstract BranchDto map(Branch branch, @Context NamespaceAndName namespaceAndName);
+ public abstract BranchDto map(Branch branch, @Context Repository repository);
@ObjectFactory
- BranchDto createDto(@Context NamespaceAndName namespaceAndName, Branch branch) {
+ BranchDto createDto(@Context Repository repository, Branch branch) {
+ NamespaceAndName namespaceAndName = new NamespaceAndName(repository.getNamespace(), repository.getName());
Links.Builder linksBuilder = linkingTo()
- .self(resourceLinks.branch().self(namespaceAndName, branch.getName()))
- .single(linkBuilder("history", resourceLinks.branch().history(namespaceAndName, branch.getName())).build())
+ .self(resourceLinks.branch().self(repository.getNamespace(), repository.getName(), branch.getName()))
+ .single(linkBuilder("history", resourceLinks.branch().history(repository.getNamespace(), repository.getName(), branch.getName())).build())
.single(linkBuilder("changeset", resourceLinks.changeset().changeset(namespaceAndName.getNamespace(), namespaceAndName.getName(), branch.getRevision())).build())
.single(linkBuilder("source", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), branch.getRevision())).build());
+ if (!branch.isDefaultBranch() && RepositoryPermissions.modify(repository).isPermitted()) {
+ linksBuilder.single(linkBuilder("delete", resourceLinks.branch().delete(repository.getNamespace(), repository.getName(), branch.getName())).build());
+ }
+
Embedded.Builder embeddedBuilder = Embedded.embeddedBuilder();
applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), branch, namespaceAndName);
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperBase.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperBase.java
index 0556b7a636..0e582309ad 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperBase.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperBase.java
@@ -56,7 +56,7 @@ class ChangesetCollectionToDtoMapperBase extends PagedCollectionToDtoMapper Branch.normalBranch(branchName, source.getId()))));
}
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java
index c826ee8e5e..bbb02a64fc 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java
@@ -25,6 +25,7 @@
package sonia.scm.api.v2.resources;
import sonia.scm.repository.NamespaceAndName;
+import sonia.scm.repository.Repository;
import sonia.scm.security.gpg.UserPublicKeyResource;
import javax.inject.Inject;
@@ -485,17 +486,21 @@ class ResourceLinks {
branchLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, BranchRootResource.class);
}
- String self(NamespaceAndName namespaceAndName, String branch) {
- return branchLinkBuilder.method("getRepositoryResource").parameters(namespaceAndName.getNamespace(), namespaceAndName.getName()).method("branches").parameters().method("get").parameters(branch).href();
+ String self(String namespace, String name, String branch) {
+ return branchLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("branches").parameters().method("get").parameters(branch).href();
}
- public String history(NamespaceAndName namespaceAndName, String branch) {
- return branchLinkBuilder.method("getRepositoryResource").parameters(namespaceAndName.getNamespace(), namespaceAndName.getName()).method("branches").parameters().method("history").parameters(branch).href();
+ public String history(String namespace, String name, String branch) {
+ return branchLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("branches").parameters().method("history").parameters(branch).href();
}
public String create(String namespace, String name) {
return branchLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("branches").parameters().method("create").parameters().href();
}
+
+ public String delete(String namespace, String name, String branch) {
+ return branchLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("branches").parameters().method("delete").parameters(branch).href();
+ }
}
public IncomingLinks incoming() {
diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java
index f1b44bee08..e46dc6eea3 100644
--- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java
@@ -24,28 +24,49 @@
package sonia.scm.api.v2.resources;
+import org.apache.shiro.subject.Subject;
+import org.apache.shiro.util.ThreadContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
+import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.Branch;
import sonia.scm.repository.NamespaceAndName;
+import sonia.scm.repository.Repository;
+import sonia.scm.repository.RepositoryTestData;
import java.net.URI;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class BranchToBranchDtoMapperTest {
- private final URI baseUri = URI.create("https://hitchhiker.com");
+ private final URI baseUri = URI.create("https://hitchhiker.com/api/");
@SuppressWarnings("unused") // Is injected
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
+ @Mock
+ private Subject subject;
+
@InjectMocks
private BranchToBranchDtoMapperImpl mapper;
+ @BeforeEach
+ void setupSubject() {
+ ThreadContext.bind(subject);
+ }
+
+ @AfterEach
+ void tearDown() {
+ ThreadContext.unbindSubject();
+ }
+
@Test
void shouldAppendLinks() {
HalEnricherRegistry registry = new HalEnricherRegistry();
@@ -59,7 +80,37 @@ class BranchToBranchDtoMapperTest {
Branch branch = Branch.normalBranch("master", "42");
- BranchDto dto = mapper.map(branch, new NamespaceAndName("hitchhiker", "heart-of-gold"));
- assertThat(dto.getLinks().getLinkBy("ka").get().getHref()).isEqualTo("http://hitchhiker/heart-of-gold/master");
+ BranchDto dto = mapper.map(branch, RepositoryTestData.createHeartOfGold());
+ assertThat(dto.getLinks().getLinkBy("ka").get().getHref()).isEqualTo("http://hitchhiker/HeartOfGold/master");
}
+
+ @Test
+ void shouldAppendDeleteLink() {
+ Repository repository = RepositoryTestData.createHeartOfGold();
+ when(subject.isPermitted("repository:modify:" + repository.getId())).thenReturn(true);
+ Branch branch = Branch.normalBranch("master", "42");
+
+ BranchDto dto = mapper.map(branch, repository);
+ assertThat(dto.getLinks().getLinkBy("delete").get().getHref()).isEqualTo("https://hitchhiker.com/api/v2/repositories/hitchhiker/HeartOfGold/branches/master");
+ }
+
+ @Test
+ void shouldNotAppendDeleteLinkIfDefaultBranch() {
+ Repository repository = RepositoryTestData.createHeartOfGold();
+ Branch branch = Branch.defaultBranch("master", "42");
+
+ BranchDto dto = mapper.map(branch, repository);
+ assertThat(dto.getLinks().getLinkBy("delete")).isNotPresent();
+ }
+
+ @Test
+ void shouldNotAppendDeleteLinkIfNotPermitted() {
+ Repository repository = RepositoryTestData.createHeartOfGold();
+ when(subject.isPermitted("repository:modify:" + repository.getId())).thenReturn(false);
+ Branch branch = Branch.normalBranch("master", "42");
+
+ BranchDto dto = mapper.map(branch, repository);
+ assertThat(dto.getLinks().getLinkBy("delete")).isNotPresent();
+ }
+
}
diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java
index a0b1c9b7cb..9889d4df33 100644
--- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java
@@ -140,13 +140,13 @@ public class ResourceLinksTest {
@Test
public void shouldCreateCorrectBranchUrl() {
- String url = resourceLinks.branch().self(new NamespaceAndName("space", "name"), "master");
+ String url = resourceLinks.branch().self("space", "name", "master");
assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/name/branches/master", url);
}
@Test
public void shouldCreateCorrectBranchHiostoryUrl() {
- String url = resourceLinks.branch().history(new NamespaceAndName("space", "name"), "master");
+ String url = resourceLinks.branch().history("space", "name", "master");
assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/name/branches/master/changesets/", url);
}
From 20d2f6b5d5fc839b7a5854e8cc5a4d19e07514c1 Mon Sep 17 00:00:00 2001
From: Eduard Heimbuch
Date: Wed, 11 Nov 2020 15:58:58 +0100
Subject: [PATCH 3/8] add 'delete branch' function to frontend
---
.../ui-components/src/modals/ConfirmAlert.tsx | 6 +-
scm-ui/ui-webapp/public/locales/de/repos.json | 14 ++-
scm-ui/ui-webapp/public/locales/en/repos.json | 14 ++-
.../repos/branches/components/BranchRow.tsx | 2 +-
.../repos/branches/components/BranchTable.tsx | 67 ++++++++++---
.../repos/branches/components/BranchView.tsx | 3 +-
.../branches/containers/BranchDangerZone.tsx | 59 ++++++++++++
.../repos/branches/containers/BranchRoot.tsx | 3 +-
.../branches/containers/BranchesOverview.tsx | 9 +-
.../branches/containers/DeleteBranch.tsx | 94 +++++++++++++++++++
.../src/repos/containers/EditRepo.tsx | 4 +-
...angerZone.tsx => RepositoryDangerZone.tsx} | 6 +-
12 files changed, 249 insertions(+), 32 deletions(-)
create mode 100644 scm-ui/ui-webapp/src/repos/branches/containers/BranchDangerZone.tsx
create mode 100644 scm-ui/ui-webapp/src/repos/branches/containers/DeleteBranch.tsx
rename scm-ui/ui-webapp/src/repos/containers/{DangerZone.tsx => RepositoryDangerZone.tsx} (93%)
diff --git a/scm-ui/ui-components/src/modals/ConfirmAlert.tsx b/scm-ui/ui-components/src/modals/ConfirmAlert.tsx
index 89f8f0a117..290f024b63 100644
--- a/scm-ui/ui-components/src/modals/ConfirmAlert.tsx
+++ b/scm-ui/ui-components/src/modals/ConfirmAlert.tsx
@@ -62,11 +62,11 @@ export const ConfirmAlert: FC = ({ title, message, buttons, close }) => {
const footer = (
- {buttons.map((button, i) => (
-
+ {buttons.map((button, index) => (
+
handleClickButton(button)}
>
{button.label}
diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json
index d3ec72b23f..eceb488c3f 100644
--- a/scm-ui/ui-webapp/public/locales/de/repos.json
+++ b/scm-ui/ui-webapp/public/locales/de/repos.json
@@ -68,7 +68,19 @@
"name": "Name:",
"commits": "Commits",
"sources": "Sources",
- "defaultTag": "Default"
+ "defaultTag": "Default",
+ "dangerZone": "Branch löschen",
+ "delete": {
+ "button": "Branch löschen",
+ "subtitle": "Branch löschen",
+ "description": "Gelöschte Branches können nicht wiederhergestellt werden.",
+ "confirmAlert": {
+ "title": "Branch löschen",
+ "message": "Möchten Sie diesen Branch wirklich löschen?",
+ "cancel": "Nein",
+ "submit": "Ja"
+ }
+ }
},
"tags": {
"overview": {
diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json
index 5f83ad7ba1..6845bf8b42 100644
--- a/scm-ui/ui-webapp/public/locales/en/repos.json
+++ b/scm-ui/ui-webapp/public/locales/en/repos.json
@@ -68,7 +68,19 @@
"name": "Name:",
"commits": "Commits",
"sources": "Sources",
- "defaultTag": "Default"
+ "defaultTag": "Default",
+ "dangerZone": "Delete branch",
+ "delete": {
+ "button": "Delete branch",
+ "subtitle": "Delete branch",
+ "description": "Deleted branches can not be restored.",
+ "confirmAlert": {
+ "title": "Delete branch",
+ "message": "Do you really want to delete the branch?",
+ "cancel": "No",
+ "submit": "Yes"
+ }
+ }
},
"tags": {
"overview": {
diff --git a/scm-ui/ui-webapp/src/repos/branches/components/BranchRow.tsx b/scm-ui/ui-webapp/src/repos/branches/components/BranchRow.tsx
index 6237bbe077..1c2df84b00 100644
--- a/scm-ui/ui-webapp/src/repos/branches/components/BranchRow.tsx
+++ b/scm-ui/ui-webapp/src/repos/branches/components/BranchRow.tsx
@@ -44,7 +44,7 @@ const BranchRow: FC = ({ baseUrl, branch, onDelete }) => {
deleteButton = (
onDelete(url)}>
-
+
);
diff --git a/scm-ui/ui-webapp/src/repos/branches/components/BranchTable.tsx b/scm-ui/ui-webapp/src/repos/branches/components/BranchTable.tsx
index 05589cffdd..ca45a8ee1c 100644
--- a/scm-ui/ui-webapp/src/repos/branches/components/BranchTable.tsx
+++ b/scm-ui/ui-webapp/src/repos/branches/components/BranchTable.tsx
@@ -21,19 +21,40 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-import React, { FC } from "react";
+import React, { FC, useState } from "react";
import { useTranslation } from "react-i18next";
import BranchRow from "./BranchRow";
import { Branch } from "@scm-manager/ui-types";
+import { apiClient, ConfirmAlert, ErrorNotification } from "@scm-manager/ui-components";
type Props = {
baseUrl: string;
branches: Branch[];
- onDelete: (url: string) => void;
+ fetchBranches: () => void;
};
-const BranchTable: FC = ({ baseUrl, branches, onDelete }) => {
+const BranchTable: FC = ({ baseUrl, branches, fetchBranches }) => {
const [t] = useTranslation("repos");
+ const [showConfirmAlert, setShowConfirmAlert] = useState(false);
+ const [error, setError] = useState();
+ const [deleteBranchUrl, setDeleteBranchUrl] = useState("");
+
+ const onDelete = (url: string) => {
+ setDeleteBranchUrl(url);
+ setShowConfirmAlert(true);
+ };
+
+ const abortDelete = () => {
+ setDeleteBranchUrl("");
+ setShowConfirmAlert(false);
+ };
+
+ const deleteBranch = () => {
+ apiClient
+ .delete(deleteBranchUrl)
+ .then(() => fetchBranches())
+ .catch(setError);
+ };
const renderRow = () => {
let rowContent = null;
@@ -44,15 +65,39 @@ const BranchTable: FC = ({ baseUrl, branches, onDelete }) => {
}
return rowContent;
};
+
+ const confirmAlert = (
+ deleteBranch()
+ },
+ {
+ label: t("branch.delete.confirmAlert.cancel"),
+ onClick: () => abortDelete()
+ }
+ ]}
+ close={() => abortDelete()}
+ />
+ );
+
return (
-
-
-
- {t("branches.table.branches")}
-
-
- {renderRow()}
-
+ <>
+ {showConfirmAlert && confirmAlert}
+ {error && }
+
+
+
+ {t("branches.table.branches")}
+
+
+ {renderRow()}
+
+ >
);
};
diff --git a/scm-ui/ui-webapp/src/repos/branches/components/BranchView.tsx b/scm-ui/ui-webapp/src/repos/branches/components/BranchView.tsx
index 9a88c08c2d..32ff0d8f7a 100644
--- a/scm-ui/ui-webapp/src/repos/branches/components/BranchView.tsx
+++ b/scm-ui/ui-webapp/src/repos/branches/components/BranchView.tsx
@@ -25,6 +25,7 @@ import React from "react";
import BranchDetail from "./BranchDetail";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import { Branch, Repository } from "@scm-manager/ui-types";
+import BranchDangerZone from "../containers/BranchDangerZone";
type Props = {
repository: Repository;
@@ -34,7 +35,6 @@ type Props = {
class BranchView extends React.Component {
render() {
const { repository, branch } = this.props;
-
return (
@@ -49,6 +49,7 @@ class BranchView extends React.Component
{
}}
/>
+
);
}
diff --git a/scm-ui/ui-webapp/src/repos/branches/containers/BranchDangerZone.tsx b/scm-ui/ui-webapp/src/repos/branches/containers/BranchDangerZone.tsx
new file mode 100644
index 0000000000..9e6c0e167a
--- /dev/null
+++ b/scm-ui/ui-webapp/src/repos/branches/containers/BranchDangerZone.tsx
@@ -0,0 +1,59 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React, { FC } from "react";
+import { Branch, Repository } from "@scm-manager/ui-types";
+import { Subtitle } from "@scm-manager/ui-components";
+import { useTranslation } from "react-i18next";
+import { DangerZoneContainer } from "../../containers/RepositoryDangerZone";
+import DeleteBranch from "./DeleteBranch";
+
+type Props = {
+ repository: Repository;
+ branch: Branch;
+};
+
+const BranchDangerZone: FC = ({ repository, branch }) => {
+ const [t] = useTranslation("repos");
+
+ const dangerZone = [];
+
+ if (branch?._links?.delete) {
+ dangerZone.push( );
+ }
+
+ if (dangerZone.length === 0) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+ {dangerZone}
+ >
+ );
+};
+
+export default BranchDangerZone;
diff --git a/scm-ui/ui-webapp/src/repos/branches/containers/BranchRoot.tsx b/scm-ui/ui-webapp/src/repos/branches/containers/BranchRoot.tsx
index cb2847a712..9a9aa02d24 100644
--- a/scm-ui/ui-webapp/src/repos/branches/containers/BranchRoot.tsx
+++ b/scm-ui/ui-webapp/src/repos/branches/containers/BranchRoot.tsx
@@ -28,10 +28,9 @@ import { compose } from "redux";
import { Redirect, Route, Switch, withRouter } from "react-router-dom";
import { Branch, Repository } from "@scm-manager/ui-types";
import { fetchBranch, getBranch, getFetchBranchFailure, isFetchBranchPending } from "../modules/branches";
-import { ErrorNotification, Loading, NotFoundError } from "@scm-manager/ui-components";
+import { ErrorNotification, Loading, NotFoundError, urls } from "@scm-manager/ui-components";
import { History } from "history";
import queryString from "query-string";
-import { urls } from "@scm-manager/ui-components";
type Props = {
repository: Repository;
diff --git a/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx b/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx
index e18593a701..8bbcac53bf 100644
--- a/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx
+++ b/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx
@@ -37,7 +37,6 @@ import {
} from "../modules/branches";
import { orderBranches } from "../util/orderBranches";
import BranchTable from "../components/BranchTable";
-import { apiClient } from "@scm-manager/ui-components/src";
type Props = WithTranslation & {
repository: Repository;
@@ -81,15 +80,11 @@ class BranchesOverview extends React.Component {
);
}
- onDelete(url: string) {
- apiClient.delete(url).catch(error => this.setState({ error }));
- }
-
renderBranchesTable() {
- const { baseUrl, branches, t } = this.props;
+ const { baseUrl, branches, repository, fetchBranches, t } = this.props;
if (branches && branches.length > 0) {
orderBranches(branches);
- return ;
+ return fetchBranches(repository)} />;
}
return {t("branches.overview.noBranches")} ;
}
diff --git a/scm-ui/ui-webapp/src/repos/branches/containers/DeleteBranch.tsx b/scm-ui/ui-webapp/src/repos/branches/containers/DeleteBranch.tsx
new file mode 100644
index 0000000000..2ff9ff9284
--- /dev/null
+++ b/scm-ui/ui-webapp/src/repos/branches/containers/DeleteBranch.tsx
@@ -0,0 +1,94 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+import React, { FC, useState } from "react";
+import { useHistory } from "react-router-dom";
+import { useTranslation } from "react-i18next";
+import { Branch, Link, Repository } from "@scm-manager/ui-types";
+import { apiClient, ConfirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components";
+
+type Props = {
+ repository: Repository;
+ branch: Branch;
+};
+
+const DeleteBranch: FC = ({ repository, branch }: Props) => {
+ const [showConfirmAlert, setShowConfirmAlert] = useState(false);
+ const [error, setError] = useState();
+ const [t] = useTranslation("repos");
+ const history = useHistory();
+
+ console.log("branchview", repository, branch);
+
+ const deleteBranch = () => {
+ apiClient
+ .delete((branch._links.delete as Link).href)
+ .then(() => history.push(`/repo/${repository.namespace}/${repository.name}/branches/`))
+ .catch(setError);
+ };
+
+ if (!branch._links.delete) {
+ return null;
+ }
+
+ let confirmAlert = null;
+ if (showConfirmAlert) {
+ confirmAlert = (
+ deleteBranch()
+ },
+ {
+ label: t("branch.delete.confirmAlert.cancel"),
+ onClick: () => null
+ }
+ ]}
+ close={() => setShowConfirmAlert(false)}
+ />
+ );
+ }
+
+ return (
+ <>
+
+ {showConfirmAlert && confirmAlert}
+
+ {t("branch.delete.subtitle")}
+
+ {t("branch.delete.description")}
+
+ }
+ right={ setShowConfirmAlert(true)} />}
+ />
+ >
+ );
+};
+
+export default DeleteBranch;
diff --git a/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx b/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx
index f2334c5d7e..2f91c8421b 100644
--- a/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx
+++ b/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx
@@ -31,7 +31,7 @@ import { History } from "history";
import { ErrorNotification } from "@scm-manager/ui-components";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import { compose } from "redux";
-import DangerZone from "./DangerZone";
+import RepositoryDangerZone from "./RepositoryDangerZone";
import { getLinks } from "../../modules/indexResource";
import { urls } from "@scm-manager/ui-components";
@@ -80,7 +80,7 @@ class EditRepo extends React.Component {
}}
/>
-
+
>
);
}
diff --git a/scm-ui/ui-webapp/src/repos/containers/DangerZone.tsx b/scm-ui/ui-webapp/src/repos/containers/RepositoryDangerZone.tsx
similarity index 93%
rename from scm-ui/ui-webapp/src/repos/containers/DangerZone.tsx
rename to scm-ui/ui-webapp/src/repos/containers/RepositoryDangerZone.tsx
index 0b2bda04b9..cb883a904f 100644
--- a/scm-ui/ui-webapp/src/repos/containers/DangerZone.tsx
+++ b/scm-ui/ui-webapp/src/repos/containers/RepositoryDangerZone.tsx
@@ -35,7 +35,7 @@ type Props = {
indexLinks: Links;
};
-const DangerZoneContainer = styled.div`
+export const DangerZoneContainer = styled.div`
padding: 1.5rem 1rem;
border: 1px solid #ff6a88;
border-radius: 5px;
@@ -56,7 +56,7 @@ const DangerZoneContainer = styled.div`
}
`;
-const DangerZone: FC = ({ repository, indexLinks }) => {
+const RepositoryDangerZone: FC = ({ repository, indexLinks }) => {
const [t] = useTranslation("repos");
const dangerZone = [];
@@ -81,4 +81,4 @@ const DangerZone: FC = ({ repository, indexLinks }) => {
);
};
-export default DangerZone;
+export default RepositoryDangerZone;
From 82c43a2012d9df7337ffe7e31ba0ed8498699c79 Mon Sep 17 00:00:00 2001
From: Eduard Heimbuch
Date: Wed, 11 Nov 2020 16:07:27 +0100
Subject: [PATCH 4/8] update CHANGELOG.md
---
CHANGELOG.md | 3 +++
1 file changed, 3 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 695fa92aa3..14c102dbbc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
+### Added
+- Delete branches directly in the UI ([#1422](https://github.com/scm-manager/scm-manager/pull/1422))
+
### Fixed
- Error on repository initialization with least-privilege user ([#1414](https://github.com/scm-manager/scm-manager/pull/1414))
From 77788bab01a75f6db3e7403a82a421d0e47ab6c0 Mon Sep 17 00:00:00 2001
From: Eduard Heimbuch
Date: Wed, 11 Nov 2020 16:42:09 +0100
Subject: [PATCH 5/8] cleanup
---
.../branches/containers/DeleteBranch.tsx | 41 ++++++++-----------
.../BranchCollectionToDtoMapper.java | 1 -
.../scm/api/v2/resources/ResourceLinks.java | 16 ++++----
3 files changed, 26 insertions(+), 32 deletions(-)
diff --git a/scm-ui/ui-webapp/src/repos/branches/containers/DeleteBranch.tsx b/scm-ui/ui-webapp/src/repos/branches/containers/DeleteBranch.tsx
index 2ff9ff9284..681cae1e0a 100644
--- a/scm-ui/ui-webapp/src/repos/branches/containers/DeleteBranch.tsx
+++ b/scm-ui/ui-webapp/src/repos/branches/containers/DeleteBranch.tsx
@@ -38,8 +38,6 @@ const DeleteBranch: FC = ({ repository, branch }: Props) => {
const [t] = useTranslation("repos");
const history = useHistory();
- console.log("branchview", repository, branch);
-
const deleteBranch = () => {
apiClient
.delete((branch._links.delete as Link).href)
@@ -51,27 +49,24 @@ const DeleteBranch: FC = ({ repository, branch }: Props) => {
return null;
}
- let confirmAlert = null;
- if (showConfirmAlert) {
- confirmAlert = (
- deleteBranch()
- },
- {
- label: t("branch.delete.confirmAlert.cancel"),
- onClick: () => null
- }
- ]}
- close={() => setShowConfirmAlert(false)}
- />
- );
- }
+ const confirmAlert = (
+ deleteBranch()
+ },
+ {
+ label: t("branch.delete.confirmAlert.cancel"),
+ onClick: () => null
+ }
+ ]}
+ close={() => setShowConfirmAlert(false)}
+ />
+ );
return (
<>
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionToDtoMapper.java
index f102d27c4f..bb0e93f27f 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionToDtoMapper.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionToDtoMapper.java
@@ -30,7 +30,6 @@ import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Link;
import de.otto.edison.hal.Links;
import sonia.scm.repository.Branch;
-import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java
index bbb02a64fc..7bd6c353f5 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java
@@ -24,15 +24,14 @@
package sonia.scm.api.v2.resources;
-import sonia.scm.repository.NamespaceAndName;
-import sonia.scm.repository.Repository;
import sonia.scm.security.gpg.UserPublicKeyResource;
import javax.inject.Inject;
import java.net.URI;
import java.net.URISyntaxException;
-@SuppressWarnings("squid:S1192") // string literals should not be duplicated
+@SuppressWarnings("squid:S1192")
+ // string literals should not be duplicated
class ResourceLinks {
private final ScmPathInfoStore scmPathInfoStore;
@@ -274,13 +273,13 @@ class ResourceLinks {
}
AutoCompleteLinks autoComplete() {
- return new AutoCompleteLinks (scmPathInfoStore.get());
+ return new AutoCompleteLinks(scmPathInfoStore.get());
}
- static class AutoCompleteLinks {
+ static class AutoCompleteLinks {
private final LinkBuilder linkBuilder;
- AutoCompleteLinks (ScmPathInfo pathInfo) {
+ AutoCompleteLinks(ScmPathInfo pathInfo) {
linkBuilder = new LinkBuilder(pathInfo, AutoCompleteResource.class);
}
@@ -515,11 +514,11 @@ class ResourceLinks {
}
public String changesets(String namespace, String name) {
- return toTemplateParams(incomingLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("incoming").parameters().method("incomingChangesets").parameters("source","target").href());
+ return toTemplateParams(incomingLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("incoming").parameters().method("incomingChangesets").parameters("source", "target").href());
}
public String changesets(String namespace, String name, String source, String target) {
- return incomingLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("incoming").parameters().method("incomingChangesets").parameters(source,target).href();
+ return incomingLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("incoming").parameters().method("incomingChangesets").parameters(source, target).href();
}
public String diff(String namespace, String name) {
@@ -596,6 +595,7 @@ class ResourceLinks {
ModificationsLinks(ScmPathInfo pathInfo) {
modificationsLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, ModificationsRootResource.class);
}
+
String self(String namespace, String name, String revision) {
return modificationsLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("modifications").parameters().method("get").parameters(revision).href();
}
From f93c9f3e7c7b5879e9d6f5be85bffdd42a870d3a Mon Sep 17 00:00:00 2001
From: Eduard Heimbuch
Date: Wed, 11 Nov 2020 17:10:37 +0100
Subject: [PATCH 6/8] update documentation
---
.../assets/repository-branch-detailView.png | Bin 244603 -> 91426 bytes
.../assets/repository-branches-overview.png | Bin 241949 -> 162190 bytes
docs/de/user/repo/branches.md | 3 +++
.../assets/repository-branch-detailView.png | Bin 241973 -> 82629 bytes
.../assets/repository-branches-overview.png | Bin 239533 -> 153531 bytes
docs/en/user/repo/branches.md | 2 ++
scm-ui/ui-webapp/public/locales/de/repos.json | 2 +-
7 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/docs/de/user/repo/assets/repository-branch-detailView.png b/docs/de/user/repo/assets/repository-branch-detailView.png
index a73b369e26f0e83637cc11f7b6624e324019c3ef..d671846fef9cc7410f363d59e81dec1c5a3bc798 100644
GIT binary patch
literal 91426
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