mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-07 14:05:44 +01:00
Add statistics for diffs (added, deleted, modified)
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<rene.pfeuffer@cloudogu.com> Pushed-by: Tarik Gürsoy<tarik.guersoy@cloudogu.com> Co-authored-by: René Pfeuffer<rene.pfeuffer@cloudogu.com> Co-authored-by: Tarik Gürsoy<tarik.guersoy@cloudogu.com>
This commit is contained in:
4
gradle/changelog/showmodifiedfiles_component_added.yml
Normal file
4
gradle/changelog/showmodifiedfiles_component_added.yml
Normal file
@@ -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
|
||||
@@ -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<DiffFile> {
|
||||
default IgnoreWhitespaceLevel getIgnoreWhitespace() {
|
||||
return IgnoreWhitespaceLevel.NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function returns statistics if they are supported.
|
||||
* @since 3.4.0
|
||||
*/
|
||||
default Optional<DiffStatistics> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -188,4 +188,26 @@ public class GitDiffResult implements DiffResult {
|
||||
public IgnoreWhitespaceLevel getIgnoreWhitespace() {
|
||||
return ignoreWhitespaceLevel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<DiffStatistics> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
56
scm-ui/ui-components/src/repos/DiffStatistics.tsx
Normal file
56
scm-ui/ui-components/src/repos/DiffStatistics.tsx
Normal file
@@ -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<DiffStatisticsProps> = ({ data }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
return !data ? (
|
||||
<div></div>
|
||||
) : (
|
||||
<DiffStatisticsContainer>
|
||||
<Trans
|
||||
t={t}
|
||||
i18nKey="changesets.showModifiedFiles"
|
||||
values={{ newFiles: data.added, modified: data.modified, deleted: data.deleted }}
|
||||
components={{ tag: <Tag size={"normal"} rounded={true} className={"mx-1"} /> }}
|
||||
></Trans>
|
||||
</DiffStatisticsContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default DiffStatistics;
|
||||
@@ -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<NotificationProps> = ({ fetchNextPage, isFetchingNextPage }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
return (
|
||||
<Notification className="mt-5" type="info">
|
||||
<div className="columns is-centered">
|
||||
@@ -55,14 +57,27 @@ const PartialNotification: FC<NotificationProps> = ({ fetchNextPage, isFetchingN
|
||||
);
|
||||
};
|
||||
|
||||
const LoadingDiff: FC<Props> = ({ url, limit, refetchOnWindowFocus, ignoreWhitespace, ...props }) => {
|
||||
const LoadingDiff: FC<Props> = ({ 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 <Notification type="info">{t("changesets.noChangesets")}</Notification>;
|
||||
@@ -75,7 +90,16 @@ const LoadingDiff: FC<Props> = ({ url, limit, refetchOnWindowFocus, ignoreWhites
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<Diff diff={data.files} ignoreWhitespace={ignoreWhitespace} {...props} />
|
||||
<div className="is-flex has-gap-4 mb-4 mt-4 is-justify-content-space-between">
|
||||
<DiffStatistics data={data.statistics} />
|
||||
<DiffDropDown collapseDiffs={collapseDiffs} ignoreWhitespaces={ignoreWhitespaces} renderOnMount={true} />
|
||||
</div>
|
||||
<Diff
|
||||
defaultCollapse={collapsed}
|
||||
diff={data.files}
|
||||
ignoreWhitespace={evaluateWhiteSpace()}
|
||||
{...props}
|
||||
/>
|
||||
{data.partial ? (
|
||||
<PartialNotification fetchNextPage={fetchNextPage} isFetchingNextPage={isFetchingNextPage} />
|
||||
) : null}
|
||||
|
||||
@@ -30,8 +30,6 @@ import { FileControlFactory } from "../DiffTypes";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
changeset: Changeset;
|
||||
defaultCollapse?: boolean;
|
||||
ignoreWhitespace?: string;
|
||||
fileControlFactory?: FileControlFactory;
|
||||
};
|
||||
|
||||
@@ -50,7 +48,8 @@ export const createUrl = (changeset: HalRepresentation) => {
|
||||
|
||||
class ChangesetDiff extends React.Component<Props> {
|
||||
render() {
|
||||
const { changeset, fileControlFactory, defaultCollapse, ignoreWhitespace, t } = this.props;
|
||||
const { changeset, fileControlFactory, t } = this.props;
|
||||
|
||||
if (!isDiffSupported(changeset)) {
|
||||
return <Notification type="danger">{t("changeset.diffNotSupported")}</Notification>;
|
||||
} else {
|
||||
@@ -58,8 +57,6 @@ class ChangesetDiff extends React.Component<Props> {
|
||||
return (
|
||||
<LoadingDiff
|
||||
url={url}
|
||||
defaultCollapse={defaultCollapse}
|
||||
ignoreWhitespace={ignoreWhitespace}
|
||||
sideBySide={false}
|
||||
fileControlFactory={fileControlFactory}
|
||||
stickyHeader={true}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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": "<tag>{{newFiles}}</tag> hinzugefügte, <tag>{{modified}}</tag> geänderte, <tag>{{deleted}}</tag> gelöschte Dateien"
|
||||
},
|
||||
"changeset": {
|
||||
"label": "Changeset",
|
||||
|
||||
@@ -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": "<tag>{{newFiles}}</tag> added, <tag>{{modified}}</tag> modified, <tag>{{deleted}}</tag> deleted"
|
||||
},
|
||||
"changeset": {
|
||||
"label": "Changeset",
|
||||
|
||||
@@ -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<Props> = ({ 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<Props> = ({ changeset, repository, fileControlFactory
|
||||
));
|
||||
const showCreateButton = "tag" in changeset._links;
|
||||
|
||||
const collapseDiffs = () => {
|
||||
setCollapsed(!collapsed);
|
||||
};
|
||||
|
||||
const ignoreWhitespaces = () => {
|
||||
setIgnoreWhitespace(!ignoreWhitespace);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classNames("content", "m-0")}>
|
||||
@@ -280,15 +269,9 @@ const ChangesetDetails: FC<Props> = ({ changeset, repository, fileControlFactory
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<div className="is-flex has-gap-4 mb-4 is-justify-content-flex-end">
|
||||
<DiffDropDown collapseDiffs={collapseDiffs} ignoreWhitespaces={ignoreWhitespaces} renderOnMount={false}/>
|
||||
</div>
|
||||
|
||||
<ChangesetDiff
|
||||
changeset={changeset}
|
||||
fileControlFactory={fileControlFactory}
|
||||
defaultCollapse={collapsed}
|
||||
ignoreWhitespace={ignoreWhitespace ? "ALL" : "NONE"}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -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<FileDto> 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 {
|
||||
|
||||
@@ -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<DiffResult.DiffStatistics> 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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user