Merge pull request #1196 from scm-manager/feature/annotate

Annotate View
This commit is contained in:
eheimbuch
2020-06-17 13:29:26 +02:00
committed by GitHub
56 changed files with 5311 additions and 2002 deletions

View File

@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added links in diff views to expand the gaps between "hunks" ([#1178](https://github.com/scm-manager/scm-manager/pull/1178))
- Show commit contributors in table on changeset details view ([#1169](https://github.com/scm-manager/scm-manager/pull/1169))
- Show changeset parents on changeset details view ([#1189](https://github.com/scm-manager/scm-manager/pull/1189))
- Annotate view to display commit metadata for each line of a file ([#1196](https://github.com/scm-manager/scm-manager/pull/1196))
### Fixed
- Avoid caching of detected browser language ([#1176](https://github.com/scm-manager/scm-manager/pull/1176))

View File

@@ -71,13 +71,12 @@ public class VndMediaType {
@SuppressWarnings("squid:S2068")
public static final String PASSWORD_OVERWRITE = PREFIX + "passwordOverwrite" + SUFFIX;
public static final String PERMISSION_COLLECTION = PREFIX + "permissionCollection" + SUFFIX;
public static final String MERGE_RESULT = PREFIX + "mergeResult" + SUFFIX;
public static final String MERGE_COMMAND = PREFIX + "mergeCommand" + SUFFIX;
public static final String NAMESPACE_STRATEGIES = PREFIX + "namespaceStrategies" + SUFFIX;
public static final String ME = PREFIX + "me" + SUFFIX;
public static final String SOURCE = PREFIX + "source" + SUFFIX;
public static final String ANNOTATE = PREFIX + "annotate" + SUFFIX;
public static final String ERROR_TYPE = PREFIX + "error" + SUFFIX;
public static final String REPOSITORY_ROLE = PREFIX + "repositoryRole" + SUFFIX;

View File

@@ -34,7 +34,7 @@
"@types/react-select": "^2.0.19",
"@types/react-syntax-highlighter": "^11.0.1",
"@types/storybook__addon-storyshots": "^5.1.1",
"@types/styled-components": "^4.1.19",
"@types/styled-components": "^5.1.0",
"enzyme-context": "^1.1.2",
"enzyme-context-react-router-4": "^2.0.0",
"fetch-mock": "^7.5.1",
@@ -60,7 +60,7 @@
"react-markdown": "^4.0.6",
"react-router-dom": "^5.1.2",
"react-select": "^2.1.2",
"react-syntax-highlighter": "^11.0.2"
"react-syntax-highlighter": "https://github.com/conorhastings/react-syntax-highlighter#08bcf49b1aa7877ce94f7208e73dfa6bef8b26e7"
},
"babel": {
"presets": [

View File

@@ -24,25 +24,41 @@
import React from "react";
import DateFromNow from "./DateFromNow";
import { storiesOf } from "@storybook/react";
import DateShort from "./DateShort";
import styled from "styled-components";
const baseProps = {
timeZone: "Europe/Berlin",
baseDate: "2019-10-12T13:56:42+02:00"
};
storiesOf("DateFromNow", module).add("Default", () => (
<div>
const dates = [
"2009-06-30T18:30:00+02:00",
"2019-06-30T18:30:00+02:00",
"2019-10-12T13:56:40+02:00",
"2019-10-11T13:56:40+02:00"
];
const Wrapper = styled.div`
padding: 2rem;
`;
storiesOf("Date", module)
.add("Date from now", () => (
<Wrapper>
{dates.map(d => (
<p>
<DateFromNow date="2009-06-30T18:30:00+02:00" {...baseProps} />
<DateFromNow date={d} {...baseProps} />
</p>
))}
</Wrapper>
))
.add("Short", () => (
<Wrapper>
{dates.map(d => (
<p>
<DateFromNow date="2019-06-30T18:30:00+02:00" {...baseProps} />
<DateShort date={d} {...baseProps} />
</p>
<p>
<DateFromNow date="2019-10-12T13:56:40+02:00" {...baseProps} />
</p>
<p>
<DateFromNow date="2019-10-11T13:56:40+02:00" {...baseProps} />
</p>
</div>
));
))}
</Wrapper>
));

View File

@@ -0,0 +1,31 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import styled from "styled-components";
const DateElement = styled.time`
border-bottom: 1px dotted rgba(219, 219, 219);
cursor: help;
`;
export default DateElement;

View File

@@ -21,109 +21,26 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
import { withTranslation, WithTranslation } from "react-i18next";
import { formatDistance, format, parseISO, Locale } from "date-fns";
import { enUS, de, es } from "date-fns/locale";
import styled from "styled-components";
type LocaleMap = {
[key: string]: Locale;
import React, { FC } from "react";
import useDateFormatter, { DateProps } from "./useDateFormatter";
import DateElement from "./DateElement";
type Props = DateProps & {
className?: string;
};
type DateInput = Date | string;
export const supportedLocales: LocaleMap = {
enUS,
en: enUS,
de,
es
};
type Props = WithTranslation & {
date?: DateInput;
timeZone?: string;
/**
* baseDate is the date from which the distance is calculated,
* default is the current time (new Date()). This property
* is required to keep snapshots tests green over the time on
* ci server.
*/
baseDate?: DateInput;
};
type Options = {
addSuffix: boolean;
locale: Locale;
timeZone?: string;
};
const DateElement = styled.time`
border-bottom: 1px dotted rgba(219, 219, 219);
cursor: help;
`;
export const chooseLocale = (language: string, languages?: string[]) => {
for (const lng of languages || []) {
const locale = supportedLocales[lng];
if (locale) {
return locale;
}
}
const locale = supportedLocales[language];
if (locale) {
return locale;
}
return enUS;
};
class DateFromNow extends React.Component<Props> {
getLocale = (): Locale => {
const { i18n } = this.props;
return chooseLocale(i18n.language, i18n.languages);
};
createOptions = () => {
const { timeZone } = this.props;
const options: Options = {
addSuffix: true,
locale: this.getLocale()
};
if (timeZone) {
options.timeZone = timeZone;
}
return options;
};
toDate = (value: DateInput): Date => {
if (value instanceof Date) {
return value;
}
return parseISO(value);
};
getBaseDate = () => {
const { baseDate } = this.props;
if (baseDate) {
return this.toDate(baseDate);
}
return new Date();
};
render() {
const { date } = this.props;
if (date) {
const isoDate = this.toDate(date);
const options = this.createOptions();
const distance = formatDistance(isoDate, this.getBaseDate(), options);
const formatted = format(isoDate, "yyyy-MM-dd HH:mm:ss", options);
return <DateElement title={formatted}>{distance}</DateElement>;
}
const DateFromNow: FC<Props> = ({ className, ...dateProps }) => {
const formatter = useDateFormatter(dateProps);
if (!formatter) {
return null;
}
}
export default withTranslation()(DateFromNow);
return (
<DateElement className={className} title={formatter.formatFull()}>
{formatter.formatDistance()}
</DateElement>
);
};
export default DateFromNow;

View File

@@ -0,0 +1,46 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC } from "react";
import useDateFormatter, { DateProps } from "./useDateFormatter";
import DateElement from "./DateElement";
type Props = DateProps & {
className?: string;
};
const DateShort: FC<Props> = ({ className, ...dateProps }) => {
const formatter = useDateFormatter(dateProps);
if (!formatter) {
return null;
}
return (
<DateElement className={className} title={formatter.formatFull()}>
{formatter.formatShort()}
</DateElement>
);
};
export default DateShort;

File diff suppressed because it is too large Load Diff

View File

@@ -44,6 +44,7 @@ import {
export { validation, urls, repositories };
export { default as DateFromNow } from "./DateFromNow";
export { default as DateShort } from "./DateShort";
export { default as ErrorNotification } from "./ErrorNotification";
export { default as ErrorPage } from "./ErrorPage";
export { default as Icon } from "./Icon";

View File

@@ -0,0 +1,131 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { storiesOf } from "@storybook/react";
import React, { FC } from "react";
import styled from "styled-components";
import Annotate from "./Annotate";
import { MemoryRouter } from "react-router-dom";
import repository from "../../__resources__/repository";
import { Binder, BinderContext } from "@scm-manager/ui-extensions";
import { Person } from "../../avatar/Avatar";
import { AnnotatedSource } from "@scm-manager/ui-types";
const Wrapper = styled.div`
margin: 2rem;
`;
const commitCreateNewApp = {
revision: "0d8c1d328f4599b363755671afe667c7ace52bae",
author: {
name: "Arthur Dent",
mail: "arthur.dent@hitchhiker.com"
},
description: "create new app",
when: new Date("2020-04-09T13:07:42Z")
};
const commitFixedMissingImport = {
revision: "fab38559ce3ab8c388e067712b4bd7ab94b9fa9b",
author: {
name: "Tricia Marie McMillan",
mail: "trillian@hitchhiker.com"
},
description: "fixed missing import",
when: new Date("2020-05-10T09:18:42Z")
};
const commitImplementMain = {
revision: "5203292ab2bc0c020dd22adc4d3897da4930e43f",
author: {
name: "Ford Prefect",
mail: "ford.prefect@hitchhiker.com"
},
description: "implemented main function",
when: new Date("2020-04-12T16:29:42Z")
};
const source: AnnotatedSource = {
language: "go",
lines: [
{
lineNumber: 1,
code: "package main",
...commitCreateNewApp
},
{
lineNumber: 2,
code: "",
...commitCreateNewApp
},
{
lineNumber: 3,
code: 'import "fmt"',
...commitFixedMissingImport
},
{
lineNumber: 4,
code: "",
...commitFixedMissingImport
},
{
lineNumber: 5,
code: "func main() {",
...commitCreateNewApp
},
{
lineNumber: 6,
code: ' fmt.Println("Hello World")',
...commitImplementMain
},
{
lineNumber: 7,
code: "}",
...commitCreateNewApp
},
{
lineNumber: 8,
code: "",
...commitCreateNewApp
}
]
};
const Robohash: FC = ({ children }) => {
const binder = new Binder("robohash");
binder.bind("avatar.factory", (person: Person) => `https://robohash.org/${person.mail}.png`);
return <BinderContext.Provider value={binder}>{children}</BinderContext.Provider>;
};
storiesOf("Annotate", module)
.addDecorator(storyFn => <MemoryRouter initialEntries={["/"]}>{storyFn()}</MemoryRouter>)
.addDecorator(storyFn => <Wrapper className="box">{storyFn()}</Wrapper>)
.add("Default", () => (
<Annotate source={source} repository={repository} baseDate={new Date("2020-04-16T09:22:42Z")} />
))
.add("With Avatars", () => (
<Robohash>
<Annotate source={source} repository={repository} baseDate={new Date("2020-04-15T09:47:42Z")} />
</Robohash>
));

View File

@@ -0,0 +1,157 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC, useReducer } from "react";
import { Repository, AnnotatedSource, AnnotatedLine } from "@scm-manager/ui-types";
// @ts-ignore
import { LightAsync as ReactSyntaxHighlighter, createElement } from "react-syntax-highlighter";
import { arduinoLight } from "react-syntax-highlighter/dist/cjs/styles/hljs";
import { DateInput } from "../../useDateFormatter";
import Popover from "./Popover";
import AnnotateLine from "./AnnotateLine";
import { Action } from "./actions";
type Props = {
source: AnnotatedSource;
repository: Repository;
baseDate?: DateInput;
};
type State = {
annotation?: AnnotatedLine;
offset?: number;
line?: number;
onPopover: boolean;
onLine: boolean;
};
const initialState = {
onPopover: false,
onLine: false
};
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "enter-line": {
if (state.onPopover) {
return state;
}
return {
annotation: action.annotation,
offset: action.offset,
line: action.line,
onLine: true,
onPopover: false
};
}
case "leave-line": {
if (state.onPopover) {
return {
...state,
onLine: false
};
}
return initialState;
}
case "enter-popover": {
return {
...state,
onPopover: true
};
}
case "leave-popover": {
if (state.onLine) {
return {
...state,
onPopover: false
};
}
return initialState;
}
}
};
const Annotate: FC<Props> = ({ source, repository, baseDate }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const defaultRenderer = ({ rows, stylesheet, useInlineStyles }: any) => {
let lastRevision = "";
return rows.map((node: React.ReactNode, i: number) => {
const line = createElement({
node,
stylesheet,
useInlineStyles,
key: `code-segment${i}`
});
if (i + 1 < rows.length) {
const annotation = source.lines[i];
const newAnnotation = annotation.revision !== lastRevision;
lastRevision = annotation.revision;
return (
<AnnotateLine dispatch={dispatch} annotation={annotation} showAnnotation={newAnnotation} nr={i + 1}>
{line}
</AnnotateLine>
);
}
return line;
});
};
let popover = null;
if ((state.onPopover || state.onLine) && state.annotation) {
popover = (
<Popover
annotation={state.annotation}
dispatch={dispatch}
offsetTop={state.offset}
repository={repository}
baseDate={baseDate}
/>
);
}
const code = source.lines.reduce((content, line) => {
content += line.code + "\n";
return content;
}, "");
return (
<div style={{ position: "relative" }}>
{popover}
<ReactSyntaxHighlighter
showLineNumbers={false}
language={source.language ? source.language : "text"}
style={arduinoLight}
renderer={defaultRenderer}
>
{code}
</ReactSyntaxHighlighter>
</div>
);
};
export default Annotate;

View File

@@ -0,0 +1,145 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC, Dispatch, useRef } from "react";
import styled from "styled-components";
import AuthorImage from "./AuthorImage";
import DateShort from "../../DateShort";
import { Action } from "./actions";
import { AnnotatedLine } from "@scm-manager/ui-types";
const LineElement = styled.div`
display: inline-block;
margin: 0;
padding: 0;
height: 100%;
vertical-align: top;
`;
const Author = styled(LineElement)`
width: 8em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
const When = styled(LineElement)`
display: inline-block;
width: 6.5em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0 0.5em;
`;
const LineNumber = styled(LineElement)`
width: 3em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
border-left: 1px solid lightgrey;
border-right: 1px solid lightgrey;
text-align: right;
padding: 0 0.5em;
`;
const Line = styled.div`
margin: 0;
padding: 0;
height: 1.5em;
vertical-align: top;
`;
const Metadata = styled(LineElement)`
cursor: help;
`;
const EmptyMetadata = styled(LineElement)`
width: 16.7em;
`;
const dispatchDeferred = (dispatch: Dispatch<Action>, action: Action) => {
setTimeout(() => dispatch(action), 250);
};
type Props = {
annotation: AnnotatedLine;
showAnnotation: boolean;
nr: number;
dispatch: Dispatch<Action>;
};
const AnnotateLine: FC<Props> = ({ annotation, showAnnotation, dispatch, nr, children }) => {
const link = useRef<HTMLDivElement>(null);
const onMouseEnter = () => {
if (showAnnotation) {
dispatchDeferred(dispatch, {
annotation,
line: nr,
offset: link.current!.offsetTop,
type: "enter-line"
});
}
};
const OnMouseLeave = () => {
if (showAnnotation) {
dispatchDeferred(dispatch, {
line: nr,
type: "leave-line"
});
}
};
if (!showAnnotation) {
return (
<Line>
<EmptyMetadata />
<LineNumber>{nr}</LineNumber> <LineElement>{children}</LineElement>
</Line>
);
}
return (
<Line>
<Metadata className="has-text-info" onMouseOver={onMouseEnter} onMouseOut={OnMouseLeave} ref={link}>
<Author>
<AuthorImage person={annotation.author} />
{annotation.author.name}
</Author>{" "}
<When>
<DateShort date={annotation.when} />
</When>{" "}
</Metadata>
<LineNumber>{nr}</LineNumber> <LineElement>{children}</LineElement>
</Line>
);
};
export default AnnotateLine;

View File

@@ -0,0 +1,36 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { AvatarImage } from "../../avatar";
import styled from "styled-components";
const AuthorImage = styled(AvatarImage)`
width: 1em;
height: 1em;
margin-right: 0.2em;
margin-bottom: 0.2em;
vertical-align: middle;
`;
export default AuthorImage;

View File

@@ -0,0 +1,142 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC, useState, useRef, useLayoutEffect, Dispatch } from "react";
import styled from "styled-components";
import { Link } from "react-router-dom";
import DateFromNow from "../../DateFromNow";
import { SingleContributor } from "../changesets";
import { DateInput } from "../../useDateFormatter";
import { Repository, AnnotatedLine } from "@scm-manager/ui-types";
import AuthorImage from "./AuthorImage";
import { Action } from "./actions";
import {useTranslation} from "react-i18next";
const PopoverContainer = styled.div`
position: absolute;
left: 2.25em;
z-index: 100;
width: 30em;
display: block;
&:before {
position: absolute;
content: "";
border-style: solid;
pointer-events: none;
height: 0;
width: 0;
top: 100%;
left: 5.5em;
border-color: transparent;
border-bottom-color: white;
border-left-color: white;
border-width: 0.4rem;
margin-left: -0.4rem;
margin-top: -0.4rem;
-webkit-transform-origin: center;
transform-origin: center;
box-shadow: -1px 1px 2px rgba(10, 10, 10, 0.2);
transform: rotate(-45deg);
}
`;
const SmallHr = styled.hr`
margin: 0.5em 0;
`;
const PopoverHeading = styled.div`
height: 1.5em;
`;
const PopoverDescription = styled.p`
margin-top: 0.5em;
`;
const shortRevision = (revision: string) => {
if (revision.length > 7) {
return revision.substring(0, 7);
}
return revision;
};
type PopoverProps = {
annotation: AnnotatedLine;
offsetTop?: number;
repository: Repository;
baseDate?: DateInput;
dispatch: Dispatch<Action>;
};
const Popover: FC<PopoverProps> = ({ annotation, offsetTop, repository, baseDate, dispatch }) => {
const [t] = useTranslation("repos");
const [height, setHeight] = useState(125);
const ref = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
if (ref.current) {
setHeight(ref.current.clientHeight);
}
}, [ref]);
const onMouseEnter = () => {
dispatch({
type: "enter-popover"
});
};
const OnMouseLeave = () => {
dispatch({
type: "leave-popover"
});
};
const top = (offsetTop || 0) - height - 5;
return (
<PopoverContainer
ref={ref}
onMouseEnter={onMouseEnter}
onMouseLeave={OnMouseLeave}
className="box"
style={{ top: `${top}px` }}
>
<PopoverHeading className="is-clearfix">
<span className="is-pulled-left">
<AuthorImage person={annotation.author} />
<SingleContributor person={annotation.author} displayTextOnly={true} />
</span>
<DateFromNow className="is-pulled-right" date={annotation.when} baseDate={baseDate} />
</PopoverHeading>
<SmallHr />
<p>
{t("changeset.label") + " "}
<Link to={`/repo/${repository.namespace}/${repository.name}/code/changeset/${annotation.revision}`}>
{shortRevision(annotation.revision)}
</Link>
</p>
<PopoverDescription className="content">{annotation.description}</PopoverDescription>
</PopoverContainer>
);
};
export default Popover;

View File

@@ -0,0 +1,47 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { AnnotatedLine } from "@scm-manager/ui-types";
type EnterLine = {
annotation: AnnotatedLine;
offset: number;
line: number;
type: "enter-line";
};
type LeaveLine = {
line: number;
type: "leave-line";
};
type EnterPopover = {
type: "enter-popover";
};
type LeavePopover = {
type: "leave-popover";
};
export type Action = EnterLine | LeaveLine | EnterPopover | LeavePopover;

View File

@@ -0,0 +1,25 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
export { default as Annotate } from "./Annotate";

View File

@@ -36,6 +36,7 @@ type Props = {
type PersonProps = {
person: Person;
className?: string;
displayTextOnly?: boolean;
};
@@ -70,7 +71,7 @@ const ContributorWithAvatar: FC<PersonAvatarProps> = ({ person, avatar }) => {
return <ContributorAvatar src={avatar} alt={person.name} title={person.name} />;
};
const SingleContributor: FC<PersonProps> = ({ person, displayTextOnly }) => {
export const SingleContributor: FC<PersonProps> = ({ person, className, displayTextOnly }) => {
const [t] = useTranslation("repos");
const avatar = useAvatar(person);
if (!displayTextOnly && avatar) {
@@ -78,12 +79,16 @@ const SingleContributor: FC<PersonProps> = ({ person, displayTextOnly }) => {
}
if (person.mail) {
return (
<a href={"mailto:" + person.mail} title={t("changeset.contributors.mailto") + " " + person.mail}>
<a
className={className}
href={"mailto:" + person.mail}
title={t("changeset.contributors.mailto") + " " + person.mail}
>
{person.name}
</a>
);
}
return <>{person.name}</>;
return <span className={className}>{person.name}</span>;
};
type PersonsProps = {

View File

@@ -25,7 +25,7 @@
import * as changesets from "./changesets";
export { changesets };
export { default as ChangesetAuthor } from "./ChangesetAuthor";
export { default as ChangesetAuthor, SingleContributor } from "./ChangesetAuthor";
export { default as ChangesetButtonGroup } from "./ChangesetButtonGroup";
export { default as ChangesetDiff } from "./ChangesetDiff";
export { default as ChangesetId } from "./ChangesetId";

View File

@@ -39,6 +39,7 @@ import {
export { diffs };
export * from "./annotate";
export * from "./changesets";
export { default as Diff } from "./Diff";

View File

@@ -22,7 +22,7 @@
* SOFTWARE.
*/
import { chooseLocale, supportedLocales } from "./DateFromNow";
import { chooseLocale, supportedLocales } from "./useDateFormatter";
describe("test choose locale", () => {
it("should choose de", () => {

View File

@@ -0,0 +1,126 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { useTranslation } from "react-i18next";
import { enUS, de, es } from "date-fns/locale";
import { formatDistance, format, Locale, parseISO } from "date-fns";
type LocaleMap = {
[key: string]: Locale;
};
export const supportedLocales: LocaleMap = {
enUS,
en: enUS,
de,
es
};
type Options = {
addSuffix: boolean;
locale: Locale;
timeZone?: string;
};
export const chooseLocale = (language: string, languages?: string[]) => {
for (const lng of languages || []) {
const locale = supportedLocales[lng];
if (locale) {
return locale;
}
}
const locale = supportedLocales[language];
if (locale) {
return locale;
}
return enUS;
};
export type DateInput = Date | string;
export type DateProps = {
date?: DateInput;
timeZone?: string;
/**
* baseDate is the date from which the distance is calculated,
* default is the current time (new Date()). This property
* is required to keep snapshots tests green over the time on
* ci server.
*/
baseDate?: DateInput;
};
const createOptions = (locale: Locale, timeZone?: string) => {
const options: Options = {
addSuffix: true,
locale
};
if (timeZone) {
options.timeZone = timeZone;
}
return options;
};
const createBaseDate = (baseDate?: DateInput) => {
if (baseDate) {
return toDate(baseDate);
}
return new Date();
};
const toDate = (value: DateInput): Date => {
if (value instanceof Date) {
return value;
}
return parseISO(value);
};
const useDateFormatter = ({ date, baseDate, timeZone }: DateProps) => {
const { i18n } = useTranslation();
if (!date) {
return null;
}
const isoDate = toDate(date);
const base = createBaseDate(baseDate);
const locale = chooseLocale(i18n.language, i18n.languages);
const options = createOptions(locale, timeZone);
return {
formatShort() {
return format(isoDate, "yyyy-MM-dd", options);
},
formatFull() {
return format(isoDate, "yyyy-MM-dd HH:mm:ss", options);
},
formatDistance() {
return formatDistance(isoDate, base, options);
}
};
};
export default useDateFormatter;

View File

@@ -15,7 +15,7 @@
"react-redux": "^5.0.7",
"react-router-dom": "^5.1.2",
"redux": "^4.0.0",
"styled-components": "^4.4.0"
"styled-components": "^5.1.0"
},
"devDependencies": {
"@scm-manager/babel-preset": "^2.0.0",
@@ -35,7 +35,7 @@
"@types/react": "^16.9.9",
"@types/react-redux": "5.0.7",
"@types/react-router-dom": "^5.1.0",
"@types/styled-components": "^4.1.19",
"@types/styled-components": "^5.1.0",
"jest": "^24.9.0"
},
"publishConfig": {

View File

@@ -0,0 +1,39 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { Person } from "./Person";
export type AnnotatedSource = {
lines: AnnotatedLine[];
language?: string;
};
export type AnnotatedLine = {
author: Person;
code: string;
description: string;
lineNumber: number;
revision: string;
when: Date;
};

View File

@@ -25,11 +25,7 @@
import { Collection, Links } from "./hal";
import { Tag } from "./Tags";
import { Branch } from "./Branches";
export type Person = {
name: string;
mail?: string;
};
import { Person } from "./Person";
export type Changeset = Collection & {
id: string;

View File

@@ -0,0 +1,28 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
export type Person = {
name: string;
mail?: string;
};

View File

@@ -34,7 +34,11 @@ export { RepositoryType, RepositoryTypeCollection } from "./RepositoryTypes";
export { Branch, BranchRequest } from "./Branches";
export { Changeset, Person, Contributor, ParentChangeset } from "./Changesets";
export { Person } from "./Person";
export { Changeset, Contributor, ParentChangeset } from "./Changesets";
export { AnnotatedSource, AnnotatedLine } from "./Annotate";
export { Tag } from "./Tags";

View File

@@ -22,7 +22,7 @@
"redux-devtools-extension": "^2.13.5",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0",
"styled-components": "^4.4.0",
"styled-components": "^5.1.0",
"systemjs": "0.21.6"
},
"scripts": {
@@ -39,7 +39,7 @@
"@types/react-dom": "^16.9.2",
"@types/react-redux": "5.0.7",
"@types/react-router-dom": "^5.1.0",
"@types/styled-components": "^4.1.19",
"@types/styled-components": "^5.1.0",
"@types/systemjs": "^0.20.6",
"fetch-mock": "^7.5.1",
"react-test-renderer": "^16.10.2",

View File

@@ -82,6 +82,7 @@
"collapseDiffs": "Auf-/Zuklappen"
},
"changeset": {
"label": "Changeset",
"description": "Beschreibung",
"summary": "Changeset <0/> wurde <1/> committet",
"shortSummary": "Committet <0/> <1/>",

View File

@@ -82,6 +82,7 @@
"collapseDiffs": "Collapse"
},
"changeset": {
"label": "Changeset",
"description": "Description",
"summary": "Changeset <0/> was committed <1/>",
"shortSummary": "Committed <0/> <1/>",

View File

@@ -24,20 +24,27 @@
import React from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import { Button, ButtonAddons } from "@scm-manager/ui-components";
import { SourceViewSelection } from "../../containers/Content";
type Props = WithTranslation & {
className?: string;
historyIsSelected: boolean;
showHistory: (p: boolean) => void;
selected: SourceViewSelection;
showSources: () => void;
showHistory: () => void;
showAnnotations: () => void;
};
class FileButtonAddons extends React.Component<Props> {
showHistory = () => {
this.props.showHistory(true);
this.props.showHistory();
};
showSources = () => {
this.props.showHistory(false);
this.props.showSources();
};
showAnnotations = () => {
this.props.showAnnotations();
};
color = (selected: boolean) => {
@@ -45,19 +52,26 @@ class FileButtonAddons extends React.Component<Props> {
};
render() {
const { className, t, historyIsSelected } = this.props;
const { className, t, selected, showSources, showHistory, showAnnotations } = this.props;
return (
<ButtonAddons className={className}>
<div title={t("sources.content.sourcesButton")}>
<Button action={this.showSources} color={this.color(!historyIsSelected)}>
<Button action={showSources} color={this.color(selected === "source")}>
<span className="icon">
<i className="fas fa-code" />
</span>
</Button>
</div>
<div title={t("sources.content.annotateButton")}>
<Button action={showAnnotations} color={this.color(selected === "annotations")}>
<span className="icon">
<i className="fas fa-user-clock" />
</span>
</Button>
</div>
<div title={t("sources.content.historyButton")}>
<Button action={this.showHistory} color={this.color(historyIsSelected)}>
<Button action={showHistory} color={this.color(selected === "history")}>
<span className="icon">
<i className="fas fa-history" />
</span>

View File

@@ -0,0 +1,67 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC, useEffect, useState } from "react";
import { Link, Repository, File, AnnotatedSource } from "@scm-manager/ui-types";
import { Annotate, apiClient, ErrorNotification, Loading } from "@scm-manager/ui-components";
import { getContentType } from "./contentType";
type Props = {
file: File;
repository: Repository;
};
const AnnotateView: FC<Props> = ({ file, repository }) => {
const [annotation, setAnnotation] = useState<AnnotatedSource | undefined>(undefined);
const [language, setLanguage] = useState<string | undefined>(undefined);
const [error, setError] = useState<Error | undefined>(undefined);
const [loading, setLoading] = useState(true);
useEffect(() => {
const languagePromise = getContentType((file._links.self as Link).href).then(result =>
setLanguage(result.language)
);
const apiClientPromise = apiClient
.get((file._links.annotate as Link).href)
.then(response => response.json())
.then(setAnnotation);
Promise.all([languagePromise, apiClientPromise])
.then(() => setLoading(false))
.catch(setError);
}, [file]);
if (error) {
return <ErrorNotification error={error} />;
}
if (!annotation || loading) {
return <Loading />;
}
return <Annotate source={{ ...annotation, language }} repository={repository} />;
};
export default AnnotateView;

View File

@@ -33,6 +33,7 @@ import { getSources } from "../modules/sources";
import FileButtonAddons from "../components/content/FileButtonAddons";
import SourcesView from "./SourcesView";
import HistoryView from "./HistoryView";
import AnnotateView from "./AnnotateView";
type Props = WithTranslation & {
loading: boolean;
@@ -45,7 +46,7 @@ type Props = WithTranslation & {
type State = {
collapsed: boolean;
showHistory: boolean;
selected: SourceViewSelection;
errorFromExtension?: Error;
};
@@ -81,13 +82,15 @@ const LighterGreyBackgroundTable = styled.table`
background-color: #fbfbfb;
`;
export type SourceViewSelection = "source" | "history" | "annotations";
class Content extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
collapsed: true,
showHistory: false
selected: "source"
};
}
@@ -97,13 +100,6 @@ class Content extends React.Component<Props, State> {
}));
};
setShowHistoryState(showHistory: boolean) {
this.setState({
...this.state,
showHistory
});
}
handleExtensionError = (error: Error) => {
this.setState({
errorFromExtension: error
@@ -112,14 +108,15 @@ class Content extends React.Component<Props, State> {
showHeader() {
const { repository, file, revision } = this.props;
const { showHistory, collapsed } = this.state;
const { selected, collapsed } = this.state;
const icon = collapsed ? "angle-right" : "angle-down";
const selector = file._links.history ? (
<RightMarginFileButtonAddons
file={file}
historyIsSelected={showHistory}
showHistory={(changeShowHistory: boolean) => this.setShowHistoryState(changeShowHistory)}
selected={selected}
showSources={() => this.setState({ selected: "source" })}
showHistory={() => this.setState({ selected: "history" })}
showAnnotations={() => this.setState({ selected: "annotations" })}
/>
) : null;
@@ -212,15 +209,26 @@ class Content extends React.Component<Props, State> {
render() {
const { file, revision, repository, path, breadcrumb } = this.props;
const { showHistory, errorFromExtension } = this.state;
const { selected, errorFromExtension } = this.state;
const header = this.showHeader();
const content =
showHistory && file._links.history ? (
<HistoryView file={file} repository={repository} />
) : (
<SourcesView revision={revision} file={file} repository={repository} path={path} />
let content;
switch (selected) {
case "source":
content = (
<SourcesView revision={revision} file={file} repository={repository} path={path}/>
);
break;
case "history":
content = (
<HistoryView file={file} repository={repository}/>
);
break;
case "annotations":
content = (
<AnnotateView file={file} repository={repository} />
);
}
const moreInformation = this.showMoreInformation();
return (

View File

@@ -24,18 +24,18 @@
import { apiClient } from "@scm-manager/ui-components";
export function getContentType(url: string) {
export type ContentType = {
type : string;
language?: string;
}
export function getContentType(url: string) : Promise<ContentType> {
return apiClient
.head(url)
.then(response => {
return {
type: response.headers.get("Content-Type"),
language: response.headers.get("X-Programming-Language")
type: response.headers.get("Content-Type") || "application/octet-stream",
language: response.headers.get("X-Programming-Language") || undefined
};
})
.catch(err => {
return {
error: err
};
});
}

View File

@@ -0,0 +1,95 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import java.io.IOException;
public class AnnotateResource {
private final RepositoryServiceFactory serviceFactory;
private final BlameResultToBlameDtoMapper mapper;
@Inject
public AnnotateResource(RepositoryServiceFactory serviceFactory, BlameResultToBlameDtoMapper mapper) {
this.serviceFactory = serviceFactory;
this.mapper = mapper;
}
/**
* Returns the content of a file with additional information for each line regarding the last commit that
* changed this line: The revision, the author, the date, and the description of the commit.
*
* @param namespace the namespace of the repository
* @param name the name of the repository
* @param revision the revision
* @param path The path of the file
*/
@GET
@Path("{revision}/{path: .*}")
@Produces(VndMediaType.ANNOTATE)
@Operation(summary = "File content by revision", description = "Returns the annotated file for the given revision in the repository.", tags = "Repository")
@ApiResponse(responseCode = "200", description = "success")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the repository")
@ApiResponse(
responseCode = "404",
description = "not found, no repository with the specified name available in the namespace",
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 BlameDto annotate(
@PathParam("namespace") String namespace,
@PathParam("name") String name,
@PathParam("revision") String revision,
@PathParam("path") String path
) throws IOException {
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
return mapper.map(repositoryService.getBlameCommand().setRevision(revision).getBlameResult(path), namespaceAndName, revision, path);
}
}
}

View File

@@ -63,6 +63,7 @@ abstract class BaseFileObjectDtoMapper extends HalAppenderMapper implements Inst
} else {
links.self(resourceLinks.source().content(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path));
links.single(link("history", resourceLinks.fileHistory().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path)));
links.single(link("annotate", resourceLinks.annotate().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path)));
}
if (fileObject.isTruncated()) {
links.single(link("proceed", resourceLinks.source().content(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path) + "?offset=" + (offset + BrowseCommandRequest.DEFAULT_REQUEST_LIMIT)));

View File

@@ -0,0 +1,42 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import lombok.Data;
import java.util.List;
@Data
public class BlameDto extends HalRepresentation {
private List<BlameLineDto> lines;
public BlameDto(Links links) {
super(links);
}
}

View File

@@ -0,0 +1,41 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import lombok.Data;
import java.time.Instant;
@Data
public class BlameLineDto {
private PersonDto author;
private String code;
private String description;
private int lineNumber;
private String revision;
private Instant when;
}

View File

@@ -0,0 +1,72 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import com.google.common.annotations.VisibleForTesting;
import de.otto.edison.hal.Links;
import org.mapstruct.Mapper;
import org.mapstruct.ObjectFactory;
import sonia.scm.repository.BlameLine;
import sonia.scm.repository.BlameResult;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Person;
import javax.inject.Inject;
import java.util.stream.Collectors;
@Mapper
public abstract class BlameResultToBlameDtoMapper implements InstantAttributeMapper {
@Inject
private ResourceLinks resourceLinks;
BlameDto map(BlameResult result, NamespaceAndName namespaceAndName, String revision, String path) {
BlameDto dto = createDto(namespaceAndName, revision, path);
dto.setLines(result.getBlameLines().stream().map(this::map).collect(Collectors.toList()));
return dto;
}
abstract BlameLineDto map(BlameLine line);
abstract PersonDto map(Person person);
@ObjectFactory
BlameDto createDto(NamespaceAndName namespaceAndName, String revision, String path) {
return new BlameDto(Links.linkingTo()
.self(
resourceLinks.annotate()
.self(
namespaceAndName.getNamespace(),
namespaceAndName.getName(),
revision,
path))
.build());
}
@VisibleForTesting
void setResourceLinks(ResourceLinks resourceLinks) {
this.resourceLinks = resourceLinks;
}
}

View File

@@ -32,46 +32,48 @@ import sonia.scm.web.api.RepositoryToHalMapper;
public class MapperModule extends AbstractModule {
@Override
protected void configure() {
bind(UserDtoToUserMapper.class).to(Mappers.getMapper(UserDtoToUserMapper.class).getClass());
bind(UserToUserDtoMapper.class).to(Mappers.getMapper(UserToUserDtoMapper.class).getClass());
bind(UserDtoToUserMapper.class).to(Mappers.getMapperClass(UserDtoToUserMapper.class));
bind(UserToUserDtoMapper.class).to(Mappers.getMapperClass(UserToUserDtoMapper.class));
bind(UserCollectionToDtoMapper.class);
bind(GroupDtoToGroupMapper.class).to(Mappers.getMapper(GroupDtoToGroupMapper.class).getClass());
bind(GroupToGroupDtoMapper.class).to(Mappers.getMapper(GroupToGroupDtoMapper.class).getClass());
bind(GroupDtoToGroupMapper.class).to(Mappers.getMapperClass(GroupDtoToGroupMapper.class));
bind(GroupToGroupDtoMapper.class).to(Mappers.getMapperClass(GroupToGroupDtoMapper.class));
bind(GroupCollectionToDtoMapper.class);
bind(ScmConfigurationToConfigDtoMapper.class).to(Mappers.getMapper(ScmConfigurationToConfigDtoMapper.class).getClass());
bind(ConfigDtoToScmConfigurationMapper.class).to(Mappers.getMapper(ConfigDtoToScmConfigurationMapper.class).getClass());
bind(ScmConfigurationToConfigDtoMapper.class).to(Mappers.getMapperClass(ScmConfigurationToConfigDtoMapper.class));
bind(ConfigDtoToScmConfigurationMapper.class).to(Mappers.getMapperClass(ConfigDtoToScmConfigurationMapper.class));
bind(RepositoryToRepositoryDtoMapper.class).to(Mappers.getMapper(RepositoryToRepositoryDtoMapper.class).getClass());
bind(RepositoryDtoToRepositoryMapper.class).to(Mappers.getMapper(RepositoryDtoToRepositoryMapper.class).getClass());
bind(RepositoryToRepositoryDtoMapper.class).to(Mappers.getMapperClass(RepositoryToRepositoryDtoMapper.class));
bind(RepositoryDtoToRepositoryMapper.class).to(Mappers.getMapperClass(RepositoryDtoToRepositoryMapper.class));
bind(RepositoryTypeToRepositoryTypeDtoMapper.class).to(Mappers.getMapper(RepositoryTypeToRepositoryTypeDtoMapper.class).getClass());
bind(RepositoryTypeToRepositoryTypeDtoMapper.class).to(Mappers.getMapperClass(RepositoryTypeToRepositoryTypeDtoMapper.class));
bind(RepositoryTypeCollectionToDtoMapper.class);
bind(BranchToBranchDtoMapper.class).to(Mappers.getMapper(BranchToBranchDtoMapper.class).getClass());
bind(RepositoryPermissionDtoToRepositoryPermissionMapper.class).to(Mappers.getMapper(RepositoryPermissionDtoToRepositoryPermissionMapper.class).getClass());
bind(RepositoryPermissionToRepositoryPermissionDtoMapper.class).to(Mappers.getMapper(RepositoryPermissionToRepositoryPermissionDtoMapper.class).getClass());
bind(BranchToBranchDtoMapper.class).to(Mappers.getMapperClass(BranchToBranchDtoMapper.class));
bind(RepositoryPermissionDtoToRepositoryPermissionMapper.class).to(Mappers.getMapperClass(RepositoryPermissionDtoToRepositoryPermissionMapper.class));
bind(RepositoryPermissionToRepositoryPermissionDtoMapper.class).to(Mappers.getMapperClass(RepositoryPermissionToRepositoryPermissionDtoMapper.class));
bind(RepositoryRoleToRepositoryRoleDtoMapper.class).to(Mappers.getMapper(RepositoryRoleToRepositoryRoleDtoMapper.class).getClass());
bind(RepositoryRoleDtoToRepositoryRoleMapper.class).to(Mappers.getMapper(RepositoryRoleDtoToRepositoryRoleMapper.class).getClass());
bind(RepositoryRoleToRepositoryRoleDtoMapper.class).to(Mappers.getMapperClass(RepositoryRoleToRepositoryRoleDtoMapper.class));
bind(RepositoryRoleDtoToRepositoryRoleMapper.class).to(Mappers.getMapperClass(RepositoryRoleDtoToRepositoryRoleMapper.class));
bind(RepositoryRoleCollectionToDtoMapper.class);
bind(ChangesetToChangesetDtoMapper.class).to(Mappers.getMapper(DefaultChangesetToChangesetDtoMapper.class).getClass());
bind(ChangesetToParentDtoMapper.class).to(Mappers.getMapper(ChangesetToParentDtoMapper.class).getClass());
bind(ChangesetToChangesetDtoMapper.class).to(Mappers.getMapperClass(DefaultChangesetToChangesetDtoMapper.class));
bind(ChangesetToParentDtoMapper.class).to(Mappers.getMapperClass(ChangesetToParentDtoMapper.class));
bind(TagToTagDtoMapper.class).to(Mappers.getMapper(TagToTagDtoMapper.class).getClass());
bind(TagToTagDtoMapper.class).to(Mappers.getMapperClass(TagToTagDtoMapper.class));
bind(BrowserResultToFileObjectDtoMapper.class).to(Mappers.getMapper(BrowserResultToFileObjectDtoMapper.class).getClass());
bind(ModificationsToDtoMapper.class).to(Mappers.getMapper(ModificationsToDtoMapper.class).getClass());
bind(BrowserResultToFileObjectDtoMapper.class).to(Mappers.getMapperClass(BrowserResultToFileObjectDtoMapper.class));
bind(ModificationsToDtoMapper.class).to(Mappers.getMapperClass(ModificationsToDtoMapper.class));
bind(ReducedObjectModelToDtoMapper.class).to(Mappers.getMapper(ReducedObjectModelToDtoMapper.class).getClass());
bind(ReducedObjectModelToDtoMapper.class).to(Mappers.getMapperClass(ReducedObjectModelToDtoMapper.class));
bind(ResteasyViolationExceptionToErrorDtoMapper.class).to(Mappers.getMapper(ResteasyViolationExceptionToErrorDtoMapper.class).getClass());
bind(ScmViolationExceptionToErrorDtoMapper.class).to(Mappers.getMapper(ScmViolationExceptionToErrorDtoMapper.class).getClass());
bind(ExceptionWithContextToErrorDtoMapper.class).to(Mappers.getMapper(ExceptionWithContextToErrorDtoMapper.class).getClass());
bind(ResteasyViolationExceptionToErrorDtoMapper.class).to(Mappers.getMapperClass(ResteasyViolationExceptionToErrorDtoMapper.class));
bind(ScmViolationExceptionToErrorDtoMapper.class).to(Mappers.getMapperClass(ScmViolationExceptionToErrorDtoMapper.class));
bind(ExceptionWithContextToErrorDtoMapper.class).to(Mappers.getMapperClass(ExceptionWithContextToErrorDtoMapper.class));
bind(RepositoryToHalMapper.class).to(Mappers.getMapper(RepositoryToRepositoryDtoMapper.class).getClass());
bind(RepositoryToHalMapper.class).to(Mappers.getMapperClass(RepositoryToRepositoryDtoMapper.class));
bind(BlameResultToBlameDtoMapper.class).to(Mappers.getMapperClass(BlameResultToBlameDtoMapper.class));
// no mapstruct required
bind(MeDtoFactory.class);
@@ -80,6 +82,6 @@ public class MapperModule extends AbstractModule {
bind(ScmPathInfoStore.class).in(ServletScopes.REQUEST);
bind(PluginDtoMapper.class).to(Mappers.getMapper(PluginDtoMapper.class).getClass());
bind(PluginDtoMapper.class).to(Mappers.getMapperClass(PluginDtoMapper.class));
}
}

View File

@@ -0,0 +1,111 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import javax.inject.Inject;
import javax.inject.Provider;
public class RepositoryBasedResourceProvider {
private final Provider<TagRootResource> tagRootResource;
private final Provider<BranchRootResource> branchRootResource;
private final Provider<ChangesetRootResource> changesetRootResource;
private final Provider<SourceRootResource> sourceRootResource;
private final Provider<ContentResource> contentResource;
private final Provider<RepositoryPermissionRootResource> permissionRootResource;
private final Provider<DiffRootResource> diffRootResource;
private final Provider<ModificationsRootResource> modificationsRootResource;
private final Provider<FileHistoryRootResource> fileHistoryRootResource;
private final Provider<IncomingRootResource> incomingRootResource;
private final Provider<AnnotateResource> annotateResource;
@Inject
public RepositoryBasedResourceProvider(
Provider<TagRootResource> tagRootResource,
Provider<BranchRootResource> branchRootResource,
Provider<ChangesetRootResource> changesetRootResource,
Provider<SourceRootResource> sourceRootResource,
Provider<ContentResource> contentResource,
Provider<RepositoryPermissionRootResource> permissionRootResource,
Provider<DiffRootResource> diffRootResource,
Provider<ModificationsRootResource> modificationsRootResource,
Provider<FileHistoryRootResource> fileHistoryRootResource,
Provider<IncomingRootResource> incomingRootResource, Provider<AnnotateResource> annotateResource) {
this.tagRootResource = tagRootResource;
this.branchRootResource = branchRootResource;
this.changesetRootResource = changesetRootResource;
this.sourceRootResource = sourceRootResource;
this.contentResource = contentResource;
this.permissionRootResource = permissionRootResource;
this.diffRootResource = diffRootResource;
this.modificationsRootResource = modificationsRootResource;
this.fileHistoryRootResource = fileHistoryRootResource;
this.incomingRootResource = incomingRootResource;
this.annotateResource = annotateResource;
}
public TagRootResource getTagRootResource() {
return tagRootResource.get();
}
public BranchRootResource getBranchRootResource() {
return branchRootResource.get();
}
public ChangesetRootResource getChangesetRootResource() {
return changesetRootResource.get();
}
public SourceRootResource getSourceRootResource() {
return sourceRootResource.get();
}
public ContentResource getContentResource() {
return contentResource.get();
}
public RepositoryPermissionRootResource getPermissionRootResource() {
return permissionRootResource.get();
}
public DiffRootResource getDiffRootResource() {
return diffRootResource.get();
}
public ModificationsRootResource getModificationsRootResource() {
return modificationsRootResource.get();
}
public FileHistoryRootResource getFileHistoryRootResource() {
return fileHistoryRootResource.get();
}
public IncomingRootResource getIncomingRootResource() {
return incomingRootResource.get();
}
public AnnotateResource getAnnotateResource() {
return annotateResource.get();
}
}

View File

@@ -58,46 +58,19 @@ public class RepositoryResource {
private final RepositoryManager manager;
private final SingleResourceManagerAdapter<Repository, RepositoryDto> adapter;
private final Provider<TagRootResource> tagRootResource;
private final Provider<BranchRootResource> branchRootResource;
private final Provider<ChangesetRootResource> changesetRootResource;
private final Provider<SourceRootResource> sourceRootResource;
private final Provider<ContentResource> contentResource;
private final Provider<RepositoryPermissionRootResource> permissionRootResource;
private final Provider<DiffRootResource> diffRootResource;
private final Provider<ModificationsRootResource> modificationsRootResource;
private final Provider<FileHistoryRootResource> fileHistoryRootResource;
private final Provider<IncomingRootResource> incomingRootResource;
private final RepositoryBasedResourceProvider resourceProvider;
@Inject
public RepositoryResource(
RepositoryToRepositoryDtoMapper repositoryToDtoMapper,
RepositoryDtoToRepositoryMapper dtoToRepositoryMapper, RepositoryManager manager,
Provider<TagRootResource> tagRootResource,
Provider<BranchRootResource> branchRootResource,
Provider<ChangesetRootResource> changesetRootResource,
Provider<SourceRootResource> sourceRootResource, Provider<ContentResource> contentResource,
Provider<RepositoryPermissionRootResource> permissionRootResource,
Provider<DiffRootResource> diffRootResource,
Provider<ModificationsRootResource> modificationsRootResource,
Provider<FileHistoryRootResource> fileHistoryRootResource,
Provider<IncomingRootResource> incomingRootResource
RepositoryBasedResourceProvider resourceProvider
) {
this.dtoToRepositoryMapper = dtoToRepositoryMapper;
this.manager = manager;
this.repositoryToDtoMapper = repositoryToDtoMapper;
this.adapter = new SingleResourceManagerAdapter<>(manager, Repository.class);
this.tagRootResource = tagRootResource;
this.branchRootResource = branchRootResource;
this.changesetRootResource = changesetRootResource;
this.sourceRootResource = sourceRootResource;
this.contentResource = contentResource;
this.permissionRootResource = permissionRootResource;
this.diffRootResource = diffRootResource;
this.modificationsRootResource = modificationsRootResource;
this.fileHistoryRootResource = fileHistoryRootResource;
this.incomingRootResource = incomingRootResource;
this.resourceProvider = resourceProvider;
}
/**
@@ -211,52 +184,57 @@ public class RepositoryResource {
@Path("tags/")
public TagRootResource tags() {
return tagRootResource.get();
return resourceProvider.getTagRootResource();
}
@Path("diff/")
public DiffRootResource diff() {
return diffRootResource.get();
return resourceProvider.getDiffRootResource();
}
@Path("branches/")
public BranchRootResource branches() {
return branchRootResource.get();
return resourceProvider.getBranchRootResource();
}
@Path("changesets/")
public ChangesetRootResource changesets() {
return changesetRootResource.get();
return resourceProvider.getChangesetRootResource();
}
@Path("history/")
public FileHistoryRootResource history() {
return fileHistoryRootResource.get();
return resourceProvider.getFileHistoryRootResource();
}
@Path("sources/")
public SourceRootResource sources() {
return sourceRootResource.get();
return resourceProvider.getSourceRootResource();
}
@Path("content/")
public ContentResource content() {
return contentResource.get();
return resourceProvider.getContentResource();
}
@Path("permissions/")
public RepositoryPermissionRootResource permissions() {
return permissionRootResource.get();
return resourceProvider.getPermissionRootResource();
}
@Path("modifications/")
public ModificationsRootResource modifications() {
return modificationsRootResource.get();
return resourceProvider.getModificationsRootResource();
}
@Path("incoming/")
public IncomingRootResource incoming() {
return incomingRootResource.get();
return resourceProvider.getIncomingRootResource();
}
@Path("annotate/")
public AnnotateResource annotate() {
return resourceProvider.getAnnotateResource();
}
private Supplier<Repository> loadBy(String namespace, String name) {

View File

@@ -566,6 +566,22 @@ class ResourceLinks {
}
}
public AnnotateLinks annotate() {
return new AnnotateLinks(scmPathInfoStore.get());
}
static class AnnotateLinks {
private final LinkBuilder annotateLinkBuilder;
AnnotateLinks(ScmPathInfo pathInfo) {
this.annotateLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, AnnotateResource.class);
}
String self(String namespace, String name, String revision, String path) {
return annotateLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("annotate").parameters().method("annotate").parameters(revision, path).href();
}
}
RepositoryVerbLinks repositoryVerbs() {
return new RepositoryVerbLinks(scmPathInfoStore.get());
}

View File

@@ -0,0 +1,147 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.assertj.core.api.Assertions;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.BlameLine;
import sonia.scm.repository.BlameResult;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Person;
import sonia.scm.repository.api.BlameCommandBuilder;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.web.RestDispatcher;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Iterator;
import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class AnnotateResourceTest extends RepositoryTestBase {
public static final NamespaceAndName NAMESPACE_AND_NAME = new NamespaceAndName("space", "X");
public static final String REVISION = "123";
public static final String PATH = "some/file";
@Mock
private RepositoryServiceFactory serviceFactory;
@Mock
private RepositoryService service;
@Mock
private BlameCommandBuilder blameCommandBuilder;
private final RestDispatcher dispatcher = new RestDispatcher();
private final MockHttpResponse response = new MockHttpResponse();
@BeforeEach
void initResource() {
BlameResultToBlameDtoMapperImpl mapper = new BlameResultToBlameDtoMapperImpl();
mapper.setResourceLinks(ResourceLinksMock.createMock(URI.create("/")));
annotateResource = new AnnotateResource(serviceFactory, mapper);
dispatcher.addSingletonResource(getRepositoryRootResource());
}
@BeforeEach
void initRepositoryService() {
when(serviceFactory.create(NAMESPACE_AND_NAME)).thenReturn(service);
when(service.getBlameCommand()).thenReturn(blameCommandBuilder);
}
@BeforeEach
void initBlameCommand() throws IOException {
BlameLine line1 = new BlameLine(
0,
"100",
System.currentTimeMillis(),
new Person("Arthur Dent", "arthur@hitchhiker.com"),
"first try",
"jump"
);
BlameLine line2 = new BlameLine(
1,
"42",
System.currentTimeMillis(),
new Person("Zaphod Beeblebrox", "zaphod@hitchhiker.com"),
"got it",
"heart of gold"
);
BlameResult result = new BlameResult(asList(line1, line2));
when(blameCommandBuilder.setRevision(REVISION)).thenReturn(blameCommandBuilder);
when(blameCommandBuilder.getBlameResult(PATH)).thenReturn(result);
}
@Test
void shouldReturnAnnotations() throws URISyntaxException, UnsupportedEncodingException, JsonProcessingException {
MockHttpRequest request = MockHttpRequest
.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + NAMESPACE_AND_NAME + "/annotate/" + REVISION + "/" + PATH);
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(200);
String content = response.getContentAsString();
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(content);
JsonNode blameLines = jsonNode.get("lines");
assertThat(blameLines.isArray()).isTrue();
assertThat(jsonNode.get("_links").get("self").get("href").asText())
.isEqualTo("/v2/repositories/space/X/annotate/123/some%2Ffile");
Iterator<JsonNode> lineIterator = blameLines.iterator();
JsonNode line1 = lineIterator.next();
assertThat(line1.get("author").get("mail").asText()).isEqualTo("arthur@hitchhiker.com");
assertThat(line1.get("author").get("name").asText()).isEqualTo("Arthur Dent");
assertThat(line1.get("code").asText()).isEqualTo("jump");
assertThat(line1.get("description").asText()).isEqualTo("first try");
assertThat(line1.get("lineNumber").asInt()).isEqualTo(0);
assertThat(line1.get("revision").asText()).isEqualTo("100");
JsonNode line2 = lineIterator.next();
assertThat(line2.get("author").get("mail").asText()).isEqualTo("zaphod@hitchhiker.com");
assertThat(line2.get("author").get("name").asText()).isEqualTo("Zaphod Beeblebrox");
assertThat(line2.get("code").asText()).isEqualTo("heart of gold");
assertThat(line2.get("description").asText()).isEqualTo("got it");
assertThat(line2.get("lineNumber").asInt()).isEqualTo(1);
assertThat(line2.get("revision").asText()).isEqualTo("42");
assertThat(lineIterator.hasNext()).isFalse();
}
}

View File

@@ -103,8 +103,6 @@ public class BranchRootResourceTest extends RepositoryTestBase {
private BranchChangesetCollectionToDtoMapper changesetCollectionToDtoMapper;
private BranchRootResource branchRootResource;
@Mock
private BranchCollectionToDtoMapper branchCollectionToDtoMapper;
@@ -126,7 +124,6 @@ public class BranchRootResourceTest extends RepositoryTestBase {
changesetCollectionToDtoMapper = new BranchChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks);
BranchCollectionToDtoMapper branchCollectionToDtoMapper = new BranchCollectionToDtoMapper(branchToDtoMapper, resourceLinks);
branchRootResource = new BranchRootResource(serviceFactory, branchToDtoMapper, branchCollectionToDtoMapper, changesetCollectionToDtoMapper, resourceLinks);
super.branchRootResource = Providers.of(branchRootResource);
dispatcher.addSingletonResource(getRepositoryRootResource());
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service);
when(serviceFactory.create(any(Repository.class))).thenReturn(service);

View File

@@ -91,8 +91,6 @@ public class ChangesetRootResourceTest extends RepositoryTestBase {
@InjectMocks
private DefaultChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper;
private ChangesetRootResource changesetRootResource;
private final Subject subject = mock(Subject.class);
private final ThreadState subjectThreadState = new SubjectThreadState(subject);
@@ -100,7 +98,6 @@ public class ChangesetRootResourceTest extends RepositoryTestBase {
public void prepareEnvironment() {
changesetCollectionToDtoMapper = new ChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks);
changesetRootResource = new ChangesetRootResource(serviceFactory, changesetCollectionToDtoMapper, changesetToChangesetDtoMapper);
super.changesetRootResource = Providers.of(changesetRootResource);
dispatcher.addSingletonResource(getRepositoryRootResource());
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService);
when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService);

View File

@@ -90,9 +90,6 @@ public class DiffResourceTest extends RepositoryTestBase {
@Mock
private DiffResultToDiffResultDtoMapper diffResultToDiffResultDtoMapper;
private DiffRootResource diffRootResource;
private final Subject subject = mock(Subject.class);
private final ThreadState subjectThreadState = new SubjectThreadState(subject);
@@ -100,7 +97,6 @@ public class DiffResourceTest extends RepositoryTestBase {
@Before
public void prepareEnvironment() {
diffRootResource = new DiffRootResource(serviceFactory, diffResultToDiffResultDtoMapper);
super.diffRootResource = Providers.of(diffRootResource);
dispatcher.addSingletonResource(getRepositoryRootResource());
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service);
when(serviceFactory.create(any(Repository.class))).thenReturn(service);

View File

@@ -92,8 +92,6 @@ public class FileHistoryResourceTest extends RepositoryTestBase {
@InjectMocks
private DefaultChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper;
private FileHistoryRootResource fileHistoryRootResource;
private RestDispatcher dispatcher = new RestDispatcher();
private final Subject subject = mock(Subject.class);
@@ -103,7 +101,6 @@ public class FileHistoryResourceTest extends RepositoryTestBase {
public void prepareEnvironment() {
fileHistoryCollectionToDtoMapper = new FileHistoryCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks);
fileHistoryRootResource = new FileHistoryRootResource(serviceFactory, fileHistoryCollectionToDtoMapper);
super.fileHistoryRootResource = Providers.of(fileHistoryRootResource);
dispatcher.addSingletonResource(getRepositoryRootResource());
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service);
when(serviceFactory.create(any(Repository.class))).thenReturn(service);

View File

@@ -110,9 +110,6 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
@InjectMocks
private DefaultChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper;
private IncomingRootResource incomingRootResource;
private final Subject subject = mock(Subject.class);
private final ThreadState subjectThreadState = new SubjectThreadState(subject);
@@ -121,7 +118,6 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
public void prepareEnvironment() {
incomingChangesetCollectionToDtoMapper = new IncomingChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks);
incomingRootResource = new IncomingRootResource(serviceFactory, incomingChangesetCollectionToDtoMapper, diffResultToDiffResultDtoMapper);
super.incomingRootResource = Providers.of(incomingRootResource);
dispatcher.addSingletonResource(getRepositoryRootResource());
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService);
when(serviceFactory.create(REPOSITORY)).thenReturn(repositoryService);

View File

@@ -88,9 +88,6 @@ public class ModificationsResourceTest extends RepositoryTestBase {
@InjectMocks
private ModificationsToDtoMapperImpl modificationsToDtoMapper;
private ModificationsRootResource modificationsRootResource;
private final Subject subject = mock(Subject.class);
private final ThreadState subjectThreadState = new SubjectThreadState(subject);
@@ -98,7 +95,6 @@ public class ModificationsResourceTest extends RepositoryTestBase {
@Before
public void prepareEnvironment() {
modificationsRootResource = new ModificationsRootResource(serviceFactory, modificationsToDtoMapper);
super.modificationsRootResource = Providers.of(modificationsRootResource);
dispatcher.addSingletonResource(getRepositoryRootResource());
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService);
when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService);

View File

@@ -144,8 +144,6 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase {
private RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper;
private RepositoryPermissionRootResource repositoryPermissionRootResource;
private final Subject subject = mock(Subject.class);
private final ThreadState subjectThreadState = new SubjectThreadState(subject);
@@ -154,8 +152,7 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase {
public void prepareEnvironment() {
initMocks(this);
repositoryPermissionCollectionToDtoMapper = new RepositoryPermissionCollectionToDtoMapper(permissionToPermissionDtoMapper, resourceLinks);
repositoryPermissionRootResource = new RepositoryPermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, repositoryPermissionCollectionToDtoMapper, resourceLinks, repositoryManager);
super.permissionRootResource = Providers.of(repositoryPermissionRootResource);
permissionRootResource = new RepositoryPermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, repositoryPermissionCollectionToDtoMapper, resourceLinks, repositoryManager);
dispatcher.addSingletonResource(getRepositoryRootResource());
subjectThreadState.bind();
ThreadContext.bind(subject);

View File

@@ -122,7 +122,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
super.dtoToRepositoryMapper = dtoToRepositoryMapper;
super.manager = repositoryManager;
RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks);
super.repositoryCollectionResource = Providers.of(new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks, repositoryInitializer));
super.repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks, repositoryInitializer);
dispatcher.addSingletonResource(getRepositoryRootResource());
when(serviceFactory.create(any(Repository.class))).thenReturn(service);
when(scmPathInfoStore.get()).thenReturn(uriInfo);

View File

@@ -24,46 +24,50 @@
package sonia.scm.api.v2.resources;
import com.google.inject.util.Providers;
import sonia.scm.repository.RepositoryManager;
import javax.inject.Provider;
import static com.google.inject.util.Providers.of;
import static org.mockito.Mockito.mock;
public abstract class RepositoryTestBase {
abstract class RepositoryTestBase {
protected RepositoryToRepositoryDtoMapper repositoryToDtoMapper;
protected RepositoryDtoToRepositoryMapper dtoToRepositoryMapper;
protected RepositoryManager manager;
protected Provider<TagRootResource> tagRootResource;
protected Provider<BranchRootResource> branchRootResource;
protected Provider<ChangesetRootResource> changesetRootResource;
protected Provider<SourceRootResource> sourceRootResource;
protected Provider<ContentResource> contentResource;
protected Provider<RepositoryPermissionRootResource> permissionRootResource;
protected Provider<DiffRootResource> diffRootResource;
protected Provider<ModificationsRootResource> modificationsRootResource;
protected Provider<FileHistoryRootResource> fileHistoryRootResource;
protected Provider<RepositoryCollectionResource> repositoryCollectionResource;
protected Provider<IncomingRootResource> incomingRootResource;
RepositoryToRepositoryDtoMapper repositoryToDtoMapper;
RepositoryDtoToRepositoryMapper dtoToRepositoryMapper;
RepositoryManager manager;
TagRootResource tagRootResource;
BranchRootResource branchRootResource;
ChangesetRootResource changesetRootResource;
SourceRootResource sourceRootResource;
ContentResource contentResource;
RepositoryPermissionRootResource permissionRootResource;
DiffRootResource diffRootResource;
ModificationsRootResource modificationsRootResource;
FileHistoryRootResource fileHistoryRootResource;
IncomingRootResource incomingRootResource;
RepositoryCollectionResource repositoryCollectionResource;
AnnotateResource annotateResource;
RepositoryRootResource getRepositoryRootResource() {
return new RepositoryRootResource(Providers.of(new RepositoryResource(
RepositoryBasedResourceProvider repositoryBasedResourceProvider = new RepositoryBasedResourceProvider(
of(tagRootResource),
of(branchRootResource),
of(changesetRootResource),
of(sourceRootResource),
of(contentResource),
of(permissionRootResource),
of(diffRootResource),
of(modificationsRootResource),
of(fileHistoryRootResource),
of(incomingRootResource),
of(annotateResource));
return new RepositoryRootResource(
of(new RepositoryResource(
repositoryToDtoMapper,
dtoToRepositoryMapper,
manager,
tagRootResource,
branchRootResource,
changesetRootResource,
sourceRootResource,
contentResource,
permissionRootResource,
diffRootResource,
modificationsRootResource,
fileHistoryRootResource,
incomingRootResource)), repositoryCollectionResource);
repositoryBasedResourceProvider
)),
of(repositoryCollectionResource));
}
}

View File

@@ -26,6 +26,7 @@ package sonia.scm.api.v2.resources;
import java.net.URI;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -33,47 +34,48 @@ public class ResourceLinksMock {
public static ResourceLinks createMock(URI baseUri) {
ResourceLinks resourceLinks = mock(ResourceLinks.class);
ScmPathInfo uriInfo = mock(ScmPathInfo.class);
when(uriInfo.getApiRestUri()).thenReturn(baseUri);
ScmPathInfo pathInfo = mock(ScmPathInfo.class);
when(pathInfo.getApiRestUri()).thenReturn(baseUri);
ResourceLinks.UserLinks userLinks = new ResourceLinks.UserLinks(uriInfo);
when(resourceLinks.user()).thenReturn(userLinks);
when(resourceLinks.me()).thenReturn(new ResourceLinks.MeLinks(uriInfo,userLinks));
when(resourceLinks.userCollection()).thenReturn(new ResourceLinks.UserCollectionLinks(uriInfo));
when(resourceLinks.userPermissions()).thenReturn(new ResourceLinks.UserPermissionLinks(uriInfo));
when(resourceLinks.autoComplete()).thenReturn(new ResourceLinks.AutoCompleteLinks(uriInfo));
when(resourceLinks.group()).thenReturn(new ResourceLinks.GroupLinks(uriInfo));
when(resourceLinks.groupCollection()).thenReturn(new ResourceLinks.GroupCollectionLinks(uriInfo));
when(resourceLinks.groupPermissions()).thenReturn(new ResourceLinks.GroupPermissionLinks(uriInfo));
when(resourceLinks.repository()).thenReturn(new ResourceLinks.RepositoryLinks(uriInfo));
when(resourceLinks.incoming()).thenReturn(new ResourceLinks.IncomingLinks(uriInfo));
when(resourceLinks.repositoryCollection()).thenReturn(new ResourceLinks.RepositoryCollectionLinks(uriInfo));
when(resourceLinks.tag()).thenReturn(new ResourceLinks.TagCollectionLinks(uriInfo));
when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo));
when(resourceLinks.changeset()).thenReturn(new ResourceLinks.ChangesetLinks(uriInfo));
when(resourceLinks.fileHistory()).thenReturn(new ResourceLinks.FileHistoryLinks(uriInfo));
when(resourceLinks.source()).thenReturn(new ResourceLinks.SourceLinks(uriInfo));
when(resourceLinks.repositoryPermission()).thenReturn(new ResourceLinks.RepositoryPermissionLinks(uriInfo));
when(resourceLinks.config()).thenReturn(new ResourceLinks.ConfigLinks(uriInfo));
when(resourceLinks.branch()).thenReturn(new ResourceLinks.BranchLinks(uriInfo));
when(resourceLinks.diff()).thenReturn(new ResourceLinks.DiffLinks(uriInfo));
when(resourceLinks.modifications()).thenReturn(new ResourceLinks.ModificationsLinks(uriInfo));
when(resourceLinks.repositoryType()).thenReturn(new ResourceLinks.RepositoryTypeLinks(uriInfo));
when(resourceLinks.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(uriInfo));
when(resourceLinks.installedPluginCollection()).thenReturn(new ResourceLinks.InstalledPluginCollectionLinks(uriInfo));
when(resourceLinks.availablePluginCollection()).thenReturn(new ResourceLinks.AvailablePluginCollectionLinks(uriInfo));
when(resourceLinks.pendingPluginCollection()).thenReturn(new ResourceLinks.PendingPluginCollectionLinks(uriInfo));
when(resourceLinks.installedPlugin()).thenReturn(new ResourceLinks.InstalledPluginLinks(uriInfo));
when(resourceLinks.availablePlugin()).thenReturn(new ResourceLinks.AvailablePluginLinks(uriInfo));
when(resourceLinks.uiPluginCollection()).thenReturn(new ResourceLinks.UIPluginCollectionLinks(uriInfo));
when(resourceLinks.uiPlugin()).thenReturn(new ResourceLinks.UIPluginLinks(uriInfo));
when(resourceLinks.authentication()).thenReturn(new ResourceLinks.AuthenticationLinks(uriInfo));
when(resourceLinks.index()).thenReturn(new ResourceLinks.IndexLinks(uriInfo));
when(resourceLinks.permissions()).thenReturn(new ResourceLinks.PermissionsLinks(uriInfo));
when(resourceLinks.repositoryVerbs()).thenReturn(new ResourceLinks.RepositoryVerbLinks(uriInfo));
when(resourceLinks.repositoryRole()).thenReturn(new ResourceLinks.RepositoryRoleLinks(uriInfo));
when(resourceLinks.repositoryRoleCollection()).thenReturn(new ResourceLinks.RepositoryRoleCollectionLinks(uriInfo));
when(resourceLinks.namespaceStrategies()).thenReturn(new ResourceLinks.NamespaceStrategiesLinks(uriInfo));
ResourceLinks.UserLinks userLinks = new ResourceLinks.UserLinks(pathInfo);
lenient().when(resourceLinks.user()).thenReturn(userLinks);
lenient().when(resourceLinks.me()).thenReturn(new ResourceLinks.MeLinks(pathInfo,userLinks));
lenient().when(resourceLinks.userCollection()).thenReturn(new ResourceLinks.UserCollectionLinks(pathInfo));
lenient().when(resourceLinks.userPermissions()).thenReturn(new ResourceLinks.UserPermissionLinks(pathInfo));
lenient().when(resourceLinks.autoComplete()).thenReturn(new ResourceLinks.AutoCompleteLinks(pathInfo));
lenient().when(resourceLinks.group()).thenReturn(new ResourceLinks.GroupLinks(pathInfo));
lenient().when(resourceLinks.groupCollection()).thenReturn(new ResourceLinks.GroupCollectionLinks(pathInfo));
lenient().when(resourceLinks.groupPermissions()).thenReturn(new ResourceLinks.GroupPermissionLinks(pathInfo));
lenient().when(resourceLinks.repository()).thenReturn(new ResourceLinks.RepositoryLinks(pathInfo));
lenient().when(resourceLinks.incoming()).thenReturn(new ResourceLinks.IncomingLinks(pathInfo));
lenient().when(resourceLinks.repositoryCollection()).thenReturn(new ResourceLinks.RepositoryCollectionLinks(pathInfo));
lenient().when(resourceLinks.tag()).thenReturn(new ResourceLinks.TagCollectionLinks(pathInfo));
lenient().when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(pathInfo));
lenient().when(resourceLinks.changeset()).thenReturn(new ResourceLinks.ChangesetLinks(pathInfo));
lenient().when(resourceLinks.fileHistory()).thenReturn(new ResourceLinks.FileHistoryLinks(pathInfo));
lenient().when(resourceLinks.source()).thenReturn(new ResourceLinks.SourceLinks(pathInfo));
lenient().when(resourceLinks.repositoryPermission()).thenReturn(new ResourceLinks.RepositoryPermissionLinks(pathInfo));
lenient().when(resourceLinks.config()).thenReturn(new ResourceLinks.ConfigLinks(pathInfo));
lenient().when(resourceLinks.branch()).thenReturn(new ResourceLinks.BranchLinks(pathInfo));
lenient().when(resourceLinks.diff()).thenReturn(new ResourceLinks.DiffLinks(pathInfo));
lenient().when(resourceLinks.modifications()).thenReturn(new ResourceLinks.ModificationsLinks(pathInfo));
lenient().when(resourceLinks.repositoryType()).thenReturn(new ResourceLinks.RepositoryTypeLinks(pathInfo));
lenient().when(resourceLinks.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(pathInfo));
lenient().when(resourceLinks.installedPluginCollection()).thenReturn(new ResourceLinks.InstalledPluginCollectionLinks(pathInfo));
lenient().when(resourceLinks.availablePluginCollection()).thenReturn(new ResourceLinks.AvailablePluginCollectionLinks(pathInfo));
lenient().when(resourceLinks.pendingPluginCollection()).thenReturn(new ResourceLinks.PendingPluginCollectionLinks(pathInfo));
lenient().when(resourceLinks.installedPlugin()).thenReturn(new ResourceLinks.InstalledPluginLinks(pathInfo));
lenient().when(resourceLinks.availablePlugin()).thenReturn(new ResourceLinks.AvailablePluginLinks(pathInfo));
lenient().when(resourceLinks.uiPluginCollection()).thenReturn(new ResourceLinks.UIPluginCollectionLinks(pathInfo));
lenient().when(resourceLinks.uiPlugin()).thenReturn(new ResourceLinks.UIPluginLinks(pathInfo));
lenient().when(resourceLinks.authentication()).thenReturn(new ResourceLinks.AuthenticationLinks(pathInfo));
lenient().when(resourceLinks.index()).thenReturn(new ResourceLinks.IndexLinks(pathInfo));
lenient().when(resourceLinks.permissions()).thenReturn(new ResourceLinks.PermissionsLinks(pathInfo));
lenient().when(resourceLinks.repositoryVerbs()).thenReturn(new ResourceLinks.RepositoryVerbLinks(pathInfo));
lenient().when(resourceLinks.repositoryRole()).thenReturn(new ResourceLinks.RepositoryRoleLinks(pathInfo));
lenient().when(resourceLinks.repositoryRoleCollection()).thenReturn(new ResourceLinks.RepositoryRoleCollectionLinks(pathInfo));
lenient().when(resourceLinks.namespaceStrategies()).thenReturn(new ResourceLinks.NamespaceStrategiesLinks(pathInfo));
lenient().when(resourceLinks.annotate()).thenReturn(new ResourceLinks.AnnotateLinks(pathInfo));
return resourceLinks;
}

View File

@@ -74,8 +74,7 @@ public class SourceRootResourceTest extends RepositoryTestBase {
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service);
when(service.getBrowseCommand()).thenReturn(browseCommandBuilder);
SourceRootResource sourceRootResource = new SourceRootResource(serviceFactory, browserResultToFileObjectDtoMapper);
super.sourceRootResource = Providers.of(sourceRootResource);
sourceRootResource = new SourceRootResource(serviceFactory, browserResultToFileObjectDtoMapper);
dispatcher.addSingletonResource(getRepositoryRootResource());
}

View File

@@ -84,9 +84,6 @@ public class TagRootResourceTest extends RepositoryTestBase {
@InjectMocks
private TagToTagDtoMapperImpl tagToTagDtoMapper;
private TagRootResource tagRootResource;
private final Subject subject = mock(Subject.class);
private final ThreadState subjectThreadState = new SubjectThreadState(subject);
@@ -95,7 +92,6 @@ public class TagRootResourceTest extends RepositoryTestBase {
public void prepareEnvironment() throws Exception {
tagCollectionToDtoMapper = new TagCollectionToDtoMapper(resourceLinks, tagToTagDtoMapper);
tagRootResource = new TagRootResource(serviceFactory, tagCollectionToDtoMapper, tagToTagDtoMapper);
super.tagRootResource = Providers.of(tagRootResource);
dispatcher.addSingletonResource(getRepositoryRootResource());
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService);
when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService);

154
yarn.lock
View File

@@ -54,6 +54,16 @@
semver "^5.4.1"
source-map "^0.5.0"
"@babel/generator@^7.10.1":
version "7.10.2"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.2.tgz#0fa5b5b2389db8bfdfcc3492b551ee20f5dd69a9"
integrity sha512-AxfBNHNu99DTMvlUPlt1h2+Hn7knPpH5ayJ8OqDWSeLld+Fi2AYBTC/IejWDM9Edcii4UzZRCsbUt0WlSDsDsA==
dependencies:
"@babel/types" "^7.10.2"
jsesc "^2.5.1"
lodash "^4.17.13"
source-map "^0.5.0"
"@babel/generator@^7.4.0", "@babel/generator@^7.9.6":
version "7.9.6"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.6.tgz#5408c82ac5de98cda0d77d8124e99fa1f2170a43"
@@ -145,6 +155,15 @@
"@babel/traverse" "^7.8.3"
"@babel/types" "^7.8.3"
"@babel/helper-function-name@^7.10.1":
version "7.10.1"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.1.tgz#92bd63829bfc9215aca9d9defa85f56b539454f4"
integrity sha512-fcpumwhs3YyZ/ttd5Rz0xn0TpIwVkN7X0V38B9TWNfVF42KEkhkAAuPCQ3oXmtTRtiPJrmZ0TrfS0GKF0eMaRQ==
dependencies:
"@babel/helper-get-function-arity" "^7.10.1"
"@babel/template" "^7.10.1"
"@babel/types" "^7.10.1"
"@babel/helper-function-name@^7.8.3", "@babel/helper-function-name@^7.9.5":
version "7.9.5"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz#2b53820d35275120e1874a82e5aabe1376920a5c"
@@ -154,6 +173,13 @@
"@babel/template" "^7.8.3"
"@babel/types" "^7.9.5"
"@babel/helper-get-function-arity@^7.10.1":
version "7.10.1"
resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.1.tgz#7303390a81ba7cb59613895a192b93850e373f7d"
integrity sha512-F5qdXkYGOQUb0hpRaPoetF9AnsXknKjWMZ+wmsIRsp5ge5sFh4c3h1eH2pRTTuy9KKAA2+TTYomGXAtEL2fQEw==
dependencies:
"@babel/types" "^7.10.1"
"@babel/helper-get-function-arity@^7.8.3":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz#b894b947bd004381ce63ea1db9f08547e920abd5"
@@ -248,6 +274,13 @@
"@babel/template" "^7.8.3"
"@babel/types" "^7.8.3"
"@babel/helper-split-export-declaration@^7.10.1":
version "7.10.1"
resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz#c6f4be1cbc15e3a868e4c64a17d5d31d754da35f"
integrity sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==
dependencies:
"@babel/types" "^7.10.1"
"@babel/helper-split-export-declaration@^7.8.3":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz#31a9f30070f91368a7182cf05f831781065fc7a9"
@@ -951,7 +984,7 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.3.3":
"@babel/template@^7.10.1", "@babel/template@^7.3.3":
version "7.10.1"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.1.tgz#e167154a94cb5f14b28dc58f5356d2162f539811"
integrity sha512-OQDg6SqvFSsc9A0ej6SKINWrpJiNonRIniYondK2ViKhB06i3c0s+76XUft71iqBEe9S1OKsHwPAjfHnuvnCig==
@@ -969,7 +1002,7 @@
"@babel/parser" "^7.8.6"
"@babel/types" "^7.8.6"
"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.7.0", "@babel/traverse@^7.8.3", "@babel/traverse@^7.9.6":
"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.7.0", "@babel/traverse@^7.8.3", "@babel/traverse@^7.9.6":
version "7.9.6"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.6.tgz#5540d7577697bf619cc57b92aa0f1c231a94f442"
integrity sha512-b3rAHSjbxy6VEAvlxM8OV/0X4XrG72zoxme6q1MOoe2vd0bEc+TwayhuC1+Dfgqh1QEG+pj7atQqvUprHIccsg==
@@ -984,6 +1017,21 @@
globals "^11.1.0"
lodash "^4.17.13"
"@babel/traverse@^7.4.5":
version "7.10.1"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.1.tgz#bbcef3031e4152a6c0b50147f4958df54ca0dd27"
integrity sha512-C/cTuXeKt85K+p08jN6vMDz8vSV0vZcI0wmQ36o6mjbuo++kPMdpOYw23W2XH04dbRt9/nMEfA4W3eR21CD+TQ==
dependencies:
"@babel/code-frame" "^7.10.1"
"@babel/generator" "^7.10.1"
"@babel/helper-function-name" "^7.10.1"
"@babel/helper-split-export-declaration" "^7.10.1"
"@babel/parser" "^7.10.1"
"@babel/types" "^7.10.1"
debug "^4.1.0"
globals "^11.1.0"
lodash "^4.17.13"
"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.5.5", "@babel/types@^7.7.0", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.9.0", "@babel/types@^7.9.5", "@babel/types@^7.9.6":
version "7.9.6"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.6.tgz#2c5502b427251e9de1bd2dff95add646d95cc9f7"
@@ -993,7 +1041,7 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
"@babel/types@^7.10.1", "@babel/types@^7.3.3":
"@babel/types@^7.10.1", "@babel/types@^7.10.2", "@babel/types@^7.3.3":
version "7.10.2"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.2.tgz#30283be31cad0dbf6fb00bd40641ca0ea675172d"
integrity sha512-AD3AwWBSz0AWF0AkCN9VPiWrvldXq+/e3cHa4J89vo4ymjz1XwrBFFVZmkJTsQIPNk+ZVomPSXUJqq8yyjZsng==
@@ -1068,7 +1116,7 @@
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.6.6.tgz#62266c5f0eac6941fece302abad69f2ee7e25e44"
integrity sha512-ojhgxzUHZ7am3D2jHkMzPpsBAiB005GF5YU4ea+8DNPybMk01JJUM9V9YRlF/GE95tcOm8DxQvWA2jq19bGalQ==
"@emotion/is-prop-valid@0.8.8", "@emotion/is-prop-valid@^0.8.1":
"@emotion/is-prop-valid@0.8.8", "@emotion/is-prop-valid@^0.8.8":
version "0.8.8"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a"
integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==
@@ -1129,7 +1177,7 @@
"@emotion/styled-base" "^10.0.27"
babel-plugin-emotion "^10.0.27"
"@emotion/stylis@0.8.5":
"@emotion/stylis@0.8.5", "@emotion/stylis@^0.8.4":
version "0.8.5"
resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04"
integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==
@@ -1139,7 +1187,7 @@
resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.7.1.tgz#50f63225e712d99e2b2b39c19c70fff023793ca5"
integrity sha512-/SLmSIkN13M//53TtNxgxo57mcJk/UJIDFRKwOiLIBEyBHEcipgR6hNMQ/59Sl4VjCJ0Z/3zeAZyvnSLPG/1HQ==
"@emotion/unitless@0.7.5", "@emotion/unitless@^0.7.0":
"@emotion/unitless@0.7.5", "@emotion/unitless@^0.7.4":
version "0.7.5"
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed"
integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
@@ -3260,10 +3308,10 @@
dependencies:
"@storybook/addon-storyshots" "*"
"@types/styled-components@^4.1.19":
version "4.4.3"
resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-4.4.3.tgz#74dd00ad760845a98890a8539361d8afc32059de"
integrity sha512-U0udeNOZBfUkJycmGJwmzun0FBt11rZy08weVQmE2xfUNAbX8AGOEWxWna2d+qAUKxKgMlcG+TZT0+K2FfDcnQ==
"@types/styled-components@^5.1.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.1.0.tgz#24d3412ba5395aa06e14fbc93c52f9454cebd0d6"
integrity sha512-ZFlLCuwF5r+4Vb7JUmd+Yr2S0UBdBGmI7ctFTgJMypIp3xOHI4LCFVn2dKMvpk6xDB2hLRykrEWMBwJEpUAUIQ==
dependencies:
"@types/hoist-non-react-statics" "*"
"@types/react" "*"
@@ -4098,6 +4146,11 @@ babel-code-frame@^6.22.0:
esutils "^2.0.2"
js-tokens "^3.0.2"
babel-core@7.0.0-bridge.0:
version "7.0.0-bridge.0"
resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-7.0.0-bridge.0.tgz#95a492ddd90f9b4e9a4a1da14eb335b87b634ece"
integrity sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==
babel-eslint@^10.0.3:
version "10.1.0"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232"
@@ -5891,14 +5944,14 @@ css-select@^2.0.0:
domutils "^1.7.0"
nth-check "^1.0.2"
css-to-react-native@^2.2.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-2.3.2.tgz#e75e2f8f7aa385b4c3611c52b074b70a002f2e7d"
integrity sha512-VOFaeZA053BqvvvqIA8c9n0+9vFppVBAHCp6JgFTtTMU3Mzi+XnelJ9XC9ul3BqFzZyQ5N+H0SnwsWT2Ebchxw==
css-to-react-native@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.0.0.tgz#62dbe678072a824a689bcfee011fc96e02a7d756"
integrity sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==
dependencies:
camelize "^1.0.0"
css-color-keywords "^1.0.0"
postcss-value-parser "^3.3.0"
postcss-value-parser "^4.0.2"
css-tree@1.0.0-alpha.37:
version "1.0.0-alpha.37"
@@ -7435,7 +7488,7 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
fault@^1.0.0, fault@^1.0.2:
fault@^1.0.0:
version "1.0.4"
resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13"
integrity sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==
@@ -7969,7 +8022,7 @@ gitconfiglocal@^1.0.0:
dependencies:
ini "^1.3.2"
gitdiff-parser@^0.1.2:
gitdiff-parser@^0.1.2, "gitdiff-parser@https://github.com/scm-manager/gitdiff-parser#617747460280bf4522bb84d217a9064ac8eb6d3d":
version "0.1.2"
resolved "https://github.com/scm-manager/gitdiff-parser#617747460280bf4522bb84d217a9064ac8eb6d3d"
@@ -8334,7 +8387,7 @@ hoist-non-react-statics@^2.3.1:
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0:
hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -9287,11 +9340,6 @@ is-utf8@^0.2.0:
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=
is-what@^3.3.1:
version "3.8.0"
resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.8.0.tgz#610bc46a524355f2424eb85eedc6ebbbf7e1ff8c"
integrity sha512-UKeBoQfV8bjlM4pmx1FLDHdxslW/1mTksEs8ReVsilPmUv5cORd4+2/wFcviI3cUjrLybxCjzc8DnodAzJ/Wrg==
is-whitespace-character@^1.0.0:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz#0858edd94a95594c7c9dd0b5c174ec6e45ee4aa7"
@@ -10775,7 +10823,7 @@ lower-case@^2.0.1:
dependencies:
tslib "^1.10.0"
lowlight@^1.13.0:
lowlight@1.13.1, lowlight@^1.13.0, lowlight@~1.11.0:
version "1.13.1"
resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.13.1.tgz#c4f0e03906ebd23fedf2d258f6ab2f6324cf90eb"
integrity sha512-kQ71/T6RksEVz9AlPq07/2m+SU/1kGvt9k39UtvHX760u4SaWakaYH7hYgH5n6sTsCWk4MVYzUzLU59aN5CSmQ==
@@ -10783,14 +10831,6 @@ lowlight@^1.13.0:
fault "^1.0.0"
highlight.js "~9.16.0"
lowlight@~1.11.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.11.0.tgz#1304d83005126d4e8b1dc0f07981e9b689ec2efc"
integrity sha512-xrGGN6XLL7MbTMdPD6NfWPwY43SNkjf/d0mecSx/CW36fUZTjRHEq0/Cdug3TWKtRXLWi7iMl1eP0olYxj/a4A==
dependencies:
fault "^1.0.2"
highlight.js "~9.13.0"
lru-cache@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
@@ -11019,13 +11059,6 @@ meow@^7.0.0:
type-fest "^0.13.1"
yargs-parser "^18.1.3"
merge-anything@^2.2.4:
version "2.4.4"
resolved "https://registry.yarnpkg.com/merge-anything/-/merge-anything-2.4.4.tgz#6226b2ac3d3d3fc5fb9e8d23aa400df25f98fdf0"
integrity sha512-l5XlriUDJKQT12bH+rVhAHjwIuXWdAIecGwsYjv2LJo+dA1AeRTmeQS+3QBpO6lEthBMDi2IUMpLC1yyRvGlwQ==
dependencies:
is-what "^3.3.1"
merge-deep@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/merge-deep/-/merge-deep-3.0.2.tgz#f39fa100a4f1bd34ff29f7d2bf4508fbb8d83ad2"
@@ -12783,7 +12816,7 @@ postcss-unique-selectors@^4.0.1:
postcss "^7.0.0"
uniqs "^2.0.0"
postcss-value-parser@^3.0.0, postcss-value-parser@^3.3.0:
postcss-value-parser@^3.0.0:
version "3.3.1"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
@@ -12877,7 +12910,7 @@ pretty-hrtime@^1.0.3:
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=
prismjs@^1.8.4:
prismjs@^1.16.0, prismjs@^1.8.4:
version "1.20.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.20.0.tgz#9b685fc480a3514ee7198eac6a3bf5024319ff03"
integrity sha512-AEDjSrVNkynnw6A+B1DsFkd6AVdTnp+/WoUixFRULlCLZVRZlVQMVWio/16jv7G1FscUxQxOQhWwApgbnxr6kQ==
@@ -12975,7 +13008,7 @@ prop-types-exact@^1.2.0:
object.assign "^4.1.0"
reflect.ownkeys "^0.2.0"
prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -13492,6 +13525,16 @@ react-syntax-highlighter@^11.0.2:
prismjs "^1.8.4"
refractor "^2.4.1"
"react-syntax-highlighter@https://github.com/conorhastings/react-syntax-highlighter#08bcf49b1aa7877ce94f7208e73dfa6bef8b26e7":
version "12.0.2"
resolved "https://github.com/conorhastings/react-syntax-highlighter#08bcf49b1aa7877ce94f7208e73dfa6bef8b26e7"
dependencies:
"@babel/runtime" "^7.3.1"
highlight.js "~9.13.0"
lowlight "~1.11.0"
prismjs "^1.16.0"
refractor "^2.10.1"
react-test-renderer@^16.0.0-0, react-test-renderer@^16.10.2:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.13.1.tgz#de25ea358d9012606de51e012d9742e7f0deabc1"
@@ -13804,7 +13847,7 @@ reflect.ownkeys@^0.2.0:
resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"
integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA=
refractor@^2.4.1:
refractor@^2.10.1, refractor@^2.4.1:
version "2.10.1"
resolved "https://registry.yarnpkg.com/refractor/-/refractor-2.10.1.tgz#166c32f114ed16fd96190ad21d5193d3afc7d34e"
integrity sha512-Xh9o7hQiQlDbxo5/XkOX6H+x/q8rmlmZKr97Ie1Q8ZM32IRRd3B/UxuA/yXDW79DBSXGWxm2yRTbcTVmAciJRw==
@@ -15138,23 +15181,20 @@ style-loader@^1.0.0:
loader-utils "^2.0.0"
schema-utils "^2.6.6"
styled-components@^4.4.0:
version "4.4.1"
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-4.4.1.tgz#e0631e889f01db67df4de576fedaca463f05c2f2"
integrity sha512-RNqj14kYzw++6Sr38n7197xG33ipEOktGElty4I70IKzQF1jzaD1U4xQ+Ny/i03UUhHlC5NWEO+d8olRCDji6g==
styled-components@^5.1.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.1.1.tgz#96dfb02a8025794960863b9e8e365e3b6be5518d"
integrity sha512-1ps8ZAYu2Husx+Vz8D+MvXwEwvMwFv+hqqUwhNlDN5ybg6A+3xyW1ECrAgywhvXapNfXiz79jJyU0x22z0FFTg==
dependencies:
"@babel/helper-module-imports" "^7.0.0"
"@babel/traverse" "^7.0.0"
"@emotion/is-prop-valid" "^0.8.1"
"@emotion/unitless" "^0.7.0"
"@babel/traverse" "^7.4.5"
"@emotion/is-prop-valid" "^0.8.8"
"@emotion/stylis" "^0.8.4"
"@emotion/unitless" "^0.7.4"
babel-plugin-styled-components ">= 1"
css-to-react-native "^2.2.2"
memoize-one "^5.0.0"
merge-anything "^2.2.4"
prop-types "^15.5.4"
react-is "^16.6.0"
stylis "^3.5.0"
stylis-rule-sheet "^0.0.10"
css-to-react-native "^3.0.0"
hoist-non-react-statics "^3.0.0"
shallowequal "^1.1.0"
supports-color "^5.5.0"
stylehacks@^4.0.0: