diff --git a/CHANGELOG.md b/CHANGELOG.md index 806bdb4d52..cadd3559c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,17 @@ All notable changes to this project will be documented in this file. 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 +### Added +- enrich commit mentions in markdown viewer by internal links ([#1210](https://github.com/scm-manager/scm-manager/pull/1210)) - Rename repository name (and namespace if permitted) ([#1218](https://github.com/scm-manager/scm-manager/pull/1218)) +## [2.1.1] - 2020-06-23 ### Fixed - Wait until recommended java installation is available for deb packages ([#1209](https://github.com/scm-manager/scm-manager/pull/1209)) - Do not force java home of recommended java dependency for rpm and deb packages ([#1195](https://github.com/scm-manager/scm-manager/issues/1195) and [#1208](https://github.com/scm-manager/scm-manager/pull/1208)) +- Migration of non-bare repositories ([#1213](https://github.com/scm-manager/scm-manager/pull/1213)) ## [2.1.0] - 2020-06-18 ### Added diff --git a/lerna.json b/lerna.json index 66f41fc5a6..ffa2cdc5ad 100644 --- a/lerna.json +++ b/lerna.json @@ -5,5 +5,5 @@ ], "npmClient": "yarn", "useWorkspaces": true, - "version": "2.1.0" + "version": "2.1.1" } diff --git a/scm-packaging/windows/pom.xml b/scm-packaging/windows/pom.xml index cdc1bcdf4d..3041009d2e 100644 --- a/scm-packaging/windows/pom.xml +++ b/scm-packaging/windows/pom.xml @@ -71,11 +71,11 @@ wget - https://github.com/winsw/winsw/releases/download/v2.8.0/WinSW.NETCore31.x64.exe + https://github.com/winsw/winsw/releases/download/v2.9.0/WinSW.NETCore31.x64.exe false scm-server.exe ${project.build.directory}/windows - ebb2bb0ab0746ff5a20f65c76855a71c53aa806eb55ebd08fd18ded51ea23b58 + 59d29a41652cfc9a564c9c05d77976391833a6fb686bce941ad89f8f8dff120b diff --git a/scm-plugins/scm-git-plugin/package.json b/scm-plugins/scm-git-plugin/package.json index 540c3fb207..f406d25659 100644 --- a/scm-plugins/scm-git-plugin/package.json +++ b/scm-plugins/scm-git-plugin/package.json @@ -1,7 +1,7 @@ { "name": "@scm-manager/scm-git-plugin", "private": true, - "version": "2.1.0", + "version": "2.1.1", "license": "MIT", "main": "./src/main/js/index.ts", "scripts": { @@ -20,6 +20,6 @@ }, "prettier": "@scm-manager/prettier-config", "dependencies": { - "@scm-manager/ui-plugins": "^2.1.0" + "@scm-manager/ui-plugins": "^2.1.1" } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/update/GitV2UpdateStep.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/update/GitV2UpdateStep.java index 0bbb479ed6..d805fae53c 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/update/GitV2UpdateStep.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/update/GitV2UpdateStep.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.update; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; @@ -38,6 +38,7 @@ import sonia.scm.version.Version; import javax.inject.Inject; import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import static sonia.scm.version.Version.parse; @@ -60,7 +61,8 @@ public class GitV2UpdateStep implements UpdateStep { (repositoryId, path) -> { Repository repository = repositoryMetadataAccess.read(path); if (isGitDirectory(repository)) { - try (org.eclipse.jgit.lib.Repository gitRepository = build(path.resolve("data").toFile())) { + final Path effectiveGitPath = determineEffectiveGitFolder(path); + try (org.eclipse.jgit.lib.Repository gitRepository = build(effectiveGitPath.toFile())) { new GitConfigHelper().createScmmConfig(repository, gitRepository); } catch (IOException e) { throw new UpdateException("could not update repository with id " + repositoryId + " in path " + path, e); @@ -70,6 +72,18 @@ public class GitV2UpdateStep implements UpdateStep { ); } + public Path determineEffectiveGitFolder(Path path) { + Path bareGitFolder = path.resolve("data"); + Path nonBareGitFolder = bareGitFolder.resolve(".git"); + final Path effectiveGitPath; + if (Files.exists(nonBareGitFolder)) { + effectiveGitPath = nonBareGitFolder; + } else { + effectiveGitPath = bareGitFolder; + } + return effectiveGitPath; + } + private org.eclipse.jgit.lib.Repository build(File directory) throws IOException { return new FileRepositoryBuilder() .setGitDir(directory) diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/update/GitV2UpdateStepTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/update/GitV2UpdateStepTest.java new file mode 100644 index 0000000000..c207a4bf54 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/update/GitV2UpdateStepTest.java @@ -0,0 +1,92 @@ +/* + * 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.repository.update; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.update.UpdateStepRepositoryMetadataAccess; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.BiConsumer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class GitV2UpdateStepTest { + + @Mock + RepositoryLocationResolver locationResolver; + @Mock + RepositoryLocationResolver.RepositoryLocationResolverInstance locationResolverInstance; + @Mock + UpdateStepRepositoryMetadataAccess repositoryMetadataAccess; + + @InjectMocks + GitV2UpdateStep updateStep; + + @BeforeEach + void createDataDirectory(@TempDir Path temp) throws IOException { + Files.createDirectories(temp.resolve("data")); + } + + @BeforeEach + void initRepositoryFolder(@TempDir Path temp) { + when(locationResolver.forClass(Path.class)).thenReturn(locationResolverInstance); + when(repositoryMetadataAccess.read(temp)).thenReturn(new Repository("123", "git", "space", "X")); + doAnswer(invocation -> { + invocation.getArgument(0, BiConsumer.class).accept("123", temp); + return null; + }).when(locationResolverInstance).forAllLocations(any()); + } + + @Test + void shouldWriteConfigFileForBareRepositories(@TempDir Path temp) { + updateStep.doUpdate(); + + assertThat(temp.resolve("data").resolve("config")).exists(); + } + + @Test + void shouldWriteConfigFileForNonBareRepositories(@TempDir Path temp) throws IOException { + Files.createDirectories(temp.resolve("data").resolve(".git")); + + updateStep.doUpdate(); + + assertThat(temp.resolve("data").resolve("config")).doesNotExist(); + assertThat(temp.resolve("data").resolve(".git").resolve("config")).exists(); + } +} diff --git a/scm-plugins/scm-hg-plugin/package.json b/scm-plugins/scm-hg-plugin/package.json index 82aa1b2945..8cd1e03641 100644 --- a/scm-plugins/scm-hg-plugin/package.json +++ b/scm-plugins/scm-hg-plugin/package.json @@ -1,7 +1,7 @@ { "name": "@scm-manager/scm-hg-plugin", "private": true, - "version": "2.1.0", + "version": "2.1.1", "license": "MIT", "main": "./src/main/js/index.ts", "scripts": { @@ -19,6 +19,6 @@ }, "prettier": "@scm-manager/prettier-config", "dependencies": { - "@scm-manager/ui-plugins": "^2.1.0" + "@scm-manager/ui-plugins": "^2.1.1" } } diff --git a/scm-plugins/scm-legacy-plugin/package.json b/scm-plugins/scm-legacy-plugin/package.json index 3f0dcc4e85..1b9335e950 100644 --- a/scm-plugins/scm-legacy-plugin/package.json +++ b/scm-plugins/scm-legacy-plugin/package.json @@ -1,7 +1,7 @@ { "name": "@scm-manager/scm-legacy-plugin", "private": true, - "version": "2.1.0", + "version": "2.1.1", "license": "MIT", "main": "./src/main/js/index.tsx", "scripts": { @@ -19,6 +19,6 @@ }, "prettier": "@scm-manager/prettier-config", "dependencies": { - "@scm-manager/ui-plugins": "^2.1.0" + "@scm-manager/ui-plugins": "^2.1.1" } } diff --git a/scm-plugins/scm-svn-plugin/package.json b/scm-plugins/scm-svn-plugin/package.json index ea2cacc2d8..43854a0082 100644 --- a/scm-plugins/scm-svn-plugin/package.json +++ b/scm-plugins/scm-svn-plugin/package.json @@ -1,7 +1,7 @@ { "name": "@scm-manager/scm-svn-plugin", "private": true, - "version": "2.1.0", + "version": "2.1.1", "license": "MIT", "main": "./src/main/js/index.ts", "scripts": { @@ -19,6 +19,6 @@ }, "prettier": "@scm-manager/prettier-config", "dependencies": { - "@scm-manager/ui-plugins": "^2.1.0" + "@scm-manager/ui-plugins": "^2.1.1" } } diff --git a/scm-ui/ui-components/package.json b/scm-ui/ui-components/package.json index 87871564f7..7bbd35e789 100644 --- a/scm-ui/ui-components/package.json +++ b/scm-ui/ui-components/package.json @@ -1,6 +1,6 @@ { "name": "@scm-manager/ui-components", - "version": "2.1.0", + "version": "2.1.1", "description": "UI Components for SCM-Manager and its plugins", "main": "src/index.ts", "files": [ diff --git a/scm-ui/ui-components/src/MarkdownLinkRenderer.test.tsx b/scm-ui/ui-components/src/MarkdownLinkRenderer.test.tsx index 7a5b8e68dc..037bf0f73f 100644 --- a/scm-ui/ui-components/src/MarkdownLinkRenderer.test.tsx +++ b/scm-ui/ui-components/src/MarkdownLinkRenderer.test.tsx @@ -21,7 +21,13 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import { isAnchorLink, isExternalLink, isLinkWithProtocol, createLocalLink } from "./MarkdownLinkRenderer"; +import { + isAnchorLink, + isExternalLink, + isLinkWithProtocol, + createLocalLink, + isInternalScmRepoLink +} from "./MarkdownLinkRenderer"; describe("test isAnchorLink", () => { it("should return true", () => { @@ -67,6 +73,18 @@ describe("test isLinkWithProtocol", () => { }); }); +describe("test isInternalScmRepoLink", () => { + it("should return true", () => { + expect(isInternalScmRepoLink("/repo/scmadmin/git/code/changeset/1234567")).toBe(true); + expect(isInternalScmRepoLink("/repo/scmadmin/git")).toBe(true); + }); + it("should return false", () => { + expect(isInternalScmRepoLink("repo/path/link")).toBe(false); + expect(isInternalScmRepoLink("/some/path/link")).toBe(false); + expect(isInternalScmRepoLink("#some-anchor")).toBe(false); + }); +}); + describe("test createLocalLink", () => { it("should handle relative links", () => { expectLocalLink("/src", "/src/README.md", "docs/Home.md", "/src/docs/Home.md"); diff --git a/scm-ui/ui-components/src/MarkdownLinkRenderer.tsx b/scm-ui/ui-components/src/MarkdownLinkRenderer.tsx index 8ae1bc6135..fc38bd388b 100644 --- a/scm-ui/ui-components/src/MarkdownLinkRenderer.tsx +++ b/scm-ui/ui-components/src/MarkdownLinkRenderer.tsx @@ -21,10 +21,10 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import React, {FC} from "react"; -import {Link, useLocation} from "react-router-dom"; +import React, { FC } from "react"; +import { Link, useLocation } from "react-router-dom"; import ExternalLink from "./navigation/ExternalLink"; -import {withContextPath} from "./urls"; +import { withContextPath } from "./urls"; const externalLinkRegex = new RegExp("^http(s)?://"); export const isExternalLink = (link: string) => { @@ -35,6 +35,10 @@ export const isAnchorLink = (link: string) => { return link.startsWith("#"); }; +export const isInternalScmRepoLink = (link: string) => { + return link.startsWith("/repo/"); +}; + const linkWithProtcolRegex = new RegExp("^[a-z]+:"); export const isLinkWithProtocol = (link: string) => { return linkWithProtcolRegex.test(link); @@ -56,10 +60,10 @@ const normalizePath = (path: string) => { if (part === "..") { stack.pop(); } else if (part !== ".") { - stack.push(part) + stack.push(part); } } - const normalizedPath = stack.join("/") + const normalizedPath = stack.join("/"); if (normalizedPath.startsWith("/")) { return normalizedPath; } @@ -75,6 +79,9 @@ const isSubDirectoryOf = (basePath: string, currentPath: string) => { }; export const createLocalLink = (basePath: string, currentPath: string, link: string) => { + if (isInternalScmRepoLink(link)) { + return link; + } if (isAbsolute(link)) { return join(basePath, link); } @@ -102,7 +109,7 @@ type Props = LinkProps & { base: string; }; -const MarkdownLinkRenderer: FC = ({href, base, children}) => { +const MarkdownLinkRenderer: FC = ({ href, base, children }) => { const location = useLocation(); if (isExternalLink(href)) { return {children}; diff --git a/scm-ui/ui-components/src/MarkdownView.stories.tsx b/scm-ui/ui-components/src/MarkdownView.stories.tsx index 2c2e1cc9f4..54c87cc6f2 100644 --- a/scm-ui/ui-components/src/MarkdownView.stories.tsx +++ b/scm-ui/ui-components/src/MarkdownView.stories.tsx @@ -31,6 +31,7 @@ import MarkdownWithoutLang from "./__resources__/markdown-without-lang.md"; import MarkdownXmlCodeBlock from "./__resources__/markdown-xml-codeblock.md"; import MarkdownInlineXml from "./__resources__/markdown-inline-xml.md"; import MarkdownLinks from "./__resources__/markdown-links.md"; +import MarkdownCommitLinks from "./__resources__/markdown-commit-link.md"; import Title from "./layout/Title"; import { Subtitle } from "./layout"; import { MemoryRouter } from "react-router-dom"; @@ -52,4 +53,5 @@ storiesOf("MarkdownView", module) )) - .add("Links", () => ); + .add("Links", () => ) + .add("Commit Links", () => ); diff --git a/scm-ui/ui-components/src/MarkdownView.tsx b/scm-ui/ui-components/src/MarkdownView.tsx index 546d1610a6..e46c95370a 100644 --- a/scm-ui/ui-components/src/MarkdownView.tsx +++ b/scm-ui/ui-components/src/MarkdownView.tsx @@ -22,7 +22,7 @@ * SOFTWARE. */ import React, { FC } from "react"; -import { withRouter, RouteComponentProps } from "react-router-dom"; +import { RouteComponentProps, withRouter } from "react-router-dom"; // @ts-ignore import Markdown from "react-markdown/with-html"; import { binder } from "@scm-manager/ui-extensions"; @@ -30,10 +30,11 @@ import ErrorBoundary from "./ErrorBoundary"; import SyntaxHighlighter from "./SyntaxHighlighter"; import MarkdownHeadingRenderer from "./MarkdownHeadingRenderer"; import { create } from "./MarkdownLinkRenderer"; -import { useTranslation } from "react-i18next"; +import {useTranslation, WithTranslation, withTranslation} from "react-i18next"; import Notification from "./Notification"; +import { createTransformer } from "./remarkCommitLinksParser"; -type Props = RouteComponentProps & { +type Props = RouteComponentProps & WithTranslation & { content: string; renderContext?: object; renderers?: any; @@ -100,7 +101,7 @@ class MarkdownView extends React.Component { } render() { - const { content, renderers, renderContext, enableAnchorHeadings, skipHtml, basePath } = this.props; + const { content, renderers, renderContext, enableAnchorHeadings, skipHtml, basePath, t } = this.props; const rendererFactory = binder.getExtension("markdown-renderer-factory"); let rendererList = renderers; @@ -134,6 +135,7 @@ class MarkdownView extends React.Component { escapeHtml={skipHtml} source={content} renderers={rendererList} + astPlugins={[createTransformer(t)]} /> @@ -141,4 +143,4 @@ class MarkdownView extends React.Component { } } -export default withRouter(MarkdownView); +export default withRouter(withTranslation("repos")(MarkdownView)); diff --git a/scm-ui/ui-components/src/__resources__/markdown-commit-link.md.ts b/scm-ui/ui-components/src/__resources__/markdown-commit-link.md.ts new file mode 100644 index 0000000000..462179a73d --- /dev/null +++ b/scm-ui/ui-components/src/__resources__/markdown-commit-link.md.ts @@ -0,0 +1,39 @@ +/* + * 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. + */ +export default `# Commit Links in Markdown +namespace/name@1a5s4w8a + +Please check for this commit: namespace/name@1a5s4w8a + +hitchhiker/heart-of-gold@c7237cb60689046990dc9dc2a388a517adb3e2b2 + +hitchhiker/heart-of-gold@c7237cb + +hitchhiker/heart-of-gold@42 + +[hitchhiker/heart-of-gold@42](https://scm-manager.org/) + +Prefix hitchhiker/heart-of-gold@42 some text hitchhiker/heart-of-gold@21 suffix + +`; diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index a1db5dadc0..71cc6c0ecd 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -39183,6 +39183,87 @@ exports[`Storyshots MarkdownView Code without Lang 1`] = ` `; +exports[`Storyshots MarkdownView Commit Links 1`] = ` + +`; + exports[`Storyshots MarkdownView Default 1`] = `
{ + it("should match simple names", () => { + const regExp = new RegExp(regExpPattern, "g"); + expect("namespace/name@1a5s4w8a".match(regExp)).toBeTruthy(); + }); + it("should match complex names", () => { + const regExp = new RegExp(regExpPattern, "g"); + expect("hitchhiker/heart-of-gold@c7237cb60689046990dc9dc2a388a517adb3e2b2".match(regExp)).toBeTruthy(); + }); + it("should replace match", () => { + const regExp = new RegExp(regExpPattern, "g"); + expect("Prefix namespace/name@42 suffix".replace(regExp, "replaced")).toBe("Prefix replaced suffix"); + }); + it("should match groups", () => { + const regExp = new RegExp(regExpPattern, "g"); + const match = regExp.exec("namespace/name@42"); + expect(match).toBeTruthy(); + if (match) { + expect(match[1]).toBe("namespace"); + expect(match[2]).toBe("name"); + expect(match[3]).toBe("42"); + } + }); + it("should match multiple links in text", () => { + const regExp = new RegExp(regExpPattern, "g"); + const text = "Prefix hitchhiker/heart-of-gold@42 some text hitchhiker/heart-of-gold@21 suffix"; + const matches = []; + + let match = regExp.exec(text); + while (match !== null) { + matches.push(match[0]); + match = regExp.exec(text); + } + + console.log(matches) + + expect(matches[0]).toBe("hitchhiker/heart-of-gold@42"); + expect(matches[1]).toBe("hitchhiker/heart-of-gold@21"); + }); +}); diff --git a/scm-ui/ui-components/src/remarkCommitLinksParser.ts b/scm-ui/ui-components/src/remarkCommitLinksParser.ts new file mode 100644 index 0000000000..fa26c793db --- /dev/null +++ b/scm-ui/ui-components/src/remarkCommitLinksParser.ts @@ -0,0 +1,103 @@ +/* + * 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 { MarkdownAbstractSyntaxTree, MdastPlugin } from "react-markdown"; +import { nameRegex } from "./validation"; +// @ts-ignore No types available +import visit from "unist-util-visit"; +import { TFunction } from "i18next"; + +const namePartRegex = nameRegex.source.substring(1, nameRegex.source.length - 1); + +// Visible for testing +export const regExpPattern = `(${namePartRegex})\\/(${namePartRegex})@([\\w\\d]+)`; + +function match(value: string): RegExpMatchArray[] { + const regExp = new RegExp(regExpPattern, "g"); + const matches = []; + let m = regExp.exec(value); + while (m) { + matches.push(m); + m = regExp.exec(value); + } + return matches; +} + +export const createTransformer = (t: TFunction): MdastPlugin => { + + return (tree: MarkdownAbstractSyntaxTree) => { + visit(tree, "text", (node: MarkdownAbstractSyntaxTree, index: number, parent: MarkdownAbstractSyntaxTree) => { + if (parent.type === "link" || !node.value) { + return; + } + + let nodeText = node.value; + const matches = match(nodeText); + + if (matches.length > 0) { + const children = []; + for (const m of matches) { + const i = nodeText.indexOf(m[0]); + if (i > 0) { + children.push({ + type: "text", + value: nodeText.substring(0, i) + }); + } + + children.push({ + type: "link", + url: `/repo/${m[1]}/${m[2]}/code/changeset/${m[3]}`, + title: t("changeset.shortlink.title", { + namespace: m[1], + name: m[2], + id: m[3] + }), + children: [ + { + type: "text", + value: m[0] + } + ] + }); + + nodeText = nodeText.substring(i + m[0].length); + } + + if (nodeText.length > 0) { + children.push({ + type: "text", + value: nodeText + }); + } + + parent.children![index] = { + type: "text", + children + }; + } + }); + return tree; + }; +}; diff --git a/scm-ui/ui-components/src/validation.ts b/scm-ui/ui-components/src/validation.ts index e6502a7ed9..fcdb1a6dfd 100644 --- a/scm-ui/ui-components/src/validation.ts +++ b/scm-ui/ui-components/src/validation.ts @@ -22,7 +22,7 @@ * SOFTWARE. */ -const nameRegex = /^[A-Za-z0-9\.\-_][A-Za-z0-9\.\-_@]*$/; +export const nameRegex = /^[A-Za-z0-9\.\-_][A-Za-z0-9\.\-_@]*$/; export const isNameValid = (name: string) => { return nameRegex.test(name); diff --git a/scm-ui/ui-plugins/package.json b/scm-ui/ui-plugins/package.json index 296e4570ea..6ca49827fd 100644 --- a/scm-ui/ui-plugins/package.json +++ b/scm-ui/ui-plugins/package.json @@ -1,12 +1,12 @@ { "name": "@scm-manager/ui-plugins", - "version": "2.1.0", + "version": "2.1.1", "license": "MIT", "bin": { "ui-plugins": "./bin/ui-plugins.js" }, "dependencies": { - "@scm-manager/ui-components": "^2.1.0", + "@scm-manager/ui-components": "^2.1.1", "@scm-manager/ui-extensions": "^2.1.0", "classnames": "^2.2.6", "query-string": "^5.0.1", diff --git a/scm-ui/ui-webapp/package.json b/scm-ui/ui-webapp/package.json index 1655fa19aa..c9aa79a570 100644 --- a/scm-ui/ui-webapp/package.json +++ b/scm-ui/ui-webapp/package.json @@ -1,9 +1,9 @@ { "name": "@scm-manager/ui-webapp", - "version": "2.1.0", + "version": "2.1.1", "private": true, "dependencies": { - "@scm-manager/ui-components": "^2.1.0", + "@scm-manager/ui-components": "^2.1.1", "@scm-manager/ui-extensions": "^2.1.0", "classnames": "^2.2.5", "history": "^4.10.1", diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index e6906f8800..e5c16389ea 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -88,6 +88,9 @@ "shortSummary": "Committet <0/> <1/>", "tags": "Tags", "diffNotSupported": "Diff des Changesets wird von diesem Repositorytyp nicht unterstützt", + "shortlink": { + "title": "Changeset {{id}} aus {{namespace}}/{{name}}" + }, "parents": { "label" : "Parent", "label_plural": "Parents" diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index 8cafddf9f6..09d54ff58a 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -87,6 +87,9 @@ "summary": "Changeset <0/> was committed <1/>", "shortSummary": "Committed <0/> <1/>", "tags": "Tags", + "shortlink": { + "title": "Changeset {{id}} of {{namespace}}/{{name}}" + }, "diffNotSupported": "Diff of changesets is not supported by the type of repository", "buttons": { "details": "Details",