diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java index 578944c47f..012c7ef2c6 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java @@ -8,7 +8,7 @@ public interface ModifyCommand { String execute(ModifyCommandRequest request); interface Worker { - void delete(String toBeDeleted); + void delete(String toBeDeleted) throws IOException; void create(String toBeCreated, File file, boolean overwrite) throws IOException; diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java index 76f420803a..9069154951 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java @@ -89,7 +89,7 @@ public class ModifyCommandRequest implements Resetable, Validateable { } @Override - public void execute(ModifyCommand.Worker worker) { + public void execute(ModifyCommand.Worker worker) throws IOException { worker.delete(path); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java index 9aa3eebe27..806f667515 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java @@ -9,7 +9,7 @@ import org.eclipse.jgit.revwalk.RevCommit; import sonia.scm.BadRequestException; import sonia.scm.ConcurrentModificationException; import sonia.scm.ContextEntry; -import sonia.scm.ScmConstraintViolationException; +import sonia.scm.NotFoundException; import sonia.scm.repository.GitWorkdirFactory; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; @@ -18,6 +18,7 @@ import java.io.File; import java.io.IOException; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.Optional; @@ -83,11 +84,7 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman try { Files.copy(file.toPath(), targetFile); } catch (FileAlreadyExistsException e) { - ContextEntry.ContextBuilder contextBuilder = entity("file", toBeCreated); - if (!StringUtils.isEmpty(request.getBranch())) { - contextBuilder.in("branch", request.getBranch()); - } - throw alreadyExists(contextBuilder.in(context.getRepository())); + throw alreadyExists(createFileContext(toBeCreated)); } } try { @@ -98,8 +95,27 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman } @Override - public void delete(String toBeDeleted) { + public void delete(String toBeDeleted) throws IOException { + Path fileToBeDeleted = new File(workDir, toBeDeleted).toPath(); + try { + Files.delete(fileToBeDeleted); + } catch (NoSuchFileException e) { + throw NotFoundException.notFound(createFileContext(toBeDeleted)); + } + try { + getClone().rm().addFilepattern(toBeDeleted).call(); + } catch (GitAPIException e) { + throwInternalRepositoryException("could not remove file from index", e); + } + } + private ContextEntry.ContextBuilder createFileContext(String toBeDeleted) { + ContextEntry.ContextBuilder contextBuilder = entity("file", toBeDeleted); + if (!StringUtils.isEmpty(request.getBranch())) { + contextBuilder.in("branch", request.getBranch()); + } + contextBuilder.in(context.getRepository()); + return contextBuilder; } @Override diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java index 8d9cc5bc5a..1a9bd5e04a 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java @@ -16,6 +16,7 @@ import org.junit.rules.TemporaryFolder; import sonia.scm.AlreadyExistsException; import sonia.scm.BadRequestException; import sonia.scm.ConcurrentModificationException; +import sonia.scm.NotFoundException; import sonia.scm.ScmConstraintViolationException; import sonia.scm.repository.Person; import sonia.scm.repository.util.WorkdirProvider; @@ -157,6 +158,47 @@ public class GitModifyCommandTest extends AbstractGitCommandTestBase { command.execute(request); } + @Test + public void shouldDeleteExistingFile() throws IOException, GitAPIException { + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setCommitMessage("test commit"); + request.addRequest(new ModifyCommandRequest.DeleteFileRequest("a.txt")); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + command.execute(request); + + TreeAssertions assertions = canonicalTreeParser -> assertThat(canonicalTreeParser.findFile("a.txt")).isFalse(); + + assertInTree(assertions); + } + + @Test(expected = NotFoundException.class) + public void shouldThrowNotFoundExceptionWhenFileToDeleteDoesNotExist() { + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setCommitMessage("test commit"); + request.addRequest(new ModifyCommandRequest.DeleteFileRequest("no/such/file")); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + command.execute(request); + } + + @Test(expected = NotFoundException.class) + public void shouldThrowNotFoundExceptionWhenBranchDoesNotExist() { + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setBranch("does-not-exist"); + request.setCommitMessage("test commit"); + request.addRequest(new ModifyCommandRequest.DeleteFileRequest("a.txt")); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + command.execute(request); + } + @Test(expected = ScmConstraintViolationException.class) public void shouldFailWithConstraintViolationIfBranchIsNoBranch() throws IOException { File newFile = Files.write(temporaryFolder.newFile().toPath(), "irrelevant\n".getBytes()).toFile(); diff --git a/scm-ui-components/packages/ui-components/src/Breadcrumb.js b/scm-ui-components/packages/ui-components/src/Breadcrumb.js index b9011f5936..d2f3409af6 100644 --- a/scm-ui-components/packages/ui-components/src/Breadcrumb.js +++ b/scm-ui-components/packages/ui-components/src/Breadcrumb.js @@ -1,13 +1,14 @@ //@flow import React from "react"; import { Link } from "react-router-dom"; -import type { Branch } from "@scm-manager/ui-types"; +import type { Branch, Repository } from "@scm-manager/ui-types"; import injectSheet from "react-jss"; import { ExtensionPoint, binder } from "@scm-manager/ui-extensions"; import {ButtonGroup} from "./buttons"; import classNames from "classnames"; type Props = { + repository: Repository, branch: Branch, defaultBranch: Branch, branches: Branch[], @@ -63,7 +64,7 @@ class Breadcrumb extends React.Component { } render() { - const { classes, baseUrl, branch, defaultBranch, branches, revision, path } = this.props; + const { classes, baseUrl, branch, defaultBranch, branches, revision, path, repository } = this.props; return ( <> @@ -82,7 +83,9 @@ class Breadcrumb extends React.Component { branch: branch ? branch : defaultBranch, path, isBranchUrl: branches && - branches.filter(b => b.name.replace("/", "%2F") === revision).length > 0 }} + branches.filter(b => b.name.replace("/", "%2F") === revision).length > 0, + repository + }} renderAll={true} /> diff --git a/scm-ui/src/repos/sources/containers/Content.js b/scm-ui/src/repos/sources/containers/Content.js index 14a7bfe70b..b9032ae681 100644 --- a/scm-ui/src/repos/sources/containers/Content.js +++ b/scm-ui/src/repos/sources/containers/Content.js @@ -2,7 +2,12 @@ import React from "react"; import { translate } from "react-i18next"; import type { File, Repository } from "@scm-manager/ui-types"; -import { DateFromNow, ButtonGroup, FileSize } from "@scm-manager/ui-components"; +import { + DateFromNow, + ButtonGroup, + FileSize, + ErrorNotification +} from "@scm-manager/ui-components"; import injectSheet from "react-jss"; import classNames from "classnames"; import FileButtonGroup from "../components/content/FileButtonGroup"; @@ -14,7 +19,6 @@ import { ExtensionPoint } from "@scm-manager/ui-extensions"; type Props = { loading: boolean, - error: Error, file: File, repository: Repository, revision: string, @@ -25,7 +29,8 @@ type Props = { type State = { collapsed: boolean, - showHistory: boolean + showHistory: boolean, + errorFromExtension?: Error }; const styles = { @@ -67,8 +72,12 @@ class Content extends React.Component { }); } + handleExtensionError = (error: Error) => { + this.setState({ errorFromExtension: error }); + }; + showHeader() { - const { file, classes } = this.props; + const { file, revision, classes } = this.props; const { showHistory, collapsed } = this.state; const icon = collapsed ? "fa-angle-right" : "fa-angle-down"; @@ -100,7 +109,11 @@ class Content extends React.Component { @@ -167,7 +180,7 @@ class Content extends React.Component { render() { const { file, revision, repository, path } = this.props; - const { showHistory } = this.state; + const { showHistory, errorFromExtension } = this.state; const header = this.showHeader(); const content = @@ -190,6 +203,7 @@ class Content extends React.Component { {moreInformation} {content} + {errorFromExtension && } ); } diff --git a/scm-ui/src/repos/sources/containers/Sources.js b/scm-ui/src/repos/sources/containers/Sources.js index 5e97617067..b12a716d3d 100644 --- a/scm-ui/src/repos/sources/containers/Sources.js +++ b/scm-ui/src/repos/sources/containers/Sources.js @@ -150,6 +150,7 @@ class Sources extends React.Component { branches && branches.filter(b => b.defaultBranch === true)[0] } branches={branches && branches} + repository={repository} />