diff --git a/gradle/changelog/create_invalid_branch_exception.yaml b/gradle/changelog/create_invalid_branch_exception.yaml new file mode 100644 index 0000000000..34aa626d60 --- /dev/null +++ b/gradle/changelog/create_invalid_branch_exception.yaml @@ -0,0 +1,2 @@ +- type: fixed + description: Add handling when duplicated branch part cannot be created ([#1692](https://github.com/scm-manager/scm-manager/pull/1692)) diff --git a/scm-core/src/main/java/sonia/scm/BranchAlreadyExistsException.java b/scm-core/src/main/java/sonia/scm/BranchAlreadyExistsException.java new file mode 100644 index 0000000000..09df40c0cc --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/BranchAlreadyExistsException.java @@ -0,0 +1,53 @@ +/* + * 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. + */ + +package sonia.scm; + +import java.util.List; + +import static java.util.stream.Collectors.joining; + +public class BranchAlreadyExistsException extends ExceptionWithContext { + + private static final String CODE = "FgSZpu9FR1"; + + public static BranchAlreadyExistsException branchAlreadyExists(ContextEntry.ContextBuilder builder) { + return new BranchAlreadyExistsException(builder.build()); + } + + private BranchAlreadyExistsException(List context) { + super(context, createMessage(context)); + } + + @Override + public String getCode() { + return CODE; + } + + private static String createMessage(List context) { + return context.stream() + .map(c -> c.getType().toLowerCase() + " with id " + c.getId()) + .collect(joining(" in ", "", " already exists")); + } +} diff --git a/scm-test/src/main/java/sonia/scm/web/RestDispatcher.java b/scm-test/src/main/java/sonia/scm/web/RestDispatcher.java index 4879e64307..a0c3253f72 100644 --- a/scm-test/src/main/java/sonia/scm/web/RestDispatcher.java +++ b/scm-test/src/main/java/sonia/scm/web/RestDispatcher.java @@ -37,6 +37,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.AlreadyExistsException; import sonia.scm.BadRequestException; +import sonia.scm.BranchAlreadyExistsException; import sonia.scm.ConcurrentModificationException; import sonia.scm.NotFoundException; import sonia.scm.ScmConstraintViolationException; @@ -91,6 +92,7 @@ public class RestDispatcher { public EnhanceableExceptionMapper() { registerException(NotFoundException.class, Status.NOT_FOUND); registerException(AlreadyExistsException.class, Status.CONFLICT); + registerException(BranchAlreadyExistsException.class, Status.CONFLICT); registerException(ConcurrentModificationException.class, Status.CONFLICT); registerException(UnauthorizedException.class, Status.FORBIDDEN); registerException(AuthorizationException.class, Status.FORBIDDEN); diff --git a/scm-ui/ui-webapp/src/repos/branches/containers/CreateBranch.tsx b/scm-ui/ui-webapp/src/repos/branches/containers/CreateBranch.tsx index 5bef630f0b..3ab3d76539 100644 --- a/scm-ui/ui-webapp/src/repos/branches/containers/CreateBranch.tsx +++ b/scm-ui/ui-webapp/src/repos/branches/containers/CreateBranch.tsx @@ -57,10 +57,6 @@ const CreateBranch: FC = ({ repository }) => { return ; } - if (errorCreate) { - return ; - } - if (isLoadingList || !branches) { return ; } @@ -68,6 +64,7 @@ const CreateBranch: FC = ({ repository }) => { return ( <> + {errorCreate ? : null} branchName.equals(branch.getName())); + .anyMatch(branch -> branchName.equals(branch.getName()) + || branchNamespaceAlreadyExistsInGitRepo(branchName, branch.getName(), repositoryService)); + } + + private boolean branchNamespaceAlreadyExistsInGitRepo(String branchName, String branchName2, RepositoryService repositoryService) { + if (repositoryService.getRepository().getType().equals("hg")) { + return false; + } + return (branchName.contains("/") && branchName2.equals(branchName.substring(0, branchName.indexOf("/")))) + || (branchName2.contains("/") && branchName.equals(branchName2.substring(0, branchName2.indexOf("/")))); } /** diff --git a/scm-webapp/src/main/resources/locales/de/plugins.json b/scm-webapp/src/main/resources/locales/de/plugins.json index 8ec998c15a..c87946f02a 100644 --- a/scm-webapp/src/main/resources/locales/de/plugins.json +++ b/scm-webapp/src/main/resources/locales/de/plugins.json @@ -193,11 +193,15 @@ }, "FtR7UznKU1": { "displayName": "Existiert bereits", - "description": "Ein Datensatz mit den gegebenen Schlüsselwerten existiert bereits" + "description": "Ein Datensatz mit den gegebenen Schlüsselwerten existiert bereits." + }, + "FgSZpu9FR1": { + "displayName": "Branch existiert bereits", + "description": "Ein Branch oder Branchverzeichnis mit den gegebenen Schlüsselwerten existiert bereits." }, "9BR7qpDAe1": { "displayName": "Passwortänderung nicht erlaubt", - "description": "Sie haben nicht die Berechtigung, das Passwort zu ändern" + "description": "Sie haben nicht die Berechtigung, das Passwort zu ändern." }, "2wR7UzpPG1": { "displayName": "Konkurrierende Änderungen", diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json index 723f26dee1..d4b697067c 100644 --- a/scm-webapp/src/main/resources/locales/en/plugins.json +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -195,6 +195,10 @@ "displayName": "Already exists", "description": "There is already an entity with the same key values." }, + "FgSZpu9FR1": { + "displayName": "Branch already exists", + "description": "There is already an branch or branch directory with the same key values." + }, "9BR7qpDAe1": { "displayName": "Password change not allowed", "description": "You do not have the permission to change the password." 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 f2c0a29cee..7173d49ca7 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 @@ -257,6 +257,38 @@ public class BranchRootResourceTest extends RepositoryTestBase { verify(branchCommandBuilder, never()).branch(anyString()); } + @Test + public void shouldNotCreateBranchIfDirectoryAsBranchAlreadyExists() throws Exception { + when(branchesCommandBuilder.getBranches()).thenReturn(new Branches(createBranch("existing_branch"))); + + MockHttpRequest request = MockHttpRequest + .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/branches/") + .content("{\"name\": \"existing_branch/abc\"}".getBytes()) + .contentType(VndMediaType.BRANCH_REQUEST); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(409, response.getStatus()); + verify(branchCommandBuilder, never()).branch(anyString()); + } + + @Test + public void shouldNotCreateBranchIfBranchWithDirectoryAlreadyExists() throws Exception { + when(branchesCommandBuilder.getBranches()).thenReturn(new Branches(createBranch("existing_branch/abc"))); + + MockHttpRequest request = MockHttpRequest + .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/branches/") + .content("{\"name\": \"existing_branch\"}".getBytes()) + .contentType(VndMediaType.BRANCH_REQUEST); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(409, response.getStatus()); + verify(branchCommandBuilder, never()).branch(anyString()); + } + @Test public void shouldFailForMissingParentBranch() throws Exception { when(branchesCommandBuilder.getBranches()).thenReturn(new Branches(createBranch("existing_branch")));