From 797bda4bbb7642d76c79ee42b74f18aef835a1b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tarik=20G=C3=BCrsoy?= Date: Thu, 1 Aug 2024 16:54:40 +0200 Subject: [PATCH] Add statistics for diffs (added, deleted, modified) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend the diff result from the diff command to include modified, added and deleted file count and add DiffStats component to ui-components. Pushed-by: Rene Pfeuffer Pushed-by: Tarik Gürsoy Co-authored-by: René Pfeuffer Co-authored-by: Tarik Gürsoy --- .../showmodifiedfiles_component_added.yml | 4 ++ .../sonia/scm/repository/api/DiffResult.java | 27 +++++++++ .../scm/repository/spi/GitDiffResult.java | 22 ++++++++ .../spi/GitDiffResultCommandTest.java | 8 +++ .../src/repos/DiffStatistics.tsx | 56 +++++++++++++++++++ .../ui-components/src/repos/LoadingDiff.tsx | 34 +++++++++-- .../src/repos/changesets/ChangesetDiff.tsx | 19 +++---- scm-ui/ui-components/src/repos/index.ts | 1 + scm-ui/ui-types/src/Diff.ts | 7 +++ scm-ui/ui-webapp/public/locales/de/repos.json | 3 +- scm-ui/ui-webapp/public/locales/en/repos.json | 3 +- .../changesets/ChangesetDetails.tsx | 17 ------ .../scm/api/v2/resources/DiffResultDto.java | 12 ++++ .../DiffResultToDiffResultDtoMapper.java | 13 ++++- .../DiffResultToDiffResultDtoMapperTest.java | 12 ++++ 15 files changed, 200 insertions(+), 38 deletions(-) create mode 100644 gradle/changelog/showmodifiedfiles_component_added.yml create mode 100644 scm-ui/ui-components/src/repos/DiffStatistics.tsx diff --git a/gradle/changelog/showmodifiedfiles_component_added.yml b/gradle/changelog/showmodifiedfiles_component_added.yml new file mode 100644 index 0000000000..a99bcad992 --- /dev/null +++ b/gradle/changelog/showmodifiedfiles_component_added.yml @@ -0,0 +1,4 @@ +- type: added + description: Show number of modified, deleted and added files in diffs +- type: fixed + description: Show diffs in compare view diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffResult.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffResult.java index c91b4616b4..a140e7da41 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/DiffResult.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffResult.java @@ -24,6 +24,8 @@ package sonia.scm.repository.api; +import lombok.Value; + import java.util.Optional; import static java.util.Optional.empty; @@ -49,4 +51,29 @@ public interface DiffResult extends Iterable { default IgnoreWhitespaceLevel getIgnoreWhitespace() { return IgnoreWhitespaceLevel.NONE; } + + /** + * This function returns statistics if they are supported. + * @since 3.4.0 + */ + default Optional getStatistics() { + return empty(); + } + + @Value + class DiffStatistics { + /** + * number of added files in a diff + */ + int added; + /** + * number of modified files in a diff + */ + int modified; + /** + * number of deleted files in a diff + */ + int deleted; + } + } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResult.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResult.java index 5ab23585de..d37c472c2f 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResult.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResult.java @@ -188,4 +188,26 @@ public class GitDiffResult implements DiffResult { public IgnoreWhitespaceLevel getIgnoreWhitespace() { return ignoreWhitespaceLevel; } + + @Override + public Optional getStatistics() { + int addCounter = 0; + int modifiedCounter = 0; + int deletedCounter = 0; + for (DiffEntry diffEntry : diffEntries) { + switch (diffEntry.getChangeType()) { + case ADD: + ++addCounter; + break; + case MODIFY: + ++modifiedCounter; + break; + case DELETE: + ++deletedCounter; + break; + } + } + DiffStatistics stats = new DiffStatistics(addCounter, modifiedCounter, deletedCounter); + return Optional.of(stats); + } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffResultCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffResultCommandTest.java index 625977a40a..531c84e7cb 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffResultCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffResultCommandTest.java @@ -181,6 +181,14 @@ public class GitDiffResultCommandTest extends AbstractGitCommandTestBase { assertThat(hunks).isExhausted(); } + @Test + public void shouldComputeStatistics() throws IOException { + DiffResult diffResult = createDiffResult("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); + assertThat(diffResult.getStatistics()).get().extracting("deleted").isEqualTo(1); + assertThat(diffResult.getStatistics()).get().extracting("modified").isEqualTo(1); + assertThat(diffResult.getStatistics()).get().extracting("added").isEqualTo(0); + } + @Test public void shouldNotIgnoreWhiteSpace() throws IOException { GitDiffResultCommand gitDiffResultCommand = new GitDiffResultCommand(createContext()); diff --git a/scm-ui/ui-components/src/repos/DiffStatistics.tsx b/scm-ui/ui-components/src/repos/DiffStatistics.tsx new file mode 100644 index 0000000000..4c8976398d --- /dev/null +++ b/scm-ui/ui-components/src/repos/DiffStatistics.tsx @@ -0,0 +1,56 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React, { FC } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { Statistics } from "@scm-manager/ui-types"; +import { Tag } from "@scm-manager/ui-components"; +import styled from "styled-components"; + +type DiffStatisticsProps = { data: Statistics | undefined }; + +const DiffStatisticsContainer = styled.div` + float: left; + height: 40px; + display: flex; + align-items: center; +`; + +const DiffStatistics: FC = ({ data }) => { + const [t] = useTranslation("repos"); + return !data ? ( +
+ ) : ( + + }} + > + + ); +}; + +export default DiffStatistics; diff --git a/scm-ui/ui-components/src/repos/LoadingDiff.tsx b/scm-ui/ui-components/src/repos/LoadingDiff.tsx index 508058def8..150bb1bf9c 100644 --- a/scm-ui/ui-components/src/repos/LoadingDiff.tsx +++ b/scm-ui/ui-components/src/repos/LoadingDiff.tsx @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import React, { FC } from "react"; +import React, { FC, useState } from "react"; import { useTranslation } from "react-i18next"; import { NotFoundError, useDiff } from "@scm-manager/ui-api"; import ErrorNotification from "../ErrorNotification"; @@ -30,12 +30,13 @@ import Notification from "../Notification"; import Button from "../buttons/Button"; import Diff from "./Diff"; import { DiffObjectProps } from "./DiffTypes"; +import DiffStatistics from "./DiffStatistics"; +import { DiffDropDown } from "../index"; type Props = DiffObjectProps & { url: string; limit?: number; refetchOnWindowFocus?: boolean; - ignoreWhitespace?: string; }; type NotificationProps = { @@ -45,6 +46,7 @@ type NotificationProps = { const PartialNotification: FC = ({ fetchNextPage, isFetchingNextPage }) => { const [t] = useTranslation("repos"); + return (
@@ -55,14 +57,27 @@ const PartialNotification: FC = ({ fetchNextPage, isFetchingN ); }; -const LoadingDiff: FC = ({ url, limit, refetchOnWindowFocus, ignoreWhitespace, ...props }) => { +const LoadingDiff: FC = ({ url, limit, refetchOnWindowFocus, ...props }) => { + const [ignoreWhitespace, setIgnoreWhitespace] = useState(false); + const [collapsed, setCollapsed] = useState(false); + const evaluateWhiteSpace = () => { + return ignoreWhitespace ? "ALL" : "NONE" + } const { error, isLoading, data, fetchNextPage, isFetchingNextPage } = useDiff(url, { limit, refetchOnWindowFocus, - ignoreWhitespace, + ignoreWhitespace: evaluateWhiteSpace(), }); const [t] = useTranslation("repos"); + const ignoreWhitespaces = () => { + setIgnoreWhitespace(!ignoreWhitespace); + }; + + const collapseDiffs = () => { + setCollapsed(!collapsed); + }; + if (error) { if (error instanceof NotFoundError) { return {t("changesets.noChangesets")}; @@ -75,7 +90,16 @@ const LoadingDiff: FC = ({ url, limit, refetchOnWindowFocus, ignoreWhites } else { return ( <> - +
+ + +
+ {data.partial ? ( ) : null} diff --git a/scm-ui/ui-components/src/repos/changesets/ChangesetDiff.tsx b/scm-ui/ui-components/src/repos/changesets/ChangesetDiff.tsx index ac0ae620b0..1d6625fc77 100644 --- a/scm-ui/ui-components/src/repos/changesets/ChangesetDiff.tsx +++ b/scm-ui/ui-components/src/repos/changesets/ChangesetDiff.tsx @@ -30,8 +30,6 @@ import { FileControlFactory } from "../DiffTypes"; type Props = WithTranslation & { changeset: Changeset; - defaultCollapse?: boolean; - ignoreWhitespace?: string; fileControlFactory?: FileControlFactory; }; @@ -50,20 +48,19 @@ export const createUrl = (changeset: HalRepresentation) => { class ChangesetDiff extends React.Component { render() { - const { changeset, fileControlFactory, defaultCollapse, ignoreWhitespace, t } = this.props; + const { changeset, fileControlFactory, t } = this.props; + if (!isDiffSupported(changeset)) { return {t("changeset.diffNotSupported")}; } else { const url = createUrl(changeset); return ( - + ); } } diff --git a/scm-ui/ui-components/src/repos/index.ts b/scm-ui/ui-components/src/repos/index.ts index 208a844fb0..89e5868920 100644 --- a/scm-ui/ui-components/src/repos/index.ts +++ b/scm-ui/ui-components/src/repos/index.ts @@ -54,6 +54,7 @@ export { default as CommitAuthor } from "./CommitAuthor"; export { default as HealthCheckFailureDetail } from "./HealthCheckFailureDetail"; export { default as RepositoryFlags } from "./RepositoryFlags"; export { default as DiffDropDown } from "./DiffDropDown"; +export { default as DiffStatistics } from "./DiffStatistics" export { File, diff --git a/scm-ui/ui-types/src/Diff.ts b/scm-ui/ui-types/src/Diff.ts index 6ae8eded45..f7904ae8f7 100644 --- a/scm-ui/ui-types/src/Diff.ts +++ b/scm-ui/ui-types/src/Diff.ts @@ -29,6 +29,7 @@ export type FileChangeType = "add" | "modify" | "delete" | "copy" | "rename"; export type Diff = HalRepresentation & { files: FileDiff[]; partial: boolean; + statistics?: Statistics; }; export type FileDiff = { @@ -49,6 +50,12 @@ export type FileDiff = { _links?: Links; }; +export type Statistics = { + added: number; + deleted: number; + modified: number; +} + export type Hunk = { changes: Change[]; content: string; diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index ecaf18af9a..82cd998443 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -270,7 +270,8 @@ "checkBoxHideWhitespaceChanges": "Ignoriere Diffs die nur Whitespace Änderungen enthalten", "activateWhitespace": "Whitespace-Änderungen einblenden", "moreDiffsAvailable": "Es sind weitere Diffs verfügbar", - "loadMore": "Weitere laden" + "loadMore": "Weitere laden", + "showModifiedFiles": "{{newFiles}} hinzugefügte, {{modified}} geänderte, {{deleted}} gelöschte Dateien" }, "changeset": { "label": "Changeset", diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index dac3cf9842..d3b61e6afb 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -270,7 +270,8 @@ "checkBoxHideWhitespaceChanges": "Hide Diffs which only contain whitespace changes", "activateWhitespace": "Show whitespaces changes", "moreDiffsAvailable": "There are more diffs available", - "loadMore": "Load more" + "loadMore": "Load more", + "showModifiedFiles": "{{newFiles}} added, {{modified}} modified, {{deleted}} deleted" }, "changeset": { "label": "Changeset", diff --git a/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx b/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx index 9ac125bcac..ba29d3869e 100644 --- a/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx +++ b/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx @@ -39,7 +39,6 @@ import { DateFromNow, FileControlFactory, SignatureIcon, - DiffDropDown } from "@scm-manager/ui-components"; import { Tooltip, SubSubtitle } from "@scm-manager/ui-core"; import { Button, Icon } from "@scm-manager/ui-buttons"; @@ -178,8 +177,6 @@ const ContainedInTags: FC<{ changeset: Changeset; repository: Repository }> = ({ }; const ChangesetDetails: FC = ({ changeset, repository, fileControlFactory }) => { - const [collapsed, setCollapsed] = useState(false); - const [ignoreWhitespace, setIgnoreWhitespace] = useState(false); const [isTagCreationModalVisible, setTagCreationModalVisible] = useState(false); const [t] = useTranslation("repos"); @@ -193,14 +190,6 @@ const ChangesetDetails: FC = ({ changeset, repository, fileControlFactory )); const showCreateButton = "tag" in changeset._links; - const collapseDiffs = () => { - setCollapsed(!collapsed); - }; - - const ignoreWhitespaces = () => { - setIgnoreWhitespace(!ignoreWhitespace); - }; - return ( <>
@@ -280,15 +269,9 @@ const ChangesetDetails: FC = ({ changeset, repository, fileControlFactory

-
- -
-
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffResultDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffResultDto.java index 51d05ec081..43ee163fb7 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffResultDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffResultDto.java @@ -28,8 +28,10 @@ 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.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; +import sonia.scm.repository.api.DiffResult; import java.util.List; import java.util.Map; @@ -44,6 +46,7 @@ public class DiffResultDto extends HalRepresentation { private List files; private boolean partial; + private DiffStatisticsDto statistics; @Data @EqualsAndHashCode(callSuper = false) @@ -69,6 +72,15 @@ public class DiffResultDto extends HalRepresentation { } + @Data + @EqualsAndHashCode(callSuper = false) + @AllArgsConstructor + public static class DiffStatisticsDto { + private int added; + private int deleted; + private int modified; + } + @Data @JsonInclude(JsonInclude.Include.NON_DEFAULT) public static class HunkDto { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffResultToDiffResultDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffResultToDiffResultDtoMapper.java index 5eef8b2b2c..a5b9c1c1f3 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffResultToDiffResultDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffResultToDiffResultDtoMapper.java @@ -43,9 +43,6 @@ import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Link.linkBuilder; import static de.otto.edison.hal.Links.linkingTo; -/** - * TODO conflicts - */ class DiffResultToDiffResultDtoMapper { private final ResourceLinks resourceLinks; @@ -108,6 +105,16 @@ class DiffResultToDiffResultDtoMapper { files.add(mapFile(file, result, repository, revision)); } dto.setFiles(files); + Optional statistics = result.getStatistics(); + if (statistics.isPresent()) { + DiffResult.DiffStatistics diffStatistics = statistics.get(); + DiffResultDto.DiffStatisticsDto diffStatisticsDto = new DiffResultDto.DiffStatisticsDto( + diffStatistics.getAdded(), + diffStatistics.getDeleted(), + diffStatistics.getModified() + ); + dto.setStatistics(diffStatisticsDto); + } dto.setPartial(result.isPartial()); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResultToDiffResultDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResultToDiffResultDtoMapperTest.java index a1c6a75cc5..caa8651f33 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResultToDiffResultDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResultToDiffResultDtoMapperTest.java @@ -177,6 +177,18 @@ class DiffResultToDiffResultDtoMapperTest { .isEqualTo("/scm/api/v2/repositories/space/X/incoming/feature%2Fsome/master/diff/parsed?ignoreWhitespace=ALL&offset=30&limit=10"); } + @Test + void shouldMapStatistics() { + DiffResult result = createResult(); + when(result.getStatistics()).thenReturn(of(new DiffResult.DiffStatistics(1, 2, 3))); + + DiffResultDto.DiffStatisticsDto dto = mapper.mapForIncoming(REPOSITORY, result, "feature/some", "master").getStatistics(); + + assertThat(dto.getAdded()).isEqualTo(1); + assertThat(dto.getModified()).isEqualTo(2); + assertThat(dto.getDeleted()).isEqualTo(3); + } + private void mockPartialResult(DiffResult result) { when(result.getLimit()).thenReturn(of(10)); when(result.getOffset()).thenReturn(20);