mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-07 22:15:45 +01:00
Add table to diff view
Pushed-by: Florian Scholdei<florian.scholdei@cloudogu.com> Pushed-by: Viktor Egorov<viktor.egorov-extern@cloudogu.com> Pushed-by: k8s-git-ops<admin@cloudogu.com> Committed-by: Thomas Zerr<thomas.zerr@cloudogu.com> Co-authored-by: Viktor<viktor.egorov@triology.de> Co-authored-by: Thomas Zerr<thomas.zerr@cloudogu.com> Pushed-by: Thomas Zerr<thomas.zerr@cloudogu.com>
This commit is contained in:
2
gradle/changelog/add_file_tree_to_diffs.yml
Normal file
2
gradle/changelog/add_file_tree_to_diffs.yml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
- type: added
|
||||||
|
description: A file tree is now visible while inspecting a changeset
|
||||||
@@ -26,6 +26,9 @@ package sonia.scm.repository.api;
|
|||||||
|
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import static java.util.Optional.empty;
|
import static java.util.Optional.empty;
|
||||||
@@ -54,12 +57,22 @@ public interface DiffResult extends Iterable<DiffFile> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This function returns statistics if they are supported.
|
* This function returns statistics if they are supported.
|
||||||
|
*
|
||||||
* @since 3.4.0
|
* @since 3.4.0
|
||||||
*/
|
*/
|
||||||
default Optional<DiffStatistics> getStatistics() {
|
default Optional<DiffStatistics> getStatistics() {
|
||||||
return empty();
|
return empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function returns all file paths wrapped in a tree
|
||||||
|
*
|
||||||
|
* @since 3.5.0
|
||||||
|
*/
|
||||||
|
default Optional<DiffTreeNode> getDiffTree() {
|
||||||
|
return empty();
|
||||||
|
}
|
||||||
|
|
||||||
@Value
|
@Value
|
||||||
class DiffStatistics {
|
class DiffStatistics {
|
||||||
/**
|
/**
|
||||||
@@ -76,4 +89,45 @@ public interface DiffResult extends Iterable<DiffFile> {
|
|||||||
int deleted;
|
int deleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Value
|
||||||
|
class DiffTreeNode {
|
||||||
|
|
||||||
|
String nodeName;
|
||||||
|
Map<String, DiffTreeNode> children = new LinkedHashMap<>();
|
||||||
|
Optional<DiffFile.ChangeType> changeType;
|
||||||
|
|
||||||
|
public Map<String, DiffTreeNode> getChildren() {
|
||||||
|
return Collections.unmodifiableMap(children);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DiffTreeNode createRootNode() {
|
||||||
|
return new DiffTreeNode("", Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private DiffTreeNode(String nodeName, Optional<DiffFile.ChangeType> changeType) {
|
||||||
|
this.nodeName = nodeName;
|
||||||
|
this.changeType = changeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addChild(String path, DiffFile.ChangeType changeType) {
|
||||||
|
traverseAndAddChild(path.split("/"), 0, changeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void traverseAndAddChild(String[] pathSegments, int index, DiffFile.ChangeType changeType) {
|
||||||
|
if (index == pathSegments.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String currentPathSegment = pathSegments[index];
|
||||||
|
DiffTreeNode child = children.get(currentPathSegment);
|
||||||
|
|
||||||
|
if (child == null) {
|
||||||
|
boolean isFilename = index == pathSegments.length - 1;
|
||||||
|
child = new DiffTreeNode(currentPathSegment, isFilename ? Optional.of(changeType) : Optional.empty());
|
||||||
|
children.put(currentPathSegment, child);
|
||||||
|
}
|
||||||
|
|
||||||
|
child.traverseAndAddChild(pathSegments, index + 1, changeType);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.api;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
public class DiffTreeNodeTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCreateTree() {
|
||||||
|
DiffResult.DiffTreeNode root = DiffResult.DiffTreeNode.createRootNode();
|
||||||
|
root.addChild("test/a.txt", DiffFile.ChangeType.MODIFY);
|
||||||
|
root.addChild("b.txt", DiffFile.ChangeType.DELETE);
|
||||||
|
root.addChild("test/c.txt", DiffFile.ChangeType.COPY);
|
||||||
|
root.addChild("scm-manager/d.txt", DiffFile.ChangeType.RENAME);
|
||||||
|
root.addChild("test/scm-manager/e.txt", DiffFile.ChangeType.ADD);
|
||||||
|
|
||||||
|
assertThat(root.getNodeName()).isEmpty();
|
||||||
|
assertThat(root.getChangeType()).isEmpty();
|
||||||
|
assertThat(root.getChildren()).containsOnlyKeys("test", "b.txt", "scm-manager");
|
||||||
|
|
||||||
|
assertThat(root.getChildren().get("b.txt").getNodeName()).isEqualTo("b.txt");
|
||||||
|
assertThat(root.getChildren().get("b.txt").getChangeType()).hasValue(DiffFile.ChangeType.DELETE);
|
||||||
|
assertThat(root.getChildren().get("b.txt").getChildren()).isEmpty();
|
||||||
|
|
||||||
|
assertThat(root.getChildren().get("test").getNodeName()).isEqualTo("test");
|
||||||
|
assertThat(root.getChildren().get("test").getChangeType()).isEmpty();
|
||||||
|
assertThat(root.getChildren().get("test").getChildren()).containsOnlyKeys("a.txt", "c.txt", "scm-manager");
|
||||||
|
|
||||||
|
assertThat(root.getChildren().get("test").getChildren().get("a.txt").getNodeName()).isEqualTo("a.txt");
|
||||||
|
assertThat(root.getChildren().get("test").getChildren().get("a.txt").getChangeType()).hasValue(DiffFile.ChangeType.MODIFY);
|
||||||
|
assertThat(root.getChildren().get("test").getChildren().get("a.txt").getChildren()).isEmpty();
|
||||||
|
|
||||||
|
assertThat(root.getChildren().get("test").getChildren().get("c.txt").getNodeName()).isEqualTo("c.txt");
|
||||||
|
assertThat(root.getChildren().get("test").getChildren().get("c.txt").getChangeType()).hasValue(DiffFile.ChangeType.COPY);
|
||||||
|
assertThat(root.getChildren().get("test").getChildren().get("c.txt").getChildren()).isEmpty();
|
||||||
|
|
||||||
|
assertThat(root.getChildren().get("test").getChildren().get("scm-manager").getNodeName()).isEqualTo("scm-manager");
|
||||||
|
assertThat(root.getChildren().get("test").getChildren().get("scm-manager").getChangeType()).isEmpty();
|
||||||
|
assertThat(root.getChildren().get("test").getChildren().get("scm-manager").getChildren()).containsOnlyKeys("e.txt");
|
||||||
|
|
||||||
|
assertThat(root.getChildren().get("test").getChildren().get("scm-manager").getChildren().get("e.txt").getNodeName()).isEqualTo("e.txt");
|
||||||
|
assertThat(root.getChildren().get("test").getChildren().get("scm-manager").getChildren().get("e.txt").getChangeType()).hasValue(DiffFile.ChangeType.ADD);
|
||||||
|
assertThat(root.getChildren().get("test").getChildren().get("scm-manager").getChildren().get("e.txt").getChildren()).isEmpty();
|
||||||
|
|
||||||
|
assertThat(root.getChildren().get("scm-manager").getNodeName()).isEqualTo("scm-manager");
|
||||||
|
assertThat(root.getChildren().get("scm-manager").getChangeType()).isEmpty();
|
||||||
|
assertThat(root.getChildren().get("scm-manager").getChildren()).containsOnlyKeys("d.txt");
|
||||||
|
|
||||||
|
assertThat(root.getChildren().get("scm-manager").getChildren().get("d.txt").getNodeName()).isEqualTo("d.txt");
|
||||||
|
assertThat(root.getChildren().get("scm-manager").getChildren().get("d.txt").getChangeType()).hasValue(DiffFile.ChangeType.RENAME);
|
||||||
|
assertThat(root.getChildren().get("scm-manager").getChildren().get("d.txt").getChildren()).isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,6 +57,8 @@ public class GitDiffResult implements DiffResult {
|
|||||||
private final int offset;
|
private final int offset;
|
||||||
private final Integer limit;
|
private final Integer limit;
|
||||||
|
|
||||||
|
private DiffTreeNode tree;
|
||||||
|
|
||||||
public GitDiffResult(Repository scmRepository,
|
public GitDiffResult(Repository scmRepository,
|
||||||
org.eclipse.jgit.lib.Repository repository,
|
org.eclipse.jgit.lib.Repository repository,
|
||||||
Differ.Diff diff,
|
Differ.Diff diff,
|
||||||
@@ -210,4 +212,32 @@ public class GitDiffResult implements DiffResult {
|
|||||||
DiffStatistics stats = new DiffStatistics(addCounter, modifiedCounter, deletedCounter);
|
DiffStatistics stats = new DiffStatistics(addCounter, modifiedCounter, deletedCounter);
|
||||||
return Optional.of(stats);
|
return Optional.of(stats);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<DiffTreeNode> getDiffTree() {
|
||||||
|
if (this.tree == null) {
|
||||||
|
tree = DiffTreeNode.createRootNode();
|
||||||
|
|
||||||
|
for (DiffEntry diffEntry : diffEntries) {
|
||||||
|
DiffEntry.Side side = DiffEntry.Side.NEW;
|
||||||
|
if (diffEntry.getChangeType() == DiffEntry.ChangeType.DELETE) {
|
||||||
|
side = DiffEntry.Side.OLD;
|
||||||
|
}
|
||||||
|
DiffEntry.ChangeType type = diffEntry.getChangeType();
|
||||||
|
String path = diffEntry.getPath(side);
|
||||||
|
tree.addChild(path, mapChangeType(type));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.of(tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DiffFile.ChangeType mapChangeType(DiffEntry.ChangeType changeType) {
|
||||||
|
return switch (changeType) {
|
||||||
|
case ADD -> DiffFile.ChangeType.ADD;
|
||||||
|
case MODIFY -> DiffFile.ChangeType.MODIFY;
|
||||||
|
case DELETE -> DiffFile.ChangeType.DELETE;
|
||||||
|
case COPY -> DiffFile.ChangeType.COPY;
|
||||||
|
case RENAME -> DiffFile.ChangeType.RENAME;
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,8 +33,10 @@ import sonia.scm.repository.api.IgnoreWhitespaceLevel;
|
|||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.InstanceOfAssertFactories.OPTIONAL;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
public class GitDiffResultCommandTest extends AbstractGitCommandTestBase {
|
public class GitDiffResultCommandTest extends AbstractGitCommandTestBase {
|
||||||
@@ -189,6 +191,15 @@ public class GitDiffResultCommandTest extends AbstractGitCommandTestBase {
|
|||||||
assertThat(diffResult.getStatistics()).get().extracting("added").isEqualTo(0);
|
assertThat(diffResult.getStatistics()).get().extracting("added").isEqualTo(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCreateFileTree() throws IOException {
|
||||||
|
DiffResult.DiffTreeNode root = DiffResult.DiffTreeNode.createRootNode();
|
||||||
|
root.addChild("a.txt", DiffFile.ChangeType.MODIFY);
|
||||||
|
root.addChild("b.txt", DiffFile.ChangeType.DELETE);
|
||||||
|
DiffResult diffResult = createDiffResult("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
|
||||||
|
assertEquals(Optional.of(root),diffResult.getDiffTree());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldNotIgnoreWhiteSpace() throws IOException {
|
public void shouldNotIgnoreWhiteSpace() throws IOException {
|
||||||
GitDiffResultCommand gitDiffResultCommand = new GitDiffResultCommand(createContext());
|
GitDiffResultCommand gitDiffResultCommand = new GitDiffResultCommand(createContext());
|
||||||
|
|||||||
@@ -144,4 +144,7 @@ export const getPageFromMatch = urls.getPageFromMatch;
|
|||||||
|
|
||||||
export { default as useGeneratedId } from "./useGeneratedId";
|
export { default as useGeneratedId } from "./useGeneratedId";
|
||||||
|
|
||||||
export { getAnchorId as getDiffAnchorId } from "./repos/diff/helpers";
|
export { default as DiffFileTree } from "./repos/diff/DiffFileTree";
|
||||||
|
export { FileTreeContent } from "./repos/diff/styledElements";
|
||||||
|
export { getFileNameFromHash } from "./repos/diffs";
|
||||||
|
export { getAnchorId as getDiffAnchorId } from "./repos/diff/helpers";
|
||||||
|
|||||||
@@ -21,20 +21,24 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import React, { FC, useState } from "react";
|
import React, { FC, useEffect, useState } from "react";
|
||||||
import DiffFile from "./DiffFile";
|
import DiffFile from "./DiffFile";
|
||||||
import { DiffObjectProps, FileControlFactory } from "./DiffTypes";
|
import { DiffObjectProps, FileControlFactory } from "./DiffTypes";
|
||||||
import { FileDiff } from "@scm-manager/ui-types";
|
import { FileDiff } from "@scm-manager/ui-types";
|
||||||
import { escapeWhitespace } from "./diffs";
|
import { getAnchorSelector, getFileNameFromHash } from "./diffs";
|
||||||
import Notification from "../Notification";
|
import Notification from "../Notification";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import useScrollToElement from "../useScrollToElement";
|
import useScrollToElement from "../useScrollToElement";
|
||||||
|
import { getAnchorId } from "./diff/helpers";
|
||||||
|
|
||||||
type Props = DiffObjectProps & {
|
type Props = DiffObjectProps & {
|
||||||
diff: FileDiff[];
|
diff: FileDiff[];
|
||||||
fileControlFactory?: FileControlFactory;
|
fileControlFactory?: FileControlFactory;
|
||||||
ignoreWhitespace?: string;
|
ignoreWhitespace?: string;
|
||||||
|
fetchNextPage?: () => void;
|
||||||
|
isFetchingNextPage?: boolean;
|
||||||
|
isDataPartial?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createKey = (file: FileDiff, ignoreWhitespace?: string) => {
|
const createKey = (file: FileDiff, ignoreWhitespace?: string) => {
|
||||||
@@ -43,23 +47,59 @@ const createKey = (file: FileDiff, ignoreWhitespace?: string) => {
|
|||||||
return `${file.oldPath}@${file.oldRevision}/${file.newPath}@${file.newRevision}?${ignoreWhitespace}`;
|
return `${file.oldPath}@${file.oldRevision}/${file.newPath}@${file.newRevision}?${ignoreWhitespace}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAnchorSelector = (uriHashContent: string) => {
|
const getFile = (files: FileDiff[] | undefined, path: string): FileDiff | undefined => {
|
||||||
return "#" + escapeWhitespace(decodeURIComponent(uriHashContent));
|
return files?.find((e) => (e.type !== "delete" && e.newPath === path) || (e.type === "delete" && e.oldPath === path));
|
||||||
};
|
};
|
||||||
|
|
||||||
const Diff: FC<Props> = ({ diff, ignoreWhitespace, ...fileProps }) => {
|
const selectFromHash = (hash: string) => {
|
||||||
|
const fileName = getFileNameFromHash(hash);
|
||||||
|
return fileName ? getAnchorSelector(fileName) : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const jumpToBottom = () => {
|
||||||
|
window.scrollTo(0, document.body.scrollHeight);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Diff: FC<Props> = ({
|
||||||
|
diff,
|
||||||
|
ignoreWhitespace,
|
||||||
|
fetchNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
isDataPartial,
|
||||||
|
...fileProps
|
||||||
|
}) => {
|
||||||
const [t] = useTranslation("repos");
|
const [t] = useTranslation("repos");
|
||||||
const [contentRef, setContentRef] = useState<HTMLElement | null>();
|
const [contentRef, setContentRef] = useState<HTMLElement | null>();
|
||||||
const { hash } = useLocation();
|
const { hash } = useLocation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isFetchingNextPage) {
|
||||||
|
jumpToBottom();
|
||||||
|
}
|
||||||
|
}, [isFetchingNextPage]);
|
||||||
|
|
||||||
useScrollToElement(
|
useScrollToElement(
|
||||||
contentRef,
|
contentRef,
|
||||||
() => {
|
() => {
|
||||||
const match = hash.match(/^#diff-(.*)$/);
|
if (isFetchingNextPage === undefined || isDataPartial === undefined || fetchNextPage === undefined) {
|
||||||
if (match) {
|
return selectFromHash(hash);
|
||||||
return getAnchorSelector(match[1]);
|
}
|
||||||
|
|
||||||
|
const encodedFileName = getFileNameFromHash(hash);
|
||||||
|
if (!encodedFileName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedFile = getFile(diff, decodeURIComponent(encodedFileName));
|
||||||
|
if (selectedFile) {
|
||||||
|
return getAnchorSelector(getAnchorId(selectedFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDataPartial && !isFetchingNextPage) {
|
||||||
|
fetchNextPage();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hash
|
[hash]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import React, { FC, useState } from "react";
|
import React, { FC, useCallback, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { NotFoundError, useDiff } from "@scm-manager/ui-api";
|
import { NotFoundError, useDiff } from "@scm-manager/ui-api";
|
||||||
import ErrorNotification from "../ErrorNotification";
|
import ErrorNotification from "../ErrorNotification";
|
||||||
@@ -32,6 +32,11 @@ import Diff from "./Diff";
|
|||||||
import { DiffObjectProps } from "./DiffTypes";
|
import { DiffObjectProps } from "./DiffTypes";
|
||||||
import DiffStatistics from "./DiffStatistics";
|
import DiffStatistics from "./DiffStatistics";
|
||||||
import { DiffDropDown } from "../index";
|
import { DiffDropDown } from "../index";
|
||||||
|
import DiffFileTree from "./diff/DiffFileTree";
|
||||||
|
import { FileTree } from "@scm-manager/ui-types";
|
||||||
|
import { DiffContent, FileTreeContent } from "./diff/styledElements";
|
||||||
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
|
import { getFileNameFromHash } from "./diffs";
|
||||||
|
|
||||||
type Props = DiffObjectProps & {
|
type Props = DiffObjectProps & {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -60,9 +65,17 @@ const PartialNotification: FC<NotificationProps> = ({ fetchNextPage, isFetchingN
|
|||||||
const LoadingDiff: FC<Props> = ({ url, limit, refetchOnWindowFocus, ...props }) => {
|
const LoadingDiff: FC<Props> = ({ url, limit, refetchOnWindowFocus, ...props }) => {
|
||||||
const [ignoreWhitespace, setIgnoreWhitespace] = useState(false);
|
const [ignoreWhitespace, setIgnoreWhitespace] = useState(false);
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
const location = useLocation();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const fetchNextPageAndResetAnchor = () => {
|
||||||
|
history.push("#");
|
||||||
|
fetchNextPage();
|
||||||
|
};
|
||||||
|
|
||||||
const evaluateWhiteSpace = () => {
|
const evaluateWhiteSpace = () => {
|
||||||
return ignoreWhitespace ? "ALL" : "NONE"
|
return ignoreWhitespace ? "ALL" : "NONE";
|
||||||
}
|
};
|
||||||
const { error, isLoading, data, fetchNextPage, isFetchingNextPage } = useDiff(url, {
|
const { error, isLoading, data, fetchNextPage, isFetchingNextPage } = useDiff(url, {
|
||||||
limit,
|
limit,
|
||||||
refetchOnWindowFocus,
|
refetchOnWindowFocus,
|
||||||
@@ -70,6 +83,26 @@ const LoadingDiff: FC<Props> = ({ url, limit, refetchOnWindowFocus, ...props })
|
|||||||
});
|
});
|
||||||
const [t] = useTranslation("repos");
|
const [t] = useTranslation("repos");
|
||||||
|
|
||||||
|
const getFirstFile = useCallback((tree: FileTree): string => {
|
||||||
|
if (Object.keys(tree.children).length === 0) {
|
||||||
|
return tree.nodeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in tree.children) {
|
||||||
|
let path;
|
||||||
|
if (tree.nodeName !== "") {
|
||||||
|
path = tree.nodeName + "/";
|
||||||
|
} else {
|
||||||
|
path = tree.nodeName;
|
||||||
|
}
|
||||||
|
const result = path + getFirstFile(tree.children[key]);
|
||||||
|
if (result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}, []);
|
||||||
|
|
||||||
const ignoreWhitespaces = () => {
|
const ignoreWhitespaces = () => {
|
||||||
setIgnoreWhitespace(!ignoreWhitespace);
|
setIgnoreWhitespace(!ignoreWhitespace);
|
||||||
};
|
};
|
||||||
@@ -78,6 +111,10 @@ const LoadingDiff: FC<Props> = ({ url, limit, refetchOnWindowFocus, ...props })
|
|||||||
setCollapsed(!collapsed);
|
setCollapsed(!collapsed);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setFilePath = (path: string) => {
|
||||||
|
history.push(`#diff-${encodeURIComponent(path)}`);
|
||||||
|
};
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
if (error instanceof NotFoundError) {
|
if (error instanceof NotFoundError) {
|
||||||
return <Notification type="info">{t("changesets.noChangesets")}</Notification>;
|
return <Notification type="info">{t("changesets.noChangesets")}</Notification>;
|
||||||
@@ -89,7 +126,17 @@ const LoadingDiff: FC<Props> = ({ url, limit, refetchOnWindowFocus, ...props })
|
|||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="is-flex has-gap-4 mb-4 mt-4 is-justify-content-space-between">
|
||||||
|
<FileTreeContent className={"is-three-quarters"}>
|
||||||
|
{data?.tree && (
|
||||||
|
<DiffFileTree
|
||||||
|
tree={data.tree}
|
||||||
|
currentFile={decodeURIComponent(getFileNameFromHash(location.hash) ?? "")}
|
||||||
|
setCurrentFile={setFilePath}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FileTreeContent>
|
||||||
|
<DiffContent>
|
||||||
<div className="is-flex has-gap-4 mb-4 mt-4 is-justify-content-space-between">
|
<div className="is-flex has-gap-4 mb-4 mt-4 is-justify-content-space-between">
|
||||||
<DiffStatistics data={data.statistics} />
|
<DiffStatistics data={data.statistics} />
|
||||||
<DiffDropDown collapseDiffs={collapseDiffs} ignoreWhitespaces={ignoreWhitespaces} renderOnMount={true} />
|
<DiffDropDown collapseDiffs={collapseDiffs} ignoreWhitespaces={ignoreWhitespaces} renderOnMount={true} />
|
||||||
@@ -98,12 +145,16 @@ const LoadingDiff: FC<Props> = ({ url, limit, refetchOnWindowFocus, ...props })
|
|||||||
defaultCollapse={collapsed}
|
defaultCollapse={collapsed}
|
||||||
diff={data.files}
|
diff={data.files}
|
||||||
ignoreWhitespace={evaluateWhiteSpace()}
|
ignoreWhitespace={evaluateWhiteSpace()}
|
||||||
|
fetchNextPage={fetchNextPage}
|
||||||
|
isFetchingNextPage={isFetchingNextPage}
|
||||||
|
isDataPartial={data.partial}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
{data.partial ? (
|
{data.partial ? (
|
||||||
<PartialNotification fetchNextPage={fetchNextPage} isFetchingNextPage={isFetchingNextPage} />
|
<PartialNotification fetchNextPage={fetchNextPageAndResetAnchor} isFetchingNextPage={isFetchingNextPage} />
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</DiffContent>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
131
scm-ui/ui-components/src/repos/diff/DiffFileTree.tsx
Normal file
131
scm-ui/ui-components/src/repos/diff/DiffFileTree.tsx
Normal 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 { FileTree } from "@scm-manager/ui-types";
|
||||||
|
import React, { FC } from "react";
|
||||||
|
import { FileDiffContainer, FileDiffContent, FileDiffContentTitle } from "./styledElements";
|
||||||
|
import { Icon } from "@scm-manager/ui-core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
type Props = { tree: FileTree; currentFile: string; setCurrentFile: (path: string) => void };
|
||||||
|
|
||||||
|
const StyledIcon = styled(Icon)`
|
||||||
|
min-width: 1.5rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DiffFileTree: FC<Props> = ({ tree, currentFile, setCurrentFile }) => {
|
||||||
|
const [t] = useTranslation("repos");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FileDiffContainer className={"mt-4 py-3 pr-2"}>
|
||||||
|
<FileDiffContentTitle className={"ml-4 pb-4 title is-6"}>{t("changesets.diffTreeTitle")}</FileDiffContentTitle>
|
||||||
|
<FileDiffContent>
|
||||||
|
{Object.keys(tree.children).map((key) => (
|
||||||
|
<TreeNode
|
||||||
|
key={key}
|
||||||
|
node={tree.children[key]}
|
||||||
|
parentPath={""}
|
||||||
|
currentFile={currentFile}
|
||||||
|
setCurrentFile={setCurrentFile}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</FileDiffContent>
|
||||||
|
</FileDiffContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DiffFileTree;
|
||||||
|
|
||||||
|
type NodeProps = { node: FileTree; parentPath: string; currentFile: string; setCurrentFile: (path: string) => void };
|
||||||
|
|
||||||
|
const addPath = (parentPath: string, path: string) => {
|
||||||
|
if ("" === parentPath) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
return parentPath + "/" + path;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TreeNode: FC<NodeProps> = ({ node, parentPath, currentFile, setCurrentFile }) => {
|
||||||
|
const [t] = useTranslation("repos");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
{Object.keys(node.children).length > 0 ? (
|
||||||
|
<ul className={"py-1 pr-1 pl-3"}>
|
||||||
|
<li className={"is-flex has-text-grey"}>
|
||||||
|
<StyledIcon alt={t("diff.showContent")}>folder</StyledIcon>
|
||||||
|
<div className={"ml-1"}>{node.nodeName}</div>
|
||||||
|
</li>
|
||||||
|
{Object.keys(node.children).map((key) => (
|
||||||
|
<TreeNode
|
||||||
|
key={key}
|
||||||
|
node={node.children[key]}
|
||||||
|
parentPath={addPath(parentPath, node.nodeName)}
|
||||||
|
currentFile={currentFile}
|
||||||
|
setCurrentFile={setCurrentFile}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<TreeFile
|
||||||
|
path={node.nodeName}
|
||||||
|
parentPath={parentPath}
|
||||||
|
currentFile={currentFile}
|
||||||
|
setCurrentFile={setCurrentFile}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type FileProps = { path: string; parentPath: string; currentFile: string; setCurrentFile: (path: string) => void };
|
||||||
|
|
||||||
|
export const TreeFileContent = styled.li`
|
||||||
|
cursor: pointer;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TreeFile: FC<FileProps> = ({ path, parentPath, currentFile, setCurrentFile }) => {
|
||||||
|
const [t] = useTranslation("repos");
|
||||||
|
const completePath = addPath(parentPath, path);
|
||||||
|
|
||||||
|
const isCurrentFile = () => {
|
||||||
|
return currentFile === completePath;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TreeFileContent className={"is-flex py-1 pl-3"} onClick={() => setCurrentFile(completePath)}>
|
||||||
|
{isCurrentFile() ? (
|
||||||
|
<StyledIcon style={{ minWidth: "1.5rem" }} key={completePath + "file"} alt={t("diff.showContent")}>
|
||||||
|
file
|
||||||
|
</StyledIcon>
|
||||||
|
) : (
|
||||||
|
<StyledIcon style={{ minWidth: "1.5rem" }} key={completePath + "file"} type="far" alt={t("diff.showContent")}>
|
||||||
|
file
|
||||||
|
</StyledIcon>
|
||||||
|
)}
|
||||||
|
<div className={"ml-1"}>{path}</div>
|
||||||
|
</TreeFileContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -90,3 +90,32 @@ export const PanelHeading = styled.div<{ sticky?: boolean | number }>`
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const FileTreeContent = styled.div`
|
||||||
|
min-width: 25%;
|
||||||
|
max-width: 25%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const DiffContent = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const FileDiffContainer = styled.div`
|
||||||
|
border: 1px solid var(--scm-border-color);
|
||||||
|
border-radius: 1rem;
|
||||||
|
position: sticky;
|
||||||
|
top: 5rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const FileDiffContentTitle = styled.div`
|
||||||
|
border-bottom: 1px solid var(--scm-border-color);
|
||||||
|
box-shadow: 0 24px 3px -24px var(--scm-border-color);
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const FileDiffContent = styled.ul`
|
||||||
|
overflow: auto;
|
||||||
|
@supports (-moz-appearance: none) {
|
||||||
|
max-height: calc(100vh - 11rem);
|
||||||
|
}
|
||||||
|
max-height: calc(100svh - 11rem);
|
||||||
|
`;
|
||||||
|
|||||||
@@ -44,3 +44,13 @@ export function createHunkIdentifierFromContext(ctx: BaseContext) {
|
|||||||
export function escapeWhitespace(path: string) {
|
export function escapeWhitespace(path: string) {
|
||||||
return path?.toLowerCase().replace(/\W/g, "-");
|
return path?.toLowerCase().replace(/\W/g, "-");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAnchorSelector(uriHashContent: string) {
|
||||||
|
return "#" + escapeWhitespace(decodeURIComponent(uriHashContent));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFileNameFromHash(hash: string) {
|
||||||
|
const matcher = new RegExp(/^#diff-(.*)$/, "g");
|
||||||
|
const match = matcher.exec(hash);
|
||||||
|
return match ? match[1] : undefined;
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import classNames from "classnames";
|
|||||||
|
|
||||||
type Props = React.HTMLProps<HTMLElement> & {
|
type Props = React.HTMLProps<HTMLElement> & {
|
||||||
children?: string;
|
children?: string;
|
||||||
|
type?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,11 +42,11 @@ type Props = React.HTMLProps<HTMLElement> & {
|
|||||||
* @see https://bulma.io/documentation/elements/icon/
|
* @see https://bulma.io/documentation/elements/icon/
|
||||||
* @see https://fontawesome.com/search?o=r&m=free
|
* @see https://fontawesome.com/search?o=r&m=free
|
||||||
*/
|
*/
|
||||||
const Icon = React.forwardRef<HTMLElement, Props>(({ children, className, ...props }, ref) => {
|
const Icon = React.forwardRef<HTMLElement, Props>(({ children, className, type = "fas", ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<span className={classNames(className, "icon")} aria-hidden="true" {...props} ref={ref}>
|
<span className={classNames(className, "icon")} aria-hidden="true" {...props} ref={ref}>
|
||||||
<i
|
<i
|
||||||
className={classNames(`fas fa-fw fa-${children}`, {
|
className={classNames(`${type} fa-fw fa-${children}`, {
|
||||||
"fa-xs": className?.includes("is-small"),
|
"fa-xs": className?.includes("is-small"),
|
||||||
"fa-lg": className?.includes("is-medium"),
|
"fa-lg": className?.includes("is-medium"),
|
||||||
"fa-2x": className?.includes("is-large"),
|
"fa-2x": className?.includes("is-large"),
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export type Diff = HalRepresentation & {
|
|||||||
files: FileDiff[];
|
files: FileDiff[];
|
||||||
partial: boolean;
|
partial: boolean;
|
||||||
statistics?: Statistics;
|
statistics?: Statistics;
|
||||||
|
tree?: FileTree;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FileDiff = {
|
export type FileDiff = {
|
||||||
@@ -54,7 +55,13 @@ export type Statistics = {
|
|||||||
added: number;
|
added: number;
|
||||||
deleted: number;
|
deleted: number;
|
||||||
modified: number;
|
modified: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export type FileTree = {
|
||||||
|
nodeName: string;
|
||||||
|
children: { [key: string]: FileTree };
|
||||||
|
changeType: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type Hunk = {
|
export type Hunk = {
|
||||||
changes: Change[];
|
changes: Change[];
|
||||||
|
|||||||
@@ -271,7 +271,8 @@
|
|||||||
"activateWhitespace": "Whitespace-Änderungen einblenden",
|
"activateWhitespace": "Whitespace-Änderungen einblenden",
|
||||||
"moreDiffsAvailable": "Es sind weitere Diffs verfügbar",
|
"moreDiffsAvailable": "Es sind weitere Diffs verfügbar",
|
||||||
"loadMore": "Weitere laden",
|
"loadMore": "Weitere laden",
|
||||||
"showModifiedFiles": "<tag>{{newFiles}}</tag> hinzugefügte, <tag>{{modified}}</tag> geänderte, <tag>{{deleted}}</tag> gelöschte Dateien"
|
"showModifiedFiles": "<tag>{{newFiles}}</tag> hinzugefügte, <tag>{{modified}}</tag> geänderte, <tag>{{deleted}}</tag> gelöschte Dateien",
|
||||||
|
"diffTreeTitle": "Diff-Liste"
|
||||||
},
|
},
|
||||||
"changeset": {
|
"changeset": {
|
||||||
"label": "Changeset",
|
"label": "Changeset",
|
||||||
|
|||||||
@@ -271,7 +271,8 @@
|
|||||||
"activateWhitespace": "Show whitespaces changes",
|
"activateWhitespace": "Show whitespaces changes",
|
||||||
"moreDiffsAvailable": "There are more diffs available",
|
"moreDiffsAvailable": "There are more diffs available",
|
||||||
"loadMore": "Load more",
|
"loadMore": "Load more",
|
||||||
"showModifiedFiles": "<tag>{{newFiles}}</tag> added, <tag>{{modified}}</tag> modified, <tag>{{deleted}}</tag> deleted"
|
"showModifiedFiles": "<tag>{{newFiles}}</tag> added, <tag>{{modified}}</tag> modified, <tag>{{deleted}}</tag> deleted",
|
||||||
|
"diffTreeTitle": "Diff-List"
|
||||||
},
|
},
|
||||||
"changeset": {
|
"changeset": {
|
||||||
"label": "Changeset",
|
"label": "Changeset",
|
||||||
|
|||||||
@@ -31,10 +31,11 @@ import de.otto.edison.hal.Links;
|
|||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import sonia.scm.repository.api.DiffResult;
|
import sonia.scm.repository.api.DiffFile;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = false)
|
@EqualsAndHashCode(callSuper = false)
|
||||||
@@ -47,6 +48,7 @@ public class DiffResultDto extends HalRepresentation {
|
|||||||
private List<FileDto> files;
|
private List<FileDto> files;
|
||||||
private boolean partial;
|
private boolean partial;
|
||||||
private DiffStatisticsDto statistics;
|
private DiffStatisticsDto statistics;
|
||||||
|
private DiffTreeNodeDto tree;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = false)
|
@EqualsAndHashCode(callSuper = false)
|
||||||
@@ -81,6 +83,15 @@ public class DiffResultDto extends HalRepresentation {
|
|||||||
private int modified;
|
private int modified;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class DiffTreeNodeDto {
|
||||||
|
private String nodeName;
|
||||||
|
private Map<String, DiffTreeNodeDto> children;
|
||||||
|
private Optional<DiffFile.ChangeType> changeType;
|
||||||
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
|
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
|
||||||
public static class HunkDto {
|
public static class HunkDto {
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ import sonia.scm.repository.api.DiffResult;
|
|||||||
import sonia.scm.repository.api.Hunk;
|
import sonia.scm.repository.api.Hunk;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.OptionalInt;
|
import java.util.OptionalInt;
|
||||||
|
|
||||||
@@ -99,6 +101,19 @@ class DiffResultToDiffResultDtoMapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DiffResultDto.DiffTreeNodeDto mapDiffTreeNodeDto(DiffResult.DiffTreeNode node) {
|
||||||
|
if(node == null){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Map<String, DiffResultDto.DiffTreeNodeDto> list = new LinkedHashMap<>();
|
||||||
|
if(node.getChildren() != null) {
|
||||||
|
for(Map.Entry<String, DiffResult.DiffTreeNode> entry : node.getChildren().entrySet()) {
|
||||||
|
list.put(entry.getKey(), mapDiffTreeNodeDto(entry.getValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new DiffResultDto.DiffTreeNodeDto(node.getNodeName(), list, node.getChangeType());
|
||||||
|
}
|
||||||
|
|
||||||
private void setFiles(DiffResult result, DiffResultDto dto, Repository repository, String revision) {
|
private void setFiles(DiffResult result, DiffResultDto dto, Repository repository, String revision) {
|
||||||
List<DiffResultDto.FileDto> files = new ArrayList<>();
|
List<DiffResultDto.FileDto> files = new ArrayList<>();
|
||||||
for (DiffFile file : result) {
|
for (DiffFile file : result) {
|
||||||
@@ -106,6 +121,8 @@ class DiffResultToDiffResultDtoMapper {
|
|||||||
}
|
}
|
||||||
dto.setFiles(files);
|
dto.setFiles(files);
|
||||||
Optional<DiffResult.DiffStatistics> statistics = result.getStatistics();
|
Optional<DiffResult.DiffStatistics> statistics = result.getStatistics();
|
||||||
|
Optional<DiffResult.DiffTreeNode> diffTree = result.getDiffTree();
|
||||||
|
|
||||||
if (statistics.isPresent()) {
|
if (statistics.isPresent()) {
|
||||||
DiffResult.DiffStatistics diffStatistics = statistics.get();
|
DiffResult.DiffStatistics diffStatistics = statistics.get();
|
||||||
DiffResultDto.DiffStatisticsDto diffStatisticsDto = new DiffResultDto.DiffStatisticsDto(
|
DiffResultDto.DiffStatisticsDto diffStatisticsDto = new DiffResultDto.DiffStatisticsDto(
|
||||||
@@ -115,6 +132,7 @@ class DiffResultToDiffResultDtoMapper {
|
|||||||
);
|
);
|
||||||
dto.setStatistics(diffStatisticsDto);
|
dto.setStatistics(diffStatisticsDto);
|
||||||
}
|
}
|
||||||
|
diffTree.ifPresent(diffTreeNode -> dto.setTree(new DiffResultDto.DiffTreeNodeDto(diffTreeNode.getNodeName(), mapDiffTreeNodeDto(diffTreeNode).getChildren(), diffTreeNode.getChangeType())));
|
||||||
dto.setPartial(result.isPartial());
|
dto.setPartial(result.isPartial());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import sonia.scm.repository.api.IgnoreWhitespaceLevel;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.OptionalInt;
|
import java.util.OptionalInt;
|
||||||
|
|
||||||
@@ -181,6 +182,7 @@ class DiffResultToDiffResultDtoMapperTest {
|
|||||||
void shouldMapStatistics() {
|
void shouldMapStatistics() {
|
||||||
DiffResult result = createResult();
|
DiffResult result = createResult();
|
||||||
when(result.getStatistics()).thenReturn(of(new DiffResult.DiffStatistics(1, 2, 3)));
|
when(result.getStatistics()).thenReturn(of(new DiffResult.DiffStatistics(1, 2, 3)));
|
||||||
|
when(result.getDiffTree()).thenReturn(of(DiffResult.DiffTreeNode.createRootNode()));
|
||||||
|
|
||||||
DiffResultDto.DiffStatisticsDto dto = mapper.mapForIncoming(REPOSITORY, result, "feature/some", "master").getStatistics();
|
DiffResultDto.DiffStatisticsDto dto = mapper.mapForIncoming(REPOSITORY, result, "feature/some", "master").getStatistics();
|
||||||
|
|
||||||
@@ -189,6 +191,38 @@ class DiffResultToDiffResultDtoMapperTest {
|
|||||||
assertThat(dto.getDeleted()).isEqualTo(3);
|
assertThat(dto.getDeleted()).isEqualTo(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMapDiffTree() {
|
||||||
|
DiffResult result = createResult();
|
||||||
|
DiffResult.DiffTreeNode root = DiffResult.DiffTreeNode.createRootNode();
|
||||||
|
root.addChild("a.txt", DiffFile.ChangeType.MODIFY);
|
||||||
|
root.addChild("b.txt", DiffFile.ChangeType.DELETE);
|
||||||
|
root.addChild("victory/road/c.txt", DiffFile.ChangeType.ADD);
|
||||||
|
root.addChild("victory/road/d.txt", DiffFile.ChangeType.RENAME);
|
||||||
|
root.addChild("indigo/plateau/e.txt", DiffFile.ChangeType.COPY);
|
||||||
|
when(result.getDiffTree()).thenReturn(of(root));
|
||||||
|
|
||||||
|
DiffResultDto.DiffTreeNodeDto actualTree = mapper.mapForIncoming(REPOSITORY, result, "feature/some", "master").getTree();
|
||||||
|
|
||||||
|
DiffResultDto.DiffTreeNodeDto expectedTree = new DiffResultDto.DiffTreeNodeDto("", Map.of(
|
||||||
|
"a.txt", new DiffResultDto.DiffTreeNodeDto("a.txt", Map.of(), Optional.of(DiffFile.ChangeType.MODIFY)),
|
||||||
|
"b.txt", new DiffResultDto.DiffTreeNodeDto("b.txt", Map.of(),Optional.of(DiffFile.ChangeType.DELETE)),
|
||||||
|
"victory", new DiffResultDto.DiffTreeNodeDto("victory", Map.of(
|
||||||
|
"road", new DiffResultDto.DiffTreeNodeDto("road", Map.of(
|
||||||
|
"c.txt", new DiffResultDto.DiffTreeNodeDto("c.txt", Map.of(), Optional.of(DiffFile.ChangeType.ADD)),
|
||||||
|
"d.txt", new DiffResultDto.DiffTreeNodeDto("d.txt", Map.of(), Optional.of(DiffFile.ChangeType.RENAME))
|
||||||
|
),Optional.empty())
|
||||||
|
),Optional.empty()),
|
||||||
|
"indigo", new DiffResultDto.DiffTreeNodeDto("indigo", Map.of(
|
||||||
|
"plateau", new DiffResultDto.DiffTreeNodeDto("plateau", Map.of(
|
||||||
|
"e.txt", new DiffResultDto.DiffTreeNodeDto("e.txt", Map.of(), Optional.of(DiffFile.ChangeType.COPY))
|
||||||
|
), Optional.empty())
|
||||||
|
), Optional.empty())
|
||||||
|
), Optional.empty());
|
||||||
|
|
||||||
|
assertThat(actualTree).isEqualTo(expectedTree);
|
||||||
|
}
|
||||||
|
|
||||||
private void mockPartialResult(DiffResult result) {
|
private void mockPartialResult(DiffResult result) {
|
||||||
when(result.getLimit()).thenReturn(of(10));
|
when(result.getLimit()).thenReturn(of(10));
|
||||||
when(result.getOffset()).thenReturn(20);
|
when(result.getOffset()).thenReturn(20);
|
||||||
|
|||||||
Reference in New Issue
Block a user