mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-03-05 11:50:58 +01:00
Merged in feature/diff_syntax_hightlighting (pull request #398)
Feature/diff syntax hightlighting
This commit is contained in:
@@ -20,7 +20,8 @@
|
||||
},
|
||||
"resolutions": {
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"gitdiff-parser": "https://github.com/scm-manager/gitdiff-parser#ed3fe7de73dbb0a06c3e6adbbdf22dbae6e66351"
|
||||
"gitdiff-parser": "https://github.com/scm-manager/gitdiff-parser#ed3fe7de73dbb0a06c3e6adbbdf22dbae6e66351",
|
||||
"lowlight": "1.13.1"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
|
||||
@@ -29,6 +29,7 @@ public class VndMediaType {
|
||||
public static final String BRANCH = PREFIX + "branch" + SUFFIX;
|
||||
public static final String BRANCH_REQUEST = PREFIX + "branchRequest" + SUFFIX;
|
||||
public static final String DIFF = PLAIN_TEXT_PREFIX + "diff" + PLAIN_TEXT_SUFFIX;
|
||||
public static final String DIFF_PARSED = PREFIX + "diffParsed" + SUFFIX;
|
||||
public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX;
|
||||
public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX;
|
||||
public static final String REPOSITORY_COLLECTION = PREFIX + "repositoryCollection" + SUFFIX;
|
||||
|
||||
@@ -17,35 +17,21 @@ const reportDirectory = path.join(target, "jest-reports");
|
||||
|
||||
module.exports = {
|
||||
rootDir: root,
|
||||
roots: [
|
||||
root
|
||||
],
|
||||
testPathDirs: [
|
||||
path.join(root, "src")
|
||||
],
|
||||
roots: [root],
|
||||
testPathDirs: [path.join(root, "src")],
|
||||
transform: {
|
||||
"^.+\\.(ts|tsx|js)$": "@scm-manager/jest-preset"
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
"node_modules/(?!(@scm-manager)/)"
|
||||
],
|
||||
transformIgnorePatterns: ["node_modules/(?!(@scm-manager)/)"],
|
||||
moduleNameMapper: {
|
||||
"\\.(png|svg|jpg|gif|woff2?|eot|ttf)$": path.join(
|
||||
mockDirectory,
|
||||
"fileMock.js"
|
||||
),
|
||||
"\\.(png|svg|jpg|gif|woff2?|eot|ttf)$": path.join(mockDirectory, "fileMock.js"),
|
||||
"\\.(css|scss|sass)$": path.join(mockDirectory, "styleMock.js")
|
||||
},
|
||||
setupFiles: [path.resolve(__dirname, "src", "setup.js")],
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: [
|
||||
"src/**/*.{ts,tsx,js,jsx}"
|
||||
],
|
||||
collectCoverageFrom: ["src/**/*.{ts,tsx,js,jsx}"],
|
||||
coverageDirectory: path.join(reportDirectory, "coverage-" + name),
|
||||
coveragePathIgnorePatterns: [
|
||||
"src/tests/.*",
|
||||
"src/testing/.*"
|
||||
],
|
||||
coveragePathIgnorePatterns: ["src/tests/.*", "src/testing/.*"],
|
||||
reporters: [
|
||||
"default",
|
||||
[
|
||||
|
||||
7
scm-ui/jest-preset/src/__mocks__/workerMock.js
Normal file
7
scm-ui/jest-preset/src/__mocks__/workerMock.js
Normal file
@@ -0,0 +1,7 @@
|
||||
function WorkerMock() {}
|
||||
|
||||
WorkerMock.prototype.addEventListener = function() {};
|
||||
WorkerMock.prototype.removeEventListener = function() {};
|
||||
WorkerMock.prototype.postMessage = function() {};
|
||||
|
||||
module.exports = WorkerMock;
|
||||
@@ -1,2 +1,4 @@
|
||||
import registerRequireContextHook from "babel-plugin-require-context-hook/register";
|
||||
import Worker from "./__mocks__/workerMock";
|
||||
registerRequireContextHook();
|
||||
window.Worker = Worker;
|
||||
|
||||
@@ -32,6 +32,22 @@
|
||||
<finalName>scm-ui</finalName>
|
||||
<plugins>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-clean-plugin</artifactId>
|
||||
<configuration>
|
||||
<filesets>
|
||||
<!-- delete node cache to avoid problems with hmr or fast-refresh code -->
|
||||
<fileset>
|
||||
<directory>../node_modules/.cache</directory>
|
||||
<includes>
|
||||
<include>**</include>
|
||||
</includes>
|
||||
</fileset>
|
||||
</filesets>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>com.github.sdorra</groupId>
|
||||
<artifactId>buildfrontend-maven-plugin</artifactId>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const WorkerPlugin = require("worker-plugin");
|
||||
|
||||
module.exports = {
|
||||
module: {
|
||||
rules: [
|
||||
@@ -38,8 +40,9 @@ module.exports = {
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: [
|
||||
".ts", ".tsx", ".js", ".jsx", ".css", ".scss", ".json"
|
||||
]
|
||||
}
|
||||
extensions: [".ts", ".tsx", ".js", ".jsx", ".css", ".scss", ".json"]
|
||||
},
|
||||
plugins: [
|
||||
new WorkerPlugin()
|
||||
]
|
||||
};
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"@types/enzyme": "^3.10.3",
|
||||
"@types/fetch-mock": "^7.3.1",
|
||||
"@types/jest": "^24.0.19",
|
||||
"@types/lowlight": "^0.0.0",
|
||||
"@types/query-string": "5",
|
||||
"@types/react": "^16.9.9",
|
||||
"@types/react-dom": "^16.9.2",
|
||||
@@ -40,7 +41,8 @@
|
||||
"raf": "^3.4.0",
|
||||
"react-test-renderer": "^16.10.2",
|
||||
"storybook-addon-i18next": "^1.2.1",
|
||||
"typescript": "^3.7.2"
|
||||
"typescript": "^3.7.2",
|
||||
"worker-plugin": "^3.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scm-manager/ui-extensions": "^2.0.0-SNAPSHOT",
|
||||
@@ -48,6 +50,8 @@
|
||||
"classnames": "^2.2.6",
|
||||
"date-fns": "^2.4.1",
|
||||
"event-source-polyfill": "^1.0.9",
|
||||
"gitdiff-parser": "^0.1.2",
|
||||
"lowlight": "^1.13.0",
|
||||
"query-string": "5",
|
||||
"react": "^16.8.6",
|
||||
"react-diff-view": "^2.4.1",
|
||||
@@ -56,8 +60,7 @@
|
||||
"react-markdown": "^4.0.6",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-select": "^2.1.2",
|
||||
"react-syntax-highlighter": "^11.0.2",
|
||||
"gitdiff-parser": "^0.1.2"
|
||||
"react-syntax-highlighter": "^11.0.2"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,9 @@ import simpleDiff from "../__resources__/Diff.simple";
|
||||
import hunksDiff from "../__resources__/Diff.hunks";
|
||||
import binaryDiff from "../__resources__/Diff.binary";
|
||||
import Button from "../buttons/Button";
|
||||
import { DiffEventContext } from "./DiffTypes";
|
||||
import { DiffEventContext, File } from "./DiffTypes";
|
||||
import Toast from "../toast/Toast";
|
||||
import { getPath } from "./diffs";
|
||||
|
||||
const diffFiles = parser.parse(simpleDiff);
|
||||
|
||||
@@ -57,4 +58,16 @@ storiesOf("Diff", module)
|
||||
.add("Binaries", () => {
|
||||
const binaryDiffFiles = parser.parse(binaryDiff);
|
||||
return <Diff diff={binaryDiffFiles} />;
|
||||
})
|
||||
.add("SyntaxHighlighting", () => {
|
||||
const filesWithLanguage = diffFiles.map((file: File) => {
|
||||
const ext = getPath(file).split(".")[1];
|
||||
if (ext === "tsx") {
|
||||
file.language = "typescript";
|
||||
} else {
|
||||
file.language = ext;
|
||||
}
|
||||
return file;
|
||||
});
|
||||
return <Diff diff={filesWithLanguage} />;
|
||||
});
|
||||
|
||||
@@ -3,11 +3,12 @@ import { withTranslation, WithTranslation } from "react-i18next";
|
||||
import classNames from "classnames";
|
||||
import styled from "styled-components";
|
||||
// @ts-ignore
|
||||
import { Diff as DiffComponent, getChangeKey, Hunk, Decoration } from "react-diff-view";
|
||||
import { getChangeKey, Hunk, Decoration } from "react-diff-view";
|
||||
import { Button, ButtonGroup } from "../buttons";
|
||||
import Tag from "../Tag";
|
||||
import Icon from "../Icon";
|
||||
import { ChangeEvent, Change, File, Hunk as HunkType, DiffObjectProps } from "./DiffTypes";
|
||||
import TokenizedDiffView from "./TokenizedDiffView";
|
||||
|
||||
const EMPTY_ANNOTATION_FACTORY = {};
|
||||
|
||||
@@ -57,33 +58,6 @@ const ChangeTypeTag = styled(Tag)`
|
||||
margin-left: 0.75rem;
|
||||
`;
|
||||
|
||||
const ModifiedDiffComponent = styled(DiffComponent)`
|
||||
/* align line numbers */
|
||||
& .diff-gutter {
|
||||
text-align: right;
|
||||
}
|
||||
/* column sizing */
|
||||
> colgroup .diff-gutter-col {
|
||||
width: 3.25rem;
|
||||
}
|
||||
/* prevent following content from moving down */
|
||||
> .diff-gutter:empty:hover::after {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
/* smaller font size for code */
|
||||
& .diff-line {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
/* comment padding for sidebyside view */
|
||||
&.split .diff-widget-content .is-indented-line {
|
||||
padding-left: 3.25rem;
|
||||
}
|
||||
/* comment padding for combined view */
|
||||
&.unified .diff-widget-content .is-indented-line {
|
||||
padding-left: 6.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
class DiffFile extends React.Component<Props, State> {
|
||||
static defaultProps: Partial<Props> = {
|
||||
defaultCollapse: false,
|
||||
@@ -264,9 +238,9 @@ class DiffFile extends React.Component<Props, State> {
|
||||
body = (
|
||||
<div className="panel-block is-paddingless">
|
||||
{fileAnnotations}
|
||||
<ModifiedDiffComponent className={viewType} viewType={viewType} hunks={file.hunks} diffType={file.type}>
|
||||
<TokenizedDiffView className={viewType} viewType={viewType} file={file}>
|
||||
{(hunks: HunkType[]) => this.concat(hunks.map(this.renderHunk))}
|
||||
</ModifiedDiffComponent>
|
||||
</TokenizedDiffView>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ export type File = {
|
||||
oldPath: string;
|
||||
oldRevision?: string;
|
||||
type: FileChangeType;
|
||||
language?: string;
|
||||
// TODO does this property exists?
|
||||
isBinary?: boolean;
|
||||
};
|
||||
|
||||
@@ -50,10 +50,15 @@ class LoadingDiff extends React.Component<Props, State> {
|
||||
this.setState({ loading: true });
|
||||
apiClient
|
||||
.get(url)
|
||||
.then(response => response.text())
|
||||
.then(parser.parse)
|
||||
// $FlowFixMe
|
||||
.then((diff: any) => {
|
||||
.then(response => {
|
||||
const contentType = response.headers.get("Content-Type");
|
||||
if (contentType && contentType.toLowerCase() === "application/vnd.scmm-diffparsed+json;v=2") {
|
||||
return response.json().then(data => data.files);
|
||||
} else {
|
||||
return response.text().then(parser.parse);
|
||||
}
|
||||
})
|
||||
.then((diff: File[]) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
diff: diff
|
||||
|
||||
39
scm-ui/ui-components/src/repos/Tokenize.worker.ts
Normal file
39
scm-ui/ui-components/src/repos/Tokenize.worker.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// @ts-ignore we have no types for react-diff-view
|
||||
import { tokenize } from "react-diff-view";
|
||||
import refractor from "./refractorAdapter";
|
||||
|
||||
// the WorkerGlobalScope is assigned to self
|
||||
// see https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/self
|
||||
declare const self: Worker;
|
||||
|
||||
self.addEventListener("message", ({ data: { id, payload } }) => {
|
||||
const { hunks, language } = payload;
|
||||
const options = {
|
||||
highlight: language !== "text",
|
||||
language: language,
|
||||
refractor
|
||||
};
|
||||
|
||||
const doTokenization = (worker: Worker) => {
|
||||
try {
|
||||
const tokens = tokenize(hunks, options);
|
||||
const payload = {
|
||||
success: true,
|
||||
tokens: tokens
|
||||
};
|
||||
worker.postMessage({ id, payload });
|
||||
} catch (ex) {
|
||||
const payload = {
|
||||
success: false,
|
||||
reason: ex.message
|
||||
};
|
||||
worker.postMessage({ id, payload });
|
||||
}
|
||||
};
|
||||
|
||||
const createTokenizer = (worker: Worker) => () => doTokenization(worker);
|
||||
|
||||
if (options.highlight) {
|
||||
refractor.loadLanguage(language, createTokenizer(self));
|
||||
}
|
||||
});
|
||||
67
scm-ui/ui-components/src/repos/TokenizedDiffView.tsx
Normal file
67
scm-ui/ui-components/src/repos/TokenizedDiffView.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React, { FC } from "react";
|
||||
import styled from "styled-components";
|
||||
// @ts-ignore we have no typings for react-diff-view
|
||||
import { Diff, useTokenizeWorker } from "react-diff-view";
|
||||
import { File } from "./DiffTypes";
|
||||
|
||||
// styling for the diff tokens
|
||||
// this must be aligned with th style, which is used in the SyntaxHighlighter component
|
||||
import "highlight.js/styles/arduino-light.css";
|
||||
|
||||
const DiffView = styled(Diff)`
|
||||
/* align line numbers */
|
||||
& .diff-gutter {
|
||||
text-align: right;
|
||||
}
|
||||
/* column sizing */
|
||||
> colgroup .diff-gutter-col {
|
||||
width: 3.25rem;
|
||||
}
|
||||
/* prevent following content from moving down */
|
||||
> .diff-gutter:empty:hover::after {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
/* smaller font size for code */
|
||||
& .diff-line {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
/* comment padding for sidebyside view */
|
||||
&.split .diff-widget-content .is-indented-line {
|
||||
padding-left: 3.25rem;
|
||||
}
|
||||
/* comment padding for combined view */
|
||||
&.unified .diff-widget-content .is-indented-line {
|
||||
padding-left: 6.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
// WebWorker which creates tokens for syntax highlighting
|
||||
const tokenize = new Worker("./Tokenize.worker.ts", { name: "tokenizer", type: "module" });
|
||||
|
||||
type Props = {
|
||||
file: File;
|
||||
viewType: "split" | "unified";
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const determineLanguage = (file: File) => {
|
||||
if (file.language) {
|
||||
return file.language.toLowerCase();
|
||||
}
|
||||
return "text";
|
||||
};
|
||||
|
||||
const TokenizedDiffView: FC<Props> = ({ file, viewType, className, children }) => {
|
||||
const { tokens } = useTokenizeWorker(tokenize, {
|
||||
hunks: file.hunks,
|
||||
language: determineLanguage(file)
|
||||
});
|
||||
|
||||
return (
|
||||
<DiffView className={className} viewType={viewType} tokens={tokens} hunks={file.hunks} diffType={file.type}>
|
||||
{children}
|
||||
</DiffView>
|
||||
);
|
||||
};
|
||||
|
||||
export default TokenizedDiffView;
|
||||
@@ -0,0 +1,84 @@
|
||||
import { createUrl, isDiffSupported } from "./ChangesetDiff";
|
||||
|
||||
describe("isDiffSupported tests", () => {
|
||||
it("should return true if diff link is defined", () => {
|
||||
const supported = isDiffSupported({
|
||||
_links: {
|
||||
diff: {
|
||||
href: "http://diff"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(supported).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true if parsed diff link is defined", () => {
|
||||
const supported = isDiffSupported({
|
||||
_links: {
|
||||
diffParsed: {
|
||||
href: "http://diff"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(supported).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false if not diff link was provided", () => {
|
||||
const supported = isDiffSupported({
|
||||
_links: {}
|
||||
});
|
||||
|
||||
expect(supported).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createUrl tests", () => {
|
||||
it("should return the diff url, if only diff url is defined", () => {
|
||||
const url = createUrl({
|
||||
_links: {
|
||||
diff: {
|
||||
href: "http://diff"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(url).toBe("http://diff?format=GIT");
|
||||
});
|
||||
|
||||
it("should return the diff parsed url, if only diff parsed url is defined", () => {
|
||||
const url = createUrl({
|
||||
_links: {
|
||||
diffParsed: {
|
||||
href: "http://diff-parsed"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(url).toBe("http://diff-parsed");
|
||||
});
|
||||
|
||||
it("should return the diff parsed url, if both diff links are defined", () => {
|
||||
const url = createUrl({
|
||||
_links: {
|
||||
diff: {
|
||||
href: "http://diff"
|
||||
},
|
||||
diffParsed: {
|
||||
href: "http://diff-parsed"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(url).toBe("http://diff-parsed");
|
||||
});
|
||||
|
||||
it("should throw an error if no diff link is defined", () => {
|
||||
expect(() =>
|
||||
createUrl({
|
||||
_links: {}
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { Changeset, Link } from "@scm-manager/ui-types";
|
||||
import { Changeset, Link, Collection } from "@scm-manager/ui-types";
|
||||
import LoadingDiff from "../LoadingDiff";
|
||||
import Notification from "../../Notification";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
@@ -9,26 +9,27 @@ type Props = WithTranslation & {
|
||||
defaultCollapse?: boolean;
|
||||
};
|
||||
|
||||
export const isDiffSupported = (changeset: Collection) => {
|
||||
return !!changeset._links.diff || !!changeset._links.diffParsed;
|
||||
};
|
||||
|
||||
export const createUrl = (changeset: Collection) => {
|
||||
if (changeset._links.diffParsed) {
|
||||
return (changeset._links.diffParsed as Link).href;
|
||||
} else if (changeset._links.diff) {
|
||||
return (changeset._links.diff as Link).href + "?format=GIT";
|
||||
}
|
||||
throw new Error("diff link is missing");
|
||||
};
|
||||
|
||||
class ChangesetDiff extends React.Component<Props> {
|
||||
isDiffSupported(changeset: Changeset) {
|
||||
return !!changeset._links.diff;
|
||||
}
|
||||
|
||||
createUrl(changeset: Changeset) {
|
||||
if (changeset._links.diff) {
|
||||
const link = changeset._links.diff as Link;
|
||||
return link.href + "?format=GIT";
|
||||
}
|
||||
throw new Error("diff link is missing");
|
||||
}
|
||||
|
||||
render() {
|
||||
const { changeset, defaultCollapse, t } = this.props;
|
||||
if (!this.isDiffSupported(changeset)) {
|
||||
if (!isDiffSupported(changeset)) {
|
||||
return <Notification type="danger">{t("changeset.diffNotSupported")}</Notification>;
|
||||
} else {
|
||||
const url = this.createUrl(changeset);
|
||||
return <LoadingDiff url={url} defaultCollapse={defaultCollapse} sideBySide={false}/>;
|
||||
const url = createUrl(changeset);
|
||||
return <LoadingDiff url={url} defaultCollapse={defaultCollapse} sideBySide={false} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
36
scm-ui/ui-components/src/repos/refractorAdapter.ts
Normal file
36
scm-ui/ui-components/src/repos/refractorAdapter.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import lowlight from "lowlight/lib/core";
|
||||
|
||||
// adapter to let lowlight look like refractor
|
||||
// this is required because react-diff-view does only support refractor,
|
||||
// but we want same highlighting as in the source code browser.
|
||||
|
||||
const isLanguageRegistered = (lang: string) => {
|
||||
// @ts-ignore listLanguages seems unknown to type
|
||||
const registeredLanguages = lowlight.listLanguages();
|
||||
return !!registeredLanguages[lang];
|
||||
};
|
||||
|
||||
const loadLanguage = (lang: string, callback: () => void) => {
|
||||
if (isLanguageRegistered(lang)) {
|
||||
callback();
|
||||
} else {
|
||||
import(
|
||||
/* webpackChunkName: "tokenizer-lowlight-[request]" */
|
||||
`highlight.js/lib/languages/${lang}`
|
||||
).then(loadedLanguage => {
|
||||
lowlight.registerLanguage(lang, loadedLanguage.default);
|
||||
callback();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const refractorAdapter = {
|
||||
...lowlight,
|
||||
isLanguageRegistered,
|
||||
loadLanguage,
|
||||
highlight: (value: string, language: string) => {
|
||||
return lowlight.highlight(language, value).value;
|
||||
}
|
||||
};
|
||||
|
||||
export default refractorAdapter;
|
||||
@@ -9,7 +9,6 @@ const createNodeMock = (element: any) => {
|
||||
querySelector: (selector: string) => {}
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
initStoryshots({
|
||||
@@ -17,6 +16,7 @@ initStoryshots({
|
||||
// fix snapshot tests with react-diff-view which uses a ref on tr
|
||||
// @see https://github.com/storybookjs/storybook/pull/1090
|
||||
test: snapshotWithOptions({
|
||||
// @ts-ignore types seems not to match
|
||||
createNodeMock
|
||||
})
|
||||
});
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
"dependencies": {
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.1.3",
|
||||
"babel-loader": "^8.0.6",
|
||||
"cache-loader": "^4.1.0",
|
||||
"css-loader": "^3.2.0",
|
||||
"file-loader": "^4.2.0",
|
||||
"mini-css-extract-plugin": "^0.8.0",
|
||||
@@ -23,10 +22,10 @@
|
||||
"sass-loader": "^8.0.0",
|
||||
"script-loader": "^0.7.2",
|
||||
"style-loader": "^1.0.0",
|
||||
"thread-loader": "^2.1.3",
|
||||
"webpack": "^4.41.5",
|
||||
"webpack-cli": "^3.3.10",
|
||||
"webpack-dev-server": "^3.10.1"
|
||||
"webpack-dev-server": "^3.10.1",
|
||||
"worker-plugin": "^3.2.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "@scm-manager/eslint-config",
|
||||
@@ -36,5 +35,6 @@
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,28 @@ const createIndexMiddleware = require("./middleware/IndexMiddleware");
|
||||
const createContextPathMiddleware = require("./middleware/ContextPathMiddleware");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
|
||||
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
|
||||
const WorkerPlugin = require("worker-plugin");
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV === "development";
|
||||
const root = path.resolve(process.cwd(), "scm-ui");
|
||||
|
||||
const babelPlugins = [];
|
||||
const webpackPlugins = [new WorkerPlugin()];
|
||||
|
||||
let mode = "production";
|
||||
|
||||
if (isDevelopment) {
|
||||
mode = "development";
|
||||
babelPlugins.push(require.resolve("react-refresh/babel"));
|
||||
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
|
||||
webpackPlugins.push(new ReactRefreshWebpackPlugin());
|
||||
}
|
||||
|
||||
console.log(`build ${mode} bundles`);
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
mode: isDevelopment ? "development" : "production",
|
||||
mode,
|
||||
context: root,
|
||||
entry: {
|
||||
webapp: [path.resolve(__dirname, "webpack-public-path.js"), "./ui-webapp/src/index.tsx"]
|
||||
@@ -34,18 +48,12 @@ module.exports = [
|
||||
test: /\.(js|ts|jsx|tsx)$/i,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
{
|
||||
loader: "cache-loader"
|
||||
},
|
||||
{
|
||||
loader: "thread-loader"
|
||||
},
|
||||
{
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
cacheDirectory: true,
|
||||
presets: ["@scm-manager/babel-preset"],
|
||||
plugins: [isDevelopment && require.resolve("react-refresh/babel")].filter(Boolean)
|
||||
plugins: babelPlugins
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -72,7 +80,8 @@ module.exports = [
|
||||
},
|
||||
output: {
|
||||
path: path.join(root, "target", "assets"),
|
||||
filename: "[name].bundle.js"
|
||||
filename: "[name].bundle.js",
|
||||
chunkFilename: "[name].bundle.js"
|
||||
},
|
||||
devServer: {
|
||||
contentBase: path.join(root, "ui-webapp", "public"),
|
||||
@@ -94,6 +103,7 @@ module.exports = [
|
||||
},
|
||||
optimization: {
|
||||
runtimeChunk: "single",
|
||||
namedChunks: true,
|
||||
splitChunks: {
|
||||
chunks: "all",
|
||||
cacheGroups: {
|
||||
@@ -110,7 +120,7 @@ module.exports = [
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [isDevelopment && new ReactRefreshWebpackPlugin()].filter(Boolean)
|
||||
plugins: webpackPlugins
|
||||
},
|
||||
{
|
||||
context: root,
|
||||
|
||||
@@ -278,13 +278,13 @@
|
||||
<dependency>
|
||||
<groupId>com.github.sdorra</groupId>
|
||||
<artifactId>spotter-core</artifactId>
|
||||
<version>2.0.0</version>
|
||||
<version>2.1.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.tika</groupId>
|
||||
<artifactId>tika-core</artifactId>
|
||||
<version>1.22</version>
|
||||
<version>1.23</version>
|
||||
</dependency>
|
||||
|
||||
<!-- class loader leak prevention -->
|
||||
|
||||
@@ -122,7 +122,9 @@ public class ContentResource {
|
||||
private void appendContentHeader(String path, byte[] head, Response.ResponseBuilder responseBuilder) {
|
||||
ContentType contentType = ContentTypes.detect(path, head);
|
||||
responseBuilder.header("Content-Type", contentType.getRaw());
|
||||
contentType.getLanguage().ifPresent(language -> responseBuilder.header("X-Programming-Language", language));
|
||||
contentType.getLanguage().ifPresent(
|
||||
language -> responseBuilder.header(ProgrammingLanguages.HEADER, ProgrammingLanguages.getValue(language))
|
||||
);
|
||||
}
|
||||
|
||||
private byte[] getHead(String revision, String path, RepositoryService repositoryService) throws IOException {
|
||||
|
||||
@@ -53,6 +53,12 @@ public abstract class DefaultChangesetToChangesetDtoMapper extends HalAppenderMa
|
||||
|
||||
Embedded.Builder embeddedBuilder = embeddedBuilder();
|
||||
|
||||
Links.Builder linksBuilder = linkingTo()
|
||||
.self(resourceLinks.changeset().self(repository.getNamespace(), repository.getName(), source.getId()))
|
||||
.single(link("diff", resourceLinks.diff().self(namespace, name, source.getId())))
|
||||
.single(link("sources", resourceLinks.source().self(namespace, name, source.getId())))
|
||||
.single(link("modifications", resourceLinks.modifications().self(namespace, name, source.getId())));
|
||||
|
||||
try (RepositoryService repositoryService = serviceFactory.create(repository)) {
|
||||
if (repositoryService.isSupported(Command.TAGS)) {
|
||||
embeddedBuilder.with("tags", tagCollectionToDtoMapper.getTagDtoList(namespace, name,
|
||||
@@ -62,16 +68,13 @@ public abstract class DefaultChangesetToChangesetDtoMapper extends HalAppenderMa
|
||||
embeddedBuilder.with("branches", branchCollectionToDtoMapper.getBranchDtoList(namespace, name,
|
||||
getListOfObjects(source.getBranches(), branchName -> Branch.normalBranch(branchName, source.getId()))));
|
||||
}
|
||||
|
||||
if (repositoryService.isSupported(Command.DIFF_RESULT)) {
|
||||
linksBuilder.single(link("diffParsed", resourceLinks.diff().parsed(namespace, name, source.getId())));
|
||||
}
|
||||
}
|
||||
embeddedBuilder.with("parents", getListOfObjects(source.getParents(), parent -> changesetToParentDtoMapper.map(new Changeset(parent, 0L, null), repository)));
|
||||
|
||||
Links.Builder linksBuilder = linkingTo()
|
||||
.self(resourceLinks.changeset().self(repository.getNamespace(), repository.getName(), source.getId()))
|
||||
.single(link("diff", resourceLinks.diff().self(namespace, name, source.getId())))
|
||||
.single(link("sources", resourceLinks.source().self(namespace, name, source.getId())))
|
||||
.single(link("modifications", resourceLinks.modifications().self(namespace, name, source.getId())));
|
||||
|
||||
|
||||
applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), source, repository);
|
||||
|
||||
return new ChangesetDto(linksBuilder.build(), embeddedBuilder.build());
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Links;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class DiffResultDto extends HalRepresentation {
|
||||
|
||||
public DiffResultDto(Links links) {
|
||||
super(links);
|
||||
}
|
||||
|
||||
private List<FileDto> files;
|
||||
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
|
||||
public static class FileDto {
|
||||
|
||||
private String oldPath;
|
||||
private String newPath;
|
||||
private boolean oldEndingNewLine;
|
||||
private boolean newEndingNewLine;
|
||||
private String oldRevision;
|
||||
private String newRevision;
|
||||
private String newMode;
|
||||
private String oldMode;
|
||||
private String type;
|
||||
private String language;
|
||||
private List<HunkDto> hunks;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
|
||||
public static class HunkDto {
|
||||
|
||||
private String content;
|
||||
private int oldStart;
|
||||
private int newStart;
|
||||
private int oldLines;
|
||||
private int newLines;
|
||||
private List<ChangeDto> changes;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
|
||||
public static class ChangeDto {
|
||||
|
||||
private String content;
|
||||
private String type;
|
||||
@JsonProperty("isNormal")
|
||||
private boolean isNormal;
|
||||
@JsonProperty("isInsert")
|
||||
private boolean isInsert;
|
||||
@JsonProperty("isDelete")
|
||||
private boolean isDelete;
|
||||
private int lineNumber;
|
||||
private int oldLineNumber;
|
||||
private int newLineNumber;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.github.sdorra.spotter.ContentTypes;
|
||||
import com.github.sdorra.spotter.Language;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.inject.Inject;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.api.DiffFile;
|
||||
import sonia.scm.repository.api.DiffLine;
|
||||
import sonia.scm.repository.api.DiffResult;
|
||||
import sonia.scm.repository.api.Hunk;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
|
||||
import static de.otto.edison.hal.Links.linkingTo;
|
||||
|
||||
/**
|
||||
* TODO conflicts, copy and rename
|
||||
*/
|
||||
class DiffResultToDiffResultDtoMapper {
|
||||
|
||||
private final ResourceLinks resourceLinks;
|
||||
|
||||
@Inject
|
||||
DiffResultToDiffResultDtoMapper(ResourceLinks resourceLinks) {
|
||||
this.resourceLinks = resourceLinks;
|
||||
}
|
||||
|
||||
public DiffResultDto mapForIncoming(Repository repository, DiffResult result, String source, String target) {
|
||||
DiffResultDto dto = new DiffResultDto(linkingTo().self(resourceLinks.incoming().diffParsed(repository.getNamespace(), repository.getName(), source, target)).build());
|
||||
setFiles(result, dto);
|
||||
return dto;
|
||||
}
|
||||
|
||||
public DiffResultDto mapForRevision(Repository repository, DiffResult result, String revision) {
|
||||
DiffResultDto dto = new DiffResultDto(linkingTo().self(resourceLinks.diff().parsed(repository.getNamespace(), repository.getName(), revision)).build());
|
||||
setFiles(result, dto);
|
||||
return dto;
|
||||
}
|
||||
|
||||
private void setFiles(DiffResult result, DiffResultDto dto) {
|
||||
List<DiffResultDto.FileDto> files = new ArrayList<>();
|
||||
for (DiffFile file : result) {
|
||||
files.add(mapFile(file));
|
||||
}
|
||||
dto.setFiles(files);
|
||||
}
|
||||
|
||||
private DiffResultDto.FileDto mapFile(DiffFile file) {
|
||||
DiffResultDto.FileDto dto = new DiffResultDto.FileDto();
|
||||
// ???
|
||||
dto.setOldEndingNewLine(true);
|
||||
dto.setNewEndingNewLine(true);
|
||||
|
||||
String newPath = file.getNewPath();
|
||||
String oldPath = file.getOldPath();
|
||||
|
||||
String path;
|
||||
if (isFilePath(newPath) && isFileNull(oldPath)) {
|
||||
path = newPath;
|
||||
dto.setType("add");
|
||||
} else if (isFileNull(newPath) && isFilePath(oldPath)) {
|
||||
path = oldPath;
|
||||
dto.setType("delete");
|
||||
} else if (isFilePath(newPath) && isFilePath(oldPath)) {
|
||||
path = newPath;
|
||||
dto.setType("modify");
|
||||
} else {
|
||||
// TODO copy and rename?
|
||||
throw new IllegalStateException("no file without path");
|
||||
}
|
||||
|
||||
dto.setNewPath(newPath);
|
||||
dto.setNewRevision(file.getNewRevision());
|
||||
|
||||
dto.setOldPath(oldPath);
|
||||
dto.setOldRevision(file.getOldRevision());
|
||||
|
||||
|
||||
Optional<Language> language = ContentTypes.detect(path).getLanguage();
|
||||
language.ifPresent(value -> dto.setLanguage(ProgrammingLanguages.getValue(value)));
|
||||
|
||||
List<DiffResultDto.HunkDto> hunks = new ArrayList<>();
|
||||
for (Hunk hunk : file) {
|
||||
hunks.add(mapHunk(hunk));
|
||||
}
|
||||
dto.setHunks(hunks);
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
private boolean isFilePath(String path) {
|
||||
return !isFileNull(path);
|
||||
}
|
||||
|
||||
private boolean isFileNull(String path) {
|
||||
return Strings.isNullOrEmpty(path) || "/dev/null".equals(path);
|
||||
}
|
||||
|
||||
private DiffResultDto.HunkDto mapHunk(Hunk hunk) {
|
||||
DiffResultDto.HunkDto dto = new DiffResultDto.HunkDto();
|
||||
dto.setContent(hunk.getRawHeader());
|
||||
|
||||
dto.setNewStart(hunk.getNewStart());
|
||||
dto.setNewLines(hunk.getNewLineCount());
|
||||
|
||||
dto.setOldStart(hunk.getOldStart());
|
||||
dto.setOldLines(hunk.getOldLineCount());
|
||||
|
||||
List<DiffResultDto.ChangeDto> changes = new ArrayList<>();
|
||||
for (DiffLine line : hunk) {
|
||||
changes.add(mapLine(line));
|
||||
}
|
||||
|
||||
dto.setChanges(changes);
|
||||
return dto;
|
||||
}
|
||||
|
||||
private DiffResultDto.ChangeDto mapLine(DiffLine line) {
|
||||
DiffResultDto.ChangeDto dto = new DiffResultDto.ChangeDto();
|
||||
dto.setContent(line.getContent());
|
||||
|
||||
OptionalInt newLineNumber = line.getNewLineNumber();
|
||||
OptionalInt oldLineNumber = line.getOldLineNumber();
|
||||
if (newLineNumber.isPresent() && !oldLineNumber.isPresent()) {
|
||||
dto.setType("insert");
|
||||
dto.setInsert(true);
|
||||
dto.setLineNumber(newLineNumber.getAsInt());
|
||||
} else if (!newLineNumber.isPresent() && oldLineNumber.isPresent()) {
|
||||
dto.setType("delete");
|
||||
dto.setDelete(true);
|
||||
dto.setLineNumber(oldLineNumber.getAsInt());
|
||||
} else if (newLineNumber.isPresent() && oldLineNumber.isPresent()) {
|
||||
dto.setType("normal");
|
||||
dto.setNormal(true);
|
||||
dto.setNewLineNumber(newLineNumber.getAsInt());
|
||||
dto.setOldLineNumber(oldLineNumber.getAsInt());
|
||||
} else {
|
||||
throw new IllegalStateException("line without line number");
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import sonia.scm.NotFoundException;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.api.DiffCommandBuilder;
|
||||
import sonia.scm.repository.api.DiffFormat;
|
||||
import sonia.scm.repository.api.DiffResult;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
@@ -30,10 +31,12 @@ public class DiffRootResource {
|
||||
static final String DIFF_FORMAT_VALUES_REGEX = "NATIVE|GIT|UNIFIED";
|
||||
|
||||
private final RepositoryServiceFactory serviceFactory;
|
||||
private final DiffResultToDiffResultDtoMapper parsedDiffMapper;
|
||||
|
||||
@Inject
|
||||
public DiffRootResource(RepositoryServiceFactory serviceFactory) {
|
||||
public DiffRootResource(RepositoryServiceFactory serviceFactory, DiffResultToDiffResultDtoMapper parsedDiffMapper) {
|
||||
this.serviceFactory = serviceFactory;
|
||||
this.parsedDiffMapper = parsedDiffMapper;
|
||||
}
|
||||
|
||||
|
||||
@@ -70,4 +73,23 @@ public class DiffRootResource {
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{revision}/parsed")
|
||||
@Produces(VndMediaType.DIFF_PARSED)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "Bad Request"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the diff"),
|
||||
@ResponseCode(code = 404, condition = "not found, no revision with the specified param for the repository available or repository not found"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public DiffResultDto getParsed(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) throws IOException {
|
||||
HttpUtil.checkForCRLFInjection(revision);
|
||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||
DiffResult diffResult = repositoryService.getDiffResultCommand().setRevision(revision).getDiffResult();
|
||||
return parsedDiffMapper.mapForRevision(repositoryService.getRepository(), diffResult, revision);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.repository.api.DiffCommandBuilder;
|
||||
import sonia.scm.repository.api.DiffFormat;
|
||||
import sonia.scm.repository.api.DiffResult;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
@@ -33,16 +34,16 @@ import static sonia.scm.api.v2.resources.DiffRootResource.HEADER_CONTENT_DISPOSI
|
||||
|
||||
public class IncomingRootResource {
|
||||
|
||||
|
||||
private final RepositoryServiceFactory serviceFactory;
|
||||
|
||||
private final IncomingChangesetCollectionToDtoMapper mapper;
|
||||
|
||||
private final IncomingChangesetCollectionToDtoMapper changesetMapper;
|
||||
private final DiffResultToDiffResultDtoMapper parsedDiffMapper;
|
||||
|
||||
@Inject
|
||||
public IncomingRootResource(RepositoryServiceFactory serviceFactory, IncomingChangesetCollectionToDtoMapper incomingChangesetCollectionToDtoMapper) {
|
||||
public IncomingRootResource(RepositoryServiceFactory serviceFactory, IncomingChangesetCollectionToDtoMapper incomingChangesetCollectionToDtoMapper, DiffResultToDiffResultDtoMapper parsedDiffMapper) {
|
||||
this.serviceFactory = serviceFactory;
|
||||
this.mapper = incomingChangesetCollectionToDtoMapper;
|
||||
this.changesetMapper = incomingChangesetCollectionToDtoMapper;
|
||||
this.parsedDiffMapper = parsedDiffMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,7 +110,7 @@ public class IncomingRootResource {
|
||||
.getChangesets();
|
||||
if (changesets != null && changesets.getChangesets() != null) {
|
||||
PageResult<Changeset> pageResult = new PageResult<>(changesets.getChangesets(), changesets.getTotal());
|
||||
return Response.ok(mapper.map(page, pageSize, pageResult, repository, source, target)).build();
|
||||
return Response.ok(changesetMapper.map(page, pageSize, pageResult, repository, source, target)).build();
|
||||
} else {
|
||||
return Response.ok().build();
|
||||
}
|
||||
@@ -150,4 +151,30 @@ public class IncomingRootResource {
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{source}/{target}/diff/parsed")
|
||||
@Produces(VndMediaType.DIFF_PARSED)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "Bad Request"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the diff"),
|
||||
@ResponseCode(code = 404, condition = "not found, source or target branch for the repository not available or repository not found"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public Response incomingDiffParsed(@PathParam("namespace") String namespace,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("source") String source,
|
||||
@PathParam("target") String target) throws IOException {
|
||||
HttpUtil.checkForCRLFInjection(source);
|
||||
HttpUtil.checkForCRLFInjection(target);
|
||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||
DiffResult diffResult = repositoryService.getDiffResultCommand()
|
||||
.setRevision(source)
|
||||
.setAncestorChangeset(target)
|
||||
.getDiffResult();
|
||||
return Response.ok(parsedDiffMapper.mapForIncoming(repositoryService.getRepository(), diffResult, source, target)).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.github.sdorra.spotter.Language;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
final class ProgrammingLanguages {
|
||||
|
||||
static final String HEADER = "X-Programming-Language";
|
||||
|
||||
private static final String DEFAULT = "text";
|
||||
|
||||
private ProgrammingLanguages() {
|
||||
}
|
||||
|
||||
static String getValue(Language language) {
|
||||
Optional<String> aceMode = language.getAceMode();
|
||||
if (!aceMode.isPresent()) {
|
||||
Optional<String> codemirrorMode = language.getCodemirrorMode();
|
||||
return codemirrorMode.orElse(DEFAULT);
|
||||
}
|
||||
return aceMode.get();
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,7 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
|
||||
if (repositoryService.isSupported(Feature.INCOMING_REVISION)) {
|
||||
linksBuilder.single(link("incomingChangesets", resourceLinks.incoming().changesets(repository.getNamespace(), repository.getName())));
|
||||
linksBuilder.single(link("incomingDiff", resourceLinks.incoming().diff(repository.getNamespace(), repository.getName())));
|
||||
linksBuilder.single(link("incomingDiffParsed", resourceLinks.incoming().diffParsed(repository.getNamespace(), repository.getName())));
|
||||
}
|
||||
}
|
||||
linksBuilder.single(link("changesets", resourceLinks.changeset().all(repository.getNamespace(), repository.getName())));
|
||||
|
||||
@@ -362,6 +362,10 @@ class ResourceLinks {
|
||||
return diffLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("diff").parameters().method("get").parameters(id).href();
|
||||
}
|
||||
|
||||
String parsed(String namespace, String name, String id) {
|
||||
return diffLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("diff").parameters().method("getParsed").parameters(id).href();
|
||||
}
|
||||
|
||||
String all(String namespace, String name) {
|
||||
return diffLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("diff").parameters().method("getAll").parameters().href();
|
||||
}
|
||||
@@ -412,7 +416,21 @@ class ResourceLinks {
|
||||
|
||||
public String diff(String namespace, String name) {
|
||||
return toTemplateParams(incomingLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("incoming").parameters().method("incomingDiff").parameters("source", "target").href());
|
||||
}
|
||||
|
||||
public String diffParsed(String namespace, String name) {
|
||||
return toTemplateParams(diffParsed(namespace, name, "source", "target"));
|
||||
}
|
||||
|
||||
public String diffParsed(String namespace, String name, String source, String target) {
|
||||
return incomingLinkBuilder
|
||||
.method("getRepositoryResource")
|
||||
.parameters(namespace, name)
|
||||
.method("incoming")
|
||||
.parameters()
|
||||
.method("incomingDiffParsed")
|
||||
.parameters(source, target)
|
||||
.href();
|
||||
}
|
||||
|
||||
public String toTemplateParams(String href) {
|
||||
|
||||
@@ -92,7 +92,7 @@ public class ContentResourceTest {
|
||||
Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "SomeGoCode.go");
|
||||
assertEquals(200, response.getStatus());
|
||||
|
||||
assertEquals("GO", response.getHeaderString("X-Programming-Language"));
|
||||
assertEquals("golang", response.getHeaderString("X-Programming-Language"));
|
||||
assertEquals("text/x-go", response.getHeaderString("Content-Type"));
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ public class ContentResourceTest {
|
||||
Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "Dockerfile");
|
||||
assertEquals(200, response.getStatus());
|
||||
|
||||
assertEquals("DOCKERFILE", response.getHeaderString("X-Programming-Language"));
|
||||
assertEquals("dockerfile", response.getHeaderString("X-Programming-Language"));
|
||||
assertEquals("text/plain", response.getHeaderString("Content-Type"));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package sonia.scm.api.v2.resources;
|
||||
|
||||
|
||||
import com.google.inject.util.Providers;
|
||||
import de.otto.edison.hal.Links;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.subject.support.SubjectThreadState;
|
||||
@@ -13,6 +14,7 @@ import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import sonia.scm.NotFoundException;
|
||||
@@ -20,6 +22,8 @@ import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.api.DiffCommandBuilder;
|
||||
import sonia.scm.repository.api.DiffFormat;
|
||||
import sonia.scm.repository.api.DiffResult;
|
||||
import sonia.scm.repository.api.DiffResultCommandBuilder;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.util.CRLFInjectionException;
|
||||
@@ -34,7 +38,6 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -45,6 +48,7 @@ public class DiffResourceTest extends RepositoryTestBase {
|
||||
|
||||
public static final String DIFF_PATH = "space/repo/diff/";
|
||||
public static final String DIFF_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + DIFF_PATH;
|
||||
public static final Repository REPOSITORY = new Repository("repoId", "git", "space", "repo");
|
||||
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
@@ -54,8 +58,13 @@ public class DiffResourceTest extends RepositoryTestBase {
|
||||
@Mock
|
||||
private RepositoryService service;
|
||||
|
||||
@Mock
|
||||
@Mock(answer = Answers.RETURNS_SELF)
|
||||
private DiffCommandBuilder diffCommandBuilder;
|
||||
@Mock(answer = Answers.RETURNS_SELF)
|
||||
private DiffResultCommandBuilder diffResultCommandBuilder;
|
||||
|
||||
@Mock
|
||||
private DiffResultToDiffResultDtoMapper diffResultToDiffResultDtoMapper;
|
||||
|
||||
private DiffRootResource diffRootResource;
|
||||
|
||||
@@ -66,15 +75,16 @@ public class DiffResourceTest extends RepositoryTestBase {
|
||||
|
||||
@Before
|
||||
public void prepareEnvironment() {
|
||||
diffRootResource = new DiffRootResource(serviceFactory);
|
||||
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);
|
||||
when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo"));
|
||||
when(service.getRepository()).thenReturn(REPOSITORY);
|
||||
ExceptionWithContextToErrorDtoMapperImpl mapper = new ExceptionWithContextToErrorDtoMapperImpl();
|
||||
dispatcher.registerException(CRLFInjectionException.class, Response.Status.BAD_REQUEST);
|
||||
when(service.getDiffCommand()).thenReturn(diffCommandBuilder);
|
||||
when(service.getDiffResultCommand()).thenReturn(diffResultCommandBuilder);
|
||||
subjectThreadState.bind();
|
||||
ThreadContext.bind(subject);
|
||||
when(subject.isPermitted(any(String.class))).thenReturn(true);
|
||||
@@ -87,8 +97,6 @@ public class DiffResourceTest extends RepositoryTestBase {
|
||||
|
||||
@Test
|
||||
public void shouldGetDiffs() throws Exception {
|
||||
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.retrieveContent()).thenReturn(output -> {});
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.get(DIFF_URL + "revision")
|
||||
@@ -106,6 +114,25 @@ public class DiffResourceTest extends RepositoryTestBase {
|
||||
.contains(expectedValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldGetParsedDiffs() throws Exception {
|
||||
DiffResult diffResult = mock(DiffResult.class);
|
||||
when(diffResultCommandBuilder.getDiffResult()).thenReturn(diffResult);
|
||||
when(diffResultToDiffResultDtoMapper.mapForRevision(REPOSITORY, diffResult, "revision"))
|
||||
.thenReturn(new DiffResultDto(Links.linkingTo().self("http://self").build()));
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.get(DIFF_URL + "revision/parsed")
|
||||
.accept(VndMediaType.DIFF_PARSED);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus())
|
||||
.isEqualTo(200);
|
||||
assertThat(response.getContentAsString())
|
||||
.contains("\"self\":{\"href\":\"http://self\"}");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldGet404OnMissingRepository() throws URISyntaxException {
|
||||
when(serviceFactory.create(any(NamespaceAndName.class))).thenThrow(new NotFoundException("Text", "x"));
|
||||
@@ -119,8 +146,6 @@ public class DiffResourceTest extends RepositoryTestBase {
|
||||
|
||||
@Test
|
||||
public void shouldGet404OnMissingRevision() throws Exception {
|
||||
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Text", "x"));
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
@@ -135,8 +160,6 @@ public class DiffResourceTest extends RepositoryTestBase {
|
||||
|
||||
@Test
|
||||
public void shouldGet400OnCrlfInjection() throws Exception {
|
||||
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Text", "x"));
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
@@ -149,8 +172,6 @@ public class DiffResourceTest extends RepositoryTestBase {
|
||||
|
||||
@Test
|
||||
public void shouldGet400OnUnknownFormat() throws Exception {
|
||||
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Test", "test"));
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
@@ -163,8 +184,6 @@ public class DiffResourceTest extends RepositoryTestBase {
|
||||
|
||||
@Test
|
||||
public void shouldAcceptDiffFormats() throws Exception {
|
||||
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.retrieveContent()).thenReturn(output -> {});
|
||||
|
||||
Arrays.stream(DiffFormat.values()).map(DiffFormat::name).forEach(
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.Link;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.api.DiffFile;
|
||||
import sonia.scm.repository.api.DiffLine;
|
||||
import sonia.scm.repository.api.DiffResult;
|
||||
import sonia.scm.repository.api.Hunk;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
|
||||
import static java.net.URI.create;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DiffResultToDiffResultDtoMapperTest {
|
||||
|
||||
private static final Repository REPOSITORY = new Repository("1", "git", "space", "X");
|
||||
|
||||
ResourceLinks resourceLinks = ResourceLinksMock.createMock(create("/scm/api/v2"));
|
||||
DiffResultToDiffResultDtoMapper mapper = new DiffResultToDiffResultDtoMapper(resourceLinks);
|
||||
|
||||
@Test
|
||||
void shouldMapDiffResult() {
|
||||
DiffResultDto dto = mapper.mapForRevision(REPOSITORY, createResult(), "123");
|
||||
|
||||
List<DiffResultDto.FileDto> files = dto.getFiles();
|
||||
assertAddedFile(files.get(0), "A.java", "abc", "java");
|
||||
assertModifiedFile(files.get(1), "B.ts", "abc", "def", "typescript");
|
||||
assertDeletedFile(files.get(2), "C.go", "ghi", "golang");
|
||||
|
||||
DiffResultDto.HunkDto hunk = files.get(1).getHunks().get(0);
|
||||
assertHunk(hunk, "@@ -3,4 1,2 @@", 1, 2, 3, 4);
|
||||
|
||||
List<DiffResultDto.ChangeDto> changes = hunk.getChanges();
|
||||
assertInsertedLine(changes.get(0), "a", 1);
|
||||
assertModifiedLine(changes.get(1), "b", 2);
|
||||
assertDeletedLine(changes.get(2), "c", 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateSelfLinkForRevision() {
|
||||
DiffResultDto dto = mapper.mapForRevision(REPOSITORY, createResult(), "123");
|
||||
|
||||
Optional<Link> selfLink = dto.getLinks().getLinkBy("self");
|
||||
assertThat(selfLink)
|
||||
.isPresent()
|
||||
.get()
|
||||
.extracting("href")
|
||||
.contains("/scm/api/v2/repositories/space/X/diff/123/parsed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateSelfLinkForIncoming() {
|
||||
DiffResultDto dto = mapper.mapForIncoming(REPOSITORY, createResult(), "feature/some", "master");
|
||||
|
||||
Optional<Link> selfLink = dto.getLinks().getLinkBy("self");
|
||||
assertThat(selfLink)
|
||||
.isPresent()
|
||||
.get()
|
||||
.extracting("href")
|
||||
.contains("/scm/api/v2/repositories/space/X/incoming/feature%2Fsome/master/diff/parsed");
|
||||
}
|
||||
|
||||
private DiffResult createResult() {
|
||||
return result(
|
||||
addedFile("A.java", "abc"),
|
||||
modifiedFile("B.ts", "def", "abc",
|
||||
hunk("@@ -3,4 1,2 @@", 1, 2, 3, 4,
|
||||
insertedLine("a", 1),
|
||||
modifiedLine("b", 2),
|
||||
deletedLine("c", 3)
|
||||
)
|
||||
),
|
||||
deletedFile("C.go", "ghi")
|
||||
);
|
||||
}
|
||||
|
||||
public void assertInsertedLine(DiffResultDto.ChangeDto change, String content, int lineNumber) {
|
||||
assertThat(change.getContent()).isEqualTo(content);
|
||||
assertThat(change.getLineNumber()).isEqualTo(lineNumber);
|
||||
assertThat(change.getType()).isEqualTo("insert");
|
||||
assertThat(change.isInsert()).isTrue();
|
||||
}
|
||||
|
||||
private void assertModifiedLine(DiffResultDto.ChangeDto change, String content, int lineNumber) {
|
||||
assertThat(change.getContent()).isEqualTo(content);
|
||||
assertThat(change.getNewLineNumber()).isEqualTo(lineNumber);
|
||||
assertThat(change.getOldLineNumber()).isEqualTo(lineNumber);
|
||||
assertThat(change.getType()).isEqualTo("normal");
|
||||
assertThat(change.isNormal()).isTrue();
|
||||
}
|
||||
|
||||
private void assertDeletedLine(DiffResultDto.ChangeDto change, String content, int lineNumber) {
|
||||
assertThat(change.getContent()).isEqualTo(content);
|
||||
assertThat(change.getLineNumber()).isEqualTo(lineNumber);
|
||||
assertThat(change.getType()).isEqualTo("delete");
|
||||
assertThat(change.isDelete()).isTrue();
|
||||
}
|
||||
|
||||
private void assertHunk(DiffResultDto.HunkDto hunk, String content, int newStart, int newLineCount, int oldStart, int oldLineCount) {
|
||||
assertThat(hunk.getContent()).isEqualTo(content);
|
||||
assertThat(hunk.getNewStart()).isEqualTo(newStart);
|
||||
assertThat(hunk.getNewLines()).isEqualTo(newLineCount);
|
||||
assertThat(hunk.getOldStart()).isEqualTo(oldStart);
|
||||
assertThat(hunk.getOldLines()).isEqualTo(oldLineCount);
|
||||
}
|
||||
|
||||
private void assertAddedFile(DiffResultDto.FileDto file, String path, String revision, String language) {
|
||||
assertThat(file.getNewPath()).isEqualTo(path);
|
||||
assertThat(file.getNewRevision()).isEqualTo(revision);
|
||||
assertThat(file.getType()).isEqualTo("add");
|
||||
assertThat(file.getLanguage()).isEqualTo(language);
|
||||
}
|
||||
|
||||
private void assertModifiedFile(DiffResultDto.FileDto file, String path, String oldRevision, String newRevision, String language) {
|
||||
assertThat(file.getNewPath()).isEqualTo(path);
|
||||
assertThat(file.getNewRevision()).isEqualTo(newRevision);
|
||||
assertThat(file.getOldPath()).isEqualTo(path);
|
||||
assertThat(file.getOldRevision()).isEqualTo(oldRevision);
|
||||
assertThat(file.getType()).isEqualTo("modify");
|
||||
assertThat(file.getLanguage()).isEqualTo(language);
|
||||
}
|
||||
|
||||
private void assertDeletedFile(DiffResultDto.FileDto file, String path, String revision, String language) {
|
||||
assertThat(file.getOldPath()).isEqualTo(path);
|
||||
assertThat(file.getOldRevision()).isEqualTo(revision);
|
||||
assertThat(file.getType()).isEqualTo("delete");
|
||||
assertThat(file.getLanguage()).isEqualTo(language);
|
||||
}
|
||||
|
||||
private DiffResult result(DiffFile... files) {
|
||||
DiffResult result = mock(DiffResult.class);
|
||||
when(result.iterator()).thenReturn(Arrays.asList(files).iterator());
|
||||
return result;
|
||||
}
|
||||
|
||||
private DiffFile addedFile(String path, String revision, Hunk... hunks) {
|
||||
DiffFile file = mock(DiffFile.class);
|
||||
when(file.getNewPath()).thenReturn(path);
|
||||
when(file.getNewRevision()).thenReturn(revision);
|
||||
when(file.iterator()).thenReturn(Arrays.asList(hunks).iterator());
|
||||
return file;
|
||||
}
|
||||
|
||||
private DiffFile deletedFile(String path, String revision, Hunk... hunks) {
|
||||
DiffFile file = mock(DiffFile.class);
|
||||
when(file.getOldPath()).thenReturn(path);
|
||||
when(file.getOldRevision()).thenReturn(revision);
|
||||
when(file.iterator()).thenReturn(Arrays.asList(hunks).iterator());
|
||||
return file;
|
||||
}
|
||||
|
||||
private DiffFile modifiedFile(String path, String newRevision, String oldRevision, Hunk... hunks) {
|
||||
DiffFile file = mock(DiffFile.class);
|
||||
when(file.getNewPath()).thenReturn(path);
|
||||
when(file.getNewRevision()).thenReturn(newRevision);
|
||||
when(file.getOldPath()).thenReturn(path);
|
||||
when(file.getOldRevision()).thenReturn(oldRevision);
|
||||
when(file.iterator()).thenReturn(Arrays.asList(hunks).iterator());
|
||||
return file;
|
||||
}
|
||||
|
||||
private Hunk hunk(String rawHeader, int newStart, int newLineCount, int oldStart, int oldLineCount, DiffLine... lines) {
|
||||
Hunk hunk = mock(Hunk.class);
|
||||
when(hunk.getRawHeader()).thenReturn(rawHeader);
|
||||
when(hunk.getNewStart()).thenReturn(newStart);
|
||||
when(hunk.getNewLineCount()).thenReturn(newLineCount);
|
||||
when(hunk.getOldStart()).thenReturn(oldStart);
|
||||
when(hunk.getOldLineCount()).thenReturn(oldLineCount);
|
||||
when(hunk.iterator()).thenReturn(Arrays.asList(lines).iterator());
|
||||
return hunk;
|
||||
}
|
||||
|
||||
private DiffLine insertedLine(String content, int lineNumber) {
|
||||
DiffLine line = mock(DiffLine.class);
|
||||
when(line.getContent()).thenReturn(content);
|
||||
when(line.getNewLineNumber()).thenReturn(OptionalInt.of(lineNumber));
|
||||
when(line.getOldLineNumber()).thenReturn(OptionalInt.empty());
|
||||
return line;
|
||||
}
|
||||
|
||||
private DiffLine modifiedLine(String content, int lineNumber) {
|
||||
DiffLine line = mock(DiffLine.class);
|
||||
when(line.getContent()).thenReturn(content);
|
||||
when(line.getNewLineNumber()).thenReturn(OptionalInt.of(lineNumber));
|
||||
when(line.getOldLineNumber()).thenReturn(OptionalInt.of(lineNumber));
|
||||
return line;
|
||||
}
|
||||
|
||||
private DiffLine deletedLine(String content, int lineNumber) {
|
||||
DiffLine line = mock(DiffLine.class);
|
||||
when(line.getContent()).thenReturn(content);
|
||||
when(line.getNewLineNumber()).thenReturn(OptionalInt.empty());
|
||||
when(line.getOldLineNumber()).thenReturn(OptionalInt.of(lineNumber));
|
||||
return line;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package sonia.scm.api.v2.resources;
|
||||
|
||||
|
||||
import com.google.inject.util.Providers;
|
||||
import de.otto.edison.hal.Links;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.subject.support.SubjectThreadState;
|
||||
@@ -24,6 +25,8 @@ import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.Person;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.api.DiffCommandBuilder;
|
||||
import sonia.scm.repository.api.DiffResult;
|
||||
import sonia.scm.repository.api.DiffResultCommandBuilder;
|
||||
import sonia.scm.repository.api.LogCommandBuilder;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
@@ -45,6 +48,7 @@ import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static sonia.scm.repository.api.DiffFormat.NATIVE;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.Silent.class)
|
||||
@Slf4j
|
||||
@@ -54,6 +58,7 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
|
||||
public static final String INCOMING_PATH = "space/repo/incoming/";
|
||||
public static final String INCOMING_CHANGESETS_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + INCOMING_PATH;
|
||||
public static final String INCOMING_DIFF_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + INCOMING_PATH;
|
||||
public static final Repository REPOSITORY = new Repository("repoId", "git", "space", "repo");
|
||||
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
@@ -71,7 +76,11 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
|
||||
|
||||
@Mock
|
||||
private DiffCommandBuilder diffCommandBuilder;
|
||||
@Mock
|
||||
private DiffResultCommandBuilder diffResultCommandBuilder;
|
||||
|
||||
@Mock
|
||||
private DiffResultToDiffResultDtoMapper diffResultToDiffResultDtoMapper;
|
||||
|
||||
private IncomingChangesetCollectionToDtoMapper incomingChangesetCollectionToDtoMapper;
|
||||
|
||||
@@ -88,14 +97,15 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
|
||||
@Before
|
||||
public void prepareEnvironment() {
|
||||
incomingChangesetCollectionToDtoMapper = new IncomingChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks);
|
||||
incomingRootResource = new IncomingRootResource(serviceFactory, incomingChangesetCollectionToDtoMapper);
|
||||
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(any(Repository.class))).thenReturn(repositoryService);
|
||||
when(repositoryService.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo"));
|
||||
when(serviceFactory.create(REPOSITORY)).thenReturn(repositoryService);
|
||||
when(repositoryService.getRepository()).thenReturn(REPOSITORY);
|
||||
when(repositoryService.getLogCommand()).thenReturn(logCommandBuilder);
|
||||
when(repositoryService.getDiffCommand()).thenReturn(diffCommandBuilder);
|
||||
when(repositoryService.getDiffResultCommand()).thenReturn(diffResultCommandBuilder);
|
||||
dispatcher.registerException(CRLFInjectionException.class, Response.Status.BAD_REQUEST);
|
||||
subjectThreadState.bind();
|
||||
ThreadContext.bind(subject);
|
||||
@@ -170,9 +180,9 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
|
||||
|
||||
@Test
|
||||
public void shouldGetDiffs() throws Exception {
|
||||
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setAncestorChangeset(anyString())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setRevision("src_changeset_id")).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setAncestorChangeset("target_changeset_id")).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setFormat(NATIVE)).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.retrieveContent()).thenReturn(output -> {});
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff")
|
||||
@@ -190,6 +200,28 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
|
||||
.contains(expectedValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldGetParsedDiffs() throws Exception {
|
||||
when(diffResultCommandBuilder.setRevision("src_changeset_id")).thenReturn(diffResultCommandBuilder);
|
||||
when(diffResultCommandBuilder.setAncestorChangeset("target_changeset_id")).thenReturn(diffResultCommandBuilder);
|
||||
DiffResult diffResult = mock(DiffResult.class);
|
||||
when(diffResultCommandBuilder.getDiffResult()).thenReturn(diffResult);
|
||||
when(diffResultToDiffResultDtoMapper.mapForIncoming(REPOSITORY, diffResult, "src_changeset_id", "target_changeset_id"))
|
||||
.thenReturn(new DiffResultDto(Links.linkingTo().self("http://self").build()));
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff/parsed")
|
||||
.accept(VndMediaType.DIFF_PARSED);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus())
|
||||
.isEqualTo(200);
|
||||
assertThat(response.getContentAsString())
|
||||
.contains("\"self\":{\"href\":\"http://self\"}");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldGet404OnMissingRepository() throws URISyntaxException {
|
||||
when(serviceFactory.create(any(NamespaceAndName.class))).thenThrow(new NotFoundException("Text", "x"));
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.github.sdorra.spotter.Language;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ProgrammingLanguagesTest {
|
||||
|
||||
@Test
|
||||
void shouldReturnAceModeIfPresent() {
|
||||
assertThat(ProgrammingLanguages.getValue(Language.GO)).isEqualTo("golang");
|
||||
assertThat(ProgrammingLanguages.getValue(Language.JAVA)).isEqualTo("java");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnCodemirrorIfAceModeIsMissing() {
|
||||
assertThat(ProgrammingLanguages.getValue(Language.HTML_ECR)).isEqualTo("htmlmixed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnTextIfNoModeIsPresent() {
|
||||
assertThat(ProgrammingLanguages.getValue(Language.HXML)).isEqualTo("text");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user