From 91753f16f04c6cda8c48878ff534db34ca1e41d7 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 12 Feb 2020 14:41:54 +0100 Subject: [PATCH 001/251] remove markdown formatting from MarkdownView component --- scm-ui/ui-components/src/MarkdownView.tsx | 34 ++--------------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/scm-ui/ui-components/src/MarkdownView.tsx b/scm-ui/ui-components/src/MarkdownView.tsx index 94930692b3..01a2b35512 100644 --- a/scm-ui/ui-components/src/MarkdownView.tsx +++ b/scm-ui/ui-components/src/MarkdownView.tsx @@ -2,7 +2,6 @@ import React from "react"; import { withRouter, RouteComponentProps } from "react-router-dom"; // @ts-ignore import Markdown from "react-markdown/with-html"; -import styled from "styled-components"; import { binder } from "@scm-manager/ui-extensions"; import SyntaxHighlighter from "./SyntaxHighlighter"; import MarkdownHeadingRenderer from "./MarkdownHeadingRenderer"; @@ -15,35 +14,6 @@ type Props = RouteComponentProps & { enableAnchorHeadings?: boolean; }; -const MarkdownWrapper = styled.div` - > .content { - > h1, - h2, - h3, - h4, - h5, - h6 { - margin: 0.5rem 0; - font-size: 0.9rem; - } - > h1 { - font-weight: 700; - } - > h2 { - font-weight: 600; - } - > h3, - h4, - h5, - h6 { - font-weight: 500; - } - & strong { - font-weight: 500; - } - } -`; - class MarkdownView extends React.Component { static defaultProps: Partial = { enableAnchorHeadings: false, @@ -94,7 +64,7 @@ class MarkdownView extends React.Component { } return ( - (this.contentRef = el)}> +
(this.contentRef = el)}> { source={content} renderers={rendererList} /> - +
); } } From 88d5847773ba89864d9d90f64c75595e1c4476a7 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 12 Feb 2020 14:58:12 +0100 Subject: [PATCH 002/251] update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4f080c9e0..30a4aabae1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## 2.0.0-rc3 - 2020-01-31 ### Fixed - Broken plugin order fixed +- MarkdownViewer in code section renders markdown properly ## 2.0.0-rc2 - 2020-01-29 ### Added From 550eb327c3147288ed1c1bd3b79892f898fe0248 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 13 Feb 2020 08:29:51 +0100 Subject: [PATCH 003/251] update storyshots --- .../src/__snapshots__/storyshots.test.ts.snap | 1086 ++++++++--------- 1 file changed, 541 insertions(+), 545 deletions(-) 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 730e93136d..0ec9bb0c76 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -2,7 +2,7 @@ exports[`Storyshots Buttons|AddButton Default 1`] = `
+ ); } renderSourcesTable() { - const { tree, revision, path, baseUrl, t, location } = this.props; + const { hunks, revision, path, baseUrl, t } = this.props; const files = []; @@ -115,46 +140,41 @@ class FileTree extends React.Component { } }; - if (tree._embedded && tree._embedded.children) { - const children = [...tree._embedded.children].sort(compareFiles); - files.push(...children); - } + hunks + .filter(hunk => !hunk.loading) + .forEach(hunk => { + if (hunk.tree?._embedded && hunk.tree._embedded.children) { + const children = [...hunk.tree._embedded.children]; + files.push(...children); + } + }); if (files && files.length > 0) { - let baseUrlWithRevision = baseUrl; if (revision) { baseUrlWithRevision += "/" + encodeURIComponent(revision); } else { - baseUrlWithRevision += "/" + encodeURIComponent(tree.revision); - } - - const offset = queryString.parse(location.search).offset; - if (offset) { - baseUrlWithRevision += "?offset=" + offset; + baseUrlWithRevision += "/" + encodeURIComponent(hunks[0].tree.revision); } return ( - <> - - - - - - - - - {binder.hasExtension("repos.sources.tree.row.right") && - - - {files.map((file: any) => ( - - ))} - -
{t("sources.file-tree.name")}{t("sources.file-tree.length")}{t("sources.file-tree.commitDate")}{t("sources.file-tree.description")}} -
- {tree.truncated &&

TRUNCATED

} - + + + + + + + + + {binder.hasExtension("repos.sources.tree.row.right") && + + + {files.map((file: any) => ( + + ))} + +
{t("sources.file-tree.name")}{t("sources.file-tree.length")}{t("sources.file-tree.commitDate")}{t("sources.file-tree.description")}} +
); } return {t("sources.noSources")}; @@ -164,24 +184,37 @@ class FileTree extends React.Component { const mapDispatchToProps = (dispatch: any, ownProps: Props) => { const { repository, revision, path } = ownProps; - const updateSources = () => dispatch(fetchSources(repository, revision, path, false)); - - return { updateSources }; + return { + updateSources: (hunk: number) => dispatch(fetchSources(repository, revision, path, false, hunk)), + fetchSources: (repository: Repository, revision: string, path: string, hunk: number) => { + dispatch(fetchSources(repository, revision, path, true, hunk)); + } + }; }; const mapStateToProps = (state: any, ownProps: Props) => { const { repository, revision, path } = ownProps; - const loading = isFetchSourcesPending(state, repository, revision, path); - const error = getFetchSourcesFailure(state, repository, revision, path); - const tree = getSources(state, repository, revision, path); + const loading = isFetchSourcesPending(state, repository, revision, path, 0); + const error = getFetchSourcesFailure(state, repository, revision, path, 0); + const hunkCount = getHunkCount(state, repository, revision, path); + const hunks = []; + for (let i = 0; i < hunkCount; ++i) { + console.log(`getting data for hunk ${i}`); + const tree = getSources(state, repository, revision, path, i); + const loading = isFetchSourcesPending(state, repository, revision, path, i); + hunks.push({ + tree, + loading + }); + } return { revision, path, loading, error, - tree + hunks }; }; diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts index 2c7413f7ea..51c5eeb0e1 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -10,34 +10,36 @@ export const FETCH_UPDATES_PENDING = `${FETCH_SOURCES}_UPDATE_PENDING`; export const FETCH_SOURCES_SUCCESS = `${FETCH_SOURCES}_${types.SUCCESS_SUFFIX}`; export const FETCH_SOURCES_FAILURE = `${FETCH_SOURCES}_${types.FAILURE_SUFFIX}`; -export function fetchSources(repository: Repository, revision: string, path: string, initialLoad = true) { +export function fetchSources(repository: Repository, revision: string, path: string, initialLoad = true, hunk = 0) { return function(dispatch: any, getState: () => any) { const state = getState(); if ( - isFetchSourcesPending(state, repository, revision, path) || - isUpdateSourcePending(state, repository, revision, path) + isFetchSourcesPending(state, repository, revision, path, hunk) || + isUpdateSourcePending(state, repository, revision, path, hunk) ) { return; } if (initialLoad) { - dispatch(fetchSourcesPending(repository, revision, path)); + dispatch(fetchSourcesPending(repository, revision, path, hunk)); } else { - dispatch(updateSourcesPending(repository, revision, path, getSources(state, repository, revision, path))); + dispatch( + updateSourcesPending(repository, revision, path, hunk, getSources(state, repository, revision, path, hunk)) + ); } return apiClient - .get(createUrl(repository, revision, path)) + .get(createUrl(repository, revision, path, hunk)) .then(response => response.json()) .then((sources: File) => { - dispatch(fetchSourcesSuccess(repository, revision, path, sources)); + dispatch(fetchSourcesSuccess(repository, revision, path, hunk, sources)); }) .catch(err => { - dispatch(fetchSourcesFailure(repository, revision, path, err)); + dispatch(fetchSourcesFailure(repository, revision, path, hunk, err)); }); }; } -function createUrl(repository: Repository, revision: string, path: string) { +function createUrl(repository: Repository, revision: string, path: string, hunk: number) { const base = (repository._links.sources as Link).href; if (!revision && !path) { return base; @@ -45,13 +47,14 @@ function createUrl(repository: Repository, revision: string, path: string) { // TODO handle trailing slash const pathDefined = path ? path : ""; - return `${base}${encodeURIComponent(revision)}/${pathDefined}`; + return `${base}${encodeURIComponent(revision)}/${pathDefined}?hunk=${hunk}`; } -export function fetchSourcesPending(repository: Repository, revision: string, path: string): Action { +export function fetchSourcesPending(repository: Repository, revision: string, path: string, hunk: number): Action { return { type: FETCH_SOURCES_PENDING, - itemId: createItemId(repository, revision, path) + itemId: createItemId(repository, revision, path), + payload: { hunk, pending: true, sources: {} } }; } @@ -59,24 +62,37 @@ export function updateSourcesPending( repository: Repository, revision: string, path: string, + hunk: number, currentSources: any ): Action { return { type: FETCH_UPDATES_PENDING, - payload: { updatePending: true, sources: currentSources }, + payload: { hunk, updatePending: true, sources: currentSources }, itemId: createItemId(repository, revision, path) }; } -export function fetchSourcesSuccess(repository: Repository, revision: string, path: string, sources: File) { +export function fetchSourcesSuccess( + repository: Repository, + revision: string, + path: string, + hunk: number, + sources: File +) { return { type: FETCH_SOURCES_SUCCESS, - payload: { updatePending: false, sources }, + payload: { hunk, pending: false, updatePending: false, sources }, itemId: createItemId(repository, revision, path) }; } -export function fetchSourcesFailure(repository: Repository, revision: string, path: string, error: Error): Action { +export function fetchSourcesFailure( + repository: Repository, + revision: string, + path: string, + hunk: number, + error: Error +): Action { return { type: FETCH_SOURCES_FAILURE, payload: error, @@ -99,9 +115,14 @@ export default function reducer( } ): any { if (action.itemId && (action.type === FETCH_SOURCES_SUCCESS || action.type === FETCH_UPDATES_PENDING)) { + console.log("adding payload to " + action.itemId + "/" + action.payload.hunk); return { ...state, - [action.itemId]: action.payload + [action.itemId + "/hunkCount"]: action.payload.hunk + 1, + [action.itemId + "/" + action.payload.hunk]: { + sources: action.payload.sources, + loading: false + } }; } return state; @@ -110,7 +131,7 @@ export default function reducer( // selectors export function isDirectory(state: any, repository: Repository, revision: string, path: string): boolean { - const currentFile = getSources(state, repository, revision, path); + const currentFile = getSources(state, repository, revision, path, 0); if (currentFile && !currentFile.directory) { return false; } else { @@ -118,31 +139,53 @@ export function isDirectory(state: any, repository: Repository, revision: string } } +export function getHunkCount(state: any, repository: Repository, revision: string | undefined, path: string): number { + if (state.sources) { + const count = state.sources[createItemId(repository, revision, path) + "/hunkCount"]; + return count ? count : 0; + } + return 0; +} + export function getSources( state: any, repository: Repository, revision: string | undefined, - path: string + path: string, + hunk: number ): File | null | undefined { if (state.sources) { - return state.sources[createItemId(repository, revision, path)]?.sources; + return state.sources[createItemId(repository, revision, path) + "/" + hunk]?.sources; } return null; } -export function isFetchSourcesPending(state: any, repository: Repository, revision: string, path: string): boolean { +export function isFetchSourcesPending( + state: any, + repository: Repository, + revision: string, + path: string, + hunk: number +): boolean { return state && isPending(state, FETCH_SOURCES, createItemId(repository, revision, path)); } -function isUpdateSourcePending(state: any, repository: Repository, revision: string, path: string): boolean { - return state?.sources && state.sources[createItemId(repository, revision, path)]?.updatePending; +export function isUpdateSourcePending( + state: any, + repository: Repository, + revision: string, + path: string, + hunk: number +): boolean { + return state?.sources && state.sources[createItemId(repository, revision, path) + "/" + hunk]?.updatePending; } export function getFetchSourcesFailure( state: any, repository: Repository, revision: string, - path: string + path: string, + hunk: number ): Error | null | undefined { return getFailure(state, FETCH_SOURCES, createItemId(repository, revision, path)); } From 051e6f946f9355b55f4002a3d57a8e7b0f181e21 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 20 Feb 2020 11:31:21 +0100 Subject: [PATCH 057/251] improved footer layout --- .../{avatar.png => hitchhiker.png} | Bin .../src/__resources__/marvin.jpg | Bin 0 -> 16018 bytes .../src/layout/Footer.stories.tsx | 16 ++++++---- scm-ui/ui-components/src/layout/Footer.tsx | 28 ++++++++-------- .../src/layout/FooterSection.tsx | 6 +--- .../src/navigation/ExternalLink.tsx | 30 ++++++++++++++++++ scm-ui/ui-styles/src/scm.scss | 6 +++- .../ui-webapp/public/locales/de/commons.json | 13 ++++++++ .../ui-webapp/public/locales/en/commons.json | 13 ++++++++ 9 files changed, 85 insertions(+), 27 deletions(-) rename scm-ui/ui-components/src/__resources__/{avatar.png => hitchhiker.png} (100%) create mode 100644 scm-ui/ui-components/src/__resources__/marvin.jpg create mode 100644 scm-ui/ui-components/src/navigation/ExternalLink.tsx diff --git a/scm-ui/ui-components/src/__resources__/avatar.png b/scm-ui/ui-components/src/__resources__/hitchhiker.png similarity index 100% rename from scm-ui/ui-components/src/__resources__/avatar.png rename to scm-ui/ui-components/src/__resources__/hitchhiker.png diff --git a/scm-ui/ui-components/src/__resources__/marvin.jpg b/scm-ui/ui-components/src/__resources__/marvin.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a98f6b09cd2dbc2905a2db739145fb84693338b0 GIT binary patch literal 16018 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 rhX { +const bindAvatar = (binder: Binder, avatar: string) => { binder.bind(EXTENSION_POINT, () => { return avatar; }); }; const bindLinks = (binder: Binder) => { - binder.bind("footer.links", () => REST API); - binder.bind("footer.links", () => CLI); + binder.bind("footer.information", () => ); + binder.bind("footer.information", () => ); + binder.bind("footer.support", () => ); binder.bind("profile.setting", () => ); }; @@ -42,7 +46,7 @@ storiesOf("Layout|Footer", module) }) .add("With Avatar", () => { const binder = new Binder("avatar-story"); - bindAvatar(binder); + bindAvatar(binder, hitchhiker); return withBinder(binder); }) .add("With Plugin Links", () => { @@ -52,7 +56,7 @@ storiesOf("Layout|Footer", module) }) .add("Full", () => { const binder = new Binder("link-story"); - bindAvatar(binder); + bindAvatar(binder, marvin); bindLinks(binder); return withBinder(binder); }); diff --git a/scm-ui/ui-components/src/layout/Footer.tsx b/scm-ui/ui-components/src/layout/Footer.tsx index 35916a7f52..515d3410b5 100644 --- a/scm-ui/ui-components/src/layout/Footer.tsx +++ b/scm-ui/ui-components/src/layout/Footer.tsx @@ -6,6 +6,8 @@ import NavLink from "../navigation/NavLink"; import FooterSection from "./FooterSection"; import styled from "styled-components"; import { EXTENSION_POINT } from "../avatar/Avatar"; +import ExternalLink from "../navigation/ExternalLink"; +import { useTranslation } from "react-i18next"; type Props = { me?: Me; @@ -50,10 +52,12 @@ const TitleWithAvatar: FC = ({ me }) => ( ); const Footer: FC = ({ me, version, links }) => { + const [t] = useTranslation("commons"); const binder = useBinder(); if (!me) { return null; } + const extensionProps = { me, url: "/me", links }; let meSectionTile; if (binder.hasExtension(EXTENSION_POINT)) { @@ -67,24 +71,18 @@ const Footer: FC = ({ me, version, links }) => {
- - + + - }> - - SCM-Manager {version} - - + }> + + - }> - - Learn more - - - Powered by Cloudogu - - + }> + + +
diff --git a/scm-ui/ui-components/src/layout/FooterSection.tsx b/scm-ui/ui-components/src/layout/FooterSection.tsx index 8f94499ff7..57476d31fe 100644 --- a/scm-ui/ui-components/src/layout/FooterSection.tsx +++ b/scm-ui/ui-components/src/layout/FooterSection.tsx @@ -18,11 +18,7 @@ const FooterSection: FC = ({ title, children }) => { return (
{title} - - {React.Children.map(children, (child, index) => ( -
  • {child}
  • - ))} -
    + {children}
    ); }; diff --git a/scm-ui/ui-components/src/navigation/ExternalLink.tsx b/scm-ui/ui-components/src/navigation/ExternalLink.tsx new file mode 100644 index 0000000000..de9e46ce2c --- /dev/null +++ b/scm-ui/ui-components/src/navigation/ExternalLink.tsx @@ -0,0 +1,30 @@ +import React, { FC } from "react"; +import classNames from "classnames"; + +type Props = { + to: string; + icon?: string; + label: string; +}; + +const ExternalLink: FC = ({ to, icon, label }) => { + let showIcon; + if (icon) { + showIcon = ( + <> + {" "} + + ); + } + + return ( +
  • + + {showIcon} + {label} + +
  • + ); +}; + +export default ExternalLink; diff --git a/scm-ui/ui-styles/src/scm.scss b/scm-ui/ui-styles/src/scm.scss index e4e489f880..3adb51da90 100644 --- a/scm-ui/ui-styles/src/scm.scss +++ b/scm-ui/ui-styles/src/scm.scss @@ -69,8 +69,12 @@ hr.header-with-actions { footer.footer { //height: 100px; - background-color: whitesmoke; + background-color: $white-ter; padding: inherit; + + a { + color: darken($blue, 15%); + } } // 6. Import the rest of Bulma diff --git a/scm-ui/ui-webapp/public/locales/de/commons.json b/scm-ui/ui-webapp/public/locales/de/commons.json index 8c575cc4e9..310c14ead3 100644 --- a/scm-ui/ui-webapp/public/locales/de/commons.json +++ b/scm-ui/ui-webapp/public/locales/de/commons.json @@ -86,5 +86,18 @@ "passwordConfirmFailed": "Passwörter müssen identisch sein!", "submit": "Speichern", "changedSuccessfully": "Passwort erfolgreich geändert!" + }, + "footer": { + "user": { + "profile": "Profil" + }, + "information": { + "title": "Information" + }, + "support": { + "title": "Support", + "community": "Community", + "enterprise": "Enterprise" + } } } diff --git a/scm-ui/ui-webapp/public/locales/en/commons.json b/scm-ui/ui-webapp/public/locales/en/commons.json index f7bab46d22..f57a944c65 100644 --- a/scm-ui/ui-webapp/public/locales/en/commons.json +++ b/scm-ui/ui-webapp/public/locales/en/commons.json @@ -87,5 +87,18 @@ "passwordConfirmFailed": "Passwords have to be identical", "submit": "Submit", "changedSuccessfully": "Password changed successfully" + }, + "footer": { + "user": { + "profile": "Profile" + }, + "information": { + "title": "Information" + }, + "support": { + "title": "Support", + "community": "Community", + "enterprise": "Enterprise" + } } } From c4a801a7be919c80bdd900cf9a68c6a057a7625b Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 20 Feb 2020 14:36:13 +0100 Subject: [PATCH 058/251] WIP --- .../repository/spi/BrowseCommandRequest.java | 10 ++- scm-ui/ui-types/src/Sources.ts | 6 +- .../src/repos/sources/components/FileTree.tsx | 26 ++---- .../src/repos/sources/modules/sources.test.ts | 84 ++++++++++++----- .../src/repos/sources/modules/sources.ts | 89 +++++++++++++------ 5 files changed, 137 insertions(+), 78 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java index 70dec0980a..4477a26e31 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java @@ -112,10 +112,11 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest final BrowseCommandRequest other = (BrowseCommandRequest) obj; - return super.equals(obj) && Objects.equal(recursive, other.recursive) + return super.equals(obj) + && Objects.equal(recursive, other.recursive) && Objects.equal(disableLastCommit, other.disableLastCommit) - && Objects.equal(disableSubRepositoryDetection, - other.disableSubRepositoryDetection); + && Objects.equal(disableSubRepositoryDetection, other.disableSubRepositoryDetection) + && Objects.equal(offset, other.offset); } /** @@ -128,7 +129,7 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest public int hashCode() { return Objects.hashCode(super.hashCode(), recursive, disableLastCommit, - disableSubRepositoryDetection); + disableSubRepositoryDetection, offset); } /** @@ -147,6 +148,7 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest .add("recursive", recursive) .add("disableLastCommit", disableLastCommit) .add("disableSubRepositoryDetection", disableSubRepositoryDetection) + .add("offset", offset) .toString(); //J+ } diff --git a/scm-ui/ui-types/src/Sources.ts b/scm-ui/ui-types/src/Sources.ts index 99e3bde7ac..b1cca3c59f 100644 --- a/scm-ui/ui-types/src/Sources.ts +++ b/scm-ui/ui-types/src/Sources.ts @@ -16,9 +16,9 @@ export type File = { length?: number; commitDate?: string; subRepository?: SubRepository; // TODO - partialResult: boolean; - computationAborted: boolean; - truncated: boolean; + partialResult?: boolean; + computationAborted?: boolean; + truncated?: boolean; _links: Links; _embedded: { children: File[] | null | undefined; diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx index 55a9abdf18..00cf1de981 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -12,7 +12,7 @@ import { getFetchSourcesFailure, getHunkCount, getSources, - isFetchSourcesPending, isUpdateSourcePending + isFetchSourcesPending } from "../modules/sources"; import FileTreeLeaf from "./FileTreeLeaf"; import Button from "@scm-manager/ui-components/src/buttons/Button"; @@ -86,7 +86,7 @@ class FileTree extends React.Component { } loadMore = () => { - // console.log("smth"); + this.props.fetchSources(this.props.repository, this.props.revision, this.props.path, this.props.hunks.length); }; render() { @@ -124,22 +124,6 @@ class FileTree extends React.Component { }); } - const compareFiles = function(f1: File, f2: File): number { - if (f1.directory) { - if (f2.directory) { - return f1.name.localeCompare(f2.name); - } else { - return -1; - } - } else { - if (f2.directory) { - return 1; - } else { - return f1.name.localeCompare(f2.name); - } - } - }; - hunks .filter(hunk => !hunk.loading) .forEach(hunk => { @@ -149,7 +133,9 @@ class FileTree extends React.Component { } }); - if (files && files.length > 0) { + const loading = hunks.filter(hunk => hunk.loading).length > 0; + + if (loading || (files && files.length > 0)) { let baseUrlWithRevision = baseUrl; if (revision) { baseUrlWithRevision += "/" + encodeURIComponent(revision); @@ -195,7 +181,6 @@ const mapDispatchToProps = (dispatch: any, ownProps: Props) => { const mapStateToProps = (state: any, ownProps: Props) => { const { repository, revision, path } = ownProps; - const loading = isFetchSourcesPending(state, repository, revision, path, 0); const error = getFetchSourcesFailure(state, repository, revision, path, 0); const hunkCount = getHunkCount(state, repository, revision, path); const hunks = []; @@ -212,7 +197,6 @@ const mapStateToProps = (state: any, ownProps: Props) => { return { revision, path, - loading, error, hunks }; diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts index fa6ff5c1f6..fdd7a9fc78 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts @@ -37,6 +37,9 @@ const collection = { length: 176, revision: "76aae4bb4ceacf0e88938eb5b6832738b7d537b4", subRepository: undefined, + truncated: true, + partialResult: false, + computationAborted: false, _links: { self: { href: @@ -120,12 +123,23 @@ describe("sources fetch", () => { const expectedActions = [ { type: FETCH_SOURCES_PENDING, - itemId: "scm/core/_/" + itemId: "scm/core/_//", + payload: { + hunk: 0, + updatePending: false, + pending: true, + sources: {} + } }, { type: FETCH_SOURCES_SUCCESS, - itemId: "scm/core/_/", - payload: { updatePending: false, sources: collection } + itemId: "scm/core/_//", + payload: { + hunk: 0, + updatePending: false, + pending: false, + sources: collection + } } ]; @@ -136,17 +150,28 @@ describe("sources fetch", () => { }); it("should fetch the sources of the repository with the given revision and path", () => { - fetchMock.getOnce(sourcesUrl + "abc/src", collection); + fetchMock.getOnce(sourcesUrl + "abc/src?offset=0", collection); const expectedActions = [ { type: FETCH_SOURCES_PENDING, - itemId: "scm/core/abc/src" + itemId: "scm/core/abc/src/", + payload: { + hunk: 0, + updatePending: false, + pending: true, + sources: {} + } }, { type: FETCH_SOURCES_SUCCESS, - itemId: "scm/core/abc/src", - payload: { updatePending: false, sources: collection } + itemId: "scm/core/abc/src/", + payload: { + hunk: 0, + updatePending: false, + pending: false, + sources: collection + } } ]; @@ -166,7 +191,7 @@ describe("sources fetch", () => { const actions = store.getActions(); expect(actions[0].type).toBe(FETCH_SOURCES_PENDING); expect(actions[1].type).toBe(FETCH_SOURCES_FAILURE); - expect(actions[1].itemId).toBe("scm/core/_/"); + expect(actions[1].itemId).toBe("scm/core/_//"); expect(actions[1].payload).toBeDefined(); }); }); @@ -180,16 +205,18 @@ describe("reducer tests", () => { it("should store the collection, without revision and path", () => { const expectedState = { - "scm/core/_/": { updatePending: false, sources: collection } + "scm/core/_//0": { pending: false, updatePending: false, sources: collection }, + "scm/core/_//hunkCount": 1 }; - expect(reducer({}, fetchSourcesSuccess(repository, "", "", collection))).toEqual(expectedState); + expect(reducer({}, fetchSourcesSuccess(repository, "", "", 0, collection))).toEqual(expectedState); }); it("should store the collection, with revision and path", () => { const expectedState = { - "scm/core/abc/src/main": { updatePending: false, sources: collection } + "scm/core/abc/src/main/0": { pending: false, updatePending: false, sources: collection }, + "scm/core/abc/src/main/hunkCount": 1 }; - expect(reducer({}, fetchSourcesSuccess(repository, "abc", "src/main", collection))).toEqual(expectedState); + expect(reducer({}, fetchSourcesSuccess(repository, "abc", "src/main", 0, collection))).toEqual(expectedState); }); }); @@ -197,7 +224,7 @@ describe("selector tests", () => { it("should return false if it is no directory", () => { const state = { sources: { - "scm/core/abc/src/main/package.json": { + "scm/core/abc/src/main/package.json/0": { sources: { noDirectory } } } @@ -208,7 +235,7 @@ describe("selector tests", () => { it("should return true if it is directory", () => { const state = { sources: { - "scm/core/abc/src": noDirectory + "scm/core/abc/src/0": noDirectory } }; expect(isDirectory(state, repository, "abc", "src")).toBe(true); @@ -221,7 +248,7 @@ describe("selector tests", () => { it("should return the source collection without revision and path", () => { const state = { sources: { - "scm/core/_/": { + "scm/core/_//0": { sources: collection } } @@ -232,7 +259,7 @@ describe("selector tests", () => { it("should return the source collection with revision and path", () => { const state = { sources: { - "scm/core/abc/src/main": { + "scm/core/abc/src/main/0": { sources: collection } } @@ -242,15 +269,26 @@ describe("selector tests", () => { it("should return true, when fetch sources is pending", () => { const state = { - pending: { - [FETCH_SOURCES + "/scm/core/_/"]: true + sources: { + "scm/core/_//0": { + pending: true, + sources: {} + } } }; - expect(isFetchSourcesPending(state, repository, "", "")).toEqual(true); + expect(isFetchSourcesPending(state, repository, "", "", 0)).toEqual(true); }); it("should return false, when fetch sources is not pending", () => { - expect(isFetchSourcesPending({}, repository, "", "")).toEqual(false); + const state = { + sources: { + "scm/core/_//0": { + pending: false, + sources: {} + } + } + }; + expect(isFetchSourcesPending(state, repository, "", "", 0)).toEqual(false); }); const error = new Error("incredible error from hell"); @@ -258,13 +296,13 @@ describe("selector tests", () => { it("should return error when fetch sources did fail", () => { const state = { failure: { - [FETCH_SOURCES + "/scm/core/_/"]: error + [FETCH_SOURCES + "/scm/core/_//"]: error } }; - expect(getFetchSourcesFailure(state, repository, "", "")).toEqual(error); + expect(getFetchSourcesFailure(state, repository, "", "", 0)).toEqual(error); }); it("should return undefined when fetch sources did not fail", () => { - expect(getFetchSourcesFailure({}, repository, "", "")).toBe(undefined); + expect(getFetchSourcesFailure({}, repository, "", "", 0)).toBe(undefined); }); }); diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts index 51c5eeb0e1..a12bf3ba0d 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -1,7 +1,6 @@ import * as types from "../../../modules/types"; import { Action, File, Link, Repository } from "@scm-manager/ui-types"; import { apiClient } from "@scm-manager/ui-components"; -import { isPending } from "../../../modules/pending"; import { getFailure } from "../../../modules/failure"; export const FETCH_SOURCES = "scm/repos/FETCH_SOURCES"; @@ -27,8 +26,18 @@ export function fetchSources(repository: Repository, revision: string, path: str updateSourcesPending(repository, revision, path, hunk, getSources(state, repository, revision, path, hunk)) ); } + + let offset = 0; + const hunkCount = getHunkCount(state, repository, revision, path); + for (let i = 0; i < hunkCount; ++i) { + const sources = getSources(state, repository, revision, path, i); + if (sources?._embedded.children) { + offset += sources._embedded.children.length; + } + } + return apiClient - .get(createUrl(repository, revision, path, hunk)) + .get(createUrl(repository, revision, path, offset)) .then(response => response.json()) .then((sources: File) => { dispatch(fetchSourcesSuccess(repository, revision, path, hunk, sources)); @@ -39,7 +48,7 @@ export function fetchSources(repository: Repository, revision: string, path: str }; } -function createUrl(repository: Repository, revision: string, path: string, hunk: number) { +function createUrl(repository: Repository, revision: string, path: string, offset: number) { const base = (repository._links.sources as Link).href; if (!revision && !path) { return base; @@ -47,14 +56,14 @@ function createUrl(repository: Repository, revision: string, path: string, hunk: // TODO handle trailing slash const pathDefined = path ? path : ""; - return `${base}${encodeURIComponent(revision)}/${pathDefined}?hunk=${hunk}`; + return `${base}${encodeURIComponent(revision)}/${pathDefined}?offset=${offset}`; } export function fetchSourcesPending(repository: Repository, revision: string, path: string, hunk: number): Action { return { type: FETCH_SOURCES_PENDING, - itemId: createItemId(repository, revision, path), - payload: { hunk, pending: true, sources: {} } + itemId: createItemId(repository, revision, path, ""), + payload: { hunk, pending: true, updatePending: false, sources: {} } }; } @@ -63,12 +72,12 @@ export function updateSourcesPending( revision: string, path: string, hunk: number, - currentSources: any + currentSources: File ): Action { return { type: FETCH_UPDATES_PENDING, - payload: { hunk, updatePending: true, sources: currentSources }, - itemId: createItemId(repository, revision, path) + payload: { hunk, pending: false, updatePending: true, sources: currentSources }, + itemId: createItemId(repository, revision, path, "") }; } @@ -82,7 +91,7 @@ export function fetchSourcesSuccess( return { type: FETCH_SOURCES_SUCCESS, payload: { hunk, pending: false, updatePending: false, sources }, - itemId: createItemId(repository, revision, path) + itemId: createItemId(repository, revision, path, "") }; } @@ -96,14 +105,14 @@ export function fetchSourcesFailure( return { type: FETCH_SOURCES_FAILURE, payload: error, - itemId: createItemId(repository, revision, path) + itemId: createItemId(repository, revision, path, "") }; } -function createItemId(repository: Repository, revision: string | undefined, path: string) { +function createItemId(repository: Repository, revision: string | undefined, path: string, hunk: number | string) { const revPart = revision ? revision : "_"; const pathPart = path ? path : ""; - return `${repository.namespace}/${repository.name}/${decodeURIComponent(revPart)}/${pathPart}`; + return `${repository.namespace}/${repository.name}/${decodeURIComponent(revPart)}/${pathPart}/${hunk}`; } // reducer @@ -114,18 +123,38 @@ export default function reducer( type: "UNKNOWN" } ): any { - if (action.itemId && (action.type === FETCH_SOURCES_SUCCESS || action.type === FETCH_UPDATES_PENDING)) { - console.log("adding payload to " + action.itemId + "/" + action.payload.hunk); + if (action.itemId && action.type === FETCH_SOURCES_SUCCESS) { return { ...state, - [action.itemId + "/hunkCount"]: action.payload.hunk + 1, - [action.itemId + "/" + action.payload.hunk]: { + [action.itemId + "hunkCount"]: action.payload.hunk + 1, + [action.itemId + action.payload.hunk]: { sources: action.payload.sources, - loading: false + updatePending: false, + pending: false } }; + } else if (action.itemId && action.type === FETCH_UPDATES_PENDING) { + return { + ...state, + [action.itemId + "hunkCount"]: action.payload.hunk + 1, + [action.itemId + action.payload.hunk]: { + sources: action.payload.sources, + updatePending: true, + pending: false + } + }; + } else if (action.itemId && action.type === FETCH_SOURCES_PENDING) { + return { + ...state, + [action.itemId + "hunkCount"]: action.payload.hunk + 1, + [action.itemId + action.payload.hunk]: { + updatePending: false, + pending: true + } + }; + } else { + return state; } - return state; } // selectors @@ -141,7 +170,7 @@ export function isDirectory(state: any, repository: Repository, revision: string export function getHunkCount(state: any, repository: Repository, revision: string | undefined, path: string): number { if (state.sources) { - const count = state.sources[createItemId(repository, revision, path) + "/hunkCount"]; + const count = state.sources[createItemId(repository, revision, path, "hunkCount")]; return count ? count : 0; } return 0; @@ -152,10 +181,10 @@ export function getSources( repository: Repository, revision: string | undefined, path: string, - hunk: number + hunk = 0 ): File | null | undefined { if (state.sources) { - return state.sources[createItemId(repository, revision, path) + "/" + hunk]?.sources; + return state.sources[createItemId(repository, revision, path, hunk)]?.sources; } return null; } @@ -165,9 +194,12 @@ export function isFetchSourcesPending( repository: Repository, revision: string, path: string, - hunk: number + hunk = 0 ): boolean { - return state && isPending(state, FETCH_SOURCES, createItemId(repository, revision, path)); + if (state.sources) { + return state.sources[createItemId(repository, revision, path, hunk)]?.pending; + } + return false; } export function isUpdateSourcePending( @@ -177,7 +209,10 @@ export function isUpdateSourcePending( path: string, hunk: number ): boolean { - return state?.sources && state.sources[createItemId(repository, revision, path) + "/" + hunk]?.updatePending; + if (state.sources) { + return state.sources[createItemId(repository, revision, path, hunk)]?.updatePending; + } + return false; } export function getFetchSourcesFailure( @@ -185,7 +220,7 @@ export function getFetchSourcesFailure( repository: Repository, revision: string, path: string, - hunk: number + hunk = 0 ): Error | null | undefined { - return getFailure(state, FETCH_SOURCES, createItemId(repository, revision, path)); + return getFailure(state, FETCH_SOURCES, createItemId(repository, revision, path, "")); } From c7bac50ff27505f1a5b407f8448237f9dce12820 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 20 Feb 2020 15:19:34 +0100 Subject: [PATCH 059/251] fixed authentication --- .../scm/api/v2/resources/AuthenticationResource.java | 9 ++++----- .../java/sonia/scm/api/v2/resources/IndexResource.java | 1 - 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java index 1c55b54fb1..e0c237aef6 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java @@ -43,16 +43,15 @@ import java.util.Optional; @SecurityScheme( name = "Basic Authentication", description = "HTTP Basic authentication with username and password", - scheme = "Basic", + scheme = "basic", type = SecuritySchemeType.HTTP ), @SecurityScheme( name = "Bearer Token Authentication", - in = SecuritySchemeIn.HEADER, - paramName = "Authorization", - scheme = "Bearer", + description = "Authentication with a jwt bearer token", + scheme = "bearer", bearerFormat = "JWT", - type = SecuritySchemeType.APIKEY + type = SecuritySchemeType.HTTP ) }) @OpenAPIDefinition(tags = { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java index 15e0d293af..8995e7f979 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java @@ -1,6 +1,5 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.TypeHint; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; From 42841512c579880efd8a366318a8280a7e5cc004 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 20 Feb 2020 16:17:06 +0100 Subject: [PATCH 060/251] added example to AuthenticationResource --- .../v2/resources/AuthenticationResource.java | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java index e0c237aef6..8e129570fd 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java @@ -3,10 +3,11 @@ package sonia.scm.api.v2.resources; import com.google.inject.Inject; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.security.SecurityScheme; import io.swagger.v3.oas.annotations.security.SecuritySchemes; @@ -79,9 +80,16 @@ public class AuthenticationResource { @POST @Path("access_token") + @Produces(MediaType.TEXT_PLAIN) @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - @Operation(summary = "Login via Form", description = "Form-based authentication.", tags = "Authentication") + @Operation( + summary = "Login via Form", + description = "Form-based authentication.", + tags = "Authentication", + hidden = true + ) @ApiResponse(responseCode = "200", description = "success") + @ApiResponse(responseCode = "204", description = "success without content") @ApiResponse(responseCode = "400", description = "bad request, required parameter is missing") @ApiResponse(responseCode = "401", description = "unauthorized, the specified username or password is wrong") @ApiResponse( @@ -102,9 +110,26 @@ public class AuthenticationResource { @POST @Path("access_token") + @Produces(MediaType.TEXT_PLAIN) @Consumes(MediaType.APPLICATION_JSON) - @Operation(summary = "Login via JSON", description = "JSON-based authentication.", tags = "Authentication") + @Operation( + summary = "Login via JSON", + description = "JSON-based authentication.", + tags = "Authentication", + requestBody = @RequestBody( + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = AuthenticationRequestDto.class), + examples = @ExampleObject( + name = "Simple login", + value = "{\n \"username\":\"scmadmin\",\n \"password\":\"scmadmin\",\n \"cookie\":false,\n \"grant_type\":\"password\"\n}", + summary = "Authenticate with username and password" + ) + ) + ) + ) @ApiResponse(responseCode = "200", description = "success") + @ApiResponse(responseCode = "204", description = "success without content") @ApiResponse(responseCode = "400", description = "bad request, required parameter is missing") @ApiResponse(responseCode = "401", description = "unauthorized, the specified username or password is wrong") @ApiResponse( From 39846314cb9ba3f1e6507c227dbd3e0c27a3bc06 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 20 Feb 2020 16:23:12 +0100 Subject: [PATCH 061/251] update storyshots --- .../src/__snapshots__/storyshots.test.ts.snap | 722 ++++++++++++------ 1 file changed, 482 insertions(+), 240 deletions(-) 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 17a73282c2..790c2cdc94 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -484,7 +484,7 @@ exports[`Storyshots DateFromNow Default 1`] = ` exports[`Storyshots Diff Binaries 1`] = `
    +
    + + + footer.support.title +
    + + +
    + `; @@ -32526,65 +32647,112 @@ exports[`Storyshots Layout|Footer With Avatar 1`] = `
    +
    + + + footer.support.title +
    + + +
    + `; @@ -32592,66 +32760,140 @@ exports[`Storyshots Layout|Footer With Plugin Links 1`] = `
    + + +
    - REST API - - - | - - CLI - -

    -
    -

    - Powered by - - - Cloudogu GmbH - - - | Learn more about - - - SCM-Manager - -

    -
    +
    + + + footer.support.title +
    + + +
    + `; @@ -34509,7 +34751,7 @@ PORT_NUMBER = exports[`Storyshots Table|Table Default 1`] = ` @@ -34527,7 +34769,7 @@ exports[`Storyshots Table|Table Default 1`] = ` > Last Name @@ -34612,7 +34854,7 @@ exports[`Storyshots Table|Table TextColumn 1`] = ` > Id From 264f6efd049ab8e25a7c3566cf398576441efb1b Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 21 Feb 2020 07:37:55 +0100 Subject: [PATCH 062/251] Fix loading detection --- .../src/repos/sources/components/FileTree.tsx | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx index 00cf1de981..f3ecba8982 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -105,7 +105,6 @@ class FileTree extends React.Component { return (
    {this.renderSourcesTable()} - {lastHunk.loading && } {lastHunk.tree?.truncated &&
    ); @@ -124,6 +123,10 @@ class FileTree extends React.Component { }); } + if (hunks.every(hunk => hunk.loading)) { + return ; + } + hunks .filter(hunk => !hunk.loading) .forEach(hunk => { @@ -144,23 +147,26 @@ class FileTree extends React.Component { } return ( -
    Name Description
    - - - - - - - - {binder.hasExtension("repos.sources.tree.row.right") && - - - {files.map((file: any) => ( - - ))} - -
    {t("sources.file-tree.name")}{t("sources.file-tree.length")}{t("sources.file-tree.commitDate")}{t("sources.file-tree.description")}} -
    + <> + + + + + + + + + {binder.hasExtension("repos.sources.tree.row.right") && + + + {files.map((file: any) => ( + + ))} + +
    {t("sources.file-tree.name")}{t("sources.file-tree.length")}{t("sources.file-tree.commitDate")}{t("sources.file-tree.description")}} +
    + {hunks[hunks.length - 1].loading && } + ); } return {t("sources.noSources")}; @@ -185,7 +191,6 @@ const mapStateToProps = (state: any, ownProps: Props) => { const hunkCount = getHunkCount(state, repository, revision, path); const hunks = []; for (let i = 0; i < hunkCount; ++i) { - console.log(`getting data for hunk ${i}`); const tree = getSources(state, repository, revision, path, i); const loading = isFetchSourcesPending(state, repository, revision, path, i); hunks.push({ From 722e38788b0fbd39d63525d030bdbb24a6f05770 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 21 Feb 2020 09:45:43 +0100 Subject: [PATCH 063/251] Fix update --- .../ui-webapp/src/repos/sources/components/FileTree.tsx | 8 ++++---- scm-ui/ui-webapp/src/repos/sources/modules/sources.ts | 4 +--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx index f3ecba8982..881a841a57 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -21,7 +21,6 @@ type Hunk = { tree: File; loading: boolean; error: Error; - updateSources: (hunk: number) => void; }; type Props = WithTranslation & { @@ -34,6 +33,7 @@ type Props = WithTranslation & { // dispatch props fetchSources: (repository: Repository, revision: string, path: string, hunk: number) => void; + updateSources: (hunk: number) => () => void; // context props match: any; @@ -67,10 +67,10 @@ class FileTree extends React.Component { componentDidUpdate(prevProps: Readonly, prevState: Readonly): void { if (prevState.stoppableUpdateHandler === this.state.stoppableUpdateHandler) { - const { hunks } = this.props; + const { hunks, updateSources } = this.props; hunks?.forEach((hunk, index) => { if (hunk.tree?._embedded?.children && hunk.tree._embedded.children.find(c => c.partialResult)) { - const stoppableUpdateHandler = setTimeout(hunk.updateSources, 3000); + const stoppableUpdateHandler = setTimeout(updateSources(index), 3000); this.setState(prevState => { return { stoppableUpdateHandler: [...prevState.stoppableUpdateHandler, stoppableUpdateHandler] @@ -177,7 +177,7 @@ const mapDispatchToProps = (dispatch: any, ownProps: Props) => { const { repository, revision, path } = ownProps; return { - updateSources: (hunk: number) => dispatch(fetchSources(repository, revision, path, false, hunk)), + updateSources: (hunk: number) => () => dispatch(fetchSources(repository, revision, path, false, hunk)), fetchSources: (repository: Repository, revision: string, path: string, hunk: number) => { dispatch(fetchSources(repository, revision, path, true, hunk)); } diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts index a12bf3ba0d..d37833c1c3 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -28,8 +28,7 @@ export function fetchSources(repository: Repository, revision: string, path: str } let offset = 0; - const hunkCount = getHunkCount(state, repository, revision, path); - for (let i = 0; i < hunkCount; ++i) { + for (let i = 0; i < hunk; ++i) { const sources = getSources(state, repository, revision, path, i); if (sources?._embedded.children) { offset += sources._embedded.children.length; @@ -136,7 +135,6 @@ export default function reducer( } else if (action.itemId && action.type === FETCH_UPDATES_PENDING) { return { ...state, - [action.itemId + "hunkCount"]: action.payload.hunk + 1, [action.itemId + action.payload.hunk]: { sources: action.payload.sources, updatePending: true, From a05c371910a531a78380a63658379c22458daf0a Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 21 Feb 2020 11:06:21 +0100 Subject: [PATCH 064/251] Render truncated info --- scm-ui/ui-webapp/public/locales/de/repos.json | 4 ++- scm-ui/ui-webapp/public/locales/en/repos.json | 4 ++- .../src/repos/sources/components/FileTree.tsx | 25 ++++++++++++++++--- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index 27909f338c..2b7b0f2ca4 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -128,7 +128,9 @@ "noSources": "Keine Sources in diesem Branch gefunden.", "extension": { "notBound": "Keine Erweiterung angebunden." - } + }, + "loadMore": "Laden", + "moreEntriesAvailable": "Es werden nur die ersten {{count}} Einträge angezeigt. Es sind weitere Einträge vorhanden." }, "permission": { "title": "Berechtigungen bearbeiten", diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index 793c01388d..d5a5a7fff6 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -128,7 +128,9 @@ "noSources": "No sources found for this branch.", "extension": { "notBound": "No extension bound." - } + }, + "loadMore": "Load", + "moreEntriesAvailable": "These are just the first {{count}} entries. There are more entries available." }, "permission": { "title": "Edit Permissions", diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx index 881a841a57..df70b44500 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -89,8 +89,27 @@ class FileTree extends React.Component { this.props.fetchSources(this.props.repository, this.props.revision, this.props.path, this.props.hunks.length); }; - render() { + renderTruncatedInfo = () => { const { hunks, t } = this.props; + const lastHunk = hunks[hunks.length - 1]; + const entryCount = hunks + .filter(hunk => hunk?.tree?._embedded?.children) + .map(hunk => hunk.tree._embedded.children.length) + .reduce((a, b) => a + b, 0); + if (lastHunk.tree?.truncated) { + return ( + +
    +
    {t("sources.moreEntriesAvailable", { count: entryCount })}
    +
    +
    + ); + } + }; + + render() { + const { hunks } = this.props; if (!hunks || hunks.length === 0) { return null; @@ -100,12 +119,10 @@ class FileTree extends React.Component { return hunk.error)[0]} />; } - const lastHunk = hunks[hunks.length - 1]; - return (
    {this.renderSourcesTable()} - {lastHunk.tree?.truncated &&
    ); } From a8694eb668348738f659a8fbc090bdb07b051e70 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 21 Feb 2020 11:50:04 +0100 Subject: [PATCH 065/251] Fix offset for svn repositories --- scm-ui/ui-webapp/src/repos/sources/modules/sources.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts index d37833c1c3..857229f8c2 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -50,7 +50,7 @@ export function fetchSources(repository: Repository, revision: string, path: str function createUrl(repository: Repository, revision: string, path: string, offset: number) { const base = (repository._links.sources as Link).href; if (!revision && !path) { - return base; + return `${base}?offset=${offset}`; } // TODO handle trailing slash From 4a82c541b293e198c602e598297f9d21efab19a9 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 21 Feb 2020 12:49:43 +0100 Subject: [PATCH 066/251] Sort svn files --- .../scm/repository/spi/SvnBrowseCommand.java | 6 ++++- .../repository/spi/SvnBrowseCommandTest.java | 27 +++++++------------ 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java index 1daaf5af61..a6e131fa4a 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java @@ -51,9 +51,12 @@ import sonia.scm.repository.SubRepository; import sonia.scm.repository.SvnUtil; import sonia.scm.util.Util; +import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; +import java.util.List; +import static java.util.Comparator.comparing; import static org.tmatesoft.svn.core.SVNErrorCode.FS_NO_SUCH_REVISION; import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.NotFoundException.notFound; @@ -130,7 +133,8 @@ public class SvnBrowseCommand extends AbstractSvnCommand FileObject parent, String basePath) throws SVNException { - Collection entries = svnRepository.getDir(parent.getPath(), revisionNumber, null, (Collection) null); + List entries = new ArrayList<>(svnRepository.getDir(parent.getPath(), revisionNumber, null, (Collection) null)); + entries.sort(comparing(SVNDirEntry::getName)); for (Iterator iterator = entries.iterator(); resultCount < request.getLimit() + request.getOffset() && iterator.hasNext(); ++resultCount) { SVNDirEntry entry = iterator.next(); FileObject child = createFileObject(request, svnRepository, revisionNumber, entry, basePath); diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java index 589f0d4107..0fc39c8174 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java @@ -39,6 +39,7 @@ import sonia.scm.repository.FileObject; import java.io.IOException; import java.util.Collection; +import java.util.Iterator; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; @@ -77,8 +78,9 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase Collection foList = foList1; - FileObject a = getFileObject(foList, "a.txt"); - FileObject c = getFileObject(foList, "c"); + Iterator iterator = foList.iterator(); + FileObject a = iterator.next(); + FileObject c = iterator.next(); assertFalse(a.isDirectory()); assertEquals("a.txt", a.getName()); @@ -113,20 +115,9 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase assertFalse(foList.isEmpty()); assertEquals(2, foList.size()); - FileObject d = null; - FileObject e = null; - - for (FileObject f : foList) - { - if ("d.txt".equals(f.getName())) - { - d = f; - } - else if ("e.txt".equals(f.getName())) - { - e = f; - } - } + Iterator iterator = foList.iterator(); + FileObject d = iterator.next(); + FileObject e = iterator.next(); assertNotNull(d); assertFalse(d.isDirectory()); @@ -198,7 +189,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase Collection foList = result.getFile().getChildren(); - assertThat(foList).extracting("name").containsExactlyInAnyOrder("a.txt"); + assertThat(foList).extracting("name").containsExactly("a.txt"); assertThat(result.getFile().isTruncated()).isTrue(); } @@ -212,7 +203,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase Collection foList = result.getFile().getChildren(); - assertThat(foList).extracting("name").containsExactlyInAnyOrder("c"); + assertThat(foList).extracting("name").containsExactly("c"); } /** From 7025fbbc2f0675863522b35ab6a77af66214c7ac Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Fri, 21 Feb 2020 12:34:59 +0000 Subject: [PATCH 067/251] Close branch feature/update_node_and_yarn From 736ea3d93f2b0097d57e431231bf184216be991e Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 21 Feb 2020 14:29:09 +0100 Subject: [PATCH 068/251] Sort git files --- .../scm/repository/spi/BrowseCommand.java | 15 +++++ .../java/sonia/scm/repository/GitUtil.java | 6 +- .../scm/repository/spi/GitBrowseCommand.java | 66 +++++++++++++++---- .../scm/repository/spi/SvnBrowseCommand.java | 2 +- .../repository/spi/SvnBrowseCommandTest.java | 6 +- 5 files changed, 78 insertions(+), 17 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java index ee37d6243e..7859c1845b 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java @@ -38,6 +38,9 @@ package sonia.scm.repository.spi; import sonia.scm.repository.BrowserResult; import java.io.IOException; +import java.util.List; +import java.util.Locale; +import java.util.function.Function; //~--- JDK imports ------------------------------------------------------------ @@ -60,4 +63,16 @@ public interface BrowseCommand * @throws IOException */ BrowserResult getBrowserResult(BrowseCommandRequest request) throws IOException; + + default void sort(List entries, Function isDirectory, Function nameOf) { + entries.sort((e1, e2) -> { + if (isDirectory.apply(e1).equals(isDirectory.apply(e2))) { + return nameOf.apply(e1).toLowerCase(Locale.ENGLISH).compareTo(nameOf.apply(e2).toLowerCase(Locale.ENGLISH)); + } else if (isDirectory.apply(e1)) { + return -1; + } else { + return 1; + } + }); + } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java index a93c1b5d81..e816aaf76d 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java @@ -749,9 +749,13 @@ public final class GitUtil } public static Optional getLfsPointer(org.eclipse.jgit.lib.Repository repo, TreeWalk treeWalk, Attributes attributes) throws IOException { + ObjectId blobId = treeWalk.getObjectId(0); + return getLfsPointer(repo, blobId, attributes); + } + + public static Optional getLfsPointer(org.eclipse.jgit.lib.Repository repo, ObjectId blobId, Attributes attributes) throws IOException { Attribute filter = attributes.get("filter"); if (filter != null && "lfs".equals(filter.getValue())) { - ObjectId blobId = treeWalk.getObjectId(0); try (InputStream is = repo.open(blobId, Constants.OBJ_BLOB).openStream()) { return of(LfsPointer.parseLfsPointer(is)); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java index f53b7ec0a4..fe75e6d1b1 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java @@ -69,7 +69,9 @@ import sonia.scm.web.lfs.LfsBlobStoreFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; @@ -158,14 +160,14 @@ public class GitBrowseCommand extends AbstractGitCommand } private FileObject createFileObject(org.eclipse.jgit.lib.Repository repo, - BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) + BrowseCommandRequest request, ObjectId revId, TreeEntry treeEntry) throws IOException { FileObject file = new FileObject(); - String path = treeWalk.getPathString(); + String path = treeEntry.getPathString(); - file.setName(treeWalk.getNameString()); + file.setName(treeEntry.getNameString()); file.setPath(path); SubRepository sub = null; @@ -183,7 +185,7 @@ public class GitBrowseCommand extends AbstractGitCommand } else { - ObjectLoader loader = repo.open(treeWalk.getObjectId(0)); + ObjectLoader loader = repo.open(treeEntry.getObjectId()); file.setDirectory(loader.getType() == Constants.OBJ_TREE); @@ -195,7 +197,7 @@ public class GitBrowseCommand extends AbstractGitCommand try (RevWalk walk = new RevWalk(repo)) { commit = walk.parseCommit(revId); } - Optional lfsPointer = getLfsPointer(repo, path, commit, treeWalk); + Optional lfsPointer = getLfsPointer(repo, path, commit, treeEntry); if (lfsPointer.isPresent()) { setFileLengthFromLfsBlob(lfsPointer.get(), file); @@ -253,11 +255,18 @@ public class GitBrowseCommand extends AbstractGitCommand } private FileObject findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { - List files = Lists.newArrayList(); - while (treeWalk.next() && ++resultCount <= request.getLimit() + request.getOffset()) - { + List entries = new ArrayList<>(); + while (treeWalk.next() && ++resultCount <= request.getLimit() + request.getOffset()) { + entries.add(new TreeEntry(repo, treeWalk)); + } + sort(entries, TreeEntry::isDirectory, TreeEntry::getNameString); - FileObject fileObject = createFileObject(repo, request, revId, treeWalk); + List files = Lists.newArrayList(); + Iterator entryIterator = entries.iterator(); + while (entryIterator.hasNext() && ++resultCount <= request.getLimit() + request.getOffset()) + { + TreeEntry entry = entryIterator.next(); + FileObject fileObject = createFileObject(repo, request, revId, entry); if (!fileObject.getPath().startsWith(parent.getPath())) { parent.setChildren(files); return fileObject; @@ -298,7 +307,7 @@ public class GitBrowseCommand extends AbstractGitCommand currentDepth++; if (currentDepth >= limit) { - return createFileObject(repo, request, revId, treeWalk); + return createFileObject(repo, request, revId, new TreeEntry(repo, treeWalk)); } else { treeWalk.enterSubtree(); } @@ -338,11 +347,11 @@ public class GitBrowseCommand extends AbstractGitCommand return null; } - private Optional getLfsPointer(org.eclipse.jgit.lib.Repository repo, String path, RevCommit commit, TreeWalk treeWalk) { + private Optional getLfsPointer(org.eclipse.jgit.lib.Repository repo, String path, RevCommit commit, TreeEntry treeWalk) { try { Attributes attributes = LfsFactory.getAttributesForPath(repo, path, commit); - return GitUtil.getLfsPointer(repo, treeWalk, attributes); + return GitUtil.getLfsPointer(repo, treeWalk.getObjectId(), attributes); } catch (IOException e) { throw new InternalRepositoryException(repository, "could not read lfs pointer", e); } @@ -448,4 +457,37 @@ public class GitBrowseCommand extends AbstractGitCommand return changed; } } + + private static class TreeEntry { + + private final String pathString; + private final String nameString; + private final ObjectId objectId; + private final boolean directory; + + public TreeEntry(org.eclipse.jgit.lib.Repository repo, TreeWalk treeWalk) throws IOException { + this.pathString = treeWalk.getPathString(); + this.nameString = treeWalk.getNameString(); + this.objectId = treeWalk.getObjectId(0); + ObjectLoader loader = repo.open(objectId); + + this.directory = loader.getType() == Constants.OBJ_TREE; + } + + public String getPathString() { + return pathString; + } + + public String getNameString() { + return nameString; + } + + public ObjectId getObjectId() { + return objectId; + } + + public boolean isDirectory() { + return directory; + } + } } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java index a6e131fa4a..423b82004a 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java @@ -134,7 +134,7 @@ public class SvnBrowseCommand extends AbstractSvnCommand throws SVNException { List entries = new ArrayList<>(svnRepository.getDir(parent.getPath(), revisionNumber, null, (Collection) null)); - entries.sort(comparing(SVNDirEntry::getName)); + sort(entries, entry -> entry.getKind() == SVNNodeKind.DIR, SVNDirEntry::getName); for (Iterator iterator = entries.iterator(); resultCount < request.getLimit() + request.getOffset() && iterator.hasNext(); ++resultCount) { SVNDirEntry entry = iterator.next(); FileObject child = createFileObject(request, svnRepository, revisionNumber, entry, basePath); diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java index 0fc39c8174..1c6e228a6d 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java @@ -79,8 +79,8 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase Collection foList = foList1; Iterator iterator = foList.iterator(); - FileObject a = iterator.next(); FileObject c = iterator.next(); + FileObject a = iterator.next(); assertFalse(a.isDirectory()); assertEquals("a.txt", a.getName()); @@ -189,7 +189,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase Collection foList = result.getFile().getChildren(); - assertThat(foList).extracting("name").containsExactly("a.txt"); + assertThat(foList).extracting("name").containsExactly("c"); assertThat(result.getFile().isTruncated()).isTrue(); } @@ -203,7 +203,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase Collection foList = result.getFile().getChildren(); - assertThat(foList).extracting("name").containsExactly("c"); + assertThat(foList).extracting("name").containsExactly("a.txt"); } /** From f0da22ad296a6b3d67c354e4a6acab0ab0db0ce8 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 21 Feb 2020 14:50:07 +0100 Subject: [PATCH 069/251] Fix git sort --- .../main/java/sonia/scm/repository/spi/GitBrowseCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java index fe75e6d1b1..8d25504b86 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java @@ -256,7 +256,7 @@ public class GitBrowseCommand extends AbstractGitCommand private FileObject findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { List entries = new ArrayList<>(); - while (treeWalk.next() && ++resultCount <= request.getLimit() + request.getOffset()) { + while (treeWalk.next()) { entries.add(new TreeEntry(repo, treeWalk)); } sort(entries, TreeEntry::isDirectory, TreeEntry::getNameString); From 58625ba606dfd51ac957a34bd1cb57edf3ebaac6 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 21 Feb 2020 15:00:44 +0100 Subject: [PATCH 070/251] Sort files with an upper case letter first --- .../scm/repository/spi/BrowseCommand.java | 2 +- .../scm/repository/spi/BrowseCommandTest.java | 70 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 scm-core/src/test/java/sonia/scm/repository/spi/BrowseCommandTest.java diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java index 7859c1845b..899bda077b 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java @@ -67,7 +67,7 @@ public interface BrowseCommand default void sort(List entries, Function isDirectory, Function nameOf) { entries.sort((e1, e2) -> { if (isDirectory.apply(e1).equals(isDirectory.apply(e2))) { - return nameOf.apply(e1).toLowerCase(Locale.ENGLISH).compareTo(nameOf.apply(e2).toLowerCase(Locale.ENGLISH)); + return nameOf.apply(e1).compareTo(nameOf.apply(e2)); } else if (isDirectory.apply(e1)) { return -1; } else { diff --git a/scm-core/src/test/java/sonia/scm/repository/spi/BrowseCommandTest.java b/scm-core/src/test/java/sonia/scm/repository/spi/BrowseCommandTest.java new file mode 100644 index 0000000000..1ed39c8366 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/spi/BrowseCommandTest.java @@ -0,0 +1,70 @@ +package sonia.scm.repository.spi; + +import org.junit.jupiter.api.Test; +import sonia.scm.repository.BrowserResult; + +import java.io.IOException; +import java.util.List; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static sonia.scm.repository.spi.BrowseCommandTest.Entry.d; +import static sonia.scm.repository.spi.BrowseCommandTest.Entry.f; + +class BrowseCommandTest implements BrowseCommand { + + @Test + void shouldSort() { + List entries = asList( + f("b.txt"), + f("a.txt"), + f("Dockerfile"), + f(".gitignore"), + d("src"), + f("README") + ); + + sort(entries, Entry::isDirecotry, Entry::getName); + + assertThat(entries).extracting("name") + .containsExactly( + "src", + ".gitignore", + "Dockerfile", + "README", + "a.txt", + "b.txt" + ); + } + + @Override + public BrowserResult getBrowserResult(BrowseCommandRequest request) throws IOException { + return null; + } + + static class Entry { + private final String name; + private final boolean direcotry; + + static Entry f(String name) { + return new Entry(name, false); + } + + static Entry d(String name) { + return new Entry(name, true); + } + + public Entry(String name, boolean direcotry) { + this.name = name; + this.direcotry = direcotry; + } + + public String getName() { + return name; + } + + public boolean isDirecotry() { + return direcotry; + } + } +} From 6d9256ed9905b841364a3a1b6e29680890fb9929 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Mon, 24 Feb 2020 08:55:17 +0100 Subject: [PATCH 071/251] Add content to 404 response --- .../v2/resources/AvailablePluginResource.java | 8 ++++++- .../api/v2/resources/BranchRootResource.java | 18 ++++++++++++--- .../api/v2/resources/DiffRootResource.java | 16 ++++++++++++-- .../v2/resources/GroupPermissionResource.java | 16 ++++++++++++-- .../scm/api/v2/resources/GroupResource.java | 16 ++++++++++++-- .../v2/resources/InstalledPluginResource.java | 8 ++++++- .../v2/resources/RepositoryTypeResource.java | 8 ++++++- .../v2/resources/UserPermissionResource.java | 22 ++++++++++++------- 8 files changed, 92 insertions(+), 20 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailablePluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailablePluginResource.java index bbd9444f01..7528b43eb2 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailablePluginResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailablePluginResource.java @@ -104,7 +104,13 @@ public class AvailablePluginResource { ) @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:read\" privilege") - @ApiResponse(responseCode = "404", description = "not found") + @ApiResponse( + responseCode = "404", + description = "not found, no plugin with the specified name found", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) @ApiResponse( responseCode = "500", description = "internal server error", 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 1a41508e51..b18be9207e 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 @@ -82,7 +82,13 @@ public class BranchRootResource { @ApiResponse(responseCode = "400", description = "branches not supported for given repository") @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the branch") - @ApiResponse(responseCode = "404", description = "not found, no branch with the specified name for the repository available or repository not found") + @ApiResponse( + responseCode = "404", + description = "not found, no branch with the specified name for the repository available or repository found", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) @ApiResponse( responseCode = "500", description = "internal server error", @@ -122,7 +128,13 @@ public class BranchRootResource { ) @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the changeset") - @ApiResponse(responseCode = "404", description = "not found, no changesets available in the repository") + @ApiResponse( + responseCode = "404", + description = "not found, no changesets available in the repository", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) @ApiResponse( responseCode = "500", description = "internal server error", @@ -137,7 +149,7 @@ public class BranchRootResource { @DefaultValue("0") @QueryParam("page") int page, @DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException { try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { - if (!branchExists(branchName, repositoryService)){ + if (!branchExists(branchName, repositoryService)) { throw notFound(entity(Branch.class, branchName).in(Repository.class, namespace + "/" + name)); } Repository repository = repositoryService.getRepository(); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java index 6124cab10b..a41bdb01a3 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java @@ -59,7 +59,13 @@ public class DiffRootResource { @ApiResponse(responseCode = "400", description = "bad request") @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the diff") - @ApiResponse(responseCode = "404", description = "not found, no revision with the specified param for the repository available or repository not found") + @ApiResponse( + responseCode = "404", + description = "not found, no revision with the specified param for the repository available or repository found", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) @ApiResponse( responseCode = "500", description = "internal server error", @@ -97,7 +103,13 @@ public class DiffRootResource { @ApiResponse(responseCode = "400", description = "bad request") @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the diff") - @ApiResponse(responseCode = "404", description = "not found, no revision with the specified param for the repository available or repository not found") + @ApiResponse( + responseCode = "404", + description = "not found, no revision with the specified param for the repository available or repository not found", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) @ApiResponse( responseCode = "500", description = "internal server error", diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java index b00719abbe..8e42ef43e3 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java @@ -50,7 +50,13 @@ public class GroupPermissionResource { )) @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the group") - @ApiResponse(responseCode = "404", description = "not found, no group with the specified id/name available") + @ApiResponse( + responseCode = "404", + description = "not found, no group with the specified id/name available", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) @ApiResponse( responseCode = "500", description = "internal server error", @@ -79,7 +85,13 @@ public class GroupPermissionResource { @ApiResponse(responseCode = "400", description = "invalid body") @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current group does not have the correct privilege") - @ApiResponse(responseCode = "404", description = "not found, no group with the specified id/name available") + @ApiResponse( + responseCode = "404", + description = "not found, no group with the specified id/name available", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) @ApiResponse( responseCode = "500", description = "internal server error", diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java index 0e54d6855d..2a209408db 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java @@ -56,7 +56,13 @@ public class GroupResource { ) @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the group") - @ApiResponse(responseCode = "404", description = "not found, no group with the specified id/name available") + @ApiResponse( + responseCode = "404", + description = "not found, no group with the specified id/name available", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) @ApiResponse( responseCode = "500", description = "internal server error", @@ -110,7 +116,13 @@ public class GroupResource { @ApiResponse(responseCode = "400", description = "invalid body, e.g. illegal change of id/group name") @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"group\" privilege") - @ApiResponse(responseCode = "404", description = "not found, no group with the specified id/name available") + @ApiResponse( + responseCode = "404", + description = "not found, no group with the specified id/name available", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) @ApiResponse( responseCode = "500", description = "internal server error", diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InstalledPluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InstalledPluginResource.java index d5c891c2df..a442c70f7e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InstalledPluginResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InstalledPluginResource.java @@ -126,7 +126,13 @@ public class InstalledPluginResource { ) @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:read\" privilege") - @ApiResponse(responseCode = "404", description = "not found, plugin by given id could not be found") + @ApiResponse( + responseCode = "404", + description = "not found, plugin by given id could not be found", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) @ApiResponse( responseCode = "500", description = "internal server error", diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeResource.java index fa1c5b66d3..b04ce46d98 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeResource.java @@ -45,7 +45,13 @@ public class RepositoryTypeResource { schema = @Schema(implementation = RepositoryTypeDto.class) ) ) - @ApiResponse(responseCode = "404", description = "not found, no repository type with the specified name available") + @ApiResponse( + responseCode = "404", + description = "not found, no repository type with the specified name available", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) @ApiResponse( responseCode = "500", description = "internal server error", diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java index bf8ff4b3c1..5611b32aeb 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java @@ -87,15 +87,21 @@ public class UserPermissionResource { @ApiResponse(responseCode = "400", description = "invalid body") @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the correct privilege") - @ApiResponse(responseCode = "404", description = "not found, no user with the specified id/name available") @ApiResponse( - responseCode = "500", - description = "internal server error", - content = @Content( - mediaType = VndMediaType.ERROR_TYPE, - schema = @Schema(implementation = ErrorDto.class) - ) - ) + responseCode = "404", + description = "not found, no user with the specified id/name available", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + ) + ) public Response overwritePermissions(@PathParam("id") String id, @Valid PermissionListDto newPermissions) { Collection permissionDescriptors = Arrays.stream(newPermissions.getPermissions()) .map(PermissionDescriptor::new) From 7e1e77af2b089e2c1d5053c83c280268bd476af5 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 24 Feb 2020 13:36:34 +0100 Subject: [PATCH 072/251] remove enunciate since we are using openapi now --- pom.xml | 13 - scm-core/pom.xml | 6 - scm-plugins/pom.xml | 94 ---- .../scm/legacy/LegacyRepositoryService.java | 8 - scm-webapp/pom.xml | 98 ---- scm-webapp/src/main/doc/enunciate.xml | 67 --- .../resources/RepositoryImportResource.java | 427 ++++++------------ .../api/v2/resources/BranchRootResource.java | 13 +- .../v2/resources/GroupCollectionResource.java | 13 +- .../RepositoryCollectionResource.java | 13 +- .../RepositoryRoleCollectionResource.java | 21 +- .../v2/resources/UserCollectionResource.java | 23 +- 12 files changed, 183 insertions(+), 613 deletions(-) delete mode 100644 scm-webapp/src/main/doc/enunciate.xml diff --git a/pom.xml b/pom.xml index 535b9d9af8..999f8f5020 100644 --- a/pom.xml +++ b/pom.xml @@ -184,12 +184,6 @@ true - - com.webcohesion.enunciate - enunciate-core-annotations - ${enunciate.version} - - org.mapstruct mapstruct-jdk8 @@ -453,12 +447,6 @@ 2.3 - - com.webcohesion.enunciate - enunciate-maven-plugin - ${enunciate.version} - - sonia.scm.maven smp-maven-plugin @@ -843,7 +831,6 @@ 2.1.1 4.4.1.Final 1.19.4 - 2.11.1 2.10.0 4.0 2.3.0 diff --git a/scm-core/pom.xml b/scm-core/pom.xml index 563ac0f40f..ec3b884fd6 100644 --- a/scm-core/pom.xml +++ b/scm-core/pom.xml @@ -137,12 +137,6 @@ provided - - - com.webcohesion.enunciate - enunciate-core-annotations - - diff --git a/scm-plugins/pom.xml b/scm-plugins/pom.xml index b74effd8ce..57999aa7d0 100644 --- a/scm-plugins/pom.xml +++ b/scm-plugins/pom.xml @@ -173,101 +173,7 @@ - - - - plugin-doc - - - - - - org.apache.maven.plugins - maven-resources-plugin - - - copy-enunciate-configuration - compile - - copy-resources - - - ${project.build.directory} - - - src/main/doc - true - - **/enunciate.xml - - - - - - - - - - com.webcohesion.enunciate - enunciate-maven-plugin - - - - docs - - compile - - - - ${project.build.directory}/enunciate.xml - ${project.build.directory} - restdocs - - - - com.webcohesion.enunciate - enunciate-top - ${enunciate.version} - - - com.webcohesion.enunciate - enunciate-swagger - - - - - com.webcohesion.enunciate - enunciate-lombok - ${enunciate.version} - - - - - - org.apache.maven.plugins - maven-assembly-plugin - - - src/main/doc/assembly.xml - - - - - package - - single - - - - - - - - - - - diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java index d6b923a927..282a802e2e 100644 --- a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java @@ -1,8 +1,6 @@ package sonia.scm.legacy; import com.google.inject.Inject; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; import sonia.scm.NotFoundException; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; @@ -26,12 +24,6 @@ public class LegacyRepositoryService { @GET @Path("{id}") @Produces(MediaType.APPLICATION_JSON) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repository:read:global\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) public NamespaceAndNameDto getNameAndNamespaceForRepositoryId(@PathParam("id") String repositoryId) { Repository repo = repositoryManager.get(repositoryId); if (repo == null) { diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index d8068f1bb8..aa52c2dc62 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -916,107 +916,9 @@ - - - - - - - - doc - - - - - - org.apache.maven.plugins - maven-resources-plugin - - - copy-enunciate-configuration - compile - - copy-resources - - - ${project.build.directory} - - - src/main/doc - true - - **/enunciate.xml - - - - - - - - - - com.webcohesion.enunciate - enunciate-maven-plugin - - - - docs - - compile - - - - ${project.build.directory}/enunciate.xml - ${project.build.directory} - restdocs - - - - com.webcohesion.enunciate - enunciate-top - ${enunciate.version} - - - com.webcohesion.enunciate - enunciate-swagger - - - - - com.webcohesion.enunciate - enunciate-lombok - ${enunciate.version} - - - org.mapstruct - mapstruct-processor - ${org.mapstruct.version} - - - - - - org.apache.maven.plugins - maven-assembly-plugin - - - src/main/doc/assembly.xml - - - - - package - - single - - - - - - diff --git a/scm-webapp/src/main/doc/enunciate.xml b/scm-webapp/src/main/doc/enunciate.xml deleted file mode 100644 index 225d2e0a2c..0000000000 --- a/scm-webapp/src/main/doc/enunciate.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - SCM-Manager API - - - SCM-Manager API -

    This page describes the RESTful Web Service API of SCM-Manager ${project.version}.

    - ]]> -
    - - - - - - - - - - - - - -
    diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java index 40d5458812..cc2f7792c7 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java @@ -1,19 +1,19 @@ /** * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. - * + *

    * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + *

    * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. + * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + *

    * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -24,13 +24,11 @@ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * + *

    * http://bitbucket.org/sdorra/scm-manager - * */ - package sonia.scm.api.rest.resources; import com.google.common.base.MoreObjects; @@ -38,10 +36,6 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.io.Files; import com.google.inject.Inject; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.ResponseHeader; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.FeatureNotSupportedException; @@ -100,8 +94,7 @@ import static com.google.common.base.Preconditions.checkNotNull; * @author Sebastian Sdorra */ // @Path("import/repositories") -public class RepositoryImportResource -{ +public class RepositoryImportResource { /** * the logger for RepositoryImportResource @@ -114,13 +107,12 @@ public class RepositoryImportResource /** * Constructs a new repository import resource. * - * @param manager repository manager + * @param manager repository manager * @param serviceFactory */ @Inject public RepositoryImportResource(RepositoryManager manager, - RepositoryServiceFactory serviceFactory) - { + RepositoryServiceFactory serviceFactory) { this.manager = manager; this.serviceFactory = serviceFactory; } @@ -133,37 +125,23 @@ public class RepositoryImportResource * bundle file is passed to the {@link UnbundleCommandBuilder}. Note: This method * requires admin privileges. * - * @param uriInfo uri info - * @param type repository type - * @param name name of the repository + * @param uriInfo uri info + * @param type repository type + * @param name name of the repository * @param inputStream input bundle - * @param compressed true if the bundle is gzip compressed - * + * @param compressed true if the bundle is gzip compressed * @return empty response with location header which points to the imported repository * @since 1.43 */ @POST @Path("{type}/bundle") - @StatusCodes({ - @ResponseCode(code = 201, condition = "created", additionalHeaders = { - @ResponseHeader(name = "Location", description = "uri to the imported repository") - }), - @ResponseCode( - code = 400, - condition = "bad request, the import bundle feature is not supported by this type of repositories or the parameters are not valid" - ), - @ResponseCode(code = 409, condition = "conflict, a repository with the name already exists"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) @Consumes(MediaType.MULTIPART_FORM_DATA) public Response importFromBundle(@Context UriInfo uriInfo, - @PathParam("type") String type, @FormParam("name") String name, - @FormParam("bundle") InputStream inputStream, @QueryParam("compressed") - @DefaultValue("false") boolean compressed) - { + @PathParam("type") String type, @FormParam("name") String name, + @FormParam("bundle") InputStream inputStream, @QueryParam("compressed") + @DefaultValue("false") boolean compressed) { Repository repository = doImportFromBundle(type, name, inputStream, - compressed); + compressed); return buildResponse(uriInfo, repository); } @@ -175,43 +153,28 @@ public class RepositoryImportResource * workaround of the javascript ui extjs. Note: This method requires admin * privileges. * - * @param type repository type - * @param name name of the repository + * @param type repository type + * @param name name of the repository * @param inputStream input bundle - * @param compressed true if the bundle is gzip compressed - * + * @param compressed true if the bundle is gzip compressed * @return empty response with location header which points to the imported - * repository + * repository * @since 1.43 */ @POST @Path("{type}/bundle.html") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode( - code = 400, - condition = "bad request, the import bundle feature is not supported by this type of repositories or the parameters are not valid" - ), - @ResponseCode(code = 409, condition = "conflict, a repository with the name already exists"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(RestActionUploadResult.class) @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.TEXT_HTML) public Response importFromBundleUI(@PathParam("type") String type, - @FormParam("name") String name, - @FormParam("bundle") InputStream inputStream, @QueryParam("compressed") - @DefaultValue("false") boolean compressed) - { + @FormParam("name") String name, + @FormParam("bundle") InputStream inputStream, @QueryParam("compressed") + @DefaultValue("false") boolean compressed) { Response response; - try - { + try { doImportFromBundle(type, name, inputStream, compressed); response = Response.ok(new RestActionUploadResult(true)).build(); - } - catch (WebApplicationException ex) - { + } catch (WebApplicationException ex) { logger.warn("error durring bundle import", ex); response = Response.fromResponse(ex.getResponse()).entity( new RestActionUploadResult(false)).build(); @@ -227,31 +190,17 @@ public class RepositoryImportResource * repository. Note: This method requires admin privileges. * * @param uriInfo uri info - * @param type repository type + * @param type repository type * @param request request object - * * @return empty response with location header which points to the imported - * repository + * repository * @since 1.43 */ @POST @Path("{type}/url") - @StatusCodes({ - @ResponseCode(code = 201, condition = "created", additionalHeaders = { - @ResponseHeader(name = "Location", description = "uri to the imported repository") - }), - @ResponseCode( - code = 400, - condition = "bad request, the import feature is not supported by this type of repositories or the parameters are not valid" - ), - @ResponseCode(code = 409, condition = "conflict, a repository with the name already exists"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) - @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Response importFromUrl(@Context UriInfo uriInfo, - @PathParam("type") String type, UrlImportRequest request) - { + @PathParam("type") String type, UrlImportRequest request) { RepositoryPermissions.create().check(); checkNotNull(request, "request is required"); checkArgument(!Strings.isNullOrEmpty(request.getName()), @@ -268,17 +217,12 @@ public class RepositoryImportResource Repository repository = create(type, request.getName()); RepositoryService service = null; - try - { + try { service = serviceFactory.create(repository); service.getPullCommand().pull(request.getUrl()); - } - catch (IOException ex) - { + } catch (IOException ex) { handleImportFailure(ex, repository); - } - finally - { + } finally { IOUtil.close(service); } @@ -290,23 +234,12 @@ public class RepositoryImportResource * directory. Note: This method requires admin privileges. * * @param type repository type - * * @return imported repositories */ @POST @Path("{type}") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode( - code = 400, - condition = "bad request, the import feature is not supported by this type of repositories" - ), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(Repository[].class) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public Response importRepositories(@PathParam("type") String type) - { + @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + public Response importRepositories(@PathParam("type") String type) { RepositoryPermissions.create().check(); List repositories = new ArrayList(); @@ -315,7 +248,8 @@ public class RepositoryImportResource //J- return Response.ok( - new GenericEntity>(repositories) {} + new GenericEntity>(repositories) { + } ).build(); //J+ } @@ -327,32 +261,22 @@ public class RepositoryImportResource * @return imported repositories */ @POST - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode( - code = 400, - condition = "bad request, the import feature is not supported by this type of repositories" - ), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(Repository[].class) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public Response importRepositories() - { + @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + public Response importRepositories() { RepositoryPermissions.create().check(); logger.info("start directory import for all supported repository types"); List repositories = new ArrayList(); - for (Type t : findImportableTypes()) - { + for (Type t : findImportableTypes()) { importFromDirectory(repositories, t.getName()); } //J- return Response.ok( - new GenericEntity>(repositories) {} + new GenericEntity>(repositories) { + } ).build(); //J+ } @@ -363,72 +287,50 @@ public class RepositoryImportResource * of failed directories. Note: This method requires admin privileges. * * @param type repository type - * * @return imported repositories * @since 1.43 */ @POST @Path("{type}/directory") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode( - code = 400, - condition = "bad request, the import feature is not supported by this type of repositories" - ), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(ImportResult.class) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Response importRepositoriesFromDirectory( - @PathParam("type") String type) - { + @PathParam("type") String type) { RepositoryPermissions.create().check(); Response response; RepositoryHandler handler = manager.getHandler(type); - if (handler != null) - { + if (handler != null) { logger.info("start directory import for repository type {}", type); - try - { + try { ImportResult result; ImportHandler importHandler = handler.getImportHandler(); - if (importHandler instanceof AdvancedImportHandler) - { + if (importHandler instanceof AdvancedImportHandler) { logger.debug("start directory import, using advanced import handler"); result = ((AdvancedImportHandler) importHandler) .importRepositoriesFromDirectory(manager); - } - else - { + } else { logger.debug("start directory import, using normal import handler"); result = new ImportResult(importHandler.importRepositories(manager), ImmutableList.of()); } response = Response.ok(result).build(); - } - catch (FeatureNotSupportedException ex) - { + } catch (FeatureNotSupportedException ex) { logger .warn( "import feature is not supported by repository handler for type " .concat(type), ex); response = Response.status(Response.Status.BAD_REQUEST).build(); - } - catch (IOException ex) - { + } catch (IOException ex) { logger.warn("exception occured durring directory import", ex); response = Response.serverError().build(); } - } - else - { + } else { logger.warn("could not find reposiotry handler for type {}", type); response = Response.status(Response.Status.BAD_REQUEST).build(); } @@ -445,25 +347,16 @@ public class RepositoryImportResource * @return list of repository types */ @GET - @TypeHint(Type[].class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode( - code = 400, - condition = "bad request, the import feature is not supported by this type of repositories" - ), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public Response getImportableTypes() - { + @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + public Response getImportableTypes() { RepositoryPermissions.create().check(); List types = findImportableTypes(); //J- return Response.ok( - new GenericEntity>(types) {} + new GenericEntity>(types) { + } ).build(); //J+ } @@ -473,16 +366,13 @@ public class RepositoryImportResource /** * Build rest response for repository. * - * - * @param uriInfo uri info + * @param uriInfo uri info * @param repository imported repository - * * @return rest response */ - private Response buildResponse(UriInfo uriInfo, Repository repository) - { + private Response buildResponse(UriInfo uriInfo, Repository repository) { URI location = uriInfo.getBaseUriBuilder().path( - RepositoryResource.class).path(repository.getId()).build(); + RepositoryResource.class).path(repository.getId()).build(); return Response.created(location).build(); } @@ -490,15 +380,12 @@ public class RepositoryImportResource /** * Check repository type for support for the given command. * - * - * @param type repository type - * @param cmd command + * @param type repository type + * @param cmd command * @param request request object */ - private void checkSupport(Type type, Command cmd, Object request) - { - if (!(type instanceof RepositoryType)) - { + private void checkSupport(Type type, Command cmd, Object request) { + if (!(type instanceof RepositoryType)) { logger.warn("type {} is not a repository type", type.getName()); throw new WebApplicationException(Response.Status.BAD_REQUEST); @@ -506,8 +393,7 @@ public class RepositoryImportResource Set cmds = ((RepositoryType) type).getSupportedCommands(); - if (!cmds.contains(cmd)) - { + if (!cmds.contains(cmd)) { logger.warn("type {} does not support this type of import: {}", type.getName(), request); @@ -518,24 +404,18 @@ public class RepositoryImportResource /** * Creates a new repository with the given name and type. * - * * @param type repository type * @param name repository name - * * @return newly created repository */ - private Repository create(String type, String name) - { + private Repository create(String type, String name) { Repository repository = null; - try - { + try { // TODO #8783 // repository = new Repository(null, type, name); manager.create(repository); - } - catch (InternalRepositoryException ex) - { + } catch (InternalRepositoryException ex) { handleGenericCreationFailure(ex, type, name); } @@ -545,17 +425,14 @@ public class RepositoryImportResource /** * Start bundle import. * - * - * @param type repository type - * @param name name of the repository + * @param type repository type + * @param name name of the repository * @param inputStream bundle stream - * @param compressed true if the bundle is gzip compressed - * + * @param compressed true if the bundle is gzip compressed * @return imported repository */ private Repository doImportFromBundle(String type, String name, - InputStream inputStream, boolean compressed) - { + InputStream inputStream, boolean compressed) { RepositoryPermissions.create().check(); checkArgument(!Strings.isNullOrEmpty(name), @@ -564,8 +441,7 @@ public class RepositoryImportResource Repository repository; - try - { + try { Type t = type(type); checkSupport(t, Command.UNBUNDLE, "bundle"); @@ -576,26 +452,19 @@ public class RepositoryImportResource File file = File.createTempFile("scm-import-", ".bundle"); - try - { + try { long length = Files.asByteSink(file).writeFrom(inputStream); logger.info("copied {} bytes to temp, start bundle import", length); service = serviceFactory.create(repository); service.getUnbundleCommand().setCompressed(compressed).unbundle(file); - } - catch (InternalRepositoryException ex) - { + } catch (InternalRepositoryException ex) { handleImportFailure(ex, repository); - } - finally - { + } finally { IOUtil.close(service); IOUtil.delete(file); } - } - catch (IOException ex) - { + } catch (IOException ex) { logger.warn("could not create temporary file", ex); throw new WebApplicationException(ex); @@ -607,42 +476,29 @@ public class RepositoryImportResource /** * Method description * - * * @return */ - private List findImportableTypes() - { + private List findImportableTypes() { List types = new ArrayList(); Collection handlerTypes = manager.getTypes(); - for (Type t : handlerTypes) - { + for (Type t : handlerTypes) { RepositoryHandler handler = manager.getHandler(t.getName()); - if (handler != null) - { - try - { - if (handler.getImportHandler() != null) - { + if (handler != null) { + try { + if (handler.getImportHandler() != null) { types.add(t); } - } - catch (FeatureNotSupportedException ex) - { - if (logger.isTraceEnabled()) - { + } catch (FeatureNotSupportedException ex) { + if (logger.isTraceEnabled()) { logger.trace("import handler is not supported", ex); - } - else if (logger.isInfoEnabled()) - { + } else if (logger.isInfoEnabled()) { logger.info("{} handler does not support import of repositories", t.getName()); } } - } - else if (logger.isWarnEnabled()) - { + } else if (logger.isWarnEnabled()) { logger.warn("could not find handler for type {}", t.getName()); } } @@ -653,14 +509,12 @@ public class RepositoryImportResource /** * Handle creation failures. * - * - * @param ex exception + * @param ex exception * @param type repository type * @param name name of the repository */ private void handleGenericCreationFailure(Exception ex, String type, - String name) - { + String name) { logger.error(String.format("could not create repository %s with type %s", type, name), ex); @@ -670,20 +524,15 @@ public class RepositoryImportResource /** * Handle import failures. * - * - * @param ex exception + * @param ex exception * @param repository repository */ - private void handleImportFailure(Exception ex, Repository repository) - { + private void handleImportFailure(Exception ex, Repository repository) { logger.error("import for repository failed, delete repository", ex); - try - { + try { manager.delete(repository); - } - catch (InternalRepositoryException | NotFoundException e) - { + } catch (InternalRepositoryException | NotFoundException e) { logger.error("can not delete repository after import failure", e); } @@ -694,27 +543,21 @@ public class RepositoryImportResource /** * Import repositories from a specific type. * - * * @param repositories repository list - * @param type type of repository + * @param type type of repository */ - private void importFromDirectory(List repositories, String type) - { + private void importFromDirectory(List repositories, String type) { RepositoryHandler handler = manager.getHandler(type); - if (handler != null) - { + if (handler != null) { logger.info("start directory import for repository type {}", type); - try - { + try { List repositoryNames = handler.getImportHandler().importRepositories(manager); - if (repositoryNames != null) - { - for (String repositoryName : repositoryNames) - { + if (repositoryNames != null) { + for (String repositoryName : repositoryNames) { // TODO #8783 /*Repository repository = null; //manager.get(type, repositoryName); @@ -729,22 +572,14 @@ public class RepositoryImportResource }*/ } } - } - catch (FeatureNotSupportedException ex) - { + } catch (FeatureNotSupportedException ex) { throw new WebApplicationException(ex, Response.Status.BAD_REQUEST); - } - catch (IOException ex) - { + } catch (IOException ex) { + throw new WebApplicationException(ex); + } catch (InternalRepositoryException ex) { throw new WebApplicationException(ex); } - catch (InternalRepositoryException ex) - { - throw new WebApplicationException(ex); - } - } - else - { + } else { throw new WebApplicationException(Response.Status.BAD_REQUEST); } } @@ -752,17 +587,13 @@ public class RepositoryImportResource /** * Method description * - * * @param type - * * @return */ - private Type type(String type) - { + private Type type(String type) { RepositoryHandler handler = manager.getHandler(type); - if (handler == null) - { + if (handler == null) { logger.warn("no handler for type {} found", type); throw new WebApplicationException(Response.Status.NOT_FOUND); @@ -778,24 +609,21 @@ public class RepositoryImportResource */ @XmlRootElement(name = "import") @XmlAccessorType(XmlAccessType.FIELD) - public static class UrlImportRequest - { + public static class UrlImportRequest { /** * Constructs ... - * */ - public UrlImportRequest() {} + public UrlImportRequest() { + } /** * Constructs a new {@link UrlImportRequest} * - * * @param name name of the repository - * @param url external url of the repository + * @param url external url of the repository */ - public UrlImportRequest(String name, String url) - { + public UrlImportRequest(String name, String url) { this.name = name; this.url = url; } @@ -806,13 +634,12 @@ public class RepositoryImportResource * {@inheritDoc} */ @Override - public String toString() - { + public String toString() { //J- return MoreObjects.toStringHelper(this) - .add("name", name) - .add("url", url) - .toString(); + .add("name", name) + .add("url", url) + .toString(); //J+ } @@ -821,40 +648,44 @@ public class RepositoryImportResource /** * Returns name of the repository. * - * * @return name of the repository */ - public String getName() - { + public String getName() { return name; } /** * Returns external url of the repository. * - * * @return external url of the repository */ - public String getUrl() - { + public String getUrl() { return url; } //~--- fields ------------------------------------------------------------- - /** name of the repository */ + /** + * name of the repository + */ private String name; - /** external url of the repository */ + /** + * external url of the repository + */ private String url; } //~--- fields --------------------------------------------------------------- - /** repository manager */ + /** + * repository manager + */ private final RepositoryManager manager; - /** repository service factory */ + /** + * repository service factory + */ private final RepositoryServiceFactory serviceFactory; } 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 b18be9207e..33c6fb0908 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 @@ -1,9 +1,8 @@ package sonia.scm.api.v2.resources; import com.google.common.base.Strings; -import com.webcohesion.enunciate.metadata.rs.ResponseHeader; -import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -181,7 +180,14 @@ public class BranchRootResource { @Path("") @Consumes(VndMediaType.BRANCH_REQUEST) @Operation(summary = "Create branch", description = "Creates a new branch.", tags = "Repository") - @ApiResponse(responseCode = "201", description = "create success") + @ApiResponse( + responseCode = "201", + description = "create success", + headers = @Header( + name = "Location", + description = "uri to the created branch" + ) + ) @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"push\" privilege") @ApiResponse(responseCode = "409", description = "conflict, a branch with this name already exists") @@ -192,7 +198,6 @@ public class BranchRootResource { mediaType = VndMediaType.ERROR_TYPE, schema = @Schema(implementation = ErrorDto.class) )) - @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created branch")) public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid BranchRequestDto branchRequest) throws IOException { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java index cfb498dcb3..455a82c4f9 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java @@ -1,8 +1,7 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseHeader; -import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -97,7 +96,14 @@ public class GroupCollectionResource { @Path("") @Consumes(VndMediaType.GROUP) @Operation(summary = "Create group", description = "Creates a new group.", tags = "Group") - @ApiResponse(responseCode = "201", description = "create success") + @ApiResponse( + responseCode = "201", + description = "create success", + headers = @Header( + name = "Location", + description = "uri to the created group" + ) + ) @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"group\" privilege") @ApiResponse(responseCode = "409", description = "conflict, a group with this name already exists") @@ -109,7 +115,6 @@ public class GroupCollectionResource { schema = @Schema(implementation = ErrorDto.class) ) ) - @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created group")) public Response create(@Valid GroupDto group) { return adapter.create(group, () -> dtoToGroupMapper.map(group), diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java index f4eb28770f..d7fd6a79a4 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java @@ -1,8 +1,7 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseHeader; -import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -105,7 +104,14 @@ public class RepositoryCollectionResource { @Path("") @Consumes(VndMediaType.REPOSITORY) @Operation(summary = "Create repository", description = "Creates a new repository.", tags = "Repository") - @ApiResponse(responseCode = "201", description = "create success") + @ApiResponse( + responseCode = "201", + description = "create success", + headers = @Header( + name = "Location", + description = "uri to the created repository" + ) + ) @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"repository\" privilege") @ApiResponse(responseCode = "409", description = "conflict, a repository with this name already exists") @@ -116,7 +122,6 @@ public class RepositoryCollectionResource { mediaType = VndMediaType.ERROR_TYPE, schema = @Schema(implementation = ErrorDto.class) )) - @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created repository")) public Response create(@Valid RepositoryDto repository, @QueryParam("initialize") boolean initialize) { AtomicReference reference = new AtomicReference<>(); Response response = adapter.create(repository, diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleCollectionResource.java index 60f39f774f..a039b0cb35 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleCollectionResource.java @@ -1,8 +1,7 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseHeader; -import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -72,12 +71,12 @@ public class RepositoryRoleCollectionResource { schema = @Schema(implementation = ErrorDto.class) )) public Response getAll(@DefaultValue("0") @QueryParam("page") int page, - @DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize, - @QueryParam("sortBy") String sortBy, - @DefaultValue("false") @QueryParam("desc") boolean desc + @DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize, + @QueryParam("sortBy") String sortBy, + @DefaultValue("false") @QueryParam("desc") boolean desc ) { return adapter.getAll(page, pageSize, x -> true, sortBy, desc, - pageResult -> repositoryRoleCollectionToDtoMapper.map(page, pageSize, pageResult)); + pageResult -> repositoryRoleCollectionToDtoMapper.map(page, pageSize, pageResult)); } /** @@ -92,7 +91,14 @@ public class RepositoryRoleCollectionResource { @Path("") @Consumes(VndMediaType.REPOSITORY_ROLE) @Operation(summary = "Create repository role", description = "Creates a new repository role.", tags = "Repository role") - @ApiResponse(responseCode = "201", description = "create success") + @ApiResponse( + responseCode = "201", + description = "create success", + headers = @Header( + name = "Location", + description = "uri to the created repository role" + ) + ) @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"repositoryRole\" privilege") @ApiResponse(responseCode = "409", description = "conflict, a repository role with this name already exists") @@ -103,7 +109,6 @@ public class RepositoryRoleCollectionResource { mediaType = VndMediaType.ERROR_TYPE, schema = @Schema(implementation = ErrorDto.class) )) - @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created repositoryRole")) public Response create(@Valid RepositoryRoleDto repositoryRole) { return adapter.create(repositoryRole, () -> dtoToRepositoryRoleMapper.map(repositoryRole), u -> resourceLinks.repositoryRole().self(u.getName())); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java index 72c0a534ef..ef8b230f8a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java @@ -1,8 +1,7 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseHeader; -import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -80,13 +79,13 @@ public class UserCollectionResource { schema = @Schema(implementation = ErrorDto.class) )) public Response getAll(@DefaultValue("0") @QueryParam("page") int page, - @DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize, - @QueryParam("sortBy") String sortBy, - @DefaultValue("false") @QueryParam("desc") boolean desc, - @DefaultValue("") @QueryParam("q") String search + @DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize, + @QueryParam("sortBy") String sortBy, + @DefaultValue("false") @QueryParam("desc") boolean desc, + @DefaultValue("") @QueryParam("q") String search ) { return adapter.getAll(page, pageSize, createSearchPredicate(search), sortBy, desc, - pageResult -> userCollectionToDtoMapper.map(page, pageSize, pageResult)); + pageResult -> userCollectionToDtoMapper.map(page, pageSize, pageResult)); } /** @@ -101,7 +100,14 @@ public class UserCollectionResource { @Path("") @Consumes(VndMediaType.USER) @Operation(summary = "Create user", description = "Creates a new user.", tags = "User") - @ApiResponse(responseCode = "201", description = "create success") + @ApiResponse( + responseCode = "201", + description = "create success", + headers = @Header( + name = "Location", + description = "uri to the created user" + ) + ) @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"user\" privilege") @ApiResponse(responseCode = "409", description = "conflict, a user with this name already exists") @@ -112,7 +118,6 @@ public class UserCollectionResource { mediaType = VndMediaType.ERROR_TYPE, schema = @Schema(implementation = ErrorDto.class) )) - @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created user")) public Response create(@Valid UserDto user) { return adapter.create(user, () -> dtoToUserMapper.map(user, passwordService.encryptPassword(user.getPassword())), u -> resourceLinks.user().self(u.getName())); } From a016710c3531305247a062954ce76b6447250565 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Mon, 24 Feb 2020 15:02:03 +0100 Subject: [PATCH 073/251] Sorted extension point entries with supplied extensionName --- CHANGELOG.md | 3 ++ .../src/config/ConfigurationBinder.tsx | 2 +- scm-ui/ui-extensions/src/binder.test.ts | 49 +++++++++++++------ scm-ui/ui-extensions/src/binder.ts | 28 ++++++++++- 4 files changed, 65 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd845f009c..3f40686a7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added +- Extension point entries with supplied extensionName are sorted ascending + ### Fixed - Modification for mercurial repositories with enabled XSRF protection diff --git a/scm-ui/ui-components/src/config/ConfigurationBinder.tsx b/scm-ui/ui-components/src/config/ConfigurationBinder.tsx index f2e3669a5e..d99c3d12ce 100644 --- a/scm-ui/ui-components/src/config/ConfigurationBinder.tsx +++ b/scm-ui/ui-components/src/config/ConfigurationBinder.tsx @@ -41,7 +41,7 @@ class ConfigurationBinder { }); // bind navigation link to extension point - binder.bind("admin.setting", ConfigNavLink, configPredicate); + binder.bind("admin.setting", ConfigNavLink, configPredicate, labelI18nKey); // route for global configuration, passes the link from the index resource to component const ConfigRoute = ({ url, links, ...additionalProps }: GlobalRouteProps) => { diff --git a/scm-ui/ui-extensions/src/binder.test.ts b/scm-ui/ui-extensions/src/binder.test.ts index 2bcd302a36..18cd969eb3 100644 --- a/scm-ui/ui-extensions/src/binder.test.ts +++ b/scm-ui/ui-extensions/src/binder.test.ts @@ -13,31 +13,31 @@ describe("binder tests", () => { }); it("should return the binded extensions", () => { - binder.bind("hitchhicker.trillian", "heartOfGold"); - binder.bind("hitchhicker.trillian", "earth"); + binder.bind("hitchhiker.trillian", "heartOfGold"); + binder.bind("hitchhiker.trillian", "earth"); - const extensions = binder.getExtensions("hitchhicker.trillian"); + const extensions = binder.getExtensions("hitchhiker.trillian"); expect(extensions).toEqual(["heartOfGold", "earth"]); }); it("should return the first bound extension", () => { - binder.bind("hitchhicker.trillian", "heartOfGold"); - binder.bind("hitchhicker.trillian", "earth"); + binder.bind("hitchhiker.trillian", "heartOfGold"); + binder.bind("hitchhiker.trillian", "earth"); - expect(binder.getExtension("hitchhicker.trillian")).toBe("heartOfGold"); + expect(binder.getExtension("hitchhiker.trillian")).toBe("heartOfGold"); }); it("should return null if no extension was bound", () => { - expect(binder.getExtension("hitchhicker.trillian")).toBe(null); + expect(binder.getExtension("hitchhiker.trillian")).toBe(null); }); it("should return true, if an extension is bound", () => { - binder.bind("hitchhicker.trillian", "heartOfGold"); - expect(binder.hasExtension("hitchhicker.trillian")).toBe(true); + binder.bind("hitchhiker.trillian", "heartOfGold"); + expect(binder.hasExtension("hitchhiker.trillian")).toBe(true); }); it("should return false, if no extension is bound", () => { - expect(binder.hasExtension("hitchhicker.trillian")).toBe(false); + expect(binder.hasExtension("hitchhiker.trillian")).toBe(false); }); type Props = { @@ -45,13 +45,34 @@ describe("binder tests", () => { }; it("should return only extensions which predicates matches", () => { - binder.bind("hitchhicker.trillian", "heartOfGold", (props: Props) => props.category === "a"); - binder.bind("hitchhicker.trillian", "earth", (props: Props) => props.category === "b"); - binder.bind("hitchhicker.trillian", "earth2", (props: Props) => props.category === "a"); + binder.bind("hitchhiker.trillian", "heartOfGold", (props: Props) => props.category === "a"); + binder.bind("hitchhiker.trillian", "earth", (props: Props) => props.category === "b"); + binder.bind("hitchhiker.trillian", "earth2", (props: Props) => props.category === "a"); - const extensions = binder.getExtensions("hitchhicker.trillian", { + const extensions = binder.getExtensions("hitchhiker.trillian", { category: "b" }); expect(extensions).toEqual(["earth"]); }); + + it("should return extensions in ascending order", () => { + binder.bind("hitchhiker.trillian", "planetA", () => true, "zeroWaste"); + binder.bind("hitchhiker.trillian", "planetB", () => true, "EPSILON"); + binder.bind("hitchhiker.trillian", "planetC", () => true, "emptyBin"); + binder.bind("hitchhiker.trillian", "planetD", () => true, "absolute"); + + const extensions = binder.getExtensions("hitchhiker.trillian"); + expect(extensions).toEqual(["planetD", "planetC", "planetB", "planetA"]); + }); + + it("should return extensions starting with entries with specified extensionName", () => { + binder.bind("hitchhiker.trillian", "planetA", () => true); + binder.bind("hitchhiker.trillian", "planetB", () => true, "zeroWaste"); + binder.bind("hitchhiker.trillian", "planetC", () => true); + binder.bind("hitchhiker.trillian", "planetD", () => true, "emptyBin"); + + const extensions = binder.getExtensions("hitchhiker.trillian"); + expect(extensions[0]).toEqual("planetD"); + expect(extensions[1]).toEqual("planetB"); + }); }); diff --git a/scm-ui/ui-extensions/src/binder.ts b/scm-ui/ui-extensions/src/binder.ts index a359973a50..e0077b0b67 100644 --- a/scm-ui/ui-extensions/src/binder.ts +++ b/scm-ui/ui-extensions/src/binder.ts @@ -3,6 +3,7 @@ type Predicate = (props: any) => boolean; type ExtensionRegistration = { predicate: Predicate; extension: any; + extensionName: string; }; /** @@ -25,13 +26,14 @@ export class Binder { * @param extension provided extension * @param predicate to decide if the extension gets rendered for the given props */ - bind(extensionPoint: string, extension: any, predicate?: Predicate) { + bind(extensionPoint: string, extension: any, predicate?: Predicate, extensionName?: string) { if (!this.extensionPoints[extensionPoint]) { this.extensionPoints[extensionPoint] = []; } const registration = { predicate: predicate ? predicate : () => true, - extension + extension, + extensionName: extensionName ? extensionName : "" }; this.extensionPoints[extensionPoint].push(registration); } @@ -61,6 +63,7 @@ export class Binder { if (props) { registrations = registrations.filter(reg => reg.predicate(props || {})); } + registrations.sort(this.sortExtensions); return registrations.map(reg => reg.extension); } @@ -70,6 +73,27 @@ export class Binder { hasExtension(extensionPoint: string, props?: object): boolean { return this.getExtensions(extensionPoint, props).length > 0; } + + /** + * Sort extensions in ascending order. + */ + sortExtensions = (a: ExtensionRegistration, b: ExtensionRegistration) => { + const regA = a.extensionName ? a.extensionName.toUpperCase() : ""; + const regB = b.extensionName ? b.extensionName.toUpperCase() : ""; + + if (regA === "" && regB === "") { + return 0; + } else if (regA === "") { + return 1; + } else if (regB === "") { + return -1; + } else if (regA > regB) { + return 1; + } else if (regA < regB) { + return -1; + } + return 0; + }; } // singleton binder From a5f27adc71073ef138c7a6da56b8ddadb5fc6aa0 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Mon, 24 Feb 2020 15:05:13 +0100 Subject: [PATCH 074/251] Described sort method in a more understandable way --- scm-ui/ui-extensions/src/binder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/ui-extensions/src/binder.ts b/scm-ui/ui-extensions/src/binder.ts index e0077b0b67..32c814b8fd 100644 --- a/scm-ui/ui-extensions/src/binder.ts +++ b/scm-ui/ui-extensions/src/binder.ts @@ -75,7 +75,7 @@ export class Binder { } /** - * Sort extensions in ascending order. + * Sort extensions in ascending order, starting with entries with specified extensionName. */ sortExtensions = (a: ExtensionRegistration, b: ExtensionRegistration) => { const regA = a.extensionName ? a.extensionName.toUpperCase() : ""; From 36469d64a1bfa59704b75441957b748bab830d89 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 24 Feb 2020 15:21:05 +0100 Subject: [PATCH 075/251] Describe REST API in Swagger UI --- scm-webapp/src/main/doc/openapi.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/scm-webapp/src/main/doc/openapi.md b/scm-webapp/src/main/doc/openapi.md index c014899c30..949dbd03a0 100644 --- a/scm-webapp/src/main/doc/openapi.md +++ b/scm-webapp/src/main/doc/openapi.md @@ -1,3 +1,17 @@ -# openapi docs from code +# OpenAPI REST documentation -describe hateoas +The following REST documentation describes all public endpoints of your SCM-Manager instance. +You can try the endpoints with or without authentication right on the swagger surface provided by the OpenAPI-Plugin. + +For authenticated requests please use the "Authorize" button and insert your preferred authentication method. +For basic authentication simply use your SCM-Manager credentials. If you want to use the bearer token authentication, you can generate an +valid token using the authentication endpoint and copy the response body. + +SCM-Manager defines a modern ["Level 3"-REST API](https://martinfowler.com/articles/richardsonMaturityModel.html). +Using the HATEOAS architecture for REST allows us to provide discoverable and self explanatory endpoint definitions. +The responses are build using the [HAL format](http://stateless.co/hal_specification.html) as JSON or XML. +HAL makes the API human-friendly and simplifies the communication between the frontend and the server using links and embedded resources. + +We highly suggest using the HAL links when creating new functions for the SCM-Manager since they are consistent and can be +permission checked before being append to the response. The links and embedded resources can also be used by plugins, which can +define new resources or enrich existing ones. From 24de6c89a5f14fa2b111df43564d7d7aec44bac3 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Mon, 24 Feb 2020 16:27:42 +0100 Subject: [PATCH 076/251] update jgit to version 5.6.1.202002131546-r-scm1 --- pom.xml | 2 +- .../java/sonia/scm/web/CollectingPackParserListener.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 6bd458e551..02d3d35fc8 100644 --- a/pom.xml +++ b/pom.xml @@ -849,7 +849,7 @@ 1.4.1 - 5.4.3.201909031940-r-scm1 + 5.6.1.202002131546-r-scm1 1.9.0-scm3 diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/CollectingPackParserListener.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/CollectingPackParserListener.java index fdb96d18d7..20873b3229 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/CollectingPackParserListener.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/CollectingPackParserListener.java @@ -39,8 +39,8 @@ import com.google.common.collect.ImmutableSet; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectIdSubclassMap; import org.eclipse.jgit.revwalk.RevObject; -import org.eclipse.jgit.transport.BaseReceivePack; -import org.eclipse.jgit.transport.BaseReceivePack.PackParserListener; +import org.eclipse.jgit.transport.ReceivePack; +import org.eclipse.jgit.transport.ReceivePack.PackParserListener; import org.eclipse.jgit.transport.PackParser; import org.slf4j.Logger; @@ -76,7 +76,7 @@ public class CollectingPackParserListener implements PackParserListener * * @return listener */ - public static CollectingPackParserListener get(BaseReceivePack pack) + public static CollectingPackParserListener get(ReceivePack pack) { PackParserListener listener = pack.getPackParserListener(); @@ -101,7 +101,7 @@ public class CollectingPackParserListener implements PackParserListener * * @param pack receive pack */ - public static void set(BaseReceivePack pack) + public static void set(ReceivePack pack) { logger.trace("apply collecting listener to receive pack"); pack.setPackParserListener(new CollectingPackParserListener()); From 76665b4dbe5b9105edb39cd6a8855e365210d7e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 25 Feb 2020 08:25:25 +0100 Subject: [PATCH 077/251] Sort hg files --- .../src/main/resources/sonia/scm/hg/ext/fileview.py | 3 ++- .../java/sonia/scm/repository/spi/HgBrowseCommandTest.java | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py index 86e6175d55..51e33349cf 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py @@ -153,7 +153,8 @@ class File_Walker: return path def walk(self, structure, parent = ""): - for key, value in structure.iteritems(): + sortedItems = sorted(structure.iteritems(), key = lambda item: item[1]) + for key, value in sortedItems: if key == FILE_MARKER: if value: for v in value: diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java index f74d036843..bf34a0ad92 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java @@ -190,7 +190,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { Collection foList = root.getChildren(); - assertThat(foList).extracting("name").containsExactlyInAnyOrder("a.txt", "b.txt"); + assertThat(foList).extracting("name").containsExactlyInAnyOrder("c", "a.txt"); assertThat(root.isTruncated()).isTrue(); } @@ -205,7 +205,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { Collection foList = root.getChildren(); - assertThat(foList).extracting("name").containsExactlyInAnyOrder("c", "f.txt"); + assertThat(foList).extracting("name").containsExactlyInAnyOrder("b.txt", "f.txt"); assertThat(root.isTruncated()).isFalse(); } From d85cfc23e216ca68c91552bf65ac778b898d284c Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Tue, 25 Feb 2020 07:34:31 +0000 Subject: [PATCH 078/251] fix typo in Changelog.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa1a8ed5aa..760da98469 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added footer extension points for links and avatar -- Create OpenAPI specification durring build +- Create OpenAPI specification during build ### Changed - New footer design From a557997fa4fc736061885125f73bc65dbb0ea4e0 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Tue, 25 Feb 2020 09:18:29 +0100 Subject: [PATCH 079/251] Simplify sort extensions method --- scm-ui/ui-extensions/src/binder.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scm-ui/ui-extensions/src/binder.ts b/scm-ui/ui-extensions/src/binder.ts index 32c814b8fd..693fbb655a 100644 --- a/scm-ui/ui-extensions/src/binder.ts +++ b/scm-ui/ui-extensions/src/binder.ts @@ -81,11 +81,9 @@ export class Binder { const regA = a.extensionName ? a.extensionName.toUpperCase() : ""; const regB = b.extensionName ? b.extensionName.toUpperCase() : ""; - if (regA === "" && regB === "") { - return 0; - } else if (regA === "") { + if (regA === "" && regB !== "") { return 1; - } else if (regB === "") { + } else if (regA !== "" && regB === "") { return -1; } else if (regA > regB) { return 1; From 7fe8b58e7d2aa147c6479efa26d2f4e6a98a8843 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Tue, 25 Feb 2020 09:49:23 +0100 Subject: [PATCH 080/251] make secondary navigation collapsable // save collapse status in local storage --- .../ui-components/src/navigation/NavLink.tsx | 10 ++- .../ui-components/src/navigation/Section.tsx | 46 ++++++++--- .../src/navigation/SubNavigation.tsx | 11 ++- .../src/repos/containers/RepositoryRoot.tsx | 78 ++++++++++++++----- scm-ui/ui-webapp/src/repos/modules/repos.ts | 16 +++- 5 files changed, 122 insertions(+), 39 deletions(-) diff --git a/scm-ui/ui-components/src/navigation/NavLink.tsx b/scm-ui/ui-components/src/navigation/NavLink.tsx index f87351d496..9a858dc5af 100644 --- a/scm-ui/ui-components/src/navigation/NavLink.tsx +++ b/scm-ui/ui-components/src/navigation/NavLink.tsx @@ -10,6 +10,7 @@ type Props = { label: string; activeOnlyWhenExact?: boolean; activeWhenMatch?: (route: any) => boolean; + collapsed: boolean; }; class NavLink extends React.Component { @@ -23,7 +24,7 @@ class NavLink extends React.Component { } renderLink = (route: any) => { - const { to, icon, label } = this.props; + const { to, icon, label, collapsed } = this.props; let showIcon = null; if (icon) { @@ -36,9 +37,12 @@ class NavLink extends React.Component { return (

  • - + {showIcon} - {label} + {collapsed ? null : label}
  • ); diff --git a/scm-ui/ui-components/src/navigation/Section.tsx b/scm-ui/ui-components/src/navigation/Section.tsx index b6f0542506..7890afe442 100644 --- a/scm-ui/ui-components/src/navigation/Section.tsx +++ b/scm-ui/ui-components/src/navigation/Section.tsx @@ -1,20 +1,44 @@ -import React, { ReactNode } from "react"; +import React, { FC, ReactNode } from "react"; +import Icon from "../Icon"; +import { Button } from "../buttons"; +import styled from "styled-components"; type Props = { label: string; children?: ReactNode; + collapsed?: boolean; + onCollapse?: (newStatus: boolean) => void; }; -class Section extends React.Component { - render() { - const { label, children } = this.props; - return ( -
    -

    {label}

    -
      {children}
    -
    - ); +const SmallButton = styled(Button)` + height: 1.5rem; + width: 1rem; + position: absolute; + right: 1.5rem; + > { + outline: none; } -} +`; + +const MenuLabel = styled.p` + min-height: 2.5rem; +`; + +const Section: FC = ({ label, children, collapsed, onCollapse }) => { + const childrenWithProps = React.Children.map(children, child => React.cloneElement(child, { collapsed: collapsed })); + return ( +
    + + {collapsed ? "" : label} + {onCollapse && ( + + + + )} + +
      {childrenWithProps}
    +
    + ); +}; export default Section; diff --git a/scm-ui/ui-components/src/navigation/SubNavigation.tsx b/scm-ui/ui-components/src/navigation/SubNavigation.tsx index 258658561a..4cb9d2b306 100644 --- a/scm-ui/ui-components/src/navigation/SubNavigation.tsx +++ b/scm-ui/ui-components/src/navigation/SubNavigation.tsx @@ -9,6 +9,8 @@ type Props = { activeOnlyWhenExact?: boolean; activeWhenMatch?: (route: any) => boolean; children?: ReactNode; + collapsed?: boolean; + onCollapsed?: (newStatus: boolean) => void; }; class SubNavigation extends React.Component { @@ -22,7 +24,7 @@ class SubNavigation extends React.Component { } renderLink = (route: any) => { - const { to, icon, label } = this.props; + const { to, icon, label, collapsed } = this.props; let defaultIcon = "fas fa-cog"; if (icon) { @@ -36,8 +38,11 @@ class SubNavigation extends React.Component { return (
  • - - {label} + + {collapsed ? "" : label} {children}
  • diff --git a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx index e0452416f7..62b482cbaa 100644 --- a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx @@ -1,12 +1,18 @@ import React from "react"; import { connect } from "react-redux"; -import { Redirect, Route, Switch } from "react-router-dom"; +import { Redirect, Route, Switch, RouteComponentProps } from "react-router-dom"; import { WithTranslation, withTranslation } from "react-i18next"; -import { History } from "history"; import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; import { Repository } from "@scm-manager/ui-types"; import { ErrorPage, Loading, Navigation, NavLink, Page, Section, SubNavigation } from "@scm-manager/ui-components"; -import { fetchRepoByName, getFetchRepoFailure, getRepository, isFetchRepoPending } from "../modules/repos"; +import { + fetchRepoByName, + getFetchRepoFailure, + getRepository, + isFetchRepoPending, + isRepositoryMenuCollapsed, + switchRepositoryMenuCollapsed +} from "../modules/repos"; import RepositoryDetails from "../components/RepositoryDetails"; import EditRepo from "./EditRepo"; import BranchesOverview from "../branches/containers/BranchesOverview"; @@ -21,29 +27,46 @@ import CodeOverview from "../codeSection/containers/CodeOverview"; import ChangesetView from "./ChangesetView"; import SourceExtensions from "../sources/containers/SourceExtensions"; -type Props = WithTranslation & { - namespace: string; - name: string; - repository: Repository; - loading: boolean; - error: Error; - repoLink: string; - indexLinks: object; +type Props = RouteComponentProps & + WithTranslation & { + namespace: string; + name: string; + repository: Repository; + loading: boolean; + error: Error; + repoLink: string; + indexLinks: object; - // dispatch functions - fetchRepoByName: (link: string, namespace: string, name: string) => void; + // dispatch functions + fetchRepoByName: (link: string, namespace: string, name: string) => void; + }; - // context props - history: History; - match: any; +type State = { + collapsedMenu: boolean; }; -class RepositoryRoot extends React.Component { +class RepositoryRoot extends React.Component { + constructor(props: Props) { + super(props); + this.state = { + collapsedMenu: isRepositoryMenuCollapsed() + }; + } componentDidMount() { const { fetchRepoByName, namespace, name, repoLink } = this.props; fetchRepoByName(repoLink, namespace, name); } + componentDidUpdate() { + if (this.state.collapsedMenu && this.isCollapseForbidden()) { + this.onCollapse(false); + } + } + + isCollapseForbidden= () => { + return this.props.location.pathname.includes("/settings/"); + }; + stripEndingSlash = (url: string) => { if (url.endsWith("/")) { return url.substring(0, url.length - 1); @@ -87,8 +110,13 @@ class RepositoryRoot extends React.Component { return `${url}/changesets`; }; + onCollapse = (newStatus: boolean) => { + this.setState({ collapsedMenu: newStatus }, () => switchRepositoryMenuCollapsed(newStatus)); + }; + render() { const { loading, error, indexLinks, repository, t } = this.props; + const { collapsedMenu } = this.state; if (error) { return ( @@ -119,7 +147,7 @@ class RepositoryRoot extends React.Component { return (
    -
    +
    @@ -169,9 +197,13 @@ class RepositoryRoot extends React.Component {
    -
    +
    -
    +
    this.onCollapse(!collapsedMenu) : undefined} + collapsed={collapsedMenu} + > { activeOnlyWhenExact={false} /> - + this.onCollapse(false)} + > diff --git a/scm-ui/ui-webapp/src/repos/modules/repos.ts b/scm-ui/ui-webapp/src/repos/modules/repos.ts index dbea9c24fd..ac9e8fd81b 100644 --- a/scm-ui/ui-webapp/src/repos/modules/repos.ts +++ b/scm-ui/ui-webapp/src/repos/modules/repos.ts @@ -155,7 +155,12 @@ export function fetchRepoFailure(namespace: string, name: string, error: Error): // create repo -export function createRepo(link: string, repository: Repository, initRepository: boolean, callback?: (repo: Repository) => void) { +export function createRepo( + link: string, + repository: Repository, + initRepository: boolean, + callback?: (repo: Repository) => void +) { return function(dispatch: any) { dispatch(createRepoPending()); const repoLink = initRepository ? link + "?initialize=true" : link; @@ -436,3 +441,12 @@ export function getPermissionsLink(state: object, namespace: string, name: strin const repo = getRepository(state, namespace, name); return repo && repo._links ? repo._links.permissions.href : undefined; } + +const REPOSITORY_NAVIGATION_COLLAPSED = "repository-menu-collapsed"; + +export function isRepositoryMenuCollapsed() { + return localStorage.getItem(REPOSITORY_NAVIGATION_COLLAPSED) === "true"; +} +export function switchRepositoryMenuCollapsed(newStatus: boolean) { + localStorage.setItem(REPOSITORY_NAVIGATION_COLLAPSED, String(newStatus)); +} From b3a9b8a42ccf3695c5d00f790e71a0718fa07282 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 25 Feb 2020 10:10:15 +0100 Subject: [PATCH 081/251] remove heading, because the one from swagger-ui is enough --- scm-webapp/src/main/doc/openapi.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/scm-webapp/src/main/doc/openapi.md b/scm-webapp/src/main/doc/openapi.md index 949dbd03a0..47ed0c1c83 100644 --- a/scm-webapp/src/main/doc/openapi.md +++ b/scm-webapp/src/main/doc/openapi.md @@ -1,5 +1,3 @@ -# OpenAPI REST documentation - The following REST documentation describes all public endpoints of your SCM-Manager instance. You can try the endpoints with or without authentication right on the swagger surface provided by the OpenAPI-Plugin. From 039ea46958e34e764c130538554a9977fddbef14 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 25 Feb 2020 10:12:04 +0100 Subject: [PATCH 082/251] improve openapi description --- scm-webapp/src/main/doc/openapi.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scm-webapp/src/main/doc/openapi.md b/scm-webapp/src/main/doc/openapi.md index 47ed0c1c83..bf565699d4 100644 --- a/scm-webapp/src/main/doc/openapi.md +++ b/scm-webapp/src/main/doc/openapi.md @@ -7,9 +7,9 @@ valid token using the authentication endpoint and copy the response body. SCM-Manager defines a modern ["Level 3"-REST API](https://martinfowler.com/articles/richardsonMaturityModel.html). Using the HATEOAS architecture for REST allows us to provide discoverable and self explanatory endpoint definitions. -The responses are build using the [HAL format](http://stateless.co/hal_specification.html) as JSON or XML. +The responses are build using the [HAL JSON format](http://stateless.co/hal_specification.html). HAL makes the API human-friendly and simplifies the communication between the frontend and the server using links and embedded resources. -We highly suggest using the HAL links when creating new functions for the SCM-Manager since they are consistent and can be +We highly suggest using HAL links when creating new functions for SCM-Manager since they are consistent and can be permission checked before being append to the response. The links and embedded resources can also be used by plugins, which can define new resources or enrich existing ones. From 2fdd7f31ffcdc0ff16acb2480adff29728cc597b Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 25 Feb 2020 09:12:45 +0000 Subject: [PATCH 083/251] Close branch feature/openapi_doc From 412cae3c21f6b57b8befaf3481b4da9a3ba82fa3 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 25 Feb 2020 11:12:04 +0100 Subject: [PATCH 084/251] do not try to archive restdocs package, because it does not exists anymore --- Jenkinsfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 43fece6567..8218808237 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -29,7 +29,7 @@ node('docker') { } stage('Build') { - mvn 'clean install -Pdoc -DskipTests' + mvn 'clean install -DskipTests' } stage('Unit Test') { @@ -67,7 +67,6 @@ node('docker') { stage('Archive') { archiveArtifacts 'scm-webapp/target/scm-webapp.war' archiveArtifacts 'scm-server/target/scm-server-app.*' - archiveArtifacts 'scm-webapp/target/scm-webapp-restdocs.zip' } stage('Docker') { From 2b0d4a65a078a59d0f7d51c70e9d331ad4bbffd4 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 25 Feb 2020 15:23:56 +0000 Subject: [PATCH 085/251] Close branch feature/sort_extensionpoint_entries From eee6cad1d350f3d5e760bf0fda9fb1973429503c Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Tue, 25 Feb 2020 17:15:23 +0100 Subject: [PATCH 086/251] make repository navigation fixed // add title for collapsed navigation items --- .../ui-components/src/navigation/NavLink.tsx | 5 ++-- .../ui-components/src/navigation/Section.tsx | 30 ++++++++++++++----- .../src/navigation/SubNavigation.tsx | 5 ++-- .../repos/components/RepositoryNavLink.tsx | 1 + .../src/repos/containers/RepositoryRoot.tsx | 4 +++ scm-ui/ui-webapp/src/repos/modules/repos.ts | 8 ++--- 6 files changed, 37 insertions(+), 16 deletions(-) diff --git a/scm-ui/ui-components/src/navigation/NavLink.tsx b/scm-ui/ui-components/src/navigation/NavLink.tsx index 9a858dc5af..bff7e71e1c 100644 --- a/scm-ui/ui-components/src/navigation/NavLink.tsx +++ b/scm-ui/ui-components/src/navigation/NavLink.tsx @@ -11,6 +11,7 @@ type Props = { activeOnlyWhenExact?: boolean; activeWhenMatch?: (route: any) => boolean; collapsed: boolean; + title?: string; }; class NavLink extends React.Component { @@ -24,7 +25,7 @@ class NavLink extends React.Component { } renderLink = (route: any) => { - const { to, icon, label, collapsed } = this.props; + const { to, icon, label, collapsed, title } = this.props; let showIcon = null; if (icon) { @@ -36,7 +37,7 @@ class NavLink extends React.Component { } return ( -
  • +
  • void; }; +const SectionContainer = styled.div` + position: ${props => (props.scrollPositionY > 210 ? "fixed" : "absolute")}; + top: ${props => props.scrollPositionY > 210 && "4.5rem"}; + width: ${props => (props.collapsed ? "5.5rem" : "20.5rem")}; +`; + const SmallButton = styled(Button)` height: 1.5rem; width: 1rem; position: absolute; right: 1.5rem; - > { - outline: none; - } `; const MenuLabel = styled.p` @@ -25,19 +27,31 @@ const MenuLabel = styled.p` `; const Section: FC = ({ label, children, collapsed, onCollapse }) => { + const [scrollPositionY, setScrollPositionY] = useState(0); + + useEffect(() => { + window.addEventListener("scroll", () => setScrollPositionY(window.pageYOffset)); + + return () => { + window.removeEventListener("scroll", () => setScrollPositionY(window.pageYOffset)); + }; + }, []); + const childrenWithProps = React.Children.map(children, child => React.cloneElement(child, { collapsed: collapsed })); + const arrowIcon = collapsed ? : ; + return ( -
    + {collapsed ? "" : label} {onCollapse && ( - + {arrowIcon} )}
      {childrenWithProps}
    -
    + ); }; diff --git a/scm-ui/ui-components/src/navigation/SubNavigation.tsx b/scm-ui/ui-components/src/navigation/SubNavigation.tsx index 4cb9d2b306..e254975e0c 100644 --- a/scm-ui/ui-components/src/navigation/SubNavigation.tsx +++ b/scm-ui/ui-components/src/navigation/SubNavigation.tsx @@ -11,6 +11,7 @@ type Props = { children?: ReactNode; collapsed?: boolean; onCollapsed?: (newStatus: boolean) => void; + title?: string }; class SubNavigation extends React.Component { @@ -24,7 +25,7 @@ class SubNavigation extends React.Component { } renderLink = (route: any) => { - const { to, icon, label, collapsed } = this.props; + const { to, icon, label, collapsed, title } = this.props; let defaultIcon = "fas fa-cog"; if (icon) { @@ -37,7 +38,7 @@ class SubNavigation extends React.Component { } return ( -
  • +
  • boolean; activeOnlyWhenExact: boolean; icon?: string; + title?: string; }; /** diff --git a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx index 62b482cbaa..96f941c21a 100644 --- a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx @@ -209,6 +209,7 @@ class RepositoryRoot extends React.Component { to={`${url}/info`} icon="fas fa-info-circle" label={t("repositoryRoot.menu.informationNavLink")} + title={t("repositoryRoot.menu.informationNavLink")} /> { label={t("repositoryRoot.menu.branchesNavLink")} activeWhenMatch={this.matchesBranches} activeOnlyWhenExact={false} + title={t("repositoryRoot.menu.branchesNavLink")} /> { label={t("repositoryRoot.menu.sourcesNavLink")} activeWhenMatch={this.matchesCode} activeOnlyWhenExact={false} + title={t("repositoryRoot.menu.sourcesNavLink")} /> this.onCollapse(false)} + title={t("repositoryRoot.menu.settingsNavLink")} > diff --git a/scm-ui/ui-webapp/src/repos/modules/repos.ts b/scm-ui/ui-webapp/src/repos/modules/repos.ts index ac9e8fd81b..5dde4e4104 100644 --- a/scm-ui/ui-webapp/src/repos/modules/repos.ts +++ b/scm-ui/ui-webapp/src/repos/modules/repos.ts @@ -442,11 +442,11 @@ export function getPermissionsLink(state: object, namespace: string, name: strin return repo && repo._links ? repo._links.permissions.href : undefined; } -const REPOSITORY_NAVIGATION_COLLAPSED = "repository-menu-collapsed"; +const REPOSITORY_MENU_COLLAPSED = "repository-menu-collapsed"; export function isRepositoryMenuCollapsed() { - return localStorage.getItem(REPOSITORY_NAVIGATION_COLLAPSED) === "true"; + return localStorage.getItem(REPOSITORY_MENU_COLLAPSED) === "true"; } -export function switchRepositoryMenuCollapsed(newStatus: boolean) { - localStorage.setItem(REPOSITORY_NAVIGATION_COLLAPSED, String(newStatus)); +export function switchRepositoryMenuCollapsed(status: boolean) { + localStorage.setItem(REPOSITORY_MENU_COLLAPSED, String(status)); } From c0e0ed3d17eab175bbf658072f3c89428883f312 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 25 Feb 2020 17:34:41 +0100 Subject: [PATCH 087/251] Fix tests --- .../sonia/scm/repository/spi/GitBrowseCommandTest.java | 8 ++++---- .../ui-webapp/src/repos/sources/modules/sources.test.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java index ce8214e2fa..23d3bfabca 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java @@ -87,7 +87,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { assertThat(foList) .extracting("name") - .containsExactly("a.txt", "b.txt", "c", "f.txt"); + .containsExactly("c", "a.txt", "b.txt", "f.txt"); } @Test @@ -100,7 +100,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { Collection foList = root.getChildren(); assertThat(foList) .extracting("name") - .containsExactly("a.txt", "c"); + .containsExactly("c", "a.txt"); } @Test @@ -207,7 +207,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { assertThat(foList) .extracting("name") - .containsExactly("a.txt", "b.txt", "c", "f.txt"); + .containsExactly("c", "a.txt", "b.txt", "f.txt"); FileObject c = findFile(foList, "c"); @@ -262,7 +262,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { Collection foList = root.getChildren(); - assertThat(foList).extracting("name").contains("c", "f.txt"); + assertThat(foList).extracting("name").contains("b.txt", "f.txt"); assertFalse(root.isTruncated()); } diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts index fdd7a9fc78..f49488a7df 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts @@ -118,7 +118,7 @@ describe("sources fetch", () => { }); it("should fetch the sources of the repository", () => { - fetchMock.getOnce(sourcesUrl, collection); + fetchMock.getOnce(sourcesUrl + "?offset=0", collection); const expectedActions = [ { @@ -182,7 +182,7 @@ describe("sources fetch", () => { }); it("should dispatch FETCH_SOURCES_FAILURE on server error", () => { - fetchMock.getOnce(sourcesUrl, { + fetchMock.getOnce(sourcesUrl + "?offset=0", { status: 500 }); From 72328159000d4cee7d91aaec7da68bce5294a381 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 26 Feb 2020 10:41:39 +0100 Subject: [PATCH 088/251] use react context to toggle collapsable repository menu --- .../src/__snapshots__/storyshots.test.ts.snap | 48 ++-- .../ui-components/src/navigation/NavLink.tsx | 2 +- .../ui-components/src/navigation/Section.tsx | 16 +- .../src/navigation/SubNavigation.tsx | 3 +- .../src/repos/containers/RepositoryRoot.tsx | 222 +++++++++--------- scm-ui/ui-webapp/src/repos/modules/repos.ts | 6 + 6 files changed, 160 insertions(+), 137 deletions(-) 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 0ec9bb0c76..4fefa33757 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -484,7 +484,7 @@ exports[`Storyshots DateFromNow Default 1`] = ` exports[`Storyshots Diff Binaries 1`] = `
  • + +
    -
    - + + ); } } diff --git a/scm-ui/ui-webapp/src/repos/modules/repos.ts b/scm-ui/ui-webapp/src/repos/modules/repos.ts index 5dde4e4104..b2c1150fc3 100644 --- a/scm-ui/ui-webapp/src/repos/modules/repos.ts +++ b/scm-ui/ui-webapp/src/repos/modules/repos.ts @@ -3,6 +3,7 @@ import * as types from "../../modules/types"; import { Action, Repository, RepositoryCollection } from "@scm-manager/ui-types"; import { isPending } from "../../modules/pending"; import { getFailure } from "../../modules/failure"; +import React from "react"; export const FETCH_REPOS = "scm/repos/FETCH_REPOS"; export const FETCH_REPOS_PENDING = `${FETCH_REPOS}_${types.PENDING_SUFFIX}`; @@ -450,3 +451,8 @@ export function isRepositoryMenuCollapsed() { export function switchRepositoryMenuCollapsed(status: boolean) { localStorage.setItem(REPOSITORY_MENU_COLLAPSED, String(status)); } + +export const RepositoryContext = React.createContext({ + menuCollapsed: isRepositoryMenuCollapsed(), + toggleMenuCollapsed: () => {} +}); From 67192a203e7500854690f874a424e7188ea75430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 26 Feb 2020 10:54:16 +0100 Subject: [PATCH 089/251] Read and sort tree first before applying limit --- .../scm/repository/spi/GitBrowseCommand.java | 73 +++++++++++++------ .../repository/spi/GitBrowseCommandTest.java | 4 +- 2 files changed, 54 insertions(+), 23 deletions(-) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java index 8d25504b86..5bd25e549b 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java @@ -77,6 +77,7 @@ import java.util.Map; import java.util.Optional; import java.util.function.Consumer; +import static java.util.Collections.emptyList; import static java.util.Optional.empty; import static java.util.Optional.of; import static sonia.scm.ContextEntry.ContextBuilder.entity; @@ -254,34 +255,26 @@ public class GitBrowseCommand extends AbstractGitCommand return Strings.isNullOrEmpty(request.getPath()) || "/".equals(request.getPath()); } - private FileObject findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { - List entries = new ArrayList<>(); - while (treeWalk.next()) { - entries.add(new TreeEntry(repo, treeWalk)); - } - sort(entries, TreeEntry::isDirectory, TreeEntry::getNameString); + private void findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { + TreeEntry entry = new TreeEntry(); + createTree(parent.getPath(), entry, repo, request, treeWalk); + convertToFileObject(parent, repo, request, revId, entry.getChildren()); + } + private void convertToFileObject(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, List entries) throws IOException { List files = Lists.newArrayList(); Iterator entryIterator = entries.iterator(); while (entryIterator.hasNext() && ++resultCount <= request.getLimit() + request.getOffset()) { TreeEntry entry = entryIterator.next(); FileObject fileObject = createFileObject(repo, request, revId, entry); - if (!fileObject.getPath().startsWith(parent.getPath())) { - parent.setChildren(files); - return fileObject; - } if (resultCount > request.getOffset()) { files.add(fileObject); } if (request.isRecursive() && fileObject.isDirectory()) { - treeWalk.enterSubtree(); - FileObject rc = findChildren(fileObject, repo, request, revId, treeWalk); - if (rc != null) { - files.add(rc); - } + convertToFileObject(fileObject, repo, request, revId, entry.getChildren()); } } @@ -290,8 +283,27 @@ public class GitBrowseCommand extends AbstractGitCommand if (resultCount > request.getLimit() + request.getOffset()) { parent.setTruncated(true); } + } - return null; + private Optional createTree(String path, TreeEntry parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, TreeWalk treeWalk) throws IOException { + List entries = new ArrayList<>(); + while (treeWalk.next()) { + TreeEntry treeEntry = new TreeEntry(repo, treeWalk); + if (!treeEntry.getPathString().startsWith(path)) { + parent.setChildren(entries); + return of(treeEntry); + } + + entries.add(treeEntry); + + if (request.isRecursive() && treeEntry.isDirectory()) { + treeWalk.enterSubtree(); + Optional surplus = createTree(treeEntry.getNameString(), treeEntry, repo, request, treeWalk); + surplus.ifPresent(entries::add); + } + } + parent.setChildren(entries); + return empty(); } private FileObject findFirstMatch(org.eclipse.jgit.lib.Repository repo, @@ -458,14 +470,22 @@ public class GitBrowseCommand extends AbstractGitCommand } } - private static class TreeEntry { + private class TreeEntry { private final String pathString; private final String nameString; private final ObjectId objectId; private final boolean directory; + private List children = emptyList(); - public TreeEntry(org.eclipse.jgit.lib.Repository repo, TreeWalk treeWalk) throws IOException { + TreeEntry() { + pathString = ""; + nameString = ""; + objectId = null; + directory = true; + } + + TreeEntry(org.eclipse.jgit.lib.Repository repo, TreeWalk treeWalk) throws IOException { this.pathString = treeWalk.getPathString(); this.nameString = treeWalk.getNameString(); this.objectId = treeWalk.getObjectId(0); @@ -474,20 +494,29 @@ public class GitBrowseCommand extends AbstractGitCommand this.directory = loader.getType() == Constants.OBJ_TREE; } - public String getPathString() { + String getPathString() { return pathString; } - public String getNameString() { + String getNameString() { return nameString; } - public ObjectId getObjectId() { + ObjectId getObjectId() { return objectId; } - public boolean isDirectory() { + boolean isDirectory() { return directory; } + + List getChildren() { + return children; + } + + void setChildren(List children) { + sort(children, TreeEntry::isDirectory, TreeEntry::getNameString); + this.children = children; + } } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java index 23d3bfabca..53a5bb0296 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java @@ -175,7 +175,9 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { Collection foList = root.getChildren(); - assertThat(foList).hasSize(2); + assertThat(foList) + .extracting("name") + .containsExactly("d.txt", "e.txt"); FileObject d = findFile(foList, "d.txt"); FileObject e = findFile(foList, "e.txt"); From 4e7381b98f0cc3d352731329088f591764172d34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 26 Feb 2020 11:10:01 +0100 Subject: [PATCH 090/251] Fix offset in recursion --- .../scm/repository/spi/GitBrowseCommand.java | 8 +-- .../repository/spi/GitBrowseCommandTest.java | 69 +++++++++++++++++++ 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java index 5bd25e549b..f4b6f15eba 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java @@ -269,13 +269,13 @@ public class GitBrowseCommand extends AbstractGitCommand TreeEntry entry = entryIterator.next(); FileObject fileObject = createFileObject(repo, request, revId, entry); - if (resultCount > request.getOffset()) { - files.add(fileObject); - } - if (request.isRecursive() && fileObject.isDirectory()) { convertToFileObject(fileObject, repo, request, revId, entry.getChildren()); } + + if (resultCount > request.getOffset()) { + files.add(fileObject); + } } parent.setChildren(files); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java index 53a5bb0296..8684837d37 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java @@ -268,6 +268,75 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { assertFalse(root.isTruncated()); } + @Test + public void testRecursiveLimit() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setLimit(4); + request.setRecursive(true); + + FileObject root = createCommand().getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c", "a.txt"); + + FileObject c = findFile(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("d.txt", "e.txt"); + } + + @Test + public void testRecursiveLimitInSubDir() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setLimit(2); + request.setRecursive(true); + + FileObject root = createCommand().getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c"); + + FileObject c = findFile(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("d.txt"); + } + + @Test + public void testRecursiveOffset() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setOffset(2); + request.setRecursive(true); + + FileObject root = createCommand().getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c", "a.txt", "b.txt", "f.txt"); + + FileObject c = findFile(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("e.txt"); + } + private FileObject findFile(Collection foList, String name) { return foList.stream() .filter(f -> name.equals(f.getName())) From 77100888655635760c8b9f9bd38cb77c62e88d53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 26 Feb 2020 13:24:31 +0100 Subject: [PATCH 091/251] Fix offset in recursion --- .../scm/repository/spi/SvnBrowseCommand.java | 11 +-- .../repository/spi/SvnBrowseCommandTest.java | 69 +++++++++++++++++++ 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java index 423b82004a..274155fd55 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java @@ -135,17 +135,18 @@ public class SvnBrowseCommand extends AbstractSvnCommand { List entries = new ArrayList<>(svnRepository.getDir(parent.getPath(), revisionNumber, null, (Collection) null)); sort(entries, entry -> entry.getKind() == SVNNodeKind.DIR, SVNDirEntry::getName); - for (Iterator iterator = entries.iterator(); resultCount < request.getLimit() + request.getOffset() && iterator.hasNext(); ++resultCount) { + for (Iterator iterator = entries.iterator(); resultCount < request.getLimit() + request.getOffset() && iterator.hasNext(); ) { + ++resultCount; SVNDirEntry entry = iterator.next(); FileObject child = createFileObject(request, svnRepository, revisionNumber, entry, basePath); - if (resultCount >= request.getOffset()) { - parent.addChild(child); - } - if (child.isDirectory() && request.isRecursive()) { traverse(svnRepository, revisionNumber, request, child, createBasePath(child.getPath())); } + + if (resultCount > request.getOffset()) { + parent.addChild(child); + } } if (resultCount >= request.getLimit() + request.getOffset()) { parent.setTruncated(true); diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java index 1c6e228a6d..f0bdb38c4a 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java @@ -206,6 +206,75 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase assertThat(foList).extracting("name").containsExactly("a.txt"); } + @Test + public void testRecursiveLimit() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setLimit(4); + request.setRecursive(true); + + FileObject root = createCommand().getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c", "a.txt"); + + FileObject c = getFileObject(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("d.txt", "e.txt"); + } + + @Test + public void testRecursiveLimitInSubDir() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setLimit(2); + request.setRecursive(true); + + FileObject root = createCommand().getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c"); + + FileObject c = getFileObject(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("d.txt"); + } + + @Test + public void testRecursiveOffset() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setOffset(2); + request.setRecursive(true); + + FileObject root = createCommand().getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c", "a.txt"); + + FileObject c = getFileObject(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("e.txt"); + } + /** * Method description * From 119236a2276e48eeca737455fa79c74968aac931 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 26 Feb 2020 15:10:56 +0100 Subject: [PATCH 092/251] move MenuContext to ui-components --- scm-ui/ui-components/src/contexts/MenuContext.ts | 15 +++++++++++++++ scm-ui/ui-components/src/contexts/index.ts | 3 +++ scm-ui/ui-components/src/index.ts | 1 + scm-ui/ui-webapp/src/repos/modules/repos.ts | 14 -------------- 4 files changed, 19 insertions(+), 14 deletions(-) create mode 100644 scm-ui/ui-components/src/contexts/MenuContext.ts create mode 100644 scm-ui/ui-components/src/contexts/index.ts diff --git a/scm-ui/ui-components/src/contexts/MenuContext.ts b/scm-ui/ui-components/src/contexts/MenuContext.ts new file mode 100644 index 0000000000..fcd83ce801 --- /dev/null +++ b/scm-ui/ui-components/src/contexts/MenuContext.ts @@ -0,0 +1,15 @@ +import React from "react"; + +const MENU_COLLAPSED = "secondary-menu-collapsed"; + +export const MenuContext = React.createContext({ + menuCollapsed: localStorage.getItem(MENU_COLLAPSED) === "true", + setMenuCollapsed: (collapsed: boolean) => {} +}); + +export function isMenuCollapsed() { + return localStorage.getItem(MENU_COLLAPSED) === "true"; +} +export function storeMenuCollapsed(status: boolean) { + localStorage.setItem(MENU_COLLAPSED, String(status)); +} diff --git a/scm-ui/ui-components/src/contexts/index.ts b/scm-ui/ui-components/src/contexts/index.ts new file mode 100644 index 0000000000..56f9c0126a --- /dev/null +++ b/scm-ui/ui-components/src/contexts/index.ts @@ -0,0 +1,3 @@ +// @create-index + +export { MenuContext, storeMenuCollapsed, isMenuCollapsed } from "./MenuContext"; diff --git a/scm-ui/ui-components/src/index.ts b/scm-ui/ui-components/src/index.ts index 46ccbb48ec..566bcf38aa 100644 --- a/scm-ui/ui-components/src/index.ts +++ b/scm-ui/ui-components/src/index.ts @@ -67,6 +67,7 @@ export * from "./navigation"; export * from "./repos"; export * from "./table"; export * from "./toast"; +export * from "./contexts"; export { File, diff --git a/scm-ui/ui-webapp/src/repos/modules/repos.ts b/scm-ui/ui-webapp/src/repos/modules/repos.ts index b2c1150fc3..38da52611f 100644 --- a/scm-ui/ui-webapp/src/repos/modules/repos.ts +++ b/scm-ui/ui-webapp/src/repos/modules/repos.ts @@ -442,17 +442,3 @@ export function getPermissionsLink(state: object, namespace: string, name: strin const repo = getRepository(state, namespace, name); return repo && repo._links ? repo._links.permissions.href : undefined; } - -const REPOSITORY_MENU_COLLAPSED = "repository-menu-collapsed"; - -export function isRepositoryMenuCollapsed() { - return localStorage.getItem(REPOSITORY_MENU_COLLAPSED) === "true"; -} -export function switchRepositoryMenuCollapsed(status: boolean) { - localStorage.setItem(REPOSITORY_MENU_COLLAPSED, String(status)); -} - -export const RepositoryContext = React.createContext({ - menuCollapsed: isRepositoryMenuCollapsed(), - toggleMenuCollapsed: () => {} -}); From 5cd28f27c6a68cc7b474c91d6938b38678125de1 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 26 Feb 2020 15:14:45 +0100 Subject: [PATCH 093/251] removed deprecated warnings plugin --- Jenkinsfile | 3 --- 1 file changed, 3 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 8218808237..1601970586 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -96,9 +96,6 @@ node('docker') { // Archive Unit and integration test results, if any junit allowEmptyResults: true, testResults: '**/target/failsafe-reports/TEST-*.xml,**/target/surefire-reports/TEST-*.xml,**/target/jest-reports/TEST-*.xml' - // Find maven warnings and visualize in job - warnings consoleParsers: [[parserName: 'Maven']], canRunOnFailed: true - mailIfStatusChanged(commitAuthorEmail) } } From ed53745d9f083868b80c3a348b583708a6aa035a Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 26 Feb 2020 15:45:24 +0100 Subject: [PATCH 094/251] make secondary navigation also for user, group and administration collapsable --- .../ui-webapp/src/admin/containers/Admin.tsx | 221 +++++++++++------- .../src/groups/containers/SingleGroup.tsx | 133 +++++++---- .../src/repos/containers/RepositoryRoot.tsx | 160 ++++++------- .../src/users/containers/SingleUser.tsx | 132 +++++++---- 4 files changed, 400 insertions(+), 246 deletions(-) diff --git a/scm-ui/ui-webapp/src/admin/containers/Admin.tsx b/scm-ui/ui-webapp/src/admin/containers/Admin.tsx index 23b1a58c65..82b564a607 100644 --- a/scm-ui/ui-webapp/src/admin/containers/Admin.tsx +++ b/scm-ui/ui-webapp/src/admin/containers/Admin.tsx @@ -2,11 +2,18 @@ import React from "react"; import { connect } from "react-redux"; import { compose } from "redux"; import { WithTranslation, withTranslation } from "react-i18next"; -import { Redirect, Route, Switch } from "react-router-dom"; -import { History } from "history"; +import { Redirect, Route, RouteComponentProps, Switch } from "react-router-dom"; import { ExtensionPoint } from "@scm-manager/ui-extensions"; import { Links } from "@scm-manager/ui-types"; -import { Navigation, NavLink, Page, Section, SubNavigation } from "@scm-manager/ui-components"; +import { + Navigation, + NavLink, + Page, + Section, + SubNavigation, + isMenuCollapsed, + MenuContext +} from "@scm-manager/ui-components"; import { getAvailablePluginsLink, getInstalledPluginsLink, getLinks } from "../../modules/indexResource"; import AdminDetails from "./AdminDetails"; import PluginsOverview from "../plugins/containers/PluginsOverview"; @@ -14,18 +21,44 @@ import GlobalConfig from "./GlobalConfig"; import RepositoryRoles from "../roles/containers/RepositoryRoles"; import SingleRepositoryRole from "../roles/containers/SingleRepositoryRole"; import CreateRepositoryRole from "../roles/containers/CreateRepositoryRole"; +import { storeMenuCollapsed } from "@scm-manager/ui-components/src"; -type Props = WithTranslation & { - links: Links; - availablePluginsLink: string; - installedPluginsLink: string; +type Props = RouteComponentProps & + WithTranslation & { + links: Links; + availablePluginsLink: string; + installedPluginsLink: string; + }; - // context objects - match: any; - history: History; +type State = { + menuCollapsed: boolean; + setMenuCollapsed: (collapsed: boolean) => void; }; -class Admin extends React.Component { +class Admin extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + menuCollapsed: isMenuCollapsed(), + setMenuCollapsed: (collapsed: boolean) => this.setState({ menuCollapsed: collapsed }) + }; + } + + componentDidUpdate() { + if (this.state.menuCollapsed && this.isCollapseForbidden()) { + this.setState({ menuCollapsed: false }); + } + } + + isCollapseForbidden = () => { + return this.props.location.pathname.includes("/settings/") || this.props.location.pathname.includes("/plugins/"); + }; + + onCollapseAdminMenu = (collapsed: boolean) => { + this.setState({ menuCollapsed: collapsed }, () => storeMenuCollapsed(collapsed)); + }; + stripEndingSlash = (url: string) => { if (url.endsWith("/")) { if (url.includes("role")) { @@ -48,6 +81,7 @@ class Admin extends React.Component { render() { const { links, availablePluginsLink, installedPluginsLink, t } = this.props; + const { menuCollapsed } = this.state; const url = this.matchedUrl(); const extensionProps = { @@ -56,82 +90,99 @@ class Admin extends React.Component { }; return ( - -
    -
    - - - - - - } - /> - } - /> - } - /> - } - /> - } - /> - } /> - } - /> - } /> - - -
    -
    - -
    - - {(availablePluginsLink || installedPluginsLink) && ( - - {installedPluginsLink && ( - - )} - {availablePluginsLink && ( - - )} - - )} - + +
    +
    + + + + + + } /> - - - - - -
    -
    + } + /> + } + /> + } + /> + } + /> + } /> + } + /> + } /> + + +
    +
    + +
    this.onCollapseAdminMenu(!menuCollapsed)} + collapsed={menuCollapsed} + > + + {(availablePluginsLink || installedPluginsLink) && ( + + {installedPluginsLink && ( + + )} + {availablePluginsLink && ( + + )} + + )} + + + + + + +
    +
    +
    -
    -
    + + ); } } diff --git a/scm-ui/ui-webapp/src/groups/containers/SingleGroup.tsx b/scm-ui/ui-webapp/src/groups/containers/SingleGroup.tsx index 90e15b5e43..c80a3225c9 100644 --- a/scm-ui/ui-webapp/src/groups/containers/SingleGroup.tsx +++ b/scm-ui/ui-webapp/src/groups/containers/SingleGroup.tsx @@ -1,38 +1,73 @@ import React from "react"; import { connect } from "react-redux"; -import { Route } from "react-router-dom"; +import { Route, RouteComponentProps } from "react-router-dom"; import { WithTranslation, withTranslation } from "react-i18next"; -import { History } from "history"; import { ExtensionPoint } from "@scm-manager/ui-extensions"; import { Group } from "@scm-manager/ui-types"; -import { ErrorPage, Loading, Navigation, NavLink, Page, Section, SubNavigation } from "@scm-manager/ui-components"; +import { + ErrorPage, + Loading, + Navigation, + NavLink, + Page, + Section, + SubNavigation, + isMenuCollapsed, + MenuContext +} from "@scm-manager/ui-components"; import { getGroupsLink } from "../../modules/indexResource"; import { fetchGroupByName, getFetchGroupFailure, getGroupByName, isFetchGroupPending } from "../modules/groups"; import { Details } from "./../components/table"; import { EditGroupNavLink, SetPermissionsNavLink } from "./../components/navLinks"; import EditGroup from "./EditGroup"; import SetPermissions from "../../permissions/components/SetPermissions"; +import { storeMenuCollapsed } from "@scm-manager/ui-components/src"; -type Props = WithTranslation & { - name: string; - group: Group; - loading: boolean; - error: Error; - groupLink: string; +type Props = RouteComponentProps & + WithTranslation & { + name: string; + group: Group; + loading: boolean; + error: Error; + groupLink: string; - // dispatcher functions - fetchGroupByName: (p1: string, p2: string) => void; + // dispatcher functions + fetchGroupByName: (p1: string, p2: string) => void; + }; - // context objects - match: any; - history: History; +type State = { + menuCollapsed: boolean; + setMenuCollapsed: (collapsed: boolean) => void; }; -class SingleGroup extends React.Component { +class SingleGroup extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + menuCollapsed: isMenuCollapsed(), + setMenuCollapsed: (collapsed: boolean) => this.setState({ menuCollapsed: collapsed }) + }; + } + componentDidMount() { this.props.fetchGroupByName(this.props.groupLink, this.props.name); } + componentDidUpdate() { + if (this.state.menuCollapsed && this.isCollapseForbidden()) { + this.setState({ menuCollapsed: false }); + } + } + + isCollapseForbidden = () => { + return this.props.location.pathname.includes("/settings/"); + }; + + onCollapseGroupMenu = (collapsed: boolean) => { + this.setState({ menuCollapsed: collapsed }, () => storeMenuCollapsed(collapsed)); + }; + stripEndingSlash = (url: string) => { if (url.endsWith("/")) { return url.substring(0, url.length - 2); @@ -46,6 +81,7 @@ class SingleGroup extends React.Component { render() { const { t, loading, error, group } = this.props; + const { menuCollapsed } = this.state; if (error) { return ; @@ -63,33 +99,48 @@ class SingleGroup extends React.Component { }; return ( - -
    -
    -
    } /> - } /> - } - /> - + + +
    +
    +
    } /> + } /> + } + /> + +
    +
    + +
    this.onCollapseGroupMenu(!menuCollapsed)} + collapsed={menuCollapsed} + > + + + + + + + +
    +
    +
    -
    - -
    - - - - - - - -
    -
    -
    -
    - + + ); } } diff --git a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx index 06184842ff..c436219e8b 100644 --- a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx @@ -4,16 +4,19 @@ import { Redirect, Route, Switch, RouteComponentProps } from "react-router-dom"; import { WithTranslation, withTranslation } from "react-i18next"; import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; import { Repository } from "@scm-manager/ui-types"; -import { ErrorPage, Loading, Navigation, NavLink, Page, Section, SubNavigation } from "@scm-manager/ui-components"; import { - fetchRepoByName, - getFetchRepoFailure, - getRepository, - isFetchRepoPending, - isRepositoryMenuCollapsed, - switchRepositoryMenuCollapsed, - RepositoryContext -} from "../modules/repos"; + ErrorPage, + Loading, + Navigation, + NavLink, + Page, + Section, + SubNavigation, + MenuContext, + storeMenuCollapsed, + isMenuCollapsed +} from "@scm-manager/ui-components"; +import { fetchRepoByName, getFetchRepoFailure, getRepository, isFetchRepoPending } from "../modules/repos"; import RepositoryDetails from "../components/RepositoryDetails"; import EditRepo from "./EditRepo"; import BranchesOverview from "../branches/containers/BranchesOverview"; @@ -43,24 +46,28 @@ type Props = RouteComponentProps & }; type State = { - collapsedRepositoryMenu: boolean; + menuCollapsed: boolean; + setMenuCollapsed: (collapsed: boolean) => void; }; class RepositoryRoot extends React.Component { constructor(props: Props) { super(props); + this.state = { - collapsedRepositoryMenu: isRepositoryMenuCollapsed() + menuCollapsed: isMenuCollapsed(), + setMenuCollapsed: (collapsed: boolean) => this.setState({ menuCollapsed: collapsed }) }; } + componentDidMount() { const { fetchRepoByName, namespace, name, repoLink } = this.props; fetchRepoByName(repoLink, namespace, name); } componentDidUpdate() { - if (this.state.collapsedRepositoryMenu && this.isCollapseForbidden()) { - this.onCollapseRepositoryMenu(false); + if (this.state.menuCollapsed && this.isCollapseForbidden()) { + this.setState({ menuCollapsed: false }); } } @@ -111,13 +118,13 @@ class RepositoryRoot extends React.Component { return `${url}/changesets`; }; - onCollapseRepositoryMenu = (status: boolean) => { - this.setState({ collapsedRepositoryMenu: status }, () => switchRepositoryMenuCollapsed(status)); + onCollapseRepositoryMenu = (collapsed: boolean) => { + this.setState({ menuCollapsed: collapsed }, () => storeMenuCollapsed(collapsed)); }; render() { const { loading, error, indexLinks, repository, t } = this.props; - const { collapsedRepositoryMenu } = this.state; + const { menuCollapsed } = this.state; if (error) { return ( @@ -135,7 +142,7 @@ class RepositoryRoot extends React.Component { repository, url, indexLinks, - collapsedRepositoryMenu + collapsedRepositoryMenu: menuCollapsed }; const redirectUrlFactory = binder.getExtension("repository.redirect", this.props); @@ -147,15 +154,10 @@ class RepositoryRoot extends React.Component { } return ( - this.setState({ collapsedRepositoryMenu: !this.state.collapsedRepositoryMenu }) - }} - > - -
    -
    + +
    +
    + @@ -204,61 +206,59 @@ class RepositoryRoot extends React.Component { } /> -
    -
    - -
    this.onCollapseRepositoryMenu(!collapsedRepositoryMenu) - } - collapsed={collapsedRepositoryMenu} - > - - - - - - - - - - -
    -
    -
    +
    -
    - +
    + +
    this.onCollapseRepositoryMenu(!menuCollapsed) + } + collapsed={menuCollapsed} + > + + + + + + + + + + +
    +
    +
    +
    + ); } } diff --git a/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx b/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx index 8b0826ee6b..9d0dd2b9c5 100644 --- a/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx +++ b/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx @@ -1,10 +1,20 @@ import React from "react"; import { connect } from "react-redux"; -import { Route } from "react-router-dom"; +import { Route, RouteComponentProps } from "react-router-dom"; import { History } from "history"; import { ExtensionPoint } from "@scm-manager/ui-extensions"; import { User } from "@scm-manager/ui-types"; -import { ErrorPage, Loading, Navigation, NavLink, Page, Section, SubNavigation } from "@scm-manager/ui-components"; +import { + ErrorPage, + Loading, + Navigation, + NavLink, + Page, + Section, + SubNavigation, + MenuContext, + isMenuCollapsed +} from "@scm-manager/ui-components"; import { Details } from "./../components/table"; import EditUser from "./EditUser"; import { fetchUserByName, getFetchUserFailure, getUserByName, isFetchUserPending } from "../modules/users"; @@ -13,27 +23,45 @@ import { WithTranslation, withTranslation } from "react-i18next"; import { getUsersLink } from "../../modules/indexResource"; import SetUserPassword from "../components/SetUserPassword"; import SetPermissions from "../../permissions/components/SetPermissions"; +import { storeMenuCollapsed } from "@scm-manager/ui-components/src"; -type Props = WithTranslation & { - name: string; - user: User; - loading: boolean; - error: Error; - usersLink: string; +type Props = RouteComponentProps & + WithTranslation & { + name: string; + user: User; + loading: boolean; + error: Error; + usersLink: string; - // dispatcher function - fetchUserByName: (p1: string, p2: string) => void; + // dispatcher function + fetchUserByName: (p1: string, p2: string) => void; + }; - // context objects - match: any; - history: History; +type State = { + menuCollapsed: boolean; + setMenuCollapsed: (collapsed: boolean) => void; }; -class SingleUser extends React.Component { +class SingleUser extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + menuCollapsed: isMenuCollapsed(), + setMenuCollapsed: (collapsed: boolean) => this.setState({ menuCollapsed: collapsed }) + }; + } + componentDidMount() { this.props.fetchUserByName(this.props.usersLink, this.props.name); } + componentDidUpdate() { + if (this.state.menuCollapsed && this.isCollapseForbidden()) { + this.setState({ menuCollapsed: false }); + } + } + stripEndingSlash = (url: string) => { if (url.endsWith("/")) { return url.substring(0, url.length - 2); @@ -41,12 +69,21 @@ class SingleUser extends React.Component { return url; }; + isCollapseForbidden = () => { + return this.props.location.pathname.includes("/settings/"); + }; + + onCollapseUserMenu = (collapsed: boolean) => { + this.setState({ menuCollapsed: collapsed }, () => storeMenuCollapsed(collapsed)); + }; + matchedUrl = () => { return this.stripEndingSlash(this.props.match.url); }; render() { const { t, loading, error, user } = this.props; + const { menuCollapsed } = this.state; if (error) { return ; @@ -64,33 +101,48 @@ class SingleUser extends React.Component { }; return ( - -
    -
    -
    } /> - } /> - } /> - } - /> - + + +
    +
    +
    } /> + } /> + } /> + } + /> + +
    +
    + +
    this.onCollapseUserMenu(!menuCollapsed)} + collapsed={menuCollapsed} + > + + + + + + + +
    +
    +
    -
    - -
    - - - - - - - -
    -
    -
    -
    - + + ); } } From ffdd80df6168e2ae9cadef6942f04f9c74caaece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 26 Feb 2020 15:59:03 +0100 Subject: [PATCH 095/251] Correct hash and equals for cache --- .../java/sonia/scm/repository/spi/BrowseCommandRequest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java index 4477a26e31..c50ab06cee 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java @@ -116,7 +116,8 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest && Objects.equal(recursive, other.recursive) && Objects.equal(disableLastCommit, other.disableLastCommit) && Objects.equal(disableSubRepositoryDetection, other.disableSubRepositoryDetection) - && Objects.equal(offset, other.offset); + && Objects.equal(offset, other.offset) + && Objects.equal(limit, other.limit); } /** @@ -129,7 +130,7 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest public int hashCode() { return Objects.hashCode(super.hashCode(), recursive, disableLastCommit, - disableSubRepositoryDetection, offset); + disableSubRepositoryDetection, offset, limit); } /** From 8ff870ccb1411f4456c6d5ec20b2dd5fdccdb7e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 26 Feb 2020 16:05:49 +0100 Subject: [PATCH 096/251] Remove unused property --- .../scm/repository/spi/FileBaseCommandRequest.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java index 0a2192897a..9f563345fd 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java @@ -147,10 +147,6 @@ public abstract class FileBaseCommandRequest this.revision = revision; } - public void setLimit(int limit) { - this.limit = limit; - } - //~--- get methods ---------------------------------------------------------- /** @@ -175,10 +171,6 @@ public abstract class FileBaseCommandRequest return revision; } - public int getLimit() { - return limit; - } - //~--- methods -------------------------------------------------------------- /** @@ -216,6 +208,4 @@ public abstract class FileBaseCommandRequest /** Field description */ private String revision; - - private int limit; } From 1567cd87653a33479da7c2500910b191b2d7088c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 26 Feb 2020 17:36:41 +0100 Subject: [PATCH 097/251] Add tests for recursive request --- .../repository/spi/HgBrowseCommandTest.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java index bf34a0ad92..c017d0632c 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java @@ -209,6 +209,76 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { assertThat(root.isTruncated()).isFalse(); } + + @Test + public void testRecursiveLimit() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setLimit(4); + request.setRecursive(true); + + FileObject root = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c", "a.txt"); + + FileObject c = getFileObject(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("d.txt", "e.txt"); + } + + @Test + public void testRecursiveLimitInSubDir() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setLimit(2); + request.setRecursive(true); + + FileObject root = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c"); + + FileObject c = getFileObject(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("d.txt"); + } + + @Test + public void testRecursiveOffset() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setOffset(2); + request.setRecursive(true); + + FileObject root = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c", "a.txt", "b.txt", "f.txt"); + + FileObject c = getFileObject(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("e.txt"); + } + //~--- get methods ---------------------------------------------------------- /** From eb81bf4005b899ed88a983ad2de857f202007d98 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 27 Feb 2020 08:36:47 +0100 Subject: [PATCH 098/251] Enable tests for mercurial Therefore disable build in docker and use native worker. --- Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 1601970586..2e555b447f 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -33,7 +33,7 @@ node('docker') { } stage('Unit Test') { - mvn 'test -Pcoverage -Dsonia.scm.test.skip.hg=true -Dmaven.test.failure.ignore=true' + mvn 'test -Pcoverage -Dmaven.test.failure.ignore=true' } stage('Integration Test') { @@ -104,7 +104,7 @@ String mainBranch Maven setupMavenBuild() { // Keep this version number in sync with .mvn/maven-wrapper.properties - Maven mvn = new MavenInDocker(this, '3.6.3-jdk-11') + Maven mvn = new MavenWrapper(this) if (isMainBranch()) { // Release starts javadoc, which takes very long, so do only for certain branches From f8720087a14e6f81cff9c207d0af33d0831c32ef Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 27 Feb 2020 09:27:02 +0100 Subject: [PATCH 099/251] refactor --- scm-ui/ui-webapp/src/containers/Profile.tsx | 106 +++++++++++++----- .../src/groups/containers/SingleGroup.tsx | 6 +- .../src/users/containers/SingleUser.tsx | 7 +- 3 files changed, 86 insertions(+), 33 deletions(-) diff --git a/scm-ui/ui-webapp/src/containers/Profile.tsx b/scm-ui/ui-webapp/src/containers/Profile.tsx index 4852993afd..68350deca5 100644 --- a/scm-ui/ui-webapp/src/containers/Profile.tsx +++ b/scm-ui/ui-webapp/src/containers/Profile.tsx @@ -1,24 +1,62 @@ import React from "react"; -import { Route, withRouter } from "react-router-dom"; +import { Route, RouteComponentProps, withRouter } from "react-router-dom"; import { getMe } from "../modules/auth"; import { compose } from "redux"; import { connect } from "react-redux"; import { WithTranslation, withTranslation } from "react-i18next"; import { Me } from "@scm-manager/ui-types"; -import { ErrorPage, Navigation, NavLink, Page, Section, SubNavigation } from "@scm-manager/ui-components"; +import { + ErrorPage, + isMenuCollapsed, + MenuContext, + Navigation, + NavLink, + Page, + Section, + SubNavigation +} from "@scm-manager/ui-components"; import ChangeUserPassword from "./ChangeUserPassword"; import ProfileInfo from "./ProfileInfo"; import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { storeMenuCollapsed } from "@scm-manager/ui-components/src"; -type Props = WithTranslation & { - me: Me; +type Props = RouteComponentProps & + WithTranslation & { + me: Me; - // Context props - match: any; + // Context props + match: any; + }; + +type State = { + menuCollapsed: boolean; + setMenuCollapsed: (collapsed: boolean) => void; }; -type State = {}; class Profile extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + menuCollapsed: isMenuCollapsed(), + setMenuCollapsed: (collapsed: boolean) => this.setState({ menuCollapsed: collapsed }) + }; + } + + componentDidUpdate() { + if (this.state.menuCollapsed && this.isCollapseForbidden()) { + this.setState({ menuCollapsed: false }); + } + } + + isCollapseForbidden = () => { + return this.props.location.pathname.includes("/settings/"); + }; + + onCollapseProfileMenu = (collapsed: boolean) => { + this.setState({ menuCollapsed: collapsed }, () => storeMenuCollapsed(collapsed)); + }; + stripEndingSlash = (url: string) => { if (url.endsWith("/")) { return url.substring(0, url.length - 2); @@ -34,6 +72,7 @@ class Profile extends React.Component { const url = this.matchedUrl(); const { me, t } = this.props; + const { menuCollapsed } = this.state; if (!me) { return ( @@ -54,26 +93,41 @@ class Profile extends React.Component { }; return ( - -
    -
    - } /> - } /> - + + +
    +
    + } /> + } /> + +
    +
    + +
    this.onCollapseProfileMenu(!menuCollapsed)} + collapsed={menuCollapsed} + > + + + + + +
    +
    +
    -
    - -
    - - - - - -
    -
    -
    -
    - + + ); } } diff --git a/scm-ui/ui-webapp/src/groups/containers/SingleGroup.tsx b/scm-ui/ui-webapp/src/groups/containers/SingleGroup.tsx index c80a3225c9..ddcb387ca7 100644 --- a/scm-ui/ui-webapp/src/groups/containers/SingleGroup.tsx +++ b/scm-ui/ui-webapp/src/groups/containers/SingleGroup.tsx @@ -6,14 +6,14 @@ import { ExtensionPoint } from "@scm-manager/ui-extensions"; import { Group } from "@scm-manager/ui-types"; import { ErrorPage, + isMenuCollapsed, Loading, + MenuContext, Navigation, NavLink, Page, Section, - SubNavigation, - isMenuCollapsed, - MenuContext + SubNavigation } from "@scm-manager/ui-components"; import { getGroupsLink } from "../../modules/indexResource"; import { fetchGroupByName, getFetchGroupFailure, getGroupByName, isFetchGroupPending } from "../modules/groups"; diff --git a/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx b/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx index 9d0dd2b9c5..d2718650bd 100644 --- a/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx +++ b/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx @@ -1,19 +1,18 @@ import React from "react"; import { connect } from "react-redux"; import { Route, RouteComponentProps } from "react-router-dom"; -import { History } from "history"; import { ExtensionPoint } from "@scm-manager/ui-extensions"; import { User } from "@scm-manager/ui-types"; import { ErrorPage, + isMenuCollapsed, Loading, + MenuContext, Navigation, NavLink, Page, Section, - SubNavigation, - MenuContext, - isMenuCollapsed + SubNavigation } from "@scm-manager/ui-components"; import { Details } from "./../components/table"; import EditUser from "./EditUser"; From 33037385e4a5fc33ad43074f7ea303acbfbdf60d Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 27 Feb 2020 09:56:25 +0100 Subject: [PATCH 100/251] Handle unexpected merge results It is possible that a git work tree is dirty directly after the clone of a repository, eg. when files are not changed correctly due to bogous .gitattribute files (though this is just a guess). In these cases a merge might fail due to these dirty files and not due to merge conflicts. Without this change such results lead to null pointer exceptions, because result.getConflicts() is null. --- .../scm/repository/spi/GitMergeStrategy.java | 9 +++- .../spi/UnexpectedMergeResultException.java | 27 ++++++++++ .../spi/AbstractGitCommandTestBase.java | 1 - .../repository/spi/GitMergeCommandTest.java | 49 ++++++++++++++++++- .../main/resources/locales/de/plugins.json | 4 ++ .../main/resources/locales/en/plugins.json | 4 ++ 6 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/UnexpectedMergeResultException.java diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java index 8e9f79d1b9..4b8280c669 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java @@ -88,7 +88,14 @@ abstract class GitMergeStrategy extends AbstractGitCommand.GitCloneWorker { + FileWriter fw = null; + try { + fw = new FileWriter(new File(git.getRepository().getWorkTree(), "b.txt"), true); + BufferedWriter bw = new BufferedWriter(fw); + bw.write("change"); + bw.newLine(); + bw.close(); + } catch (IOException e) { + e.printStackTrace(); + } + }); + MergeCommandRequest request = new MergeCommandRequest(); + request.setBranchToMerge("mergeable"); + request.setTargetBranch("master"); + request.setMergeStrategy(MergeStrategy.MERGE_COMMIT); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + request.setMessageTemplate("simple"); + + Assertions.assertThrows(UnexpectedMergeResultException.class, () -> command.merge(request)); + } + @Test public void shouldTakeAuthorFromSubjectIfNotSet() throws IOException, GitAPIException { SimplePrincipalCollection principals = new SimplePrincipalCollection(); principals.add("admin", REALM); - principals.add( new User("dirk", "Dirk Gently", "dirk@holistic.det"), REALM); + principals.add(new User("dirk", "Dirk Gently", "dirk@holistic.det"), REALM); shiro.setSubject( new Subject.Builder() .principals(principals) @@ -364,6 +395,20 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { } private GitMergeCommand createCommand() { - return new GitMergeCommand(createContext(), repository, new SimpleGitWorkdirFactory(new WorkdirProvider())); + return createCommand(git -> { + }); + } + + private GitMergeCommand createCommand(Consumer interceptor) { + return new GitMergeCommand(createContext(), repository, new SimpleGitWorkdirFactory(new WorkdirProvider())) { + @Override + > R inClone(Function workerSupplier, GitWorkdirFactory workdirFactory, String initialBranch) { + Function interceptedWorkerSupplier = git -> { + interceptor.accept(git); + return workerSupplier.apply(git); + }; + return super.inClone(interceptedWorkerSupplier, workdirFactory, initialBranch); + } + }; } } diff --git a/scm-webapp/src/main/resources/locales/de/plugins.json b/scm-webapp/src/main/resources/locales/de/plugins.json index 95179a94e2..809da9d0df 100644 --- a/scm-webapp/src/main/resources/locales/de/plugins.json +++ b/scm-webapp/src/main/resources/locales/de/plugins.json @@ -199,6 +199,10 @@ "8LRncum0S1": { "displayName": "Interner Fehler im Repository", "description": "Bei der Bearbeitung des internen Repositories ist ein Fehler oder ein unerwarteter Zustand aufgetreten. Bitte prüfen Sie die Logs für weitere Informationen." + }, + "4GRrgkSC01": { + "displayName": "Unerwartetes Merge-Ergebnis", + "description": "Der Merge hatte ein unerwartetes Ergebis, das nicht automatisiert behandelt werden konnte. Nähere Details sind im Log zu finden. Führen Sie den Merge ggf. manuell durch." } }, "namespaceStrategies": { diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json index 2353184ada..69e8d057ee 100644 --- a/scm-webapp/src/main/resources/locales/en/plugins.json +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -199,6 +199,10 @@ "8LRncum0S1": { "displayName": "Internal repository error", "description": "There was an error or an unexpected condition while handling the native repository. Please consult the logs for further information." + }, + "4GRrgkSC01": { + "displayName": "Unexpected merge result", + "description": "The merge led to an unexpected result, that could not be handled automatically. More details could be found in the log. Please merge the branches manually." } }, "namespaceStrategies": { From f9b680f548ece6959ca7fe1fad4f12067d5a919a Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 27 Feb 2020 10:00:29 +0100 Subject: [PATCH 101/251] fix responsiveness for section --- .../ui-components/src/navigation/Section.tsx | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/scm-ui/ui-components/src/navigation/Section.tsx b/scm-ui/ui-components/src/navigation/Section.tsx index 2214b974c5..c4af45e203 100644 --- a/scm-ui/ui-components/src/navigation/Section.tsx +++ b/scm-ui/ui-components/src/navigation/Section.tsx @@ -1,33 +1,33 @@ -import React, {FC, ReactChild, useEffect, useState} from "react"; +import React, { FC, ReactElement, useEffect, useState } from "react"; import { Button } from "../buttons"; import styled from "styled-components"; type Props = { label: string; - children?: ReactChild; + children: ReactElement[]; collapsed?: boolean; onCollapse?: (newStatus: boolean) => void; }; +type StylingProps = { + scrollPositionY: number; + collapsed: boolean; +}; const SectionContainer = styled.div` -// @ts-ignore - position: ${props => (props.scrollPositionY > 210 ? "fixed" : "absolute")}; - // @ts-ignore - top: ${props => props.scrollPositionY > 210 && "4.5rem"}; - // @ts-ignore - width: ${props => (props.collapsed ? "5.5rem" : "20.5rem")}; + position: ${(props: StylingProps) => (props.scrollPositionY > 210 && window.innerWidth > 770 ? "fixed" : "inherit")}; + top: ${(props: StylingProps) => props.scrollPositionY > 210 && window.innerWidth > 770 && "4.5rem"}; + width: ${(props: StylingProps) => (props.collapsed ? "5.5rem" : "20.5rem")}; `; const SmallButton = styled(Button)` height: 1.5rem; - width: 1rem; - position: absolute; - right: 1.5rem; `; const MenuLabel = styled.p` min-height: 2.5rem; + display: flex; + justify-content: ${(props: StylingProps) => (props.collapsed ? "center" : "space-between")}; `; const Section: FC = ({ label, children, collapsed, onCollapse }) => { @@ -41,14 +41,14 @@ const Section: FC = ({ label, children, collapsed, onCollapse }) => { }; }, []); - // @ts-ignore - const childrenWithProps = React.Children.map(children, (child: ReactChild) => React.cloneElement(child, { collapsed: collapsed })); + const childrenWithProps = React.Children.map(children, (child: ReactElement) => + React.cloneElement(child, { collapsed: collapsed }) + ); const arrowIcon = collapsed ? : ; return ( - // @ts-ignore - - + + {collapsed ? "" : label} {onCollapse && ( onCollapse(!collapsed)}> From fbededbcaa8d352acd55ffdc617dd57553c9adec Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 27 Feb 2020 10:34:48 +0100 Subject: [PATCH 102/251] cleanup test --- .../java/sonia/scm/repository/spi/GitMergeCommandTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java index 8699533c00..f39644119e 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java @@ -173,9 +173,8 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { @Test public void shouldHandleUnexpectedMergeResults() { GitMergeCommand command = createCommand(git -> { - FileWriter fw = null; try { - fw = new FileWriter(new File(git.getRepository().getWorkTree(), "b.txt"), true); + FileWriter fw = new FileWriter(new File(git.getRepository().getWorkTree(), "b.txt"), true); BufferedWriter bw = new BufferedWriter(fw); bw.write("change"); bw.newLine(); From d36063be2426049f50f7d4927707b8291c1cdb68 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 27 Feb 2020 09:35:43 +0000 Subject: [PATCH 103/251] Close branch bugfix/merge_failures_without_conflicts From ba2b6e41e1b881150329725842d9bee56dbfb938 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 27 Feb 2020 10:38:47 +0100 Subject: [PATCH 104/251] Log change --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a715533bac..0b863c3b5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Modification for mercurial repositories with enabled XSRF protection +- Does not throw NullPointerException when merge fails without normal merge conflicts ### Removed - Enunciate rest documentation From 9c75ff95c8f50fd129c494aebc2e7240d7bcdbfc Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 27 Feb 2020 11:42:37 +0100 Subject: [PATCH 105/251] Keep file attributes on modification --- CHANGELOG.md | 1 + .../repository/spi/ModifyWorkerHelper.java | 33 +++++++ .../spi/ModifyWorkerHelperTest.java | 90 +++++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 scm-core/src/test/java/sonia/scm/repository/spi/ModifyWorkerHelperTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b863c3b5d..2387635e02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Modification for mercurial repositories with enabled XSRF protection - Does not throw NullPointerException when merge fails without normal merge conflicts +- Keep file attributes on modification ### Removed - Enunciate rest documentation diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyWorkerHelper.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyWorkerHelper.java index a1ff6b0eb3..875e06334a 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyWorkerHelper.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyWorkerHelper.java @@ -1,6 +1,8 @@ package sonia.scm.repository.spi; import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sonia.scm.ContextEntry; import sonia.scm.repository.Repository; @@ -10,8 +12,13 @@ import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.util.Optional; +import java.util.Set; import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; +import static java.util.Optional.empty; +import static java.util.Optional.of; import static sonia.scm.AlreadyExistsException.alreadyExists; import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.NotFoundException.notFound; @@ -22,6 +29,8 @@ import static sonia.scm.NotFoundException.notFound; */ public interface ModifyWorkerHelper extends ModifyCommand.Worker { + Logger LOG = LoggerFactory.getLogger(ModifyWorkerHelper.class); + @Override default void delete(String toBeDeleted) throws IOException { Path fileToBeDeleted = new File(getWorkDir(), toBeDeleted).toPath(); @@ -57,10 +66,34 @@ public interface ModifyWorkerHelper extends ModifyCommand.Worker { if (!targetFile.toFile().exists()) { throw notFound(createFileContext(path)); } + Optional> posixFilePermissions = getPosixFilePermissions(targetFile); Files.move(file.toPath(), targetFile, REPLACE_EXISTING); + posixFilePermissions.ifPresent(permissions -> setPosixFilePermissions(targetFile, permissions)); addFileToScm(path, targetFile); } + default Optional> getPosixFilePermissions(Path targetFile) { + try { + return of(Files.getPosixFilePermissions(targetFile)); + } catch (UnsupportedOperationException e) { + LOG.trace("posix file permissions not supported"); + return empty(); + } catch (IOException e) { + LOG.info("could not read posix file permissions for file {}", targetFile); + return empty(); + } + } + + default void setPosixFilePermissions(Path targetFile, Set permissions) { + try { + Files.setPosixFilePermissions(targetFile, permissions); + } catch (UnsupportedOperationException e) { + LOG.trace("posix file permissions not supported"); + } catch (IOException e) { + LOG.info("could not write posix file permissions to file {}", targetFile); + } + } + void addFileToScm(String name, Path file); default ContextEntry.ContextBuilder createFileContext(String path) { diff --git a/scm-core/src/test/java/sonia/scm/repository/spi/ModifyWorkerHelperTest.java b/scm-core/src/test/java/sonia/scm/repository/spi/ModifyWorkerHelperTest.java new file mode 100644 index 0000000000..c2996429a1 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/spi/ModifyWorkerHelperTest.java @@ -0,0 +1,90 @@ +package sonia.scm.repository.spi; + +import com.google.common.collect.ImmutableSet; +import org.assertj.core.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junitpioneer.jupiter.TempDirectory; +import sonia.scm.repository.Repository; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static java.nio.file.attribute.PosixFilePermission.GROUP_EXECUTE; +import static java.nio.file.attribute.PosixFilePermission.GROUP_READ; +import static java.nio.file.attribute.PosixFilePermission.OTHERS_EXECUTE; +import static java.nio.file.attribute.PosixFilePermission.OTHERS_READ; +import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; +import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; +import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(TempDirectory.class) +class ModifyWorkerHelperTest { + + @Test + void shouldKeepExecutableFlag(@TempDirectory.TempDir Path temp) throws IOException { + + File target = createFile(temp, "executable.sh"); + File newFile = createFile(temp, "other"); + + Assumptions.assumeThatThrownBy(() -> { + Files.setPosixFilePermissions(target.toPath(), + ImmutableSet.of(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE, GROUP_READ, GROUP_EXECUTE, OTHERS_READ, OTHERS_EXECUTE)); + Files.setPosixFilePermissions(newFile.toPath(), + ImmutableSet.of(OWNER_READ, OWNER_WRITE, GROUP_READ, OTHERS_READ)); + }).doesNotThrowAnyException(); // ignore this test on systems that does not support posix file permissions + + ModifyWorkerHelper helper = new MinimalModifyWorkerHelper(temp); + + helper.modify("executable.sh", newFile); + + assertThat(Files.getPosixFilePermissions(target.toPath())) + .contains(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE, GROUP_READ, GROUP_EXECUTE, OTHERS_READ, OTHERS_EXECUTE); + } + + private File createFile(Path temp, String fileName) throws IOException { + File file = new File(temp.toFile(), fileName); + FileWriter source = new FileWriter(file); + source.write("something"); + source.close(); + return file; + } + + private static class MinimalModifyWorkerHelper implements ModifyWorkerHelper { + + private final Path temp; + + public MinimalModifyWorkerHelper(Path temp) { + this.temp = temp; + } + + @Override + public void doScmDelete(String toBeDeleted) { + + } + + @Override + public void addFileToScm(String name, Path file) { + + } + + @Override + public File getWorkDir() { + return temp.toFile(); + } + + @Override + public Repository getRepository() { + return null; + } + + @Override + public String getBranch() { + return null; + } + } +} From 9b7d5daae317a5a74d5622379b9730aa1ca0683e Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 27 Feb 2020 13:13:09 +0100 Subject: [PATCH 106/251] Use simple executable check from old java io --- .../repository/spi/ModifyWorkerHelper.java | 33 +++---------------- .../spi/ModifyWorkerHelperTest.java | 20 ++--------- 2 files changed, 6 insertions(+), 47 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyWorkerHelper.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyWorkerHelper.java index 875e06334a..fe19523261 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyWorkerHelper.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyWorkerHelper.java @@ -12,13 +12,8 @@ import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; -import java.nio.file.attribute.PosixFilePermission; -import java.util.Optional; -import java.util.Set; import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; -import static java.util.Optional.empty; -import static java.util.Optional.of; import static sonia.scm.AlreadyExistsException.alreadyExists; import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.NotFoundException.notFound; @@ -66,34 +61,14 @@ public interface ModifyWorkerHelper extends ModifyCommand.Worker { if (!targetFile.toFile().exists()) { throw notFound(createFileContext(path)); } - Optional> posixFilePermissions = getPosixFilePermissions(targetFile); + boolean executable = Files.isExecutable(targetFile); Files.move(file.toPath(), targetFile, REPLACE_EXISTING); - posixFilePermissions.ifPresent(permissions -> setPosixFilePermissions(targetFile, permissions)); + if (targetFile.toFile().setExecutable(executable)) { + LOG.warn("could not set executable flag for file {}", targetFile); + } addFileToScm(path, targetFile); } - default Optional> getPosixFilePermissions(Path targetFile) { - try { - return of(Files.getPosixFilePermissions(targetFile)); - } catch (UnsupportedOperationException e) { - LOG.trace("posix file permissions not supported"); - return empty(); - } catch (IOException e) { - LOG.info("could not read posix file permissions for file {}", targetFile); - return empty(); - } - } - - default void setPosixFilePermissions(Path targetFile, Set permissions) { - try { - Files.setPosixFilePermissions(targetFile, permissions); - } catch (UnsupportedOperationException e) { - LOG.trace("posix file permissions not supported"); - } catch (IOException e) { - LOG.info("could not write posix file permissions to file {}", targetFile); - } - } - void addFileToScm(String name, Path file); default ContextEntry.ContextBuilder createFileContext(String path) { diff --git a/scm-core/src/test/java/sonia/scm/repository/spi/ModifyWorkerHelperTest.java b/scm-core/src/test/java/sonia/scm/repository/spi/ModifyWorkerHelperTest.java index c2996429a1..1455b300d2 100644 --- a/scm-core/src/test/java/sonia/scm/repository/spi/ModifyWorkerHelperTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/spi/ModifyWorkerHelperTest.java @@ -1,7 +1,5 @@ package sonia.scm.repository.spi; -import com.google.common.collect.ImmutableSet; -import org.assertj.core.api.Assumptions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junitpioneer.jupiter.TempDirectory; @@ -10,16 +8,8 @@ import sonia.scm.repository.Repository; import java.io.File; import java.io.FileWriter; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; -import static java.nio.file.attribute.PosixFilePermission.GROUP_EXECUTE; -import static java.nio.file.attribute.PosixFilePermission.GROUP_READ; -import static java.nio.file.attribute.PosixFilePermission.OTHERS_EXECUTE; -import static java.nio.file.attribute.PosixFilePermission.OTHERS_READ; -import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; -import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; -import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; import static org.assertj.core.api.Assertions.assertThat; @ExtendWith(TempDirectory.class) @@ -31,19 +21,13 @@ class ModifyWorkerHelperTest { File target = createFile(temp, "executable.sh"); File newFile = createFile(temp, "other"); - Assumptions.assumeThatThrownBy(() -> { - Files.setPosixFilePermissions(target.toPath(), - ImmutableSet.of(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE, GROUP_READ, GROUP_EXECUTE, OTHERS_READ, OTHERS_EXECUTE)); - Files.setPosixFilePermissions(newFile.toPath(), - ImmutableSet.of(OWNER_READ, OWNER_WRITE, GROUP_READ, OTHERS_READ)); - }).doesNotThrowAnyException(); // ignore this test on systems that does not support posix file permissions + target.setExecutable(true); ModifyWorkerHelper helper = new MinimalModifyWorkerHelper(temp); helper.modify("executable.sh", newFile); - assertThat(Files.getPosixFilePermissions(target.toPath())) - .contains(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE, GROUP_READ, GROUP_EXECUTE, OTHERS_READ, OTHERS_EXECUTE); + assertThat(target.canExecute()).isTrue(); } private File createFile(Path temp, String fileName) throws IOException { From d59778c3bd24e2bd41f715ef878a876643f007b7 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 27 Feb 2020 16:41:19 +0100 Subject: [PATCH 107/251] fix secondary navigation responsiveness --- scm-ui/ui-components/src/navigation/Section.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scm-ui/ui-components/src/navigation/Section.tsx b/scm-ui/ui-components/src/navigation/Section.tsx index c4af45e203..951ac71a3a 100644 --- a/scm-ui/ui-components/src/navigation/Section.tsx +++ b/scm-ui/ui-components/src/navigation/Section.tsx @@ -27,7 +27,7 @@ const SmallButton = styled(Button)` const MenuLabel = styled.p` min-height: 2.5rem; display: flex; - justify-content: ${(props: StylingProps) => (props.collapsed ? "center" : "space-between")}; + justify-content: ${(props: { collapsed: boolean }) => (props.collapsed ? "center" : "space-between")}; `; const Section: FC = ({ label, children, collapsed, onCollapse }) => { @@ -48,10 +48,10 @@ const Section: FC = ({ label, children, collapsed, onCollapse }) => { return ( - + {collapsed ? "" : label} {onCollapse && ( - onCollapse(!collapsed)}> + onCollapse(!collapsed)}> {arrowIcon} )} From aa05b8ede11745925c86531e8e717549b8ba0c4a Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Fri, 28 Feb 2020 08:13:58 +0100 Subject: [PATCH 108/251] update openapi.md --- scm-webapp/src/main/doc/openapi.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scm-webapp/src/main/doc/openapi.md b/scm-webapp/src/main/doc/openapi.md index bf565699d4..4f5fa57955 100644 --- a/scm-webapp/src/main/doc/openapi.md +++ b/scm-webapp/src/main/doc/openapi.md @@ -1,7 +1,7 @@ The following REST documentation describes all public endpoints of your SCM-Manager instance. You can try the endpoints with or without authentication right on the swagger surface provided by the OpenAPI-Plugin. -For authenticated requests please use the "Authorize" button and insert your preferred authentication method. +For authenticated requests please login to the SCM-Manager. You can also use the "Authorize" button and insert your preferred authentication method. For basic authentication simply use your SCM-Manager credentials. If you want to use the bearer token authentication, you can generate an valid token using the authentication endpoint and copy the response body. @@ -10,6 +10,6 @@ Using the HATEOAS architecture for REST allows us to provide discoverable and se The responses are build using the [HAL JSON format](http://stateless.co/hal_specification.html). HAL makes the API human-friendly and simplifies the communication between the frontend and the server using links and embedded resources. -We highly suggest using HAL links when creating new functions for SCM-Manager since they are consistent and can be -permission checked before being append to the response. The links and embedded resources can also be used by plugins, which can +We highly suggest using HAL links when creating new functions for SCM-Manager since they are consistent and are only +appended to the response when user has the necessary permissions. The links and embedded resources can also be used by plugins, which can define new resources or enrich existing ones. From 2d038327d0ceb7ba899c18c8c92419619b921b75 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Fri, 28 Feb 2020 13:15:27 +0100 Subject: [PATCH 109/251] fix re-render bug for changesets --- scm-ui/ui-components/src/repos/DiffFile.tsx | 26 +++++++++----- .../src/repos/containers/Changesets.tsx | 34 ++++++++++--------- .../src/repos/containers/RepositoryRoot.tsx | 14 +++++--- .../src/repos/modules/changesets.test.ts | 34 +++++++++++++++++++ .../ui-webapp/src/repos/modules/changesets.ts | 14 ++++++-- 5 files changed, 89 insertions(+), 33 deletions(-) diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx index edf1d8c79a..90a4239971 100644 --- a/scm-ui/ui-components/src/repos/DiffFile.tsx +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -10,6 +10,7 @@ import Icon from "../Icon"; import { Change, ChangeEvent, DiffObjectProps, File, Hunk as HunkType } from "./DiffTypes"; import TokenizedDiffView from "./TokenizedDiffView"; import DiffButton from "./DiffButton"; +import { MenuContext } from "@scm-manager/ui-components"; const EMPTY_ANNOTATION_FACTORY = {}; @@ -100,10 +101,13 @@ class DiffFile extends React.Component { } }; - toggleSideBySide = () => { - this.setState(state => ({ - sideBySide: !state.sideBySide - })); + toggleSideBySide = (callback: (collapsed: boolean) => void) => { + this.setState( + state => ({ + sideBySide: !state.sideBySide + }), + () => callback(this.state.sideBySide ? this.state.sideBySide : false) + ); }; setCollapse = (collapsed: boolean) => { @@ -259,11 +263,15 @@ class DiffFile extends React.Component { file.hunks && file.hunks.length > 0 ? ( - + + {({ setMenuCollapsed }) => ( + this.toggleSideBySide(() => setMenuCollapsed(sideBySide ? true : false))} + /> + )} + {fileControls} diff --git a/scm-ui/ui-webapp/src/repos/containers/Changesets.tsx b/scm-ui/ui-webapp/src/repos/containers/Changesets.tsx index a5ca949cee..9b58662805 100644 --- a/scm-ui/ui-webapp/src/repos/containers/Changesets.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/Changesets.tsx @@ -1,7 +1,7 @@ import React from "react"; import { connect } from "react-redux"; import { compose } from "redux"; -import { withRouter } from "react-router-dom"; +import { withRouter, RouteComponentProps } from "react-router-dom"; import { WithTranslation, withTranslation } from "react-i18next"; import { Branch, Changeset, PagedCollection, Repository } from "@scm-manager/ui-types"; import { @@ -20,23 +20,21 @@ import { selectListAsCollection } from "../modules/changesets"; -type Props = WithTranslation & { - repository: Repository; - branch: Branch; - page: number; +type Props = RouteComponentProps & + WithTranslation & { + repository: Repository; + branch: Branch; + page: number; - // State props - changesets: Changeset[]; - list: PagedCollection; - loading: boolean; - error: Error; + // State props + changesets: Changeset[]; + list: PagedCollection; + loading: boolean; + error: Error; - // Dispatch props - fetchChangesets: (p1: Repository, p2: Branch, p3: number) => void; - - // context props - match: any; -}; + // Dispatch props + fetchChangesets: (p1: Repository, p2: Branch, p3: number) => void; + }; class Changesets extends React.Component { componentDidMount() { @@ -44,6 +42,10 @@ class Changesets extends React.Component { fetchChangesets(repository, branch, page); } + shouldComponentUpdate(nextProps: Readonly, nextState: Readonly<{}>, nextContext: any): boolean { + return this.props.changesets !== nextProps.changesets; + } + render() { const { changesets, loading, error, t } = this.props; diff --git a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx index c436219e8b..3d65d16cb2 100644 --- a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx @@ -138,10 +138,14 @@ class RepositoryRoot extends React.Component { const url = this.matchedUrl(); - const extensionProps = { + const extensionProps: any = { repository, url, - indexLinks, + indexLinks + }; + + const navExtensionProps = { + ...extensionProps, collapsedRepositoryMenu: menuCollapsed }; @@ -217,7 +221,7 @@ class RepositoryRoot extends React.Component { } collapsed={menuCollapsed} > - + { activeOnlyWhenExact={false} title={t("repositoryRoot.menu.sourcesNavLink")} /> - + { > - + diff --git a/scm-ui/ui-webapp/src/repos/modules/changesets.test.ts b/scm-ui/ui-webapp/src/repos/modules/changesets.test.ts index b0d9675c40..45191b1e69 100644 --- a/scm-ui/ui-webapp/src/repos/modules/changesets.test.ts +++ b/scm-ui/ui-webapp/src/repos/modules/changesets.test.ts @@ -581,6 +581,31 @@ describe("changesets", () => { ]); }); + it("should return always the same changeset array for the given parameters", () => { + const state = { + changesets: { + "foo/bar": { + byId: { + id2: { + id: "id2" + }, + id1: { + id: "id1" + } + }, + byBranch: { + "": { + entries: ["id1", "id2"] + } + } + } + } + }; + const one = getChangesets(state, repository); + const two = getChangesets(state, repository); + expect(one).toBe(two); + }); + it("should return true, when fetching changesets is pending", () => { const state = { pending: { @@ -639,5 +664,14 @@ describe("changesets", () => { expect(collection.page).toBe(1); expect(collection.pageTotal).toBe(10); }); + + it("should return always the same empty object", () => { + const state = { + changesets: {} + }; + const one = selectListAsCollection(state, repository); + const two = selectListAsCollection(state, repository); + expect(one).toBe(two); + }); }); }); diff --git a/scm-ui/ui-webapp/src/repos/modules/changesets.ts b/scm-ui/ui-webapp/src/repos/modules/changesets.ts index 5be98cf4df..3e75a1668d 100644 --- a/scm-ui/ui-webapp/src/repos/modules/changesets.ts +++ b/scm-ui/ui-webapp/src/repos/modules/changesets.ts @@ -3,6 +3,7 @@ import { apiClient, urls } from "@scm-manager/ui-components"; import { isPending } from "../../modules/pending"; import { getFailure } from "../../modules/failure"; import { Action, Branch, PagedCollection, Repository } from "@scm-manager/ui-types"; +import memoizeOne from "memoize-one"; export const FETCH_CHANGESETS = "scm/repos/FETCH_CHANGESETS"; export const FETCH_CHANGESETS_PENDING = `${FETCH_CHANGESETS}_${PENDING_SUFFIX}`; @@ -254,10 +255,15 @@ export function getChangesets(state: object, repository: Repository, branch?: Br return null; } + return collectChangesets(stateRoot, changesets); +} +const mapChangesets = (stateRoot, changesets) => { return changesets.entries.map((id: string) => { return stateRoot.byId[id]; }); -} +}; + +const collectChangesets = memoizeOne(mapChangesets); export function getChangeset(state: object, repository: Repository, id: string) { const key = createItemId(repository); @@ -291,6 +297,8 @@ export function getFetchChangesetsFailure(state: object, repository: Repository, return getFailure(state, FETCH_CHANGESETS, createItemId(repository, branch)); } +const EMPTY = {}; + const selectList = (state: object, repository: Repository, branch?: Branch) => { const repoId = createItemId(repository); @@ -302,7 +310,7 @@ const selectList = (state: object, repository: Repository, branch?: Branch) => { return repoState.byBranch[branchName]; } } - return {}; + return EMPTY; }; const selectListEntry = (state: object, repository: Repository, branch?: Branch): object => { @@ -310,7 +318,7 @@ const selectListEntry = (state: object, repository: Repository, branch?: Branch) if (list.entry) { return list.entry; } - return {}; + return EMPTY; }; export const selectListAsCollection = (state: object, repository: Repository, branch?: Branch): PagedCollection => { From af3c1c52e82c851d83a5eac998ff07bc8c1db271 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Fri, 28 Feb 2020 12:18:37 +0000 Subject: [PATCH 110/251] Close branch bugfix/keep_file_attributes From 4df33fcc7396a924ce890c259670280daae27792 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Fri, 28 Feb 2020 14:58:56 +0100 Subject: [PATCH 111/251] fix styling --- scm-ui/ui-components/src/navigation/Section.tsx | 2 ++ scm-ui/ui-components/src/repos/DiffFile.tsx | 2 +- .../src/repos/containers/RepositoryRoot.tsx | 12 ++++-------- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/scm-ui/ui-components/src/navigation/Section.tsx b/scm-ui/ui-components/src/navigation/Section.tsx index 951ac71a3a..5b1ba58aa3 100644 --- a/scm-ui/ui-components/src/navigation/Section.tsx +++ b/scm-ui/ui-components/src/navigation/Section.tsx @@ -21,6 +21,8 @@ const SectionContainer = styled.div` `; const SmallButton = styled(Button)` + padding-left: 1rem; + padding-right: 1rem; height: 1.5rem; `; diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx index 90a4239971..09abcc6e5a 100644 --- a/scm-ui/ui-components/src/repos/DiffFile.tsx +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -268,7 +268,7 @@ class DiffFile extends React.Component { this.toggleSideBySide(() => setMenuCollapsed(sideBySide ? true : false))} + onClick={() => this.toggleSideBySide(() => setMenuCollapsed(true))} /> )} diff --git a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx index 3d65d16cb2..58eb18391d 100644 --- a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx @@ -141,11 +141,7 @@ class RepositoryRoot extends React.Component { const extensionProps: any = { repository, url, - indexLinks - }; - - const navExtensionProps = { - ...extensionProps, + indexLinks, collapsedRepositoryMenu: menuCollapsed }; @@ -221,7 +217,7 @@ class RepositoryRoot extends React.Component { } collapsed={menuCollapsed} > - + { activeOnlyWhenExact={false} title={t("repositoryRoot.menu.sourcesNavLink")} /> - + { > - + From 9705347c1fa97f97d023630e2cd70c94c3ae2c64 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Fri, 28 Feb 2020 15:14:13 +0100 Subject: [PATCH 112/251] update storyshots after merge --- .../src/__snapshots__/storyshots.test.ts.snap | 104 +++++++++--------- 1 file changed, 52 insertions(+), 52 deletions(-) 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 790c2cdc94..cb24897d56 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -484,7 +484,7 @@ exports[`Storyshots DateFromNow Default 1`] = ` exports[`Storyshots Diff Binaries 1`] = `