From d09b254f009b2dc8208cc5a2b4f4ccdf85a21d64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 27 May 2020 09:43:59 +0200 Subject: [PATCH 01/38] Add line limits to content endpoint --- .../scm/api/v2/resources/ContentResource.java | 31 +++++-- .../resources/LineFilteredOutputStream.java | 72 ++++++++++++++++ .../api/v2/resources/ContentResourceTest.java | 28 +++++-- .../LineFilteredOutputStreamTest.java | 82 +++++++++++++++++++ 4 files changed, 197 insertions(+), 16 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/LineFilteredOutputStream.java create mode 100644 scm-webapp/src/test/java/sonia/scm/api/v2/resources/LineFilteredOutputStreamTest.java diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java index 8cd62a4bc6..47e0de16d1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; import com.github.sdorra.spotter.ContentType; @@ -44,12 +44,14 @@ import javax.ws.rs.GET; import javax.ws.rs.HEAD; import javax.ws.rs.Path; import javax.ws.rs.PathParam; +import javax.ws.rs.QueryParam; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.StreamingOutput; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.util.Arrays; public class ContentResource { @@ -68,11 +70,12 @@ public class ContentResource { * Returns the content of a file for the given revision in the repository. The content type depends on the file * content and can be discovered calling HEAD on the same URL. If a programming languge could be * recognized, this will be given in the header Language. - * - * @param namespace the namespace of the repository + * @param namespace the namespace of the repository * @param name the name of the repository * @param revision the revision * @param path The path of the file + * @param start + * @param end */ @GET @Path("{revision}/{path: .*}") @@ -94,8 +97,14 @@ public class ContentResource { mediaType = VndMediaType.ERROR_TYPE, schema = @Schema(implementation = ErrorDto.class) )) - public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path) { - StreamingOutput stream = createStreamingOutput(namespace, name, revision, path); + public Response get( + @PathParam("namespace") String namespace, + @PathParam("name") String name, + @PathParam("revision") String revision, + @PathParam("path") String path, + @QueryParam("start") Integer start, + @QueryParam("end") Integer end) { + StreamingOutput stream = createStreamingOutput(namespace, name, revision, path, start, end); try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { Response.ResponseBuilder responseBuilder = Response.ok(stream); return createContentHeader(namespace, name, revision, path, repositoryService, responseBuilder); @@ -105,11 +114,17 @@ public class ContentResource { } } - private StreamingOutput createStreamingOutput(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path) { + private StreamingOutput createStreamingOutput(String namespace, String name, String revision, String path, Integer start, Integer end) { return os -> { + OutputStream sourceOut; + if (start != null || end != null) { + sourceOut = new LineFilteredOutputStream(os, start, end); + } else { + sourceOut = os; + } try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { - repositoryService.getCatCommand().setRevision(revision).retriveContent(os, path); - os.close(); + repositoryService.getCatCommand().setRevision(revision).retriveContent(sourceOut, path); + sourceOut.close(); } catch (NotFoundException e) { LOG.debug(e.getMessage()); throw new WebApplicationException(Status.NOT_FOUND); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/LineFilteredOutputStream.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/LineFilteredOutputStream.java new file mode 100644 index 0000000000..5de813cabd --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/LineFilteredOutputStream.java @@ -0,0 +1,72 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.api.v2.resources; + +import java.io.IOException; +import java.io.OutputStream; + +class LineFilteredOutputStream extends OutputStream { + private final OutputStream target; + private final int start; + private final Integer end; + + private boolean inLineBreak; + private int currentLine = 0; + + LineFilteredOutputStream(OutputStream target, Integer start, Integer end) { + this.target = target; + this.start = start == null ? 0 : start; + this.end = end == null ? Integer.MAX_VALUE : end; + } + + @Override + public void write(int b) throws IOException { + switch (b) { + case '\n': + case '\r': + if (!inLineBreak) { + inLineBreak = true; + ++currentLine; + } + break; + default: + if (inLineBreak && currentLine > start && currentLine <= end) { + target.write('\n'); + } + inLineBreak = false; + if (currentLine >= start && currentLine < end) { + target.write(b); + } + } + } + + @Override + public void close() throws IOException { + if (inLineBreak && currentLine >= start && currentLine < end) { + target.write('\n'); + } + target.close(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java index a1f12bba2b..c009751195 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; import com.google.common.io.Resources; @@ -89,7 +89,7 @@ public class ContentResourceTest { public void shouldReadSimpleFile() throws Exception { mockContent("file", "Hello".getBytes()); - Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "file"); + Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "file", null, null); assertEquals(200, response.getStatus()); ByteArrayOutputStream baos = readOutputStream(response); @@ -97,15 +97,27 @@ public class ContentResourceTest { assertEquals("Hello", baos.toString()); } + @Test + public void shouldLimitOutputByLines() throws Exception { + mockContent("file", "line 1\nline 2\nline 3\nline 4".getBytes()); + + Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "file", 1, 3); + assertEquals(200, response.getStatus()); + + ByteArrayOutputStream baos = readOutputStream(response); + + assertEquals("line 2\nline 3\n", baos.toString()); + } + @Test public void shouldHandleMissingFile() { - Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "doesNotExist"); + Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "doesNotExist", null, null); assertEquals(404, response.getStatus()); } @Test public void shouldHandleMissingRepository() { - Response response = contentResource.get("no", "repo", REV, "anything"); + Response response = contentResource.get("no", "repo", REV, "anything", null, null); assertEquals(404, response.getStatus()); } @@ -113,7 +125,7 @@ public class ContentResourceTest { public void shouldRecognizeTikaSourceCode() throws Exception { mockContentFromResource("SomeGoCode.go"); - Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "SomeGoCode.go"); + Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "SomeGoCode.go", null, null); assertEquals(200, response.getStatus()); assertEquals("golang", response.getHeaderString("X-Programming-Language")); @@ -124,7 +136,7 @@ public class ContentResourceTest { public void shouldRecognizeSpecialSourceCode() throws Exception { mockContentFromResource("Dockerfile"); - Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "Dockerfile"); + Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "Dockerfile", null, null); assertEquals(200, response.getStatus()); assertEquals("dockerfile", response.getHeaderString("X-Programming-Language")); @@ -135,7 +147,7 @@ public class ContentResourceTest { public void shouldHandleRandomByteFile() throws Exception { mockContentFromResource("JustBytes"); - Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "JustBytes"); + Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "JustBytes", null, null); assertEquals(200, response.getStatus()); assertFalse(response.getHeaders().containsKey("Language")); @@ -158,7 +170,7 @@ public class ContentResourceTest { public void shouldHandleEmptyFile() throws Exception { mockContent("empty", new byte[]{}); - Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "empty"); + Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "empty", null, null); assertEquals(200, response.getStatus()); assertFalse(response.getHeaders().containsKey("Language")); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/LineFilteredOutputStreamTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/LineFilteredOutputStreamTest.java new file mode 100644 index 0000000000..ecaff9c88a --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/LineFilteredOutputStreamTest.java @@ -0,0 +1,82 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.api.v2.resources; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +class LineFilteredOutputStreamTest { + + static final String INPUT_LF = "line 1\nline 2\nline 3\nline 4"; + static final String INPUT_CR_LF = "line 1\r\nline 2\r\nline 3\r\nline 4"; + static final String INPUT_CR = "line 1\rline 2\rline 3\rline 4"; + + ByteArrayOutputStream target = new ByteArrayOutputStream(); + + @ParameterizedTest + @ValueSource(strings = {INPUT_LF, INPUT_CR_LF, INPUT_CR}) + void shouldNotFilterIfStartAndEndAreNotSet(String input) throws IOException { + try (LineFilteredOutputStream filtered = new LineFilteredOutputStream(target, null, null)) { + filtered.write(input.getBytes()); + } + + assertThat(target.toString()).isEqualTo(INPUT_LF); + } + + @ParameterizedTest + @ValueSource(strings = {INPUT_LF, INPUT_CR_LF, INPUT_CR}) + void shouldNotFilterIfStartAndEndAreSetToLimits(String input) throws IOException { + try (LineFilteredOutputStream filtered = new LineFilteredOutputStream(target, 0, 4)) { + filtered.write(input.getBytes()); + } + + assertThat(target.toString()).isEqualTo(INPUT_LF); + } + + @ParameterizedTest + @ValueSource(strings = {INPUT_LF, INPUT_CR_LF, INPUT_CR}) + void shouldRemoveFirstLinesIfStartIsSetGreaterThat1(String input) throws IOException { + LineFilteredOutputStream filtered = new LineFilteredOutputStream(target, 2, null); + + filtered.write(input.getBytes()); + + assertThat(target.toString()).isEqualTo("line 3\nline 4"); + } + + @ParameterizedTest + @ValueSource(strings = {INPUT_LF, INPUT_CR_LF, INPUT_CR}) + void shouldOmitLastLinesIfEndIsSetLessThatLength(String input) throws IOException { + LineFilteredOutputStream filtered = new LineFilteredOutputStream(target, null, 2); + + filtered.write(input.getBytes()); + + assertThat(target.toString()).isEqualTo("line 1\nline 2\n"); + } +} From 4093e734eb6fc792e6cbd023e9930835f842a1bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 27 May 2020 12:52:30 +0200 Subject: [PATCH 02/38] Create links to load more lines in diffs --- .../scm/api/v2/resources/DiffResultDto.java | 8 ++++++-- .../DiffResultToDiffResultDtoMapper.java | 18 ++++++++++++------ .../DiffResultToDiffResultDtoMapperTest.java | 13 +++++++++++++ 3 files changed, 31 insertions(+), 8 deletions(-) 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 8df431632a..aa1f617945 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 @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; import com.fasterxml.jackson.annotation.JsonInclude; @@ -43,7 +43,11 @@ public class DiffResultDto extends HalRepresentation { @Data @JsonInclude(JsonInclude.Include.NON_DEFAULT) - public static class FileDto { + public static class FileDto extends HalRepresentation { + + public FileDto(Links links) { + super(links); + } private String oldPath; private String newPath; 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 5057950149..02b54b7e7d 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 @@ -27,6 +27,7 @@ package sonia.scm.api.v2.resources; import com.github.sdorra.spotter.ContentTypes; import com.github.sdorra.spotter.Language; import com.google.inject.Inject; +import de.otto.edison.hal.Links; import sonia.scm.repository.Repository; import sonia.scm.repository.api.DiffFile; import sonia.scm.repository.api.DiffLine; @@ -38,6 +39,7 @@ import java.util.List; import java.util.Optional; import java.util.OptionalInt; +import static de.otto.edison.hal.Link.linkBuilder; import static de.otto.edison.hal.Links.linkingTo; /** @@ -54,26 +56,30 @@ class DiffResultToDiffResultDtoMapper { 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); + setFiles(result, dto, repository, target); 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); + setFiles(result, dto, repository, revision); return dto; } - private void setFiles(DiffResult result, DiffResultDto dto) { + private void setFiles(DiffResult result, DiffResultDto dto, Repository repository, String revision) { List files = new ArrayList<>(); for (DiffFile file : result) { - files.add(mapFile(file)); + files.add(mapFile(file, repository, revision)); } dto.setFiles(files); } - private DiffResultDto.FileDto mapFile(DiffFile file) { - DiffResultDto.FileDto dto = new DiffResultDto.FileDto(); + private DiffResultDto.FileDto mapFile(DiffFile file, Repository repository, String revision) { + Links.Builder links = linkingTo(); + if (file.iterator().hasNext()) { + links.single(linkBuilder("lines", resourceLinks.source().content(repository.getNamespace(), repository.getName(), revision, file.getNewPath()) + "?start={start}?end={end}").build()); + } + DiffResultDto.FileDto dto = new DiffResultDto.FileDto(links.build()); // ??? dto.setOldEndingNewLine(true); dto.setNewEndingNewLine(true); 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 291b04e00c..efc1cbfff0 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 @@ -86,6 +86,19 @@ class DiffResultToDiffResultDtoMapperTest { .isEqualTo("/scm/api/v2/repositories/space/X/diff/123/parsed"); } + @Test + void shouldCreateLinkToLoadMoreLinesForFilesWithHunks() { + DiffResultDto dto = mapper.mapForRevision(REPOSITORY, createResult(), "123"); + + assertThat(dto.getFiles().get(0).getLinks().getLinkBy("lines")) + .isNotPresent(); + assertThat(dto.getFiles().get(1).getLinks().getLinkBy("lines")) + .isPresent() + .get() + .extracting("href") + .isEqualTo("/scm/api/v2/repositories/space/X/content/123/B.ts?start={start}?end={end}"); + } + @Test void shouldCreateSelfLinkForIncoming() { DiffResultDto dto = mapper.mapForIncoming(REPOSITORY, createResult(), "feature/some", "master"); From 99b7b92fbe4ef86b8d787efaab456af0d7751233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 27 May 2020 17:23:00 +0200 Subject: [PATCH 03/38] Introduce expandable diffs --- .../src/repos/DiffExpander.test.ts | 312 ++++++++++++++++++ .../ui-components/src/repos/DiffExpander.ts | 61 ++++ scm-ui/ui-components/src/repos/DiffFile.tsx | 62 ++-- scm-ui/ui-components/src/repos/DiffTypes.ts | 6 + 4 files changed, 420 insertions(+), 21 deletions(-) create mode 100644 scm-ui/ui-components/src/repos/DiffExpander.test.ts create mode 100644 scm-ui/ui-components/src/repos/DiffExpander.ts diff --git a/scm-ui/ui-components/src/repos/DiffExpander.test.ts b/scm-ui/ui-components/src/repos/DiffExpander.test.ts new file mode 100644 index 0000000000..c4f640cf20 --- /dev/null +++ b/scm-ui/ui-components/src/repos/DiffExpander.test.ts @@ -0,0 +1,312 @@ +/* + * 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 DiffExpander from "./DiffExpander"; + +const HUNK_0 = { + content: "@@ -1,8 +1,8 @@", + oldStart: 1, + newStart: 1, + oldLines: 8, + newLines: 8, + changes: [ + { + content: "// @flow", + type: "normal", + oldLineNumber: 1, + newLineNumber: 1, + isNormal: true + }, + { + content: 'import React from "react";', + type: "normal", + oldLineNumber: 2, + newLineNumber: 2, + isNormal: true + }, + { + content: 'import { translate } from "react-i18next";', + type: "delete", + lineNumber: 3, + isDelete: true + }, + { + content: 'import { Textarea } from "@scm-manager/ui-components";', + type: "delete", + lineNumber: 4, + isDelete: true + }, + { + content: 'import type { Me } from "@scm-manager/ui-types";', + type: "delete", + lineNumber: 5, + isDelete: true + }, + { + content: 'import {translate} from "react-i18next";', + type: "insert", + lineNumber: 3, + isInsert: true + }, + { + content: 'import {Textarea} from "@scm-manager/ui-components";', + type: "insert", + lineNumber: 4, + isInsert: true + }, + { + content: 'import type {Me} from "@scm-manager/ui-types";', + type: "insert", + lineNumber: 5, + isInsert: true + }, + { + content: 'import injectSheet from "react-jss";', + type: "normal", + oldLineNumber: 6, + newLineNumber: 6, + isNormal: true + }, + { + content: "", + type: "normal", + oldLineNumber: 7, + newLineNumber: 7, + isNormal: true + }, + { + content: "const styles = {", + type: "normal", + oldLineNumber: 8, + newLineNumber: 8, + isNormal: true + } + ] +}; +const HUNK_1 = { + content: "@@ -14,6 +14,7 @@", + oldStart: 14, + newStart: 14, + oldLines: 6, + newLines: 7, + changes: [ + { + content: "type Props = {", + type: "normal", + oldLineNumber: 14, + newLineNumber: 14, + isNormal: true + }, + { + content: " me: Me,", + type: "normal", + oldLineNumber: 15, + newLineNumber: 15, + isNormal: true + }, + { + content: " onChange: string => void,", + type: "normal", + oldLineNumber: 16, + newLineNumber: 16, + isNormal: true + }, + { + content: " disabled: boolean,", + type: "insert", + lineNumber: 17, + isInsert: true + }, + { + content: " //context props", + type: "normal", + oldLineNumber: 17, + newLineNumber: 18, + isNormal: true + }, + { + content: " t: string => string,", + type: "normal", + oldLineNumber: 18, + newLineNumber: 19, + isNormal: true + }, + { + content: " classes: any", + type: "normal", + oldLineNumber: 19, + newLineNumber: 20, + isNormal: true + } + ] +}; +const HUNK_2 = { + content: "@@ -21,7 +22,7 @@", + oldStart: 21, + newStart: 22, + oldLines: 7, + newLines: 7, + changes: [ + { + content: "", + type: "normal", + oldLineNumber: 21, + newLineNumber: 22, + isNormal: true + }, + { + content: "class CommitMessage extends React.Component {", + type: "normal", + oldLineNumber: 22, + newLineNumber: 23, + isNormal: true + }, + { + content: " render() {", + type: "normal", + oldLineNumber: 23, + newLineNumber: 24, + isNormal: true + }, + { + content: " const { t, classes, me, onChange } = this.props;", + type: "delete", + lineNumber: 24, + isDelete: true + }, + { + content: " const {t, classes, me, onChange, disabled} = this.props;", + type: "insert", + lineNumber: 25, + isInsert: true + }, + { + content: " return (", + type: "normal", + oldLineNumber: 25, + newLineNumber: 26, + isNormal: true + }, + { + content: " <>", + type: "normal", + oldLineNumber: 26, + newLineNumber: 27, + isNormal: true + }, + { + content: "
", + type: "normal", + oldLineNumber: 27, + newLineNumber: 28, + isNormal: true + } + ] +}; +const HUNK_3 = { + content: "@@ -33,6 +34,7 @@", + oldStart: 33, + newStart: 34, + oldLines: 6, + newLines: 7, + changes: [ + { + content: " onChange(message)}", + type: "normal", + oldLineNumber: 35, + newLineNumber: 36, + isNormal: true + }, + { + content: " disabled={disabled}", + type: "insert", + lineNumber: 37, + isInsert: true + }, + { + content: " />", + type: "normal", + oldLineNumber: 36, + newLineNumber: 38, + isNormal: true + }, + { + content: " ", + type: "normal", + oldLineNumber: 37, + newLineNumber: 39, + isNormal: true + }, + { + content: " );", + type: "normal", + oldLineNumber: 38, + newLineNumber: 40, + isNormal: true + } + ] +}; +const TEST_CONTENT = { + oldPath: "src/main/js/CommitMessage.js", + newPath: "src/main/js/CommitMessage.js", + oldEndingNewLine: true, + newEndingNewLine: true, + oldRevision: "e05c8495bb1dc7505d73af26210c8ff4825c4500", + newRevision: "4305a8df175b7bec25acbe542a13fbe2a718a608", + type: "modify", + language: "javascript", + hunks: [HUNK_0, HUNK_1, HUNK_2, HUNK_3], + _links: { + lines: { + href: + "http://localhost:8081/scm/api/v2/repositories/scm-manager/scm-editor-plugin/content/f7a23064f3f2418f26140a9545559e72d595feb5/src/main/js/CommitMessage.js?start={start}?end={end}", + templated: true + } + } +}; + +describe("diff expander", () => { + const diffExpander = new DiffExpander(TEST_CONTENT); + it("should have hunk count from origin", () => { + expect(diffExpander.hunkCount()).toBe(4); + }); + + it("should return correct hunk", () => { + expect(diffExpander.getHunk(1).hunk).toBe(HUNK_1); + }); +}); diff --git a/scm-ui/ui-components/src/repos/DiffExpander.ts b/scm-ui/ui-components/src/repos/DiffExpander.ts new file mode 100644 index 0000000000..a6a135e7b7 --- /dev/null +++ b/scm-ui/ui-components/src/repos/DiffExpander.ts @@ -0,0 +1,61 @@ +/* + * 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 { File, Hunk } from "./DiffTypes"; + +class DiffExpander { + file: File; + + constructor(file: File) { + this.file = file; + } + + hunkCount = () => { + return this.file.hunks.length; + }; + + getHunk: (n: number) => ExpandableHunk = (n: number) => { + return { + maxExpandHeadRange: 10, + maxExpandBottomRange: 10, + expandHead: () => { + console.log("expand head", n); + }, + expandBottom: () => { + console.log("expand bottom", n); + }, + hunk: this.file.hunks[n] + }; + }; +} + +export type ExpandableHunk = { + hunk: Hunk; + maxExpandHeadRange: number; + maxExpandBottomRange: number; + expandHead: () => void; + expandBottom: () => void; +}; + +export default DiffExpander; diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx index 4b042f0c4d..e2b4a36f7d 100644 --- a/scm-ui/ui-components/src/repos/DiffFile.tsx +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -34,6 +34,7 @@ import { Change, ChangeEvent, DiffObjectProps, File, Hunk as HunkType } from "./ import TokenizedDiffView from "./TokenizedDiffView"; import DiffButton from "./DiffButton"; import { MenuContext } from "@scm-manager/ui-components"; +import DiffExpander, { ExpandableHunk } from "./DiffExpander"; const EMPTY_ANNOTATION_FACTORY = {}; @@ -48,6 +49,7 @@ type Collapsible = { type State = Collapsible & { sideBySide?: boolean; + diffExpander: DiffExpander; }; const DiffFilePanel = styled.div` @@ -74,8 +76,9 @@ const ButtonWrapper = styled.div` margin-left: auto; `; -const HunkDivider = styled.hr` - margin: 0.5rem 0; +const HunkDivider = styled.div` + background: #33b2e8; + font-size: 0.7rem; `; const ChangeTypeTag = styled(Tag)` @@ -92,7 +95,8 @@ class DiffFile extends React.Component { super(props); this.state = { collapsed: this.defaultCollapse(), - sideBySide: props.sideBySide + sideBySide: props.sideBySide, + diffExpander: new DiffExpander(props.file) }; } @@ -139,9 +143,25 @@ class DiffFile extends React.Component { }); }; - createHunkHeader = (hunk: HunkType, i: number) => { - if (i > 0) { - return ; + createHunkHeader = (expandableHunk: ExpandableHunk) => { + if (expandableHunk.maxExpandHeadRange > 0) { + return ( + + {"Load first n lines"} + + ); + } + // hunk header must be defined + return ; + }; + + createHunkFooter = (expandableHunk: ExpandableHunk) => { + if (expandableHunk.maxExpandBottomRange > 0) { + return ( + + {"Load last n lines"} + + ); } // hunk header must be defined return ; @@ -183,19 +203,27 @@ class DiffFile extends React.Component { } }; - renderHunk = (hunk: HunkType, i: number) => { + renderHunk = (file: File, expandableHunk: ExpandableHunk, i: number) => { + const hunk = expandableHunk.hunk; if (this.props.markConflicts && hunk.changes) { this.markConflicts(hunk); } - return [ - {this.createHunkHeader(hunk, i)}, + const items = []; + if (file._links?.lines) { + items.push(this.createHunkHeader(expandableHunk)); + } + items.push( - ]; + ); + if (file._links?.lines) { + items.push(this.createHunkFooter(expandableHunk)); + } + return items; }; markConflicts = (hunk: HunkType) => { @@ -251,19 +279,11 @@ class DiffFile extends React.Component { return ; }; - concat = (array: object[][]) => { - if (array.length > 0) { - return array.reduce((a, b) => a.concat(b)); - } else { - return []; - } - }; - hasContent = (file: File) => file && !file.isBinary && file.hunks && file.hunks.length > 0; render() { const { file, fileControlFactory, fileAnnotationFactory, t } = this.props; - const { collapsed, sideBySide } = this.state; + const { collapsed, sideBySide, diffExpander } = this.state; const viewType = sideBySide ? "split" : "unified"; let body = null; @@ -275,7 +295,7 @@ class DiffFile extends React.Component {
{fileAnnotations} - {(hunks: HunkType[]) => this.concat(hunks.map(this.renderHunk))} + {(hunks: HunkType[]) => hunks.map((hunk, n) => this.renderHunk(file, diffExpander.getHunk(n), n))}
); diff --git a/scm-ui/ui-components/src/repos/DiffTypes.ts b/scm-ui/ui-components/src/repos/DiffTypes.ts index 7ab15e5750..aba76d422d 100644 --- a/scm-ui/ui-components/src/repos/DiffTypes.ts +++ b/scm-ui/ui-components/src/repos/DiffTypes.ts @@ -24,6 +24,7 @@ import { ReactNode } from "react"; import { DefaultCollapsed } from "./defaultCollapsed"; +import { Links } from "@scm-manager/ui-types"; // We place the types here and not in @scm-manager/ui-types, // because they represent not a real scm-manager related type. @@ -46,11 +47,16 @@ export type File = { language?: string; // TODO does this property exists? isBinary?: boolean; + _links: Links; }; export type Hunk = { changes: Change[]; content: string; + oldStart: number; + newStart: number; + oldLines: number; + newLines: number; }; export type ChangeType = "insert" | "delete" | "normal" | "conflict"; From abca9e9746436f492b62919e0da79d7613c25a55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 28 May 2020 09:38:56 +0200 Subject: [PATCH 04/38] Show number of lines that can be expanded --- .../src/repos/DiffExpander.test.ts | 16 +++++++++ .../ui-components/src/repos/DiffExpander.ts | 34 +++++++++++++++---- scm-ui/ui-components/src/repos/DiffFile.tsx | 8 +++-- 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/scm-ui/ui-components/src/repos/DiffExpander.test.ts b/scm-ui/ui-components/src/repos/DiffExpander.test.ts index c4f640cf20..3da9c69c76 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.test.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.test.ts @@ -309,4 +309,20 @@ describe("diff expander", () => { it("should return correct hunk", () => { expect(diffExpander.getHunk(1).hunk).toBe(HUNK_1); }); + + it("should return max expand head range for first hunk", () => { + expect(diffExpander.getHunk(0).maxExpandHeadRange).toBe(0); + }); + + it("should return max expand head range for hunks in the middle", () => { + expect(diffExpander.getHunk(1).maxExpandHeadRange).toBe(5); + }); + + it("should return max expand bottom range for hunks in the middle", () => { + expect(diffExpander.getHunk(1).maxExpandBottomRange).toBe(1); + }); + + it("should return a really bix number for the expand bottom range of the last hunk", () => { + expect(diffExpander.getHunk(3).maxExpandBottomRange).toBeGreaterThan(99999); + }); }); diff --git a/scm-ui/ui-components/src/repos/DiffExpander.ts b/scm-ui/ui-components/src/repos/DiffExpander.ts index a6a135e7b7..2ef1f74a80 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.ts @@ -35,15 +35,37 @@ class DiffExpander { return this.file.hunks.length; }; + minLineNumber = (n: number) => { + return this.file.hunks[n].newStart; + }; + + maxLineNumber = (n: number) => { + return this.file.hunks[n].newStart + this.file.hunks[n].newLines; + }; + + computeMaxExpandHeadRange = (n: number) => { + if (n === 0) { + return this.minLineNumber(n) - 1; + } + return this.minLineNumber(n) - this.maxLineNumber(n - 1); + }; + + computeMaxExpandBottomRange = (n: number) => { + if (n === this.file.hunks.length - 1) { + return Number.MAX_SAFE_INTEGER; + } + return this.minLineNumber(n + 1) - this.maxLineNumber(n); + }; + getHunk: (n: number) => ExpandableHunk = (n: number) => { return { - maxExpandHeadRange: 10, - maxExpandBottomRange: 10, + maxExpandHeadRange: this.computeMaxExpandHeadRange(n), + maxExpandBottomRange: this.computeMaxExpandBottomRange(n), expandHead: () => { - console.log("expand head", n); + return this; }, expandBottom: () => { - console.log("expand bottom", n); + return this; }, hunk: this.file.hunks[n] }; @@ -54,8 +76,8 @@ export type ExpandableHunk = { hunk: Hunk; maxExpandHeadRange: number; maxExpandBottomRange: number; - expandHead: () => void; - expandBottom: () => void; + expandHead: () => DiffExpander; + expandBottom: () => DiffExpander; }; export default DiffExpander; diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx index e2b4a36f7d..04bd541728 100644 --- a/scm-ui/ui-components/src/repos/DiffFile.tsx +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -147,7 +147,9 @@ class DiffFile extends React.Component { if (expandableHunk.maxExpandHeadRange > 0) { return ( - {"Load first n lines"} + this.setState({ diffExpander: expandableHunk.expandHead() })}> + {`Load ${expandableHunk.maxExpandHeadRange} more lines`} + ); } @@ -159,7 +161,9 @@ class DiffFile extends React.Component { if (expandableHunk.maxExpandBottomRange > 0) { return ( - {"Load last n lines"} + this.setState({ diffExpander: expandableHunk.expandBottom() })}> + {`Load ${expandableHunk.maxExpandBottomRange} more lines`} + ); } From 2efd21d4661c858eee2bfd2584042ccd9a05a277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 28 May 2020 19:51:51 +0200 Subject: [PATCH 05/38] Handle added and deleted files correctly --- .../src/repos/DiffExpander.test.ts | 96 ++++++++++++++++++- .../ui-components/src/repos/DiffExpander.ts | 24 +++-- scm-ui/ui-components/src/repos/DiffFile.tsx | 2 +- scm-ui/ui-components/src/repos/DiffTypes.ts | 10 +- 4 files changed, 115 insertions(+), 17 deletions(-) diff --git a/scm-ui/ui-components/src/repos/DiffExpander.test.ts b/scm-ui/ui-components/src/repos/DiffExpander.test.ts index 3da9c69c76..68ee58f112 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.test.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.test.ts @@ -281,7 +281,7 @@ const HUNK_3 = { } ] }; -const TEST_CONTENT = { +const TEST_CONTENT_WITH_HUNKS = { oldPath: "src/main/js/CommitMessage.js", newPath: "src/main/js/CommitMessage.js", oldEndingNewLine: true, @@ -300,8 +300,67 @@ const TEST_CONTENT = { } }; -describe("diff expander", () => { - const diffExpander = new DiffExpander(TEST_CONTENT); +const TEST_CONTENT_WIT_NEW_BINARY_FILE = { + oldPath: "/dev/null", + newPath: "src/main/fileUploadV2.png", + oldEndingNewLine: true, + newEndingNewLine: true, + oldRevision: "0000000000000000000000000000000000000000", + newRevision: "86c370aae0727d628a5438f79a5cdd45752b9d99", + type: "add" +}; + +const TEST_CONTENT_WITH_NEW_TEXT_FILE = { + oldPath: "/dev/null", + newPath: "src/main/markdown/README.md", + oldEndingNewLine: true, + newEndingNewLine: true, + oldRevision: "0000000000000000000000000000000000000000", + newRevision: "4e173d365d796b9a9e7562fcd0ef90398ae37046", + type: "add", + language: "markdown", + hunks: [ + { + content: "@@ -0,0 +1,2 @@", + newStart: 1, + newLines: 2, + changes: [ + { content: "line 1", type: "insert", lineNumber: 1, isInsert: true }, + { content: "line 2", type: "insert", lineNumber: 2, isInsert: true } + ] + } + ], + _links: { + lines: { + href: + "http://localhost:8081/scm/api/v2/repositories/scm-manager/scm-editor-plugin/content/c63898d35520ee47bcc3a8291660979918715762/src/main/markdown/README.md?start={start}?end={end}", + templated: true + } + } +}; + +const TEST_CONTENT_WITH_DELETED_TEXT_FILE = { + oldPath: "README.md", + newPath: "/dev/null", + oldEndingNewLine: true, + newEndingNewLine: true, + oldRevision: "4875ab3b7a1bb117e1948895148557fc5c0b6f75", + newRevision: "0000000000000000000000000000000000000000", + type: "delete", + language: "markdown", + hunks: [ + { + content: "@@ -1 +0,0 @@", + oldStart: 1, + oldLines: 1, + changes: [{ content: "# scm-editor-plugin", type: "delete", lineNumber: 1, isDelete: true }] + } + ], + _links: { lines: { href: "http://localhost:8081/dev/null?start={start}?end={end}", templated: true } } +}; + +describe("with hunks the diff expander", () => { + const diffExpander = new DiffExpander(TEST_CONTENT_WITH_HUNKS); it("should have hunk count from origin", () => { expect(diffExpander.hunkCount()).toBe(4); }); @@ -326,3 +385,34 @@ describe("diff expander", () => { expect(diffExpander.getHunk(3).maxExpandBottomRange).toBeGreaterThan(99999); }); }); + +describe("for a new file with text input the diff expander", () => { + const diffExpander = new DiffExpander(TEST_CONTENT_WITH_NEW_TEXT_FILE); + it("should create answer for single hunk", () => { + expect(diffExpander.hunkCount()).toBe(1); + }); + it("should neither give expandable lines for top nor bottom", () => { + const hunk = diffExpander.getHunk(0); + expect(hunk.maxExpandHeadRange).toBe(0); + expect(hunk.maxExpandBottomRange).toBe(0); + }); +}); + +describe("for a deleted file with text input the diff expander", () => { + const diffExpander = new DiffExpander(TEST_CONTENT_WITH_DELETED_TEXT_FILE); + it("should create answer for single hunk", () => { + expect(diffExpander.hunkCount()).toBe(1); + }); + it("should neither give expandable lines for top nor bottom", () => { + const hunk = diffExpander.getHunk(0); + expect(hunk.maxExpandHeadRange).toBe(0); + expect(hunk.maxExpandBottomRange).toBe(0); + }); +}); + +describe("for a new file with binary input the diff expander", () => { + const diffExpander = new DiffExpander(TEST_CONTENT_WIT_NEW_BINARY_FILE); + it("should create answer for no hunk", () => { + expect(diffExpander.hunkCount()).toBe(0); + }); +}); diff --git a/scm-ui/ui-components/src/repos/DiffExpander.ts b/scm-ui/ui-components/src/repos/DiffExpander.ts index 2ef1f74a80..f5fd72ca98 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.ts @@ -32,26 +32,34 @@ class DiffExpander { } hunkCount = () => { - return this.file.hunks.length; + if (this.file.hunks) { + return this.file.hunks.length; + } else { + return 0; + } }; - minLineNumber = (n: number) => { - return this.file.hunks[n].newStart; + minLineNumber: (n: number) => number = (n: number) => { + return this.file.hunks![n]!.newStart!; }; - maxLineNumber = (n: number) => { - return this.file.hunks[n].newStart + this.file.hunks[n].newLines; + maxLineNumber: (n: number) => number = (n: number) => { + return this.file.hunks![n]!.newStart! + this.file.hunks![n]!.newLines!; }; computeMaxExpandHeadRange = (n: number) => { - if (n === 0) { + if (this.file.type === "delete") { + return 0; + } else if (n === 0) { return this.minLineNumber(n) - 1; } return this.minLineNumber(n) - this.maxLineNumber(n - 1); }; computeMaxExpandBottomRange = (n: number) => { - if (n === this.file.hunks.length - 1) { + if (this.file.type === "add" || this.file.type === "delete") { + return 0; + } else if (n === this.file!.hunks!.length - 1) { return Number.MAX_SAFE_INTEGER; } return this.minLineNumber(n + 1) - this.maxLineNumber(n); @@ -67,7 +75,7 @@ class DiffExpander { expandBottom: () => { return this; }, - hunk: this.file.hunks[n] + hunk: this.file?.hunks![n] }; }; } diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx index 04bd541728..1c75191bb6 100644 --- a/scm-ui/ui-components/src/repos/DiffFile.tsx +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -299,7 +299,7 @@ class DiffFile extends React.Component {
{fileAnnotations} - {(hunks: HunkType[]) => hunks.map((hunk, n) => this.renderHunk(file, diffExpander.getHunk(n), n))} + {(hunks: HunkType[]) => hunks?.map((hunk, n) => this.renderHunk(file, diffExpander.getHunk(n), n))}
); diff --git a/scm-ui/ui-components/src/repos/DiffTypes.ts b/scm-ui/ui-components/src/repos/DiffTypes.ts index aba76d422d..56d225d85f 100644 --- a/scm-ui/ui-components/src/repos/DiffTypes.ts +++ b/scm-ui/ui-components/src/repos/DiffTypes.ts @@ -34,7 +34,7 @@ import { Links } from "@scm-manager/ui-types"; export type FileChangeType = "add" | "modify" | "delete" | "copy" | "rename"; export type File = { - hunks: Hunk[]; + hunks?: Hunk[]; newEndingNewLine: boolean; newMode?: string; newPath: string; @@ -53,10 +53,10 @@ export type File = { export type Hunk = { changes: Change[]; content: string; - oldStart: number; - newStart: number; - oldLines: number; - newLines: number; + oldStart?: number; + newStart?: number; + oldLines?: number; + newLines?: number; }; export type ChangeType = "insert" | "delete" | "normal" | "conflict"; From ebfc267b931db2a5a6998fdd4cea3bd43e4a84f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 29 May 2020 14:00:14 +0200 Subject: [PATCH 06/38] Patch hunks with new lines --- .../src/repos/DiffExpander.test.ts | 44 ++++++- .../ui-components/src/repos/DiffExpander.ts | 112 ++++++++++++++++-- scm-ui/ui-components/src/repos/DiffFile.tsx | 30 +++-- .../DiffResultToDiffResultDtoMapper.java | 2 +- 4 files changed, 165 insertions(+), 23 deletions(-) diff --git a/scm-ui/ui-components/src/repos/DiffExpander.test.ts b/scm-ui/ui-components/src/repos/DiffExpander.test.ts index 68ee58f112..36023da707 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.test.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.test.ts @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - +import fetchMock from "fetch-mock"; import DiffExpander from "./DiffExpander"; const HUNK_0 = { @@ -294,7 +294,7 @@ const TEST_CONTENT_WITH_HUNKS = { _links: { lines: { href: - "http://localhost:8081/scm/api/v2/repositories/scm-manager/scm-editor-plugin/content/f7a23064f3f2418f26140a9545559e72d595feb5/src/main/js/CommitMessage.js?start={start}?end={end}", + "http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start={start}&end={end}", templated: true } } @@ -333,7 +333,7 @@ const TEST_CONTENT_WITH_NEW_TEXT_FILE = { _links: { lines: { href: - "http://localhost:8081/scm/api/v2/repositories/scm-manager/scm-editor-plugin/content/c63898d35520ee47bcc3a8291660979918715762/src/main/markdown/README.md?start={start}?end={end}", + "http://localhost:8081/scm/api/v2/repositories/scm-manager/scm-editor-plugin/content/c63898d35520ee47bcc3a8291660979918715762/src/main/markdown/README.md?start={start}&end={end}", templated: true } } @@ -356,11 +356,17 @@ const TEST_CONTENT_WITH_DELETED_TEXT_FILE = { changes: [{ content: "# scm-editor-plugin", type: "delete", lineNumber: 1, isDelete: true }] } ], - _links: { lines: { href: "http://localhost:8081/dev/null?start={start}?end={end}", templated: true } } + _links: { lines: { href: "http://localhost:8081/dev/null?start={start}&end={end}", templated: true } } }; describe("with hunks the diff expander", () => { const diffExpander = new DiffExpander(TEST_CONTENT_WITH_HUNKS); + + afterEach(() => { + fetchMock.reset(); + fetchMock.restore(); + }); + it("should have hunk count from origin", () => { expect(diffExpander.hunkCount()).toBe(4); }); @@ -384,6 +390,36 @@ describe("with hunks the diff expander", () => { it("should return a really bix number for the expand bottom range of the last hunk", () => { expect(diffExpander.getHunk(3).maxExpandBottomRange).toBeGreaterThan(99999); }); + it("should expand hunk with new line from api client at the bottom", async () => { + expect(diffExpander.getHunk(1).hunk.changes.length).toBe(7); + fetchMock.get("http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=22&end=22", "new line 1\nnew line 2"); + let newFile; + diffExpander.getHunk(1).expandBottom(file => { + newFile = file; + }); + await fetchMock.flush(true); + expect(fetchMock.done()).toBe(true); + expect(newFile.hunks[1].changes.length).toBe(9); + expect(newFile.hunks[1].changes[7].content).toBe("new line 1"); + expect(newFile.hunks[1].changes[8].content).toBe("new line 2"); + }); + it("should expand hunk with new line from api client at the top", async () => { + expect(diffExpander.getHunk(1).hunk.changes.length).toBe(7); + fetchMock.get("http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=9&end=13", "new line 1\nnew line 2"); + let newFile; + diffExpander.getHunk(1).expandHead(file => { + newFile = file; + }); + await fetchMock.flush(true); + expect(fetchMock.done()).toBe(true); + expect(newFile.hunks[1].changes.length).toBe(9); + expect(newFile.hunks[1].changes[0].content).toBe("new line 1"); + expect(newFile.hunks[1].changes[0].oldLineNumber).toBe(12); + expect(newFile.hunks[1].changes[0].newLineNumber).toBe(12); + expect(newFile.hunks[1].changes[1].content).toBe("new line 2"); + expect(newFile.hunks[1].changes[1].oldLineNumber).toBe(13); + expect(newFile.hunks[1].changes[1].newLineNumber).toBe(13); + }); }); describe("for a new file with text input the diff expander", () => { diff --git a/scm-ui/ui-components/src/repos/DiffExpander.ts b/scm-ui/ui-components/src/repos/DiffExpander.ts index f5fd72ca98..cac5818de4 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.ts @@ -22,7 +22,8 @@ * SOFTWARE. */ -import { File, Hunk } from "./DiffTypes"; +import { apiClient } from "@scm-manager/ui-components"; +import { Change, File, Hunk } from "./DiffTypes"; class DiffExpander { file: File; @@ -65,16 +66,109 @@ class DiffExpander { return this.minLineNumber(n + 1) - this.maxLineNumber(n); }; + expandHead = (n: number, callback: (newFile: File) => void) => { + const lineRequestUrl = this.file._links.lines.href + .replace("{start}", this.minLineNumber(n) - Math.min(10, this.computeMaxExpandHeadRange(n))) + .replace("{end}", this.minLineNumber(n) - 1); + apiClient + .get(lineRequestUrl) + .then(response => response.text()) + .then(text => text.split("\n")) + .then(lines => this.expandHunkAtHead(n, lines, callback)); + }; + + expandBottom = (n: number, callback: (newFile: File) => void) => { + const lineRequestUrl = this.file._links.lines.href + .replace("{start}", this.maxLineNumber(n) + 1) + .replace("{end}", this.maxLineNumber(n) + Math.min(10, this.computeMaxExpandBottomRange(n))); + apiClient + .get(lineRequestUrl) + .then(response => response.text()) + .then(text => text.split("\n")) + .then(lines => this.expandHunkAtBottom(n, lines, callback)); + }; + + expandHunkAtHead = (n: number, lines: string[], callback: (newFile: File) => void) => { + const hunk = this.file.hunks[n]; + const newChanges: Change[] = []; + let oldLineNumber = hunk.changes[0].oldLineNumber - lines.length; + let newLineNumber = hunk.changes[0].newLineNumber - lines.length; + + lines.forEach(line => { + newChanges.push({ + content: line, + type: "normal", + oldLineNumber, + newLineNumber, + isNormal: true + }); + oldLineNumber += 1; + newLineNumber += 1; + }); + hunk.changes.forEach(change => newChanges.push(change)); + + const newHunk = { + ...hunk, + oldStart: hunk.oldStart - lines.length, + newStart: hunk.newStart - lines.length, + oldLines: hunk.oldLines + lines.length, + newLines: hunk.newLines + lines.length, + changes: newChanges + }; + const newHunks: Hunk[] = []; + this.file.hunks!.forEach((oldHunk: Hunk, i: number) => { + if (i === n) { + newHunks.push(newHunk); + } else { + newHunks.push(oldHunk); + } + }); + const newFile = { ...this.file, hunks: newHunks }; + callback(newFile); + }; + + expandHunkAtBottom = (n: number, lines: string[], callback: (newFile: File) => void) => { + const hunk = this.file.hunks![n]; + const newChanges = [...hunk.changes]; + let oldLineNumber = newChanges[newChanges.length - 1].oldLineNumber; + let newLineNumber = newChanges[newChanges.length - 1].newLineNumber; + + lines.forEach(line => { + oldLineNumber += 1; + newLineNumber += 1; + newChanges.push({ + content: line, + type: "normal", + oldLineNumber, + newLineNumber, + isNormal: true + }); + }); + + const newHunk = { + ...hunk, + oldLines: hunk.oldLines + lines.length, + newLines: hunk.newLines + lines.length, + changes: newChanges + }; + const newHunks: Hunk[] = []; + this.file.hunks.forEach((oldHunk: Hunk, i: number) => { + if (i === n) { + newHunks.push(newHunk); + } else { + newHunks.push(oldHunk); + } + }); + const newFile = { ...this.file, hunks: newHunks }; + callback(newFile); + }; + getHunk: (n: number) => ExpandableHunk = (n: number) => { return { maxExpandHeadRange: this.computeMaxExpandHeadRange(n), maxExpandBottomRange: this.computeMaxExpandBottomRange(n), - expandHead: () => { - return this; - }, - expandBottom: () => { - return this; - }, + expandHead: (callback: (newFile: File) => void) => this.expandHead(n, callback), + expandBottom: (callback: (newFile: File) => void) => this.expandBottom(n, callback), hunk: this.file?.hunks![n] }; }; @@ -84,8 +178,8 @@ export type ExpandableHunk = { hunk: Hunk; maxExpandHeadRange: number; maxExpandBottomRange: number; - expandHead: () => DiffExpander; - expandBottom: () => DiffExpander; + expandHead: (callback: (newFile: File) => void) => void; + expandBottom: (callback: (newFile: File) => void) => void; }; export default DiffExpander; diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx index 1c75191bb6..a6dbc4c90e 100644 --- a/scm-ui/ui-components/src/repos/DiffFile.tsx +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -48,6 +48,7 @@ type Collapsible = { }; type State = Collapsible & { + file: File; sideBySide?: boolean; diffExpander: DiffExpander; }; @@ -96,7 +97,8 @@ class DiffFile extends React.Component { this.state = { collapsed: this.defaultCollapse(), sideBySide: props.sideBySide, - diffExpander: new DiffExpander(props.file) + diffExpander: new DiffExpander(props.file), + file: props.file }; } @@ -120,7 +122,7 @@ class DiffFile extends React.Component { }; toggleCollapse = () => { - const { file } = this.props; + const { file } = this.state; if (this.hasContent(file)) { this.setState(state => ({ collapsed: !state.collapsed @@ -143,11 +145,15 @@ class DiffFile extends React.Component { }); }; + diffExpanded = (newFile: File) => { + this.setState({ file: newFile, diffExpander: new DiffExpander(newFile) }); + }; + createHunkHeader = (expandableHunk: ExpandableHunk) => { if (expandableHunk.maxExpandHeadRange > 0) { return ( - this.setState({ diffExpander: expandableHunk.expandHead() })}> + expandableHunk.expandHead(this.diffExpanded)}> {`Load ${expandableHunk.maxExpandHeadRange} more lines`} @@ -161,7 +167,7 @@ class DiffFile extends React.Component { if (expandableHunk.maxExpandBottomRange > 0) { return ( - this.setState({ diffExpander: expandableHunk.expandBottom() })}> + expandableHunk.expandBottom(this.diffExpanded)}> {`Load ${expandableHunk.maxExpandBottomRange} more lines`} @@ -172,7 +178,8 @@ class DiffFile extends React.Component { }; collectHunkAnnotations = (hunk: HunkType) => { - const { annotationFactory, file } = this.props; + const { annotationFactory } = this.props; + const { file } = this.state; if (annotationFactory) { return annotationFactory({ hunk, @@ -184,7 +191,8 @@ class DiffFile extends React.Component { }; handleClickEvent = (change: Change, hunk: HunkType) => { - const { file, onClick } = this.props; + const { onClick } = this.props; + const { file } = this.state; const context = { changeId: getChangeKey(change), change, @@ -286,8 +294,8 @@ class DiffFile extends React.Component { hasContent = (file: File) => file && !file.isBinary && file.hunks && file.hunks.length > 0; render() { - const { file, fileControlFactory, fileAnnotationFactory, t } = this.props; - const { collapsed, sideBySide, diffExpander } = this.state; + const { fileControlFactory, fileAnnotationFactory, t } = this.props; + const { file, collapsed, sideBySide, diffExpander } = this.state; const viewType = sideBySide ? "split" : "unified"; let body = null; @@ -299,7 +307,11 @@ class DiffFile extends React.Component {
{fileAnnotations} - {(hunks: HunkType[]) => hunks?.map((hunk, n) => this.renderHunk(file, diffExpander.getHunk(n), n))} + {(hunks: HunkType[]) => + hunks?.map((hunk, n) => { + return this.renderHunk(file, diffExpander.getHunk(n), n); + }) + }
); 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 02b54b7e7d..03d1293083 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 @@ -77,7 +77,7 @@ class DiffResultToDiffResultDtoMapper { private DiffResultDto.FileDto mapFile(DiffFile file, Repository repository, String revision) { Links.Builder links = linkingTo(); if (file.iterator().hasNext()) { - links.single(linkBuilder("lines", resourceLinks.source().content(repository.getNamespace(), repository.getName(), revision, file.getNewPath()) + "?start={start}?end={end}").build()); + links.single(linkBuilder("lines", resourceLinks.source().content(repository.getNamespace(), repository.getName(), revision, file.getNewPath()) + "?start={start}&end={end}").build()); } DiffResultDto.FileDto dto = new DiffResultDto.FileDto(links.build()); // ??? From 83145d953b0f223152077734c1886c9c57b34cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 29 May 2020 16:41:33 +0200 Subject: [PATCH 07/38] Handle following "real" line breaks correctly --- .../resources/LineFilteredOutputStream.java | 26 ++++++++++++++----- .../LineFilteredOutputStreamTest.java | 21 +++++++++++++++ 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/LineFilteredOutputStream.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/LineFilteredOutputStream.java index 5de813cabd..7d42e8ed94 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/LineFilteredOutputStream.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/LineFilteredOutputStream.java @@ -32,7 +32,7 @@ class LineFilteredOutputStream extends OutputStream { private final int start; private final Integer end; - private boolean inLineBreak; + private Character lastLineBreakCharacter; private int currentLine = 0; LineFilteredOutputStream(OutputStream target, Integer start, Integer end) { @@ -46,25 +46,39 @@ class LineFilteredOutputStream extends OutputStream { switch (b) { case '\n': case '\r': - if (!inLineBreak) { - inLineBreak = true; + if (lastLineBreakCharacter == null) { + keepLineBreakInMind((char) b); + } else if (lastLineBreakCharacter == b) { + if (currentLine > start && currentLine <= end) { + target.write('\n'); + } ++currentLine; + } else { + if (currentLine > start && currentLine <= end) { + target.write('\n'); + } + lastLineBreakCharacter = null; } break; default: - if (inLineBreak && currentLine > start && currentLine <= end) { + if (lastLineBreakCharacter != null && currentLine > start && currentLine <= end) { target.write('\n'); } - inLineBreak = false; + lastLineBreakCharacter = null; if (currentLine >= start && currentLine < end) { target.write(b); } } } + public void keepLineBreakInMind(char b) { + lastLineBreakCharacter = b; + ++currentLine; + } + @Override public void close() throws IOException { - if (inLineBreak && currentLine >= start && currentLine < end) { + if (lastLineBreakCharacter != null && currentLine >= start && currentLine < end) { target.write('\n'); } target.close(); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/LineFilteredOutputStreamTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/LineFilteredOutputStreamTest.java index ecaff9c88a..58a4042dd7 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/LineFilteredOutputStreamTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/LineFilteredOutputStreamTest.java @@ -24,6 +24,7 @@ package sonia.scm.api.v2.resources; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -79,4 +80,24 @@ class LineFilteredOutputStreamTest { assertThat(target.toString()).isEqualTo("line 1\nline 2\n"); } + + @ParameterizedTest + @ValueSource(strings = {"line 1\n\nline 2\n\nline 3", "line 1\r\n\r\nline 2\r\n\r\nline 3"}) + void shouldHandleDoubleBlankLinesCorrectly(String input) throws IOException { + LineFilteredOutputStream filtered = new LineFilteredOutputStream(target, 4, null); + + filtered.write(input.getBytes()); + + assertThat(target.toString()).isEqualTo("line 3"); + } + + @ParameterizedTest + @ValueSource(strings = {"line 1\n\n\nline 2\n\n\nline 3", "line 1\r\n\r\n\r\nline 2\r\n\r\n\r\nline 3"}) + void shouldHandleTripleBlankLinesCorrectly(String input) throws IOException { + LineFilteredOutputStream filtered = new LineFilteredOutputStream(target, 4, 6); + + filtered.write(input.getBytes()); + + assertThat(target.toString()).isEqualTo("\n\n"); + } } From 5bf624d0872102e62bfbf29d6c708cdf247de3cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 29 May 2020 17:19:51 +0200 Subject: [PATCH 08/38] Simplify test data --- .../src/repos/DiffExpander.test.ts | 267 +++--------------- 1 file changed, 42 insertions(+), 225 deletions(-) diff --git a/scm-ui/ui-components/src/repos/DiffExpander.test.ts b/scm-ui/ui-components/src/repos/DiffExpander.test.ts index 36023da707..d01de8fd09 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.test.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.test.ts @@ -31,77 +31,17 @@ const HUNK_0 = { oldLines: 8, newLines: 8, changes: [ - { - content: "// @flow", - type: "normal", - oldLineNumber: 1, - newLineNumber: 1, - isNormal: true - }, - { - content: 'import React from "react";', - type: "normal", - oldLineNumber: 2, - newLineNumber: 2, - isNormal: true - }, - { - content: 'import { translate } from "react-i18next";', - type: "delete", - lineNumber: 3, - isDelete: true - }, - { - content: 'import { Textarea } from "@scm-manager/ui-components";', - type: "delete", - lineNumber: 4, - isDelete: true - }, - { - content: 'import type { Me } from "@scm-manager/ui-types";', - type: "delete", - lineNumber: 5, - isDelete: true - }, - { - content: 'import {translate} from "react-i18next";', - type: "insert", - lineNumber: 3, - isInsert: true - }, - { - content: 'import {Textarea} from "@scm-manager/ui-components";', - type: "insert", - lineNumber: 4, - isInsert: true - }, - { - content: 'import type {Me} from "@scm-manager/ui-types";', - type: "insert", - lineNumber: 5, - isInsert: true - }, - { - content: 'import injectSheet from "react-jss";', - type: "normal", - oldLineNumber: 6, - newLineNumber: 6, - isNormal: true - }, - { - content: "", - type: "normal", - oldLineNumber: 7, - newLineNumber: 7, - isNormal: true - }, - { - content: "const styles = {", - type: "normal", - oldLineNumber: 8, - newLineNumber: 8, - isNormal: true - } + { content: "line", type: "normal", oldLineNumber: 1, newLineNumber: 1, isNormal: true }, + { content: "line", type: "normal", oldLineNumber: 2, newLineNumber: 2, isNormal: true }, + { content: "line", type: "delete", lineNumber: 3, isDelete: true }, + { content: "line", type: "delete", lineNumber: 4, isDelete: true }, + { content: "line", type: "delete", lineNumber: 5, isDelete: true }, + { content: "line", type: "insert", lineNumber: 3, isInsert: true }, + { content: "line", type: "insert", lineNumber: 4, isInsert: true }, + { content: "line", type: "insert", lineNumber: 5, isInsert: true }, + { content: "line", type: "normal", oldLineNumber: 6, newLineNumber: 6, isNormal: true }, + { content: "line", type: "normal", oldLineNumber: 7, newLineNumber: 7, isNormal: true }, + { content: "line", type: "normal", oldLineNumber: 8, newLineNumber: 8, isNormal: true } ] }; const HUNK_1 = { @@ -111,54 +51,13 @@ const HUNK_1 = { oldLines: 6, newLines: 7, changes: [ - { - content: "type Props = {", - type: "normal", - oldLineNumber: 14, - newLineNumber: 14, - isNormal: true - }, - { - content: " me: Me,", - type: "normal", - oldLineNumber: 15, - newLineNumber: 15, - isNormal: true - }, - { - content: " onChange: string => void,", - type: "normal", - oldLineNumber: 16, - newLineNumber: 16, - isNormal: true - }, - { - content: " disabled: boolean,", - type: "insert", - lineNumber: 17, - isInsert: true - }, - { - content: " //context props", - type: "normal", - oldLineNumber: 17, - newLineNumber: 18, - isNormal: true - }, - { - content: " t: string => string,", - type: "normal", - oldLineNumber: 18, - newLineNumber: 19, - isNormal: true - }, - { - content: " classes: any", - type: "normal", - oldLineNumber: 19, - newLineNumber: 20, - isNormal: true - } + { content: "line", type: "normal", oldLineNumber: 14, newLineNumber: 14, isNormal: true }, + { content: "line", type: "normal", oldLineNumber: 15, newLineNumber: 15, isNormal: true }, + { content: "line", type: "normal", oldLineNumber: 16, newLineNumber: 16, isNormal: true }, + { content: "line", type: "insert", lineNumber: 17, isInsert: true }, + { content: "line", type: "normal", oldLineNumber: 17, newLineNumber: 18, isNormal: true }, + { content: "line", type: "normal", oldLineNumber: 18, newLineNumber: 19, isNormal: true }, + { content: "line", type: "normal", oldLineNumber: 19, newLineNumber: 20, isNormal: true } ] }; const HUNK_2 = { @@ -168,60 +67,14 @@ const HUNK_2 = { oldLines: 7, newLines: 7, changes: [ - { - content: "", - type: "normal", - oldLineNumber: 21, - newLineNumber: 22, - isNormal: true - }, - { - content: "class CommitMessage extends React.Component {", - type: "normal", - oldLineNumber: 22, - newLineNumber: 23, - isNormal: true - }, - { - content: " render() {", - type: "normal", - oldLineNumber: 23, - newLineNumber: 24, - isNormal: true - }, - { - content: " const { t, classes, me, onChange } = this.props;", - type: "delete", - lineNumber: 24, - isDelete: true - }, - { - content: " const {t, classes, me, onChange, disabled} = this.props;", - type: "insert", - lineNumber: 25, - isInsert: true - }, - { - content: " return (", - type: "normal", - oldLineNumber: 25, - newLineNumber: 26, - isNormal: true - }, - { - content: " <>", - type: "normal", - oldLineNumber: 26, - newLineNumber: 27, - isNormal: true - }, - { - content: "
", - type: "normal", - oldLineNumber: 27, - newLineNumber: 28, - isNormal: true - } + { content: "line", type: "normal", oldLineNumber: 21, newLineNumber: 22, isNormal: true }, + { content: "line", type: "normal", oldLineNumber: 22, newLineNumber: 23, isNormal: true }, + { content: "line", type: "normal", oldLineNumber: 23, newLineNumber: 24, isNormal: true }, + { content: "line", type: "delete", lineNumber: 24, isDelete: true }, + { content: "line", type: "insert", lineNumber: 25, isInsert: true }, + { content: "line", type: "normal", oldLineNumber: 25, newLineNumber: 26, isNormal: true }, + { content: "line", type: "normal", oldLineNumber: 26, newLineNumber: 27, isNormal: true }, + { content: "line", type: "normal", oldLineNumber: 27, newLineNumber: 28, isNormal: true } ] }; const HUNK_3 = { @@ -231,54 +84,13 @@ const HUNK_3 = { oldLines: 6, newLines: 7, changes: [ - { - content: " onChange(message)}", - type: "normal", - oldLineNumber: 35, - newLineNumber: 36, - isNormal: true - }, - { - content: " disabled={disabled}", - type: "insert", - lineNumber: 37, - isInsert: true - }, - { - content: " />", - type: "normal", - oldLineNumber: 36, - newLineNumber: 38, - isNormal: true - }, - { - content: " ", - type: "normal", - oldLineNumber: 37, - newLineNumber: 39, - isNormal: true - }, - { - content: " );", - type: "normal", - oldLineNumber: 38, - newLineNumber: 40, - isNormal: true - } + { content: "line", type: "normal", oldLineNumber: 33, newLineNumber: 34, isNormal: true }, + { content: "line", type: "normal", oldLineNumber: 34, newLineNumber: 35, isNormal: true }, + { content: "line", type: "normal", oldLineNumber: 35, newLineNumber: 36, isNormal: true }, + { content: "line", type: "insert", lineNumber: 37, isInsert: true }, + { content: "line", type: "normal", oldLineNumber: 36, newLineNumber: 38, isNormal: true }, + { content: "line", type: "normal", oldLineNumber: 37, newLineNumber: 39, isNormal: true }, + { content: "line", type: "normal", oldLineNumber: 38, newLineNumber: 40, isNormal: true } ] }; const TEST_CONTENT_WITH_HUNKS = { @@ -293,8 +105,7 @@ const TEST_CONTENT_WITH_HUNKS = { hunks: [HUNK_0, HUNK_1, HUNK_2, HUNK_3], _links: { lines: { - href: - "http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start={start}&end={end}", + href: "http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start={start}&end={end}", templated: true } } @@ -392,7 +203,10 @@ describe("with hunks the diff expander", () => { }); it("should expand hunk with new line from api client at the bottom", async () => { expect(diffExpander.getHunk(1).hunk.changes.length).toBe(7); - fetchMock.get("http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=22&end=22", "new line 1\nnew line 2"); + fetchMock.get( + "http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=22&end=22", + "new line 1\nnew line 2" + ); let newFile; diffExpander.getHunk(1).expandBottom(file => { newFile = file; @@ -405,7 +219,10 @@ describe("with hunks the diff expander", () => { }); it("should expand hunk with new line from api client at the top", async () => { expect(diffExpander.getHunk(1).hunk.changes.length).toBe(7); - fetchMock.get("http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=9&end=13", "new line 1\nnew line 2"); + fetchMock.get( + "http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=9&end=13", + "new line 1\nnew line 2" + ); let newFile; diffExpander.getHunk(1).expandHead(file => { newFile = file; From e7f03378a45a394e5170118010c6a6b74db9e9c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Sat, 30 May 2020 15:48:51 +0200 Subject: [PATCH 09/38] Fix line number calculation for patch --- .../src/repos/DiffExpander.test.ts | 31 +++++++++++-------- .../ui-components/src/repos/DiffExpander.ts | 16 +++++++--- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/scm-ui/ui-components/src/repos/DiffExpander.test.ts b/scm-ui/ui-components/src/repos/DiffExpander.test.ts index d01de8fd09..d7d0bde027 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.test.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.test.ts @@ -204,8 +204,8 @@ describe("with hunks the diff expander", () => { it("should expand hunk with new line from api client at the bottom", async () => { expect(diffExpander.getHunk(1).hunk.changes.length).toBe(7); fetchMock.get( - "http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=22&end=22", - "new line 1\nnew line 2" + "http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=20&end=21", + "new line 1" ); let newFile; diffExpander.getHunk(1).expandBottom(file => { @@ -213,15 +213,14 @@ describe("with hunks the diff expander", () => { }); await fetchMock.flush(true); expect(fetchMock.done()).toBe(true); - expect(newFile.hunks[1].changes.length).toBe(9); + expect(newFile.hunks[1].changes.length).toBe(8); expect(newFile.hunks[1].changes[7].content).toBe("new line 1"); - expect(newFile.hunks[1].changes[8].content).toBe("new line 2"); }); it("should expand hunk with new line from api client at the top", async () => { expect(diffExpander.getHunk(1).hunk.changes.length).toBe(7); fetchMock.get( - "http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=9&end=13", - "new line 1\nnew line 2" + "http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=8&end=13", + "new line 9\nnew line 10\nnew line 11\nnew line 12\nnew line 13" ); let newFile; diffExpander.getHunk(1).expandHead(file => { @@ -229,13 +228,19 @@ describe("with hunks the diff expander", () => { }); await fetchMock.flush(true); expect(fetchMock.done()).toBe(true); - expect(newFile.hunks[1].changes.length).toBe(9); - expect(newFile.hunks[1].changes[0].content).toBe("new line 1"); - expect(newFile.hunks[1].changes[0].oldLineNumber).toBe(12); - expect(newFile.hunks[1].changes[0].newLineNumber).toBe(12); - expect(newFile.hunks[1].changes[1].content).toBe("new line 2"); - expect(newFile.hunks[1].changes[1].oldLineNumber).toBe(13); - expect(newFile.hunks[1].changes[1].newLineNumber).toBe(13); + expect(newFile.hunks[1].changes.length).toBe(12); + expect(newFile.hunks[1].changes[0].content).toBe("new line 9"); + expect(newFile.hunks[1].changes[0].oldLineNumber).toBe(9); + expect(newFile.hunks[1].changes[0].newLineNumber).toBe(9); + expect(newFile.hunks[1].changes[1].content).toBe("new line 10"); + expect(newFile.hunks[1].changes[1].oldLineNumber).toBe(10); + expect(newFile.hunks[1].changes[1].newLineNumber).toBe(10); + expect(newFile.hunks[1].changes[4].content).toBe("new line 13"); + expect(newFile.hunks[1].changes[4].oldLineNumber).toBe(13); + expect(newFile.hunks[1].changes[4].newLineNumber).toBe(13); + expect(newFile.hunks[1].changes[5].content).toBe("line"); + expect(newFile.hunks[1].changes[5].oldLineNumber).toBe(14); + expect(newFile.hunks[1].changes[5].newLineNumber).toBe(14); }); }); diff --git a/scm-ui/ui-components/src/repos/DiffExpander.ts b/scm-ui/ui-components/src/repos/DiffExpander.ts index cac5818de4..0311636d7c 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.ts @@ -45,7 +45,7 @@ class DiffExpander { }; maxLineNumber: (n: number) => number = (n: number) => { - return this.file.hunks![n]!.newStart! + this.file.hunks![n]!.newLines!; + return this.file.hunks![n]!.newStart! + this.file.hunks![n]!.newLines! - 1; }; computeMaxExpandHeadRange = (n: number) => { @@ -54,7 +54,7 @@ class DiffExpander { } else if (n === 0) { return this.minLineNumber(n) - 1; } - return this.minLineNumber(n) - this.maxLineNumber(n - 1); + return this.minLineNumber(n) - this.maxLineNumber(n - 1) - 1; }; computeMaxExpandBottomRange = (n: number) => { @@ -63,12 +63,12 @@ class DiffExpander { } else if (n === this.file!.hunks!.length - 1) { return Number.MAX_SAFE_INTEGER; } - return this.minLineNumber(n + 1) - this.maxLineNumber(n); + return this.minLineNumber(n + 1) - this.maxLineNumber(n) - 1; }; expandHead = (n: number, callback: (newFile: File) => void) => { const lineRequestUrl = this.file._links.lines.href - .replace("{start}", this.minLineNumber(n) - Math.min(10, this.computeMaxExpandHeadRange(n))) + .replace("{start}", this.minLineNumber(n) - Math.min(10, this.computeMaxExpandHeadRange(n)) - 1) .replace("{end}", this.minLineNumber(n) - 1); apiClient .get(lineRequestUrl) @@ -79,7 +79,7 @@ class DiffExpander { expandBottom = (n: number, callback: (newFile: File) => void) => { const lineRequestUrl = this.file._links.lines.href - .replace("{start}", this.maxLineNumber(n) + 1) + .replace("{start}", this.maxLineNumber(n)) .replace("{end}", this.maxLineNumber(n) + Math.min(10, this.computeMaxExpandBottomRange(n))); apiClient .get(lineRequestUrl) @@ -90,6 +90,9 @@ class DiffExpander { expandHunkAtHead = (n: number, lines: string[], callback: (newFile: File) => void) => { const hunk = this.file.hunks[n]; + if (lines[lines.length - 1] === "") { + lines.pop(); + } const newChanges: Change[] = []; let oldLineNumber = hunk.changes[0].oldLineNumber - lines.length; let newLineNumber = hunk.changes[0].newLineNumber - lines.length; @@ -129,6 +132,9 @@ class DiffExpander { expandHunkAtBottom = (n: number, lines: string[], callback: (newFile: File) => void) => { const hunk = this.file.hunks![n]; + if (lines[lines.length - 1] === "") { + lines.pop(); + } const newChanges = [...hunk.changes]; let oldLineNumber = newChanges[newChanges.length - 1].oldLineNumber; let newLineNumber = newChanges[newChanges.length - 1].newLineNumber; From 8c1d463e094ef5a2c8acd252806079461c679d88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Sat, 30 May 2020 16:35:49 +0200 Subject: [PATCH 10/38] Give two options for diff expansion (some or all lines) --- .../src/repos/DiffExpander.test.ts | 9 +-- .../ui-components/src/repos/DiffExpander.ts | 16 ++--- scm-ui/ui-components/src/repos/DiffFile.tsx | 62 ++++++++++++++----- scm-ui/ui-webapp/public/locales/en/repos.json | 10 ++- 4 files changed, 68 insertions(+), 29 deletions(-) diff --git a/scm-ui/ui-components/src/repos/DiffExpander.test.ts b/scm-ui/ui-components/src/repos/DiffExpander.test.ts index d7d0bde027..37f7e825ef 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.test.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.test.ts @@ -203,12 +203,9 @@ describe("with hunks the diff expander", () => { }); it("should expand hunk with new line from api client at the bottom", async () => { expect(diffExpander.getHunk(1).hunk.changes.length).toBe(7); - fetchMock.get( - "http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=20&end=21", - "new line 1" - ); + fetchMock.get("http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=20&end=21", "new line 1"); let newFile; - diffExpander.getHunk(1).expandBottom(file => { + diffExpander.getHunk(1).expandBottom(1, file => { newFile = file; }); await fetchMock.flush(true); @@ -223,7 +220,7 @@ describe("with hunks the diff expander", () => { "new line 9\nnew line 10\nnew line 11\nnew line 12\nnew line 13" ); let newFile; - diffExpander.getHunk(1).expandHead(file => { + diffExpander.getHunk(1).expandHead(5, file => { newFile = file; }); await fetchMock.flush(true); diff --git a/scm-ui/ui-components/src/repos/DiffExpander.ts b/scm-ui/ui-components/src/repos/DiffExpander.ts index 0311636d7c..779c43c27c 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.ts @@ -66,9 +66,9 @@ class DiffExpander { return this.minLineNumber(n + 1) - this.maxLineNumber(n) - 1; }; - expandHead = (n: number, callback: (newFile: File) => void) => { + expandHead = (n: number, count: number, callback: (newFile: File) => void) => { const lineRequestUrl = this.file._links.lines.href - .replace("{start}", this.minLineNumber(n) - Math.min(10, this.computeMaxExpandHeadRange(n)) - 1) + .replace("{start}", this.minLineNumber(n) - Math.min(count, this.computeMaxExpandHeadRange(n)) - 1) .replace("{end}", this.minLineNumber(n) - 1); apiClient .get(lineRequestUrl) @@ -77,10 +77,10 @@ class DiffExpander { .then(lines => this.expandHunkAtHead(n, lines, callback)); }; - expandBottom = (n: number, callback: (newFile: File) => void) => { + expandBottom = (n: number, count: number, callback: (newFile: File) => void) => { const lineRequestUrl = this.file._links.lines.href .replace("{start}", this.maxLineNumber(n)) - .replace("{end}", this.maxLineNumber(n) + Math.min(10, this.computeMaxExpandBottomRange(n))); + .replace("{end}", this.maxLineNumber(n) + Math.min(count, this.computeMaxExpandBottomRange(n))); apiClient .get(lineRequestUrl) .then(response => response.text()) @@ -173,8 +173,8 @@ class DiffExpander { return { maxExpandHeadRange: this.computeMaxExpandHeadRange(n), maxExpandBottomRange: this.computeMaxExpandBottomRange(n), - expandHead: (callback: (newFile: File) => void) => this.expandHead(n, callback), - expandBottom: (callback: (newFile: File) => void) => this.expandBottom(n, callback), + expandHead: (count: number, callback: (newFile: File) => void) => this.expandHead(n, count, callback), + expandBottom: (count: number, callback: (newFile: File) => void) => this.expandBottom(n, count, callback), hunk: this.file?.hunks![n] }; }; @@ -184,8 +184,8 @@ export type ExpandableHunk = { hunk: Hunk; maxExpandHeadRange: number; maxExpandBottomRange: number; - expandHead: (callback: (newFile: File) => void) => void; - expandBottom: (callback: (newFile: File) => void) => void; + expandHead: (count: number, callback: (newFile: File) => void) => void; + expandBottom: (count: number, callback: (newFile: File) => void) => void; }; export default DiffExpander; diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx index a6dbc4c90e..0ae70c764b 100644 --- a/scm-ui/ui-components/src/repos/DiffFile.tsx +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -151,13 +151,30 @@ class DiffFile extends React.Component { createHunkHeader = (expandableHunk: ExpandableHunk) => { if (expandableHunk.maxExpandHeadRange > 0) { - return ( - - expandableHunk.expandHead(this.diffExpanded)}> - {`Load ${expandableHunk.maxExpandHeadRange} more lines`} - - - ); + if (expandableHunk.maxExpandHeadRange <= 10) { + return ( + + + expandableHunk.expandHead(expandableHunk.maxExpandHeadRange, this.diffExpanded)}> + {this.props.t("diff.expandHeadComplete", { count: expandableHunk.maxExpandHeadRange })} + + + + ); + } else { + return ( + + + expandableHunk.expandHead(10, this.diffExpanded)}> + {this.props.t("diff.expandHeadByLines", { count: 10 })} + {" "} + expandableHunk.expandHead(expandableHunk.maxExpandHeadRange, this.diffExpanded)}> + {this.props.t("diff.expandHeadComplete", { count: expandableHunk.maxExpandHeadRange })} + + + + ); + } } // hunk header must be defined return ; @@ -165,13 +182,30 @@ class DiffFile extends React.Component { createHunkFooter = (expandableHunk: ExpandableHunk) => { if (expandableHunk.maxExpandBottomRange > 0) { - return ( - - expandableHunk.expandBottom(this.diffExpanded)}> - {`Load ${expandableHunk.maxExpandBottomRange} more lines`} - - - ); + if (expandableHunk.maxExpandBottomRange <= 10) { + return ( + + + expandableHunk.expandBottom(expandableHunk.maxExpandBottomRange, this.diffExpanded)}> + {this.props.t("diff.expandBottomComplete", { count: expandableHunk.maxExpandBottomRange })} + + + + ); + } else { + return ( + + + expandableHunk.expandBottom(10, this.diffExpanded)}> + {this.props.t("diff.expandBottomByLines", { count: 10 })} + {" "} + expandableHunk.expandBottom(expandableHunk.maxExpandBottomRange, this.diffExpanded)}> + {this.props.t("diff.expandBottomComplete", { count: expandableHunk.maxExpandBottomRange })} + + + + ); + } } // hunk header must be defined return ; diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index 2abf63eb47..53d1de79b7 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -198,7 +198,15 @@ }, "sideBySide": "Switch to side-by-side view", "combined": "Switch to combined view", - "noDiffFound": "No Diff between the selected branches found." + "noDiffFound": "No Diff between the selected branches found.", + "expandHeadByLines": "> load {{count}} more line", + "expandHeadByLines_plural": "> load {{count}} more lines", + "expandHeadComplete": ">> load {{count}} line", + "expandHeadComplete_plural": ">> load all {{count}} lines", + "expandBottomByLines": "> load {{count}} more line", + "expandBottomByLines_plural": "> load {{count}} more lines", + "expandBottomComplete": ">> load {{count}} line", + "expandBottomComplete_plural": ">> load all {{count}} lines" }, "fileUpload": { "clickHere": "Click here to select your file", From b86c025b37024e652064ca504fc5a7569a420735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Sat, 30 May 2020 17:55:38 +0200 Subject: [PATCH 11/38] Remove expand marker at bottom when fully expanded --- .../src/repos/DiffExpander.test.ts | 12 +++++++++ .../ui-components/src/repos/DiffExpander.ts | 9 ++++--- scm-ui/ui-components/src/repos/DiffFile.tsx | 25 ++++++++++++++++++- scm-ui/ui-components/src/repos/DiffTypes.ts | 1 + scm-ui/ui-webapp/public/locales/en/repos.json | 4 ++- 5 files changed, 45 insertions(+), 6 deletions(-) diff --git a/scm-ui/ui-components/src/repos/DiffExpander.test.ts b/scm-ui/ui-components/src/repos/DiffExpander.test.ts index 37f7e825ef..b97609299e 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.test.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.test.ts @@ -239,6 +239,18 @@ describe("with hunks the diff expander", () => { expect(newFile.hunks[1].changes[5].oldLineNumber).toBe(14); expect(newFile.hunks[1].changes[5].newLineNumber).toBe(14); }); + it("should set fully expanded to true if expanded completely", async () => { + fetchMock.get( + "http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=40&end=50", + "new line 40\nnew line 41\nnew line 42" + ); + let newFile; + diffExpander.getHunk(3).expandBottom(10, file => { + newFile = file; + }); + await fetchMock.flush(true); + expect(newFile.hunks[3].fullyExpanded).toBe(true); + }); }); describe("for a new file with text input the diff expander", () => { diff --git a/scm-ui/ui-components/src/repos/DiffExpander.ts b/scm-ui/ui-components/src/repos/DiffExpander.ts index 779c43c27c..3dfd91a738 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.ts @@ -61,7 +61,7 @@ class DiffExpander { if (this.file.type === "add" || this.file.type === "delete") { return 0; } else if (n === this.file!.hunks!.length - 1) { - return Number.MAX_SAFE_INTEGER; + return this.file!.hunks![this.file!.hunks!.length - 1].fullyExpanded ? 0 : Number.MAX_SAFE_INTEGER; } return this.minLineNumber(n + 1) - this.maxLineNumber(n) - 1; }; @@ -85,7 +85,7 @@ class DiffExpander { .get(lineRequestUrl) .then(response => response.text()) .then(text => text.split("\n")) - .then(lines => this.expandHunkAtBottom(n, lines, callback)); + .then(lines => this.expandHunkAtBottom(n, count, lines, callback)); }; expandHunkAtHead = (n: number, lines: string[], callback: (newFile: File) => void) => { @@ -130,7 +130,7 @@ class DiffExpander { callback(newFile); }; - expandHunkAtBottom = (n: number, lines: string[], callback: (newFile: File) => void) => { + expandHunkAtBottom = (n: number, requestedLines: number, lines: string[], callback: (newFile: File) => void) => { const hunk = this.file.hunks![n]; if (lines[lines.length - 1] === "") { lines.pop(); @@ -155,7 +155,8 @@ class DiffExpander { ...hunk, oldLines: hunk.oldLines + lines.length, newLines: hunk.newLines + lines.length, - changes: newChanges + changes: newChanges, + fullyExpanded: lines.length < requestedLines }; const newHunks: Hunk[] = []; this.file.hunks.forEach((oldHunk: Hunk, i: number) => { diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx index 0ae70c764b..88f62cfecf 100644 --- a/scm-ui/ui-components/src/repos/DiffFile.tsx +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -211,6 +211,25 @@ class DiffFile extends React.Component { return ; }; + createLastHunkFooter = (expandableHunk: ExpandableHunk) => { + if (expandableHunk.maxExpandBottomRange > 0) { + return ( + + + expandableHunk.expandBottom(10, this.diffExpanded)}> + {this.props.t("diff.expandLastBottomByLines")} + {" "} + expandableHunk.expandBottom(expandableHunk.maxExpandBottomRange, this.diffExpanded)}> + {this.props.t("diff.expandLastBottomComplete")} + + + + ); + } + // hunk header must be defined + return ; + }; + collectHunkAnnotations = (hunk: HunkType) => { const { annotationFactory } = this.props; const { file } = this.state; @@ -267,7 +286,11 @@ class DiffFile extends React.Component { /> ); if (file._links?.lines) { - items.push(this.createHunkFooter(expandableHunk)); + if (i === file.hunks!.length - 1) { + items.push(this.createLastHunkFooter(expandableHunk)); + } else { + items.push(this.createHunkFooter(expandableHunk)); + } } return items; }; diff --git a/scm-ui/ui-components/src/repos/DiffTypes.ts b/scm-ui/ui-components/src/repos/DiffTypes.ts index 56d225d85f..54fb642e61 100644 --- a/scm-ui/ui-components/src/repos/DiffTypes.ts +++ b/scm-ui/ui-components/src/repos/DiffTypes.ts @@ -57,6 +57,7 @@ export type Hunk = { newStart?: number; oldLines?: number; newLines?: number; + fullyExpanded?: boolean; }; export type ChangeType = "insert" | "delete" | "normal" | "conflict"; diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index 53d1de79b7..fa97c7d6cf 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -206,7 +206,9 @@ "expandBottomByLines": "> load {{count}} more line", "expandBottomByLines_plural": "> load {{count}} more lines", "expandBottomComplete": ">> load {{count}} line", - "expandBottomComplete_plural": ">> load all {{count}} lines" + "expandBottomComplete_plural": ">> load all {{count}} lines", + "expandLastBottomByLines": "> load up to 10 more lines", + "expandLastBottomComplete": ">> load all remaining lines" }, "fileUpload": { "clickHere": "Click here to select your file", From 751a2bfa9b6fd849357a9e191163aed2d2369e74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Sat, 30 May 2020 18:12:03 +0200 Subject: [PATCH 12/38] Implement option to load rest of files completely --- .../ui-components/src/repos/DiffExpander.test.ts | 14 +++++++++++++- scm-ui/ui-components/src/repos/DiffExpander.ts | 7 ++++--- scm-ui/ui-components/src/repos/DiffFile.tsx | 2 +- .../scm/api/v2/resources/ContentResource.java | 10 ++++++++-- .../scm/api/v2/resources/ContentResourceTest.java | 12 ++++++++++++ 5 files changed, 38 insertions(+), 7 deletions(-) diff --git a/scm-ui/ui-components/src/repos/DiffExpander.test.ts b/scm-ui/ui-components/src/repos/DiffExpander.test.ts index b97609299e..8d2716de20 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.test.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.test.ts @@ -199,7 +199,7 @@ describe("with hunks the diff expander", () => { }); it("should return a really bix number for the expand bottom range of the last hunk", () => { - expect(diffExpander.getHunk(3).maxExpandBottomRange).toBeGreaterThan(99999); + expect(diffExpander.getHunk(3).maxExpandBottomRange).toBe(-1); }); it("should expand hunk with new line from api client at the bottom", async () => { expect(diffExpander.getHunk(1).hunk.changes.length).toBe(7); @@ -251,6 +251,18 @@ describe("with hunks the diff expander", () => { await fetchMock.flush(true); expect(newFile.hunks[3].fullyExpanded).toBe(true); }); + it("should set end to -1 if requested to expand to the end", async () => { + fetchMock.get( + "http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=40&end=-1", + "new line 40\nnew line 41\nnew line 42" + ); + let newFile; + diffExpander.getHunk(3).expandBottom(-1, file => { + newFile = file; + }); + await fetchMock.flush(true); + expect(newFile.hunks[3].fullyExpanded).toBe(true); + }); }); describe("for a new file with text input the diff expander", () => { diff --git a/scm-ui/ui-components/src/repos/DiffExpander.ts b/scm-ui/ui-components/src/repos/DiffExpander.ts index 3dfd91a738..676569e95e 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.ts @@ -61,7 +61,7 @@ class DiffExpander { if (this.file.type === "add" || this.file.type === "delete") { return 0; } else if (n === this.file!.hunks!.length - 1) { - return this.file!.hunks![this.file!.hunks!.length - 1].fullyExpanded ? 0 : Number.MAX_SAFE_INTEGER; + return this.file!.hunks![this.file!.hunks!.length - 1].fullyExpanded ? 0 : -1; } return this.minLineNumber(n + 1) - this.maxLineNumber(n) - 1; }; @@ -78,9 +78,10 @@ class DiffExpander { }; expandBottom = (n: number, count: number, callback: (newFile: File) => void) => { + const maxExpandBottomRange = this.computeMaxExpandBottomRange(n); const lineRequestUrl = this.file._links.lines.href .replace("{start}", this.maxLineNumber(n)) - .replace("{end}", this.maxLineNumber(n) + Math.min(count, this.computeMaxExpandBottomRange(n))); + .replace("{end}", count > 0 ? this.maxLineNumber(n) + Math.min(count, maxExpandBottomRange > 0? maxExpandBottomRange:Number.MAX_SAFE_INTEGER) : -1); apiClient .get(lineRequestUrl) .then(response => response.text()) @@ -156,7 +157,7 @@ class DiffExpander { oldLines: hunk.oldLines + lines.length, newLines: hunk.newLines + lines.length, changes: newChanges, - fullyExpanded: lines.length < requestedLines + fullyExpanded: requestedLines < 0 || lines.length < requestedLines }; const newHunks: Hunk[] = []; this.file.hunks.forEach((oldHunk: Hunk, i: number) => { diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx index 88f62cfecf..5781f3f284 100644 --- a/scm-ui/ui-components/src/repos/DiffFile.tsx +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -212,7 +212,7 @@ class DiffFile extends React.Component { }; createLastHunkFooter = (expandableHunk: ExpandableHunk) => { - if (expandableHunk.maxExpandBottomRange > 0) { + if (expandableHunk.maxExpandBottomRange != 0) { return ( diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java index 47e0de16d1..ed5ebe6e57 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java @@ -115,10 +115,16 @@ public class ContentResource { } private StreamingOutput createStreamingOutput(String namespace, String name, String revision, String path, Integer start, Integer end) { + Integer effectiveEnd; + if (end != null && end < 0) { + effectiveEnd = null; + } else { + effectiveEnd = end; + } return os -> { OutputStream sourceOut; - if (start != null || end != null) { - sourceOut = new LineFilteredOutputStream(os, start, end); + if (start != null || effectiveEnd != null) { + sourceOut = new LineFilteredOutputStream(os, start, effectiveEnd); } else { sourceOut = os; } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java index c009751195..7cb067a965 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java @@ -109,6 +109,18 @@ public class ContentResourceTest { assertEquals("line 2\nline 3\n", baos.toString()); } + @Test + public void shouldNotLimitOutputWhenEndLessThanZero() throws Exception { + mockContent("file", "line 1\nline 2\nline 3\nline 4".getBytes()); + + Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "file", 1, -1); + assertEquals(200, response.getStatus()); + + ByteArrayOutputStream baos = readOutputStream(response); + + assertEquals("line 2\nline 3\nline 4", baos.toString()); + } + @Test public void shouldHandleMissingFile() { Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "doesNotExist", null, null); From f48ee91776cb1813d1b6d5992ed24a16cdeb135a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Sat, 30 May 2020 20:55:02 +0200 Subject: [PATCH 13/38] Fix types --- .../src/repos/DiffExpander.test.ts | 69 ++++++++++--------- .../ui-components/src/repos/DiffExpander.ts | 45 +++++++----- scm-ui/ui-components/src/repos/DiffTypes.ts | 2 +- 3 files changed, 63 insertions(+), 53 deletions(-) diff --git a/scm-ui/ui-components/src/repos/DiffExpander.test.ts b/scm-ui/ui-components/src/repos/DiffExpander.test.ts index 8d2716de20..eb104a79cc 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.test.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.test.ts @@ -23,8 +23,9 @@ */ import fetchMock from "fetch-mock"; import DiffExpander from "./DiffExpander"; +import { File, Hunk } from "./DiffTypes"; -const HUNK_0 = { +const HUNK_0: Hunk = { content: "@@ -1,8 +1,8 @@", oldStart: 1, newStart: 1, @@ -44,7 +45,7 @@ const HUNK_0 = { { content: "line", type: "normal", oldLineNumber: 8, newLineNumber: 8, isNormal: true } ] }; -const HUNK_1 = { +const HUNK_1: Hunk = { content: "@@ -14,6 +14,7 @@", oldStart: 14, newStart: 14, @@ -60,7 +61,7 @@ const HUNK_1 = { { content: "line", type: "normal", oldLineNumber: 19, newLineNumber: 20, isNormal: true } ] }; -const HUNK_2 = { +const HUNK_2: Hunk = { content: "@@ -21,7 +22,7 @@", oldStart: 21, newStart: 22, @@ -77,7 +78,7 @@ const HUNK_2 = { { content: "line", type: "normal", oldLineNumber: 27, newLineNumber: 28, isNormal: true } ] }; -const HUNK_3 = { +const HUNK_3: Hunk = { content: "@@ -33,6 +34,7 @@", oldStart: 33, newStart: 34, @@ -93,16 +94,16 @@ const HUNK_3 = { { content: "line", type: "normal", oldLineNumber: 38, newLineNumber: 40, isNormal: true } ] }; -const TEST_CONTENT_WITH_HUNKS = { - oldPath: "src/main/js/CommitMessage.js", - newPath: "src/main/js/CommitMessage.js", - oldEndingNewLine: true, +const TEST_CONTENT_WITH_HUNKS: File = { + hunks: [HUNK_0, HUNK_1, HUNK_2, HUNK_3], newEndingNewLine: true, - oldRevision: "e05c8495bb1dc7505d73af26210c8ff4825c4500", + newPath: "src/main/js/CommitMessage.js", newRevision: "4305a8df175b7bec25acbe542a13fbe2a718a608", + oldEndingNewLine: true, + oldPath: "src/main/js/CommitMessage.js", + oldRevision: "e05c8495bb1dc7505d73af26210c8ff4825c4500", type: "modify", language: "javascript", - hunks: [HUNK_0, HUNK_1, HUNK_2, HUNK_3], _links: { lines: { href: "http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start={start}&end={end}", @@ -111,7 +112,7 @@ const TEST_CONTENT_WITH_HUNKS = { } }; -const TEST_CONTENT_WIT_NEW_BINARY_FILE = { +const TEST_CONTENT_WIT_NEW_BINARY_FILE: File = { oldPath: "/dev/null", newPath: "src/main/fileUploadV2.png", oldEndingNewLine: true, @@ -121,7 +122,7 @@ const TEST_CONTENT_WIT_NEW_BINARY_FILE = { type: "add" }; -const TEST_CONTENT_WITH_NEW_TEXT_FILE = { +const TEST_CONTENT_WITH_NEW_TEXT_FILE: File = { oldPath: "/dev/null", newPath: "src/main/markdown/README.md", oldEndingNewLine: true, @@ -150,7 +151,7 @@ const TEST_CONTENT_WITH_NEW_TEXT_FILE = { } }; -const TEST_CONTENT_WITH_DELETED_TEXT_FILE = { +const TEST_CONTENT_WITH_DELETED_TEXT_FILE: File = { oldPath: "README.md", newPath: "/dev/null", oldEndingNewLine: true, @@ -204,14 +205,14 @@ describe("with hunks the diff expander", () => { it("should expand hunk with new line from api client at the bottom", async () => { expect(diffExpander.getHunk(1).hunk.changes.length).toBe(7); fetchMock.get("http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=20&end=21", "new line 1"); - let newFile; + let newFile: File; diffExpander.getHunk(1).expandBottom(1, file => { newFile = file; }); await fetchMock.flush(true); expect(fetchMock.done()).toBe(true); - expect(newFile.hunks[1].changes.length).toBe(8); - expect(newFile.hunks[1].changes[7].content).toBe("new line 1"); + expect(newFile!.hunks![1].changes.length).toBe(8); + expect(newFile!.hunks![1].changes[7].content).toBe("new line 1"); }); it("should expand hunk with new line from api client at the top", async () => { expect(diffExpander.getHunk(1).hunk.changes.length).toBe(7); @@ -219,49 +220,49 @@ describe("with hunks the diff expander", () => { "http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=8&end=13", "new line 9\nnew line 10\nnew line 11\nnew line 12\nnew line 13" ); - let newFile; + let newFile: File; diffExpander.getHunk(1).expandHead(5, file => { newFile = file; }); await fetchMock.flush(true); expect(fetchMock.done()).toBe(true); - expect(newFile.hunks[1].changes.length).toBe(12); - expect(newFile.hunks[1].changes[0].content).toBe("new line 9"); - expect(newFile.hunks[1].changes[0].oldLineNumber).toBe(9); - expect(newFile.hunks[1].changes[0].newLineNumber).toBe(9); - expect(newFile.hunks[1].changes[1].content).toBe("new line 10"); - expect(newFile.hunks[1].changes[1].oldLineNumber).toBe(10); - expect(newFile.hunks[1].changes[1].newLineNumber).toBe(10); - expect(newFile.hunks[1].changes[4].content).toBe("new line 13"); - expect(newFile.hunks[1].changes[4].oldLineNumber).toBe(13); - expect(newFile.hunks[1].changes[4].newLineNumber).toBe(13); - expect(newFile.hunks[1].changes[5].content).toBe("line"); - expect(newFile.hunks[1].changes[5].oldLineNumber).toBe(14); - expect(newFile.hunks[1].changes[5].newLineNumber).toBe(14); + expect(newFile!.hunks![1].changes.length).toBe(12); + expect(newFile!.hunks![1].changes[0].content).toBe("new line 9"); + expect(newFile!.hunks![1].changes[0].oldLineNumber).toBe(9); + expect(newFile!.hunks![1].changes[0].newLineNumber).toBe(9); + expect(newFile!.hunks![1].changes[1].content).toBe("new line 10"); + expect(newFile!.hunks![1].changes[1].oldLineNumber).toBe(10); + expect(newFile!.hunks![1].changes[1].newLineNumber).toBe(10); + expect(newFile!.hunks![1].changes[4].content).toBe("new line 13"); + expect(newFile!.hunks![1].changes[4].oldLineNumber).toBe(13); + expect(newFile!.hunks![1].changes[4].newLineNumber).toBe(13); + expect(newFile!.hunks![1].changes[5].content).toBe("line"); + expect(newFile!.hunks![1].changes[5].oldLineNumber).toBe(14); + expect(newFile!.hunks![1].changes[5].newLineNumber).toBe(14); }); it("should set fully expanded to true if expanded completely", async () => { fetchMock.get( "http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=40&end=50", "new line 40\nnew line 41\nnew line 42" ); - let newFile; + let newFile: File; diffExpander.getHunk(3).expandBottom(10, file => { newFile = file; }); await fetchMock.flush(true); - expect(newFile.hunks[3].fullyExpanded).toBe(true); + expect(newFile!.hunks![3].fullyExpanded).toBe(true); }); it("should set end to -1 if requested to expand to the end", async () => { fetchMock.get( "http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=40&end=-1", "new line 40\nnew line 41\nnew line 42" ); - let newFile; + let newFile: File; diffExpander.getHunk(3).expandBottom(-1, file => { newFile = file; }); await fetchMock.flush(true); - expect(newFile.hunks[3].fullyExpanded).toBe(true); + expect(newFile!.hunks![3].fullyExpanded).toBe(true); }); }); diff --git a/scm-ui/ui-components/src/repos/DiffExpander.ts b/scm-ui/ui-components/src/repos/DiffExpander.ts index 676569e95e..859b3034a0 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.ts @@ -24,6 +24,7 @@ import { apiClient } from "@scm-manager/ui-components"; import { Change, File, Hunk } from "./DiffTypes"; +import { Link } from "@scm-manager/ui-types/src"; class DiffExpander { file: File; @@ -67,9 +68,9 @@ class DiffExpander { }; expandHead = (n: number, count: number, callback: (newFile: File) => void) => { - const lineRequestUrl = this.file._links.lines.href - .replace("{start}", this.minLineNumber(n) - Math.min(count, this.computeMaxExpandHeadRange(n)) - 1) - .replace("{end}", this.minLineNumber(n) - 1); + const lineRequestUrl = (this.file._links!.lines as Link).href + .replace("{start}", (this.minLineNumber(n) - Math.min(count, this.computeMaxExpandHeadRange(n)) - 1).toString()) + .replace("{end}", (this.minLineNumber(n) - 1).toString()); apiClient .get(lineRequestUrl) .then(response => response.text()) @@ -79,9 +80,17 @@ class DiffExpander { expandBottom = (n: number, count: number, callback: (newFile: File) => void) => { const maxExpandBottomRange = this.computeMaxExpandBottomRange(n); - const lineRequestUrl = this.file._links.lines.href - .replace("{start}", this.maxLineNumber(n)) - .replace("{end}", count > 0 ? this.maxLineNumber(n) + Math.min(count, maxExpandBottomRange > 0? maxExpandBottomRange:Number.MAX_SAFE_INTEGER) : -1); + const lineRequestUrl = (this.file._links!.lines as Link).href + .replace("{start}", this.maxLineNumber(n).toString()) + .replace( + "{end}", + count > 0 + ? ( + this.maxLineNumber(n) + + Math.min(count, maxExpandBottomRange > 0 ? maxExpandBottomRange : Number.MAX_SAFE_INTEGER) + ).toString() + : "-1" + ); apiClient .get(lineRequestUrl) .then(response => response.text()) @@ -90,13 +99,13 @@ class DiffExpander { }; expandHunkAtHead = (n: number, lines: string[], callback: (newFile: File) => void) => { - const hunk = this.file.hunks[n]; + const hunk = this.file.hunks![n]; if (lines[lines.length - 1] === "") { lines.pop(); } const newChanges: Change[] = []; - let oldLineNumber = hunk.changes[0].oldLineNumber - lines.length; - let newLineNumber = hunk.changes[0].newLineNumber - lines.length; + let oldLineNumber = hunk!.changes![0]!.oldLineNumber! - lines.length; + let newLineNumber = hunk!.changes![0]!.newLineNumber! - lines.length; lines.forEach(line => { newChanges.push({ @@ -113,10 +122,10 @@ class DiffExpander { const newHunk = { ...hunk, - oldStart: hunk.oldStart - lines.length, - newStart: hunk.newStart - lines.length, - oldLines: hunk.oldLines + lines.length, - newLines: hunk.newLines + lines.length, + oldStart: hunk.oldStart! - lines.length, + newStart: hunk.newStart! - lines.length, + oldLines: hunk.oldLines! + lines.length, + newLines: hunk.newLines! + lines.length, changes: newChanges }; const newHunks: Hunk[] = []; @@ -137,8 +146,8 @@ class DiffExpander { lines.pop(); } const newChanges = [...hunk.changes]; - let oldLineNumber = newChanges[newChanges.length - 1].oldLineNumber; - let newLineNumber = newChanges[newChanges.length - 1].newLineNumber; + let oldLineNumber: number = newChanges[newChanges.length - 1].oldLineNumber!; + let newLineNumber: number = newChanges[newChanges.length - 1].newLineNumber!; lines.forEach(line => { oldLineNumber += 1; @@ -154,13 +163,13 @@ class DiffExpander { const newHunk = { ...hunk, - oldLines: hunk.oldLines + lines.length, - newLines: hunk.newLines + lines.length, + oldLines: hunk.oldLines! + lines.length, + newLines: hunk.newLines! + lines.length, changes: newChanges, fullyExpanded: requestedLines < 0 || lines.length < requestedLines }; const newHunks: Hunk[] = []; - this.file.hunks.forEach((oldHunk: Hunk, i: number) => { + this.file.hunks!.forEach((oldHunk: Hunk, i: number) => { if (i === n) { newHunks.push(newHunk); } else { diff --git a/scm-ui/ui-components/src/repos/DiffTypes.ts b/scm-ui/ui-components/src/repos/DiffTypes.ts index 54fb642e61..23afd3349d 100644 --- a/scm-ui/ui-components/src/repos/DiffTypes.ts +++ b/scm-ui/ui-components/src/repos/DiffTypes.ts @@ -47,7 +47,7 @@ export type File = { language?: string; // TODO does this property exists? isBinary?: boolean; - _links: Links; + _links?: Links; }; export type Hunk = { From a6727162994297a618e1c4d991cf2db2237f46de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Sat, 30 May 2020 21:35:00 +0200 Subject: [PATCH 14/38] update storyshots --- .../src/__snapshots__/storyshots.test.ts.snap | 900 ------------------ 1 file changed, 900 deletions(-) diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index 0a056f61cb..09c693ba41 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -944,18 +944,6 @@ exports[`Storyshots Diff Binaries 1`] = ` /> - - - - - - - @@ -1619,18 +1607,6 @@ exports[`Storyshots Diff CollapsingWithFunction 1`] = ` /> - - - - - - - @@ -1808,20 +1784,6 @@ exports[`Storyshots Diff CollapsingWithFunction 1`] = ` - - - -
- - - @@ -2471,18 +2433,6 @@ exports[`Storyshots Diff CollapsingWithFunction 1`] = ` /> - - - - - - - @@ -2919,18 +2869,6 @@ exports[`Storyshots Diff CollapsingWithFunction 1`] = ` /> - - - - - - - @@ -3488,18 +3426,6 @@ exports[`Storyshots Diff Default 1`] = ` /> - - - - - - - @@ -4063,18 +3989,6 @@ exports[`Storyshots Diff Default 1`] = ` /> - - - - - - - @@ -4252,20 +4166,6 @@ exports[`Storyshots Diff Default 1`] = ` - - - -
- - - @@ -4915,18 +4815,6 @@ exports[`Storyshots Diff Default 1`] = ` /> - - - - - - - @@ -5363,18 +5251,6 @@ exports[`Storyshots Diff Default 1`] = ` /> - - - - - - - @@ -5811,18 +5687,6 @@ exports[`Storyshots Diff Default 1`] = ` /> - - - - - - - @@ -6317,20 +6181,6 @@ exports[`Storyshots Diff Default 1`] = ` - - - -
- - - @@ -6531,20 +6381,6 @@ exports[`Storyshots Diff Default 1`] = ` - - - -
- - - @@ -6866,18 +6702,6 @@ exports[`Storyshots Diff Default 1`] = ` /> - - - - - - - @@ -7400,18 +7224,6 @@ exports[`Storyshots Diff File Annotation 1`] = ` /> - - - - - - - @@ -7979,18 +7791,6 @@ exports[`Storyshots Diff File Annotation 1`] = ` /> - - - - - - - @@ -8168,20 +7968,6 @@ exports[`Storyshots Diff File Annotation 1`] = ` - - - -
- - - @@ -8835,18 +8621,6 @@ exports[`Storyshots Diff File Annotation 1`] = ` /> - - - - - - - @@ -9287,18 +9061,6 @@ exports[`Storyshots Diff File Annotation 1`] = ` /> - - - - - - - @@ -9739,18 +9501,6 @@ exports[`Storyshots Diff File Annotation 1`] = ` /> - - - - - - - @@ -10245,20 +9995,6 @@ exports[`Storyshots Diff File Annotation 1`] = ` - - - -
- - - @@ -10459,20 +10195,6 @@ exports[`Storyshots Diff File Annotation 1`] = ` - - - -
- - - @@ -10798,18 +10520,6 @@ exports[`Storyshots Diff File Annotation 1`] = ` /> - - - - - - - @@ -11346,18 +11056,6 @@ exports[`Storyshots Diff File Controls 1`] = ` /> - - - - - - - @@ -11939,18 +11637,6 @@ exports[`Storyshots Diff File Controls 1`] = ` /> - - - - - - - @@ -12128,20 +11814,6 @@ exports[`Storyshots Diff File Controls 1`] = ` - - - -
- - - @@ -12809,18 +12481,6 @@ exports[`Storyshots Diff File Controls 1`] = ` /> - - - - - - - @@ -13275,18 +12935,6 @@ exports[`Storyshots Diff File Controls 1`] = ` /> - - - - - - - @@ -13741,18 +13389,6 @@ exports[`Storyshots Diff File Controls 1`] = ` /> - - - - - - - @@ -14247,20 +13883,6 @@ exports[`Storyshots Diff File Controls 1`] = ` - - - -
- - - @@ -14461,20 +14083,6 @@ exports[`Storyshots Diff File Controls 1`] = ` - - - -
- - - @@ -14814,18 +14422,6 @@ exports[`Storyshots Diff File Controls 1`] = ` /> - - - - - - - @@ -15344,18 +14940,6 @@ exports[`Storyshots Diff Hunks 1`] = ` /> - - - - - - - @@ -15556,20 +15140,6 @@ exports[`Storyshots Diff Hunks 1`] = ` - - - -
- - - @@ -15747,20 +15317,6 @@ exports[`Storyshots Diff Hunks 1`] = ` - - - -
- - - @@ -16181,18 +15737,6 @@ exports[`Storyshots Diff Line Annotation 1`] = ` /> - - - - - - - @@ -16768,18 +16312,6 @@ exports[`Storyshots Diff Line Annotation 1`] = ` /> - - - - - - - @@ -16969,20 +16501,6 @@ exports[`Storyshots Diff Line Annotation 1`] = ` - - - -
- - - @@ -17632,18 +17150,6 @@ exports[`Storyshots Diff Line Annotation 1`] = ` /> - - - - - - - @@ -18080,18 +17586,6 @@ exports[`Storyshots Diff Line Annotation 1`] = ` /> - - - - - - - @@ -18528,18 +18022,6 @@ exports[`Storyshots Diff Line Annotation 1`] = ` /> - - - - - - - @@ -19034,20 +18516,6 @@ exports[`Storyshots Diff Line Annotation 1`] = ` - - - -
- - - @@ -19248,20 +18716,6 @@ exports[`Storyshots Diff Line Annotation 1`] = ` - - - -
- - - @@ -19583,18 +19037,6 @@ exports[`Storyshots Diff Line Annotation 1`] = ` /> - - - - - - - @@ -20125,18 +19567,6 @@ exports[`Storyshots Diff OnClick 1`] = ` /> - - - - - - - @@ -20740,18 +20170,6 @@ exports[`Storyshots Diff OnClick 1`] = ` /> - - - - - - - @@ -20943,20 +20361,6 @@ exports[`Storyshots Diff OnClick 1`] = ` - - - -
- - - @@ -21654,18 +21058,6 @@ exports[`Storyshots Diff OnClick 1`] = ` /> - - - - - - - @@ -22132,18 +21524,6 @@ exports[`Storyshots Diff OnClick 1`] = ` /> - - - - - - - @@ -22610,18 +21990,6 @@ exports[`Storyshots Diff OnClick 1`] = ` /> - - - - - - - @@ -23156,20 +22524,6 @@ exports[`Storyshots Diff OnClick 1`] = ` - - - -
- - - @@ -23386,20 +22740,6 @@ exports[`Storyshots Diff OnClick 1`] = ` - - - -
- - - @@ -23741,18 +23081,6 @@ exports[`Storyshots Diff OnClick 1`] = ` /> - - - - - - - @@ -24308,18 +23636,6 @@ exports[`Storyshots Diff Side-By-Side 1`] = ` /> - - - - - - - @@ -24976,18 +24292,6 @@ exports[`Storyshots Diff Side-By-Side 1`] = ` /> - - - - - - - @@ -25208,20 +24512,6 @@ exports[`Storyshots Diff Side-By-Side 1`] = ` - - - -
- - - @@ -25918,18 +25208,6 @@ exports[`Storyshots Diff Side-By-Side 1`] = ` /> - - - - - - - @@ -26418,18 +25696,6 @@ exports[`Storyshots Diff Side-By-Side 1`] = ` /> - - - - - - - @@ -26918,18 +26184,6 @@ exports[`Storyshots Diff Side-By-Side 1`] = ` /> - - - - - - - @@ -27534,20 +26788,6 @@ exports[`Storyshots Diff Side-By-Side 1`] = ` - - - -
- - - @@ -27776,20 +27016,6 @@ exports[`Storyshots Diff Side-By-Side 1`] = ` - - - -
- - - @@ -28142,18 +27368,6 @@ exports[`Storyshots Diff Side-By-Side 1`] = ` /> - - - - - - - @@ -28744,18 +27958,6 @@ exports[`Storyshots Diff SyntaxHighlighting 1`] = ` /> - - - - - - - @@ -29319,18 +28521,6 @@ exports[`Storyshots Diff SyntaxHighlighting 1`] = ` /> - - - - - - - @@ -29508,20 +28698,6 @@ exports[`Storyshots Diff SyntaxHighlighting 1`] = ` - - - -
- - - @@ -30171,18 +29347,6 @@ exports[`Storyshots Diff SyntaxHighlighting 1`] = ` /> - - - - - - - @@ -30619,18 +29783,6 @@ exports[`Storyshots Diff SyntaxHighlighting 1`] = ` /> - - - - - - - @@ -31067,18 +30219,6 @@ exports[`Storyshots Diff SyntaxHighlighting 1`] = ` /> - - - - - - - @@ -31573,20 +30713,6 @@ exports[`Storyshots Diff SyntaxHighlighting 1`] = ` - - - -
- - - @@ -31787,20 +30913,6 @@ exports[`Storyshots Diff SyntaxHighlighting 1`] = ` - - - -
- - - @@ -32122,18 +31234,6 @@ exports[`Storyshots Diff SyntaxHighlighting 1`] = ` /> - - - - - - - From 2ad3773340fb9b0ae008b7757b0dccb062611ca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Sat, 30 May 2020 21:57:05 +0200 Subject: [PATCH 15/38] Fix unit test --- .../api/v2/resources/DiffResultToDiffResultDtoMapperTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 efc1cbfff0..8fca3ddff4 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 @@ -96,7 +96,7 @@ class DiffResultToDiffResultDtoMapperTest { .isPresent() .get() .extracting("href") - .isEqualTo("/scm/api/v2/repositories/space/X/content/123/B.ts?start={start}?end={end}"); + .isEqualTo("/scm/api/v2/repositories/space/X/content/123/B.ts?start={start}&end={end}"); } @Test From 4b54597e2670eef15ed8dcfd51c7c8dbbd203024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 1 Jun 2020 17:00:47 +0200 Subject: [PATCH 16/38] Fix expanding diffs without 'normal' line at the end --- .../src/repos/DiffExpander.test.ts | 91 ++++++++++++++++++- .../ui-components/src/repos/DiffExpander.ts | 26 +++++- scm-ui/ui-components/src/repos/DiffFile.tsx | 3 +- 3 files changed, 112 insertions(+), 8 deletions(-) diff --git a/scm-ui/ui-components/src/repos/DiffExpander.test.ts b/scm-ui/ui-components/src/repos/DiffExpander.test.ts index eb104a79cc..ac930498d9 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.test.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.test.ts @@ -112,7 +112,7 @@ const TEST_CONTENT_WITH_HUNKS: File = { } }; -const TEST_CONTENT_WIT_NEW_BINARY_FILE: File = { +const TEST_CONTENT_WITH_NEW_BINARY_FILE: File = { oldPath: "/dev/null", newPath: "src/main/fileUploadV2.png", oldEndingNewLine: true, @@ -171,6 +171,79 @@ const TEST_CONTENT_WITH_DELETED_TEXT_FILE: File = { _links: { lines: { href: "http://localhost:8081/dev/null?start={start}&end={end}", templated: true } } }; +const TEST_CONTENT_WITH_DELETED_LINES_AT_END: File = { + oldPath: "pom.xml", + newPath: "pom.xml", + oldEndingNewLine: true, + newEndingNewLine: true, + oldRevision: "b207512c0eab22536c9e5173afbe54cc3a24a22e", + newRevision: "5347c3fe0c2c4d4de7f308ae61bd5546460d7e93", + type: "modify", + language: "xml", + hunks: [ + { + content: "@@ -108,15 +108,3 @@", + oldStart: 108, + newStart: 108, + oldLines: 15, + newLines: 3, + changes: [ + { content: "line", type: "normal", oldLineNumber: 108, newLineNumber: 108, isNormal: true }, + { content: "line", type: "normal", oldLineNumber: 109, newLineNumber: 109, isNormal: true }, + { content: "line", type: "normal", oldLineNumber: 110, newLineNumber: 110, isNormal: true }, + { content: "line", type: "delete", lineNumber: 111, isDelete: true }, + { content: "line", type: "delete", lineNumber: 112, isDelete: true }, + { content: "line", type: "delete", lineNumber: 113, isDelete: true }, + { content: "line", type: "delete", lineNumber: 114, isDelete: true }, + { content: "line", type: "delete", lineNumber: 115, isDelete: true }, + { content: "line", type: "delete", lineNumber: 116, isDelete: true }, + { content: "line", type: "delete", lineNumber: 117, isDelete: true }, + { content: "line", type: "delete", lineNumber: 118, isDelete: true }, + { content: "line", type: "delete", lineNumber: 119, isDelete: true }, + { content: "line", type: "delete", lineNumber: 120, isDelete: true }, + { content: "line", type: "delete", lineNumber: 121, isDelete: true }, + { content: "line", type: "delete", lineNumber: 122, isDelete: true } + ] + } + ], + _links: { + lines: { + href: "http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start={start}&end={end}", + templated: true + } + } +}; + +const TEST_CONTENT_WITH_ALL_LINES_REMOVED_FROM_FILE: File = { + oldPath: "pom.xml", + newPath: "pom.xml", + oldEndingNewLine: true, + newEndingNewLine: true, + oldRevision: "2cc811c64f71ceda28f1ec0d97e1973395b299ff", + newRevision: "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + type: "modify", + language: "xml", + hunks: [ + { + content: "@@ -1,3 +0,0 @@", + oldStart: 1, + oldLines: 3, + changes: [ + { content: "line", type: "delete", lineNumber: 1, isDelete: true }, + { content: "line", type: "delete", lineNumber: 2, isDelete: true }, + { content: "line", type: "delete", lineNumber: 3, isDelete: true } + ] + } + ], + _links: { + lines: { + href: + "http://localhost:8081/scm/api/v2/repositories/scm-manager/scm-editor-plugin/content/b313a7690f028c77df98417c1ed6cba67e5692ec/pom.xml?start={start}&end={end}", + templated: true + } + } +}; + describe("with hunks the diff expander", () => { const diffExpander = new DiffExpander(TEST_CONTENT_WITH_HUNKS); @@ -291,8 +364,22 @@ describe("for a deleted file with text input the diff expander", () => { }); describe("for a new file with binary input the diff expander", () => { - const diffExpander = new DiffExpander(TEST_CONTENT_WIT_NEW_BINARY_FILE); + const diffExpander = new DiffExpander(TEST_CONTENT_WITH_NEW_BINARY_FILE); it("should create answer for no hunk", () => { expect(diffExpander.hunkCount()).toBe(0); }); }); + +describe("with deleted lines at the end", () => { + const diffExpander = new DiffExpander(TEST_CONTENT_WITH_DELETED_LINES_AT_END); + it("should not be expandable", () => { + expect(diffExpander.getHunk(0)!.maxExpandBottomRange).toBe(0); + }); +}); + +describe("with all lines removed from file", () => { + const diffExpander = new DiffExpander(TEST_CONTENT_WITH_ALL_LINES_REMOVED_FROM_FILE); + it("should not be expandable", () => { + expect(diffExpander.getHunk(0)!.maxExpandBottomRange).toBe(0); + }); +}); diff --git a/scm-ui/ui-components/src/repos/DiffExpander.ts b/scm-ui/ui-components/src/repos/DiffExpander.ts index 859b3034a0..c4f42c620e 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.ts @@ -61,10 +61,16 @@ class DiffExpander { computeMaxExpandBottomRange = (n: number) => { if (this.file.type === "add" || this.file.type === "delete") { return 0; - } else if (n === this.file!.hunks!.length - 1) { - return this.file!.hunks![this.file!.hunks!.length - 1].fullyExpanded ? 0 : -1; } - return this.minLineNumber(n + 1) - this.maxLineNumber(n) - 1; + const changes = this.file.hunks![n].changes; + if (changes[changes.length - 1].type === "normal") { + if (n === this.file!.hunks!.length - 1) { + return this.file!.hunks![this.file!.hunks!.length - 1].fullyExpanded ? 0 : -1; + } + return this.minLineNumber(n + 1) - this.maxLineNumber(n) - 1; + } else { + return 0; + } }; expandHead = (n: number, count: number, callback: (newFile: File) => void) => { @@ -146,8 +152,8 @@ class DiffExpander { lines.pop(); } const newChanges = [...hunk.changes]; - let oldLineNumber: number = newChanges[newChanges.length - 1].oldLineNumber!; - let newLineNumber: number = newChanges[newChanges.length - 1].newLineNumber!; + let oldLineNumber: number = this.getMaxOldLineNumber(newChanges); + let newLineNumber: number = this.getMaxNewLineNumber(newChanges); lines.forEach(line => { oldLineNumber += 1; @@ -180,6 +186,16 @@ class DiffExpander { callback(newFile); }; + getMaxOldLineNumber = (newChanges: Change[]) => { + const lastChange = newChanges[newChanges.length - 1]; + return lastChange.oldLineNumber || lastChange.lineNumber!; + }; + + getMaxNewLineNumber = (newChanges: Change[]) => { + const lastChange = newChanges[newChanges.length - 1]; + return lastChange.newLineNumber || lastChange.lineNumber!; + }; + getHunk: (n: number) => ExpandableHunk = (n: number) => { return { maxExpandHeadRange: this.computeMaxExpandHeadRange(n), diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx index 5781f3f284..06f254401d 100644 --- a/scm-ui/ui-components/src/repos/DiffFile.tsx +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -212,7 +212,8 @@ class DiffFile extends React.Component { }; createLastHunkFooter = (expandableHunk: ExpandableHunk) => { - if (expandableHunk.maxExpandBottomRange != 0) { + console.log("maxExpandBottomRange:", expandableHunk.maxExpandBottomRange); + if (expandableHunk.maxExpandBottomRange !== 0) { return ( From 586d9ac0d371ed3a6ef4fda7e591d0cc72540be0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 9 Jun 2020 07:23:52 +0200 Subject: [PATCH 17/38] Remove log --- scm-ui/ui-components/src/repos/DiffFile.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx index 06f254401d..574259ae3e 100644 --- a/scm-ui/ui-components/src/repos/DiffFile.tsx +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -212,7 +212,6 @@ class DiffFile extends React.Component { }; createLastHunkFooter = (expandableHunk: ExpandableHunk) => { - console.log("maxExpandBottomRange:", expandableHunk.maxExpandBottomRange); if (expandableHunk.maxExpandBottomRange !== 0) { return ( From fde26cd8b799cdbba1f8d85905e2d75f43a2ce09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 9 Jun 2020 10:49:58 +0200 Subject: [PATCH 18/38] Create new hunks instead of expanding the existing ones This is necessary to distinguish between "real" diff lines and those created due to expansion. --- .../src/repos/DiffExpander.test.ts | 33 +++++++++---- .../ui-components/src/repos/DiffExpander.ts | 46 +++++++++++-------- 2 files changed, 49 insertions(+), 30 deletions(-) diff --git a/scm-ui/ui-components/src/repos/DiffExpander.test.ts b/scm-ui/ui-components/src/repos/DiffExpander.test.ts index ac930498d9..cefeb7085d 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.test.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.test.ts @@ -275,8 +275,11 @@ describe("with hunks the diff expander", () => { it("should return a really bix number for the expand bottom range of the last hunk", () => { expect(diffExpander.getHunk(3).maxExpandBottomRange).toBe(-1); }); - it("should expand hunk with new line from api client at the bottom", async () => { + it("should create new hunk with new line from api client at the bottom", async () => { expect(diffExpander.getHunk(1).hunk.changes.length).toBe(7); + const oldHunkCount = diffExpander.hunkCount(); + const expandedHunk = diffExpander.getHunk(1).hunk; + const subsequentHunk = diffExpander.getHunk(2).hunk; fetchMock.get("http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=20&end=21", "new line 1"); let newFile: File; diffExpander.getHunk(1).expandBottom(1, file => { @@ -284,11 +287,18 @@ describe("with hunks the diff expander", () => { }); await fetchMock.flush(true); expect(fetchMock.done()).toBe(true); - expect(newFile!.hunks![1].changes.length).toBe(8); - expect(newFile!.hunks![1].changes[7].content).toBe("new line 1"); + expect(newFile!.hunks!.length).toBe(oldHunkCount + 1); + expect(newFile!.hunks![1]).toBe(expandedHunk); + const newHunk = newFile!.hunks![2].changes; + expect(newHunk.length).toBe(1); + expect(newHunk[0].content).toBe("new line 1"); + expect(newFile!.hunks![3]).toBe(subsequentHunk); }); - it("should expand hunk with new line from api client at the top", async () => { + it("should create new hunk with new line from api client at the top", async () => { expect(diffExpander.getHunk(1).hunk.changes.length).toBe(7); + const oldHunkCount = diffExpander.hunkCount(); + const expandedHunk = diffExpander.getHunk(1).hunk; + const preceedingHunk = diffExpander.getHunk(0).hunk; fetchMock.get( "http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=8&end=13", "new line 9\nnew line 10\nnew line 11\nnew line 12\nnew line 13" @@ -299,7 +309,11 @@ describe("with hunks the diff expander", () => { }); await fetchMock.flush(true); expect(fetchMock.done()).toBe(true); - expect(newFile!.hunks![1].changes.length).toBe(12); + expect(newFile!.hunks!.length).toBe(oldHunkCount + 1); + expect(newFile!.hunks![0]).toBe(preceedingHunk); + expect(newFile!.hunks![2]).toBe(expandedHunk); + + expect(newFile!.hunks![1].changes.length).toBe(5); expect(newFile!.hunks![1].changes[0].content).toBe("new line 9"); expect(newFile!.hunks![1].changes[0].oldLineNumber).toBe(9); expect(newFile!.hunks![1].changes[0].newLineNumber).toBe(9); @@ -309,11 +323,9 @@ describe("with hunks the diff expander", () => { expect(newFile!.hunks![1].changes[4].content).toBe("new line 13"); expect(newFile!.hunks![1].changes[4].oldLineNumber).toBe(13); expect(newFile!.hunks![1].changes[4].newLineNumber).toBe(13); - expect(newFile!.hunks![1].changes[5].content).toBe("line"); - expect(newFile!.hunks![1].changes[5].oldLineNumber).toBe(14); - expect(newFile!.hunks![1].changes[5].newLineNumber).toBe(14); }); it("should set fully expanded to true if expanded completely", async () => { + const oldHunkCount = diffExpander.hunkCount(); fetchMock.get( "http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=40&end=50", "new line 40\nnew line 41\nnew line 42" @@ -323,7 +335,8 @@ describe("with hunks the diff expander", () => { newFile = file; }); await fetchMock.flush(true); - expect(newFile!.hunks![3].fullyExpanded).toBe(true); + expect(newFile!.hunks!.length).toBe(oldHunkCount + 1); + expect(newFile!.hunks![4].fullyExpanded).toBe(true); }); it("should set end to -1 if requested to expand to the end", async () => { fetchMock.get( @@ -335,7 +348,7 @@ describe("with hunks the diff expander", () => { newFile = file; }); await fetchMock.flush(true); - expect(newFile!.hunks![3].fullyExpanded).toBe(true); + expect(newFile!.hunks![4].fullyExpanded).toBe(true); }); }); diff --git a/scm-ui/ui-components/src/repos/DiffExpander.ts b/scm-ui/ui-components/src/repos/DiffExpander.ts index c4f42c620e..9b94daec15 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.ts @@ -110,8 +110,10 @@ class DiffExpander { lines.pop(); } const newChanges: Change[] = []; - let oldLineNumber = hunk!.changes![0]!.oldLineNumber! - lines.length; - let newLineNumber = hunk!.changes![0]!.newLineNumber! - lines.length; + const minOldLineNumberOfNewHunk = hunk.oldStart! - lines.length; + const minNewLineNumberOfNewHunk = hunk.newStart! - lines.length; + let oldLineNumber = minOldLineNumberOfNewHunk; + let newLineNumber = minNewLineNumberOfNewHunk; lines.forEach(line => { newChanges.push({ @@ -124,23 +126,21 @@ class DiffExpander { oldLineNumber += 1; newLineNumber += 1; }); - hunk.changes.forEach(change => newChanges.push(change)); - const newHunk = { - ...hunk, - oldStart: hunk.oldStart! - lines.length, - newStart: hunk.newStart! - lines.length, - oldLines: hunk.oldLines! + lines.length, - newLines: hunk.newLines! + lines.length, + const newHunk: Hunk = { + content: "", + oldStart: minOldLineNumberOfNewHunk, + newStart: minNewLineNumberOfNewHunk, + oldLines: lines.length, + newLines: lines.length, changes: newChanges }; const newHunks: Hunk[] = []; this.file.hunks!.forEach((oldHunk: Hunk, i: number) => { if (i === n) { newHunks.push(newHunk); - } else { - newHunks.push(oldHunk); } + newHunks.push(oldHunk); }); const newFile = { ...this.file, hunks: newHunks }; callback(newFile); @@ -151,9 +151,13 @@ class DiffExpander { if (lines[lines.length - 1] === "") { lines.pop(); } - const newChanges = [...hunk.changes]; - let oldLineNumber: number = this.getMaxOldLineNumber(newChanges); - let newLineNumber: number = this.getMaxNewLineNumber(newChanges); + const newChanges: Change[] = []; + + const maxOldLineNumberFromPrecedingHunk = this.getMaxOldLineNumber(hunk.changes); + const maxNewLineNumberFromPrecedingHunk = this.getMaxNewLineNumber(hunk.changes); + + let oldLineNumber: number = maxOldLineNumberFromPrecedingHunk; + let newLineNumber: number = maxNewLineNumberFromPrecedingHunk; lines.forEach(line => { oldLineNumber += 1; @@ -167,19 +171,21 @@ class DiffExpander { }); }); - const newHunk = { - ...hunk, - oldLines: hunk.oldLines! + lines.length, - newLines: hunk.newLines! + lines.length, + const newHunk: Hunk = { changes: newChanges, + content: "", + oldStart: maxOldLineNumberFromPrecedingHunk + 1, + newStart: maxNewLineNumberFromPrecedingHunk + 1, + oldLines: lines.length, + newLines: lines.length, fullyExpanded: requestedLines < 0 || lines.length < requestedLines }; + const newHunks: Hunk[] = []; this.file.hunks!.forEach((oldHunk: Hunk, i: number) => { + newHunks.push(oldHunk); if (i === n) { newHunks.push(newHunk); - } else { - newHunks.push(oldHunk); } }); const newFile = { ...this.file, hunks: newHunks }; From 5d7641f1293b7822f788f269fd1017739031eeea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 9 Jun 2020 13:34:40 +0200 Subject: [PATCH 19/38] Use source branch for lines --- .../scm/api/v2/resources/DiffResultToDiffResultDtoMapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 03d1293083..0384ed7d45 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 @@ -56,7 +56,7 @@ class DiffResultToDiffResultDtoMapper { 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, repository, target); + setFiles(result, dto, repository, source); return dto; } From d909ff4ae48739789b3367ba8e78b2bbc32c6fec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 9 Jun 2020 13:57:44 +0200 Subject: [PATCH 20/38] Add marker for expansion hunks --- .../src/repos/DiffExpander.test.ts | 31 +++++++++++-------- .../ui-components/src/repos/DiffExpander.ts | 4 ++- scm-ui/ui-components/src/repos/DiffTypes.ts | 1 + 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/scm-ui/ui-components/src/repos/DiffExpander.test.ts b/scm-ui/ui-components/src/repos/DiffExpander.test.ts index cefeb7085d..fcdaaa79f7 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.test.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.test.ts @@ -289,9 +289,12 @@ describe("with hunks the diff expander", () => { expect(fetchMock.done()).toBe(true); expect(newFile!.hunks!.length).toBe(oldHunkCount + 1); expect(newFile!.hunks![1]).toBe(expandedHunk); - const newHunk = newFile!.hunks![2].changes; - expect(newHunk.length).toBe(1); - expect(newHunk[0].content).toBe("new line 1"); + + const newHunk = newFile!.hunks![2]; + expect(newHunk.changes.length).toBe(1); + expect(newHunk.changes[0].content).toBe("new line 1"); + expect(newHunk.expansion).toBe(true); + expect(newFile!.hunks![3]).toBe(subsequentHunk); }); it("should create new hunk with new line from api client at the top", async () => { @@ -313,16 +316,18 @@ describe("with hunks the diff expander", () => { expect(newFile!.hunks![0]).toBe(preceedingHunk); expect(newFile!.hunks![2]).toBe(expandedHunk); - expect(newFile!.hunks![1].changes.length).toBe(5); - expect(newFile!.hunks![1].changes[0].content).toBe("new line 9"); - expect(newFile!.hunks![1].changes[0].oldLineNumber).toBe(9); - expect(newFile!.hunks![1].changes[0].newLineNumber).toBe(9); - expect(newFile!.hunks![1].changes[1].content).toBe("new line 10"); - expect(newFile!.hunks![1].changes[1].oldLineNumber).toBe(10); - expect(newFile!.hunks![1].changes[1].newLineNumber).toBe(10); - expect(newFile!.hunks![1].changes[4].content).toBe("new line 13"); - expect(newFile!.hunks![1].changes[4].oldLineNumber).toBe(13); - expect(newFile!.hunks![1].changes[4].newLineNumber).toBe(13); + const newHunk = newFile!.hunks![1]; + expect(newHunk.changes.length).toBe(5); + expect(newHunk.changes[0].content).toBe("new line 9"); + expect(newHunk.changes[0].oldLineNumber).toBe(9); + expect(newHunk.changes[0].newLineNumber).toBe(9); + expect(newHunk.changes[1].content).toBe("new line 10"); + expect(newHunk.changes[1].oldLineNumber).toBe(10); + expect(newHunk.changes[1].newLineNumber).toBe(10); + expect(newHunk.changes[4].content).toBe("new line 13"); + expect(newHunk.changes[4].oldLineNumber).toBe(13); + expect(newHunk.changes[4].newLineNumber).toBe(13); + expect(newHunk.expansion).toBe(true); }); it("should set fully expanded to true if expanded completely", async () => { const oldHunkCount = diffExpander.hunkCount(); diff --git a/scm-ui/ui-components/src/repos/DiffExpander.ts b/scm-ui/ui-components/src/repos/DiffExpander.ts index 9b94daec15..f21834c174 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.ts @@ -133,7 +133,8 @@ class DiffExpander { newStart: minNewLineNumberOfNewHunk, oldLines: lines.length, newLines: lines.length, - changes: newChanges + changes: newChanges, + expansion: true }; const newHunks: Hunk[] = []; this.file.hunks!.forEach((oldHunk: Hunk, i: number) => { @@ -178,6 +179,7 @@ class DiffExpander { newStart: maxNewLineNumberFromPrecedingHunk + 1, oldLines: lines.length, newLines: lines.length, + expansion: true, fullyExpanded: requestedLines < 0 || lines.length < requestedLines }; diff --git a/scm-ui/ui-components/src/repos/DiffTypes.ts b/scm-ui/ui-components/src/repos/DiffTypes.ts index 23afd3349d..cd004ccd44 100644 --- a/scm-ui/ui-components/src/repos/DiffTypes.ts +++ b/scm-ui/ui-components/src/repos/DiffTypes.ts @@ -58,6 +58,7 @@ export type Hunk = { oldLines?: number; newLines?: number; fullyExpanded?: boolean; + expansion?: boolean; }; export type ChangeType = "insert" | "delete" | "normal" | "conflict"; From 237bdaf89c10e9593abfe6577a332eb6a9f0e177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 9 Jun 2020 14:39:12 +0200 Subject: [PATCH 21/38] Extract common code --- .../ui-components/src/repos/DiffExpander.ts | 134 +++++++----------- 1 file changed, 54 insertions(+), 80 deletions(-) diff --git a/scm-ui/ui-components/src/repos/DiffExpander.ts b/scm-ui/ui-components/src/repos/DiffExpander.ts index f21834c174..9eeca6591c 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.ts @@ -74,95 +74,77 @@ class DiffExpander { }; expandHead = (n: number, count: number, callback: (newFile: File) => void) => { - const lineRequestUrl = (this.file._links!.lines as Link).href - .replace("{start}", (this.minLineNumber(n) - Math.min(count, this.computeMaxExpandHeadRange(n)) - 1).toString()) - .replace("{end}", (this.minLineNumber(n) - 1).toString()); - apiClient - .get(lineRequestUrl) - .then(response => response.text()) - .then(text => text.split("\n")) - .then(lines => this.expandHunkAtHead(n, lines, callback)); + const start = this.minLineNumber(n) - Math.min(count, this.computeMaxExpandHeadRange(n)) - 1; + const end = this.minLineNumber(n) - 1; + this.loadLines(start, end).then(lines => { + const hunk = this.file.hunks![n]; + + const newHunk = this.createNewHunk( + hunk.oldStart! - lines.length, + hunk.newStart! - lines.length, + lines, + lines.length + ); + + const newFile = this.addHunkToFile(newHunk, n); + callback(newFile); + }); }; expandBottom = (n: number, count: number, callback: (newFile: File) => void) => { const maxExpandBottomRange = this.computeMaxExpandBottomRange(n); - const lineRequestUrl = (this.file._links!.lines as Link).href - .replace("{start}", this.maxLineNumber(n).toString()) - .replace( - "{end}", - count > 0 - ? ( - this.maxLineNumber(n) + - Math.min(count, maxExpandBottomRange > 0 ? maxExpandBottomRange : Number.MAX_SAFE_INTEGER) - ).toString() - : "-1" + const start = this.maxLineNumber(n); + const end = + count > 0 + ? start + Math.min(count, maxExpandBottomRange > 0 ? maxExpandBottomRange : Number.MAX_SAFE_INTEGER) + : -1; + this.loadLines(start, end).then(lines => { + const hunk = this.file.hunks![n]; + + const newHunk: Hunk = this.createNewHunk( + this.getMaxOldLineNumber(hunk.changes) + 1, + this.getMaxNewLineNumber(hunk.changes) + 1, + lines, + count ); - apiClient + + const newFile = this.addHunkToFile(newHunk, n + 1); + callback(newFile); + }); + }; + + loadLines = (start: number, end: number) => { + const lineRequestUrl = (this.file._links!.lines as Link).href + .replace("{start}", start.toString()) + .replace("{end}", end.toString()); + return apiClient .get(lineRequestUrl) .then(response => response.text()) .then(text => text.split("\n")) - .then(lines => this.expandHunkAtBottom(n, count, lines, callback)); + .then(lines => (lines[lines.length - 1] === "" ? lines.slice(0, lines.length - 1) : lines)); }; - expandHunkAtHead = (n: number, lines: string[], callback: (newFile: File) => void) => { - const hunk = this.file.hunks![n]; - if (lines[lines.length - 1] === "") { - lines.pop(); - } - const newChanges: Change[] = []; - const minOldLineNumberOfNewHunk = hunk.oldStart! - lines.length; - const minNewLineNumberOfNewHunk = hunk.newStart! - lines.length; - let oldLineNumber = minOldLineNumberOfNewHunk; - let newLineNumber = minNewLineNumberOfNewHunk; - - lines.forEach(line => { - newChanges.push({ - content: line, - type: "normal", - oldLineNumber, - newLineNumber, - isNormal: true - }); - oldLineNumber += 1; - newLineNumber += 1; - }); - - const newHunk: Hunk = { - content: "", - oldStart: minOldLineNumberOfNewHunk, - newStart: minNewLineNumberOfNewHunk, - oldLines: lines.length, - newLines: lines.length, - changes: newChanges, - expansion: true - }; + addHunkToFile = (newHunk: Hunk, position: number) => { const newHunks: Hunk[] = []; this.file.hunks!.forEach((oldHunk: Hunk, i: number) => { - if (i === n) { + if (i === position) { newHunks.push(newHunk); } newHunks.push(oldHunk); }); - const newFile = { ...this.file, hunks: newHunks }; - callback(newFile); + if (position === newHunks.length) { + newHunks.push(newHunk); + } + return { ...this.file, hunks: newHunks }; }; - expandHunkAtBottom = (n: number, requestedLines: number, lines: string[], callback: (newFile: File) => void) => { - const hunk = this.file.hunks![n]; - if (lines[lines.length - 1] === "") { - lines.pop(); - } + createNewHunk = (oldFirstLineNumber: number, newFirstLineNumber: number, lines: string[], requestedLines: number) => { const newChanges: Change[] = []; - const maxOldLineNumberFromPrecedingHunk = this.getMaxOldLineNumber(hunk.changes); - const maxNewLineNumberFromPrecedingHunk = this.getMaxNewLineNumber(hunk.changes); - - let oldLineNumber: number = maxOldLineNumberFromPrecedingHunk; - let newLineNumber: number = maxNewLineNumberFromPrecedingHunk; + let oldLineNumber: number = oldFirstLineNumber; + let newLineNumber: number = newFirstLineNumber; lines.forEach(line => { - oldLineNumber += 1; - newLineNumber += 1; newChanges.push({ content: line, type: "normal", @@ -170,28 +152,20 @@ class DiffExpander { newLineNumber, isNormal: true }); + oldLineNumber += 1; + newLineNumber += 1; }); - const newHunk: Hunk = { + return { changes: newChanges, content: "", - oldStart: maxOldLineNumberFromPrecedingHunk + 1, - newStart: maxNewLineNumberFromPrecedingHunk + 1, + oldStart: oldFirstLineNumber, + newStart: newFirstLineNumber, oldLines: lines.length, newLines: lines.length, expansion: true, fullyExpanded: requestedLines < 0 || lines.length < requestedLines }; - - const newHunks: Hunk[] = []; - this.file.hunks!.forEach((oldHunk: Hunk, i: number) => { - newHunks.push(oldHunk); - if (i === n) { - newHunks.push(newHunk); - } - }); - const newFile = { ...this.file, hunks: newHunks }; - callback(newFile); }; getMaxOldLineNumber = (newChanges: Change[]) => { From 5b35328e420847820c9c9758e67374a5f917b4a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 9 Jun 2020 15:23:14 +0200 Subject: [PATCH 22/38] Allow hunk specific class names --- scm-ui/ui-components/src/repos/DiffFile.tsx | 2 ++ scm-ui/ui-components/src/repos/DiffTypes.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx index 574259ae3e..4b9b37c5a4 100644 --- a/scm-ui/ui-components/src/repos/DiffFile.tsx +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -277,12 +277,14 @@ class DiffFile extends React.Component { if (file._links?.lines) { items.push(this.createHunkHeader(expandableHunk)); } + items.push( ); if (file._links?.lines) { diff --git a/scm-ui/ui-components/src/repos/DiffTypes.ts b/scm-ui/ui-components/src/repos/DiffTypes.ts index cd004ccd44..c128f1ed33 100644 --- a/scm-ui/ui-components/src/repos/DiffTypes.ts +++ b/scm-ui/ui-components/src/repos/DiffTypes.ts @@ -111,4 +111,5 @@ export type DiffObjectProps = { annotationFactory?: AnnotationFactory; markConflicts?: boolean; defaultCollapse?: DefaultCollapsed; + hunkClass?: (hunk: Hunk) => string; }; From 850df6a641c3ab45130fa3f2d3eec130e7e263b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 9 Jun 2020 16:53:36 +0200 Subject: [PATCH 23/38] Use promise instead of callback --- .../src/repos/DiffExpander.test.ts | 31 ++++++++++--------- .../ui-components/src/repos/DiffExpander.ts | 24 +++++++------- scm-ui/ui-components/src/repos/DiffFile.tsx | 18 +++++------ 3 files changed, 36 insertions(+), 37 deletions(-) diff --git a/scm-ui/ui-components/src/repos/DiffExpander.test.ts b/scm-ui/ui-components/src/repos/DiffExpander.test.ts index fcdaaa79f7..3534cf77d2 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.test.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.test.ts @@ -282,10 +282,10 @@ describe("with hunks the diff expander", () => { const subsequentHunk = diffExpander.getHunk(2).hunk; fetchMock.get("http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=20&end=21", "new line 1"); let newFile: File; - diffExpander.getHunk(1).expandBottom(1, file => { - newFile = file; - }); - await fetchMock.flush(true); + await diffExpander + .getHunk(1) + .expandBottom(1) + .then(file => (newFile = file)); expect(fetchMock.done()).toBe(true); expect(newFile!.hunks!.length).toBe(oldHunkCount + 1); expect(newFile!.hunks![1]).toBe(expandedHunk); @@ -307,10 +307,10 @@ describe("with hunks the diff expander", () => { "new line 9\nnew line 10\nnew line 11\nnew line 12\nnew line 13" ); let newFile: File; - diffExpander.getHunk(1).expandHead(5, file => { - newFile = file; - }); - await fetchMock.flush(true); + await diffExpander + .getHunk(1) + .expandHead(5) + .then(file => (newFile = file)); expect(fetchMock.done()).toBe(true); expect(newFile!.hunks!.length).toBe(oldHunkCount + 1); expect(newFile!.hunks![0]).toBe(preceedingHunk); @@ -336,10 +336,10 @@ describe("with hunks the diff expander", () => { "new line 40\nnew line 41\nnew line 42" ); let newFile: File; - diffExpander.getHunk(3).expandBottom(10, file => { - newFile = file; - }); - await fetchMock.flush(true); + await diffExpander + .getHunk(3) + .expandBottom(10) + .then(file => (newFile = file)); expect(newFile!.hunks!.length).toBe(oldHunkCount + 1); expect(newFile!.hunks![4].fullyExpanded).toBe(true); }); @@ -349,9 +349,10 @@ describe("with hunks the diff expander", () => { "new line 40\nnew line 41\nnew line 42" ); let newFile: File; - diffExpander.getHunk(3).expandBottom(-1, file => { - newFile = file; - }); + await diffExpander + .getHunk(3) + .expandBottom(-1) + .then(file => (newFile = file)); await fetchMock.flush(true); expect(newFile!.hunks![4].fullyExpanded).toBe(true); }); diff --git a/scm-ui/ui-components/src/repos/DiffExpander.ts b/scm-ui/ui-components/src/repos/DiffExpander.ts index 9eeca6591c..cd348908fe 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.ts @@ -73,10 +73,10 @@ class DiffExpander { } }; - expandHead = (n: number, count: number, callback: (newFile: File) => void) => { + expandHead: (n: number, count: number) => Promise = (n, count) => { const start = this.minLineNumber(n) - Math.min(count, this.computeMaxExpandHeadRange(n)) - 1; const end = this.minLineNumber(n) - 1; - this.loadLines(start, end).then(lines => { + return this.loadLines(start, end).then(lines => { const hunk = this.file.hunks![n]; const newHunk = this.createNewHunk( @@ -86,19 +86,18 @@ class DiffExpander { lines.length ); - const newFile = this.addHunkToFile(newHunk, n); - callback(newFile); + return this.addHunkToFile(newHunk, n); }); }; - expandBottom = (n: number, count: number, callback: (newFile: File) => void) => { + expandBottom: (n: number, count: number) => Promise = (n, count) => { const maxExpandBottomRange = this.computeMaxExpandBottomRange(n); const start = this.maxLineNumber(n); const end = count > 0 ? start + Math.min(count, maxExpandBottomRange > 0 ? maxExpandBottomRange : Number.MAX_SAFE_INTEGER) : -1; - this.loadLines(start, end).then(lines => { + return this.loadLines(start, end).then(lines => { const hunk = this.file.hunks![n]; const newHunk: Hunk = this.createNewHunk( @@ -108,8 +107,7 @@ class DiffExpander { count ); - const newFile = this.addHunkToFile(newHunk, n + 1); - callback(newFile); + return this.addHunkToFile(newHunk, n + 1); }); }; @@ -178,12 +176,12 @@ class DiffExpander { return lastChange.newLineNumber || lastChange.lineNumber!; }; - getHunk: (n: number) => ExpandableHunk = (n: number) => { + getHunk: (n: number) => ExpandableHunk = n => { return { maxExpandHeadRange: this.computeMaxExpandHeadRange(n), maxExpandBottomRange: this.computeMaxExpandBottomRange(n), - expandHead: (count: number, callback: (newFile: File) => void) => this.expandHead(n, count, callback), - expandBottom: (count: number, callback: (newFile: File) => void) => this.expandBottom(n, count, callback), + expandHead: (count: number) => this.expandHead(n, count), + expandBottom: (count: number) => this.expandBottom(n, count), hunk: this.file?.hunks![n] }; }; @@ -193,8 +191,8 @@ export type ExpandableHunk = { hunk: Hunk; maxExpandHeadRange: number; maxExpandBottomRange: number; - expandHead: (count: number, callback: (newFile: File) => void) => void; - expandBottom: (count: number, callback: (newFile: File) => void) => void; + expandHead: (count: number) => Promise; + expandBottom: (count: number) => Promise; }; export default DiffExpander; diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx index 4b9b37c5a4..6b39407830 100644 --- a/scm-ui/ui-components/src/repos/DiffFile.tsx +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -78,7 +78,7 @@ const ButtonWrapper = styled.div` `; const HunkDivider = styled.div` - background: #33b2e8; + background: #98d8f3; font-size: 0.7rem; `; @@ -155,7 +155,7 @@ class DiffFile extends React.Component { return ( - expandableHunk.expandHead(expandableHunk.maxExpandHeadRange, this.diffExpanded)}> + expandableHunk.expandHead(expandableHunk.maxExpandHeadRange).then(this.diffExpanded)}> {this.props.t("diff.expandHeadComplete", { count: expandableHunk.maxExpandHeadRange })} @@ -165,10 +165,10 @@ class DiffFile extends React.Component { return ( - expandableHunk.expandHead(10, this.diffExpanded)}> + expandableHunk.expandHead(10).then(this.diffExpanded)}> {this.props.t("diff.expandHeadByLines", { count: 10 })} {" "} - expandableHunk.expandHead(expandableHunk.maxExpandHeadRange, this.diffExpanded)}> + expandableHunk.expandHead(expandableHunk.maxExpandHeadRange).then(this.diffExpanded)}> {this.props.t("diff.expandHeadComplete", { count: expandableHunk.maxExpandHeadRange })} @@ -186,7 +186,7 @@ class DiffFile extends React.Component { return ( - expandableHunk.expandBottom(expandableHunk.maxExpandBottomRange, this.diffExpanded)}> + expandableHunk.expandBottom(expandableHunk.maxExpandBottomRange).then(this.diffExpanded)}> {this.props.t("diff.expandBottomComplete", { count: expandableHunk.maxExpandBottomRange })} @@ -196,10 +196,10 @@ class DiffFile extends React.Component { return ( - expandableHunk.expandBottom(10, this.diffExpanded)}> + expandableHunk.expandBottom(10).then(this.diffExpanded)}> {this.props.t("diff.expandBottomByLines", { count: 10 })} {" "} - expandableHunk.expandBottom(expandableHunk.maxExpandBottomRange, this.diffExpanded)}> + expandableHunk.expandBottom(expandableHunk.maxExpandBottomRange).then(this.diffExpanded)}> {this.props.t("diff.expandBottomComplete", { count: expandableHunk.maxExpandBottomRange })} @@ -216,10 +216,10 @@ class DiffFile extends React.Component { return ( - expandableHunk.expandBottom(10, this.diffExpanded)}> + expandableHunk.expandBottom(10).then(this.diffExpanded)}> {this.props.t("diff.expandLastBottomByLines")} {" "} - expandableHunk.expandBottom(expandableHunk.maxExpandBottomRange, this.diffExpanded)}> + expandableHunk.expandBottom(expandableHunk.maxExpandBottomRange).then(this.diffExpanded)}> {this.props.t("diff.expandLastBottomComplete")} From 2ca15316fd257b39e7bab115e27fa706dc5c9fe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 10 Jun 2020 10:09:57 +0200 Subject: [PATCH 24/38] Prepare error handling for expansion --- scm-ui/ui-components/src/repos/DiffFile.tsx | 53 +++++++++++++++------ 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx index 6b39407830..4f6fceef93 100644 --- a/scm-ui/ui-components/src/repos/DiffFile.tsx +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -35,6 +35,7 @@ import TokenizedDiffView from "./TokenizedDiffView"; import DiffButton from "./DiffButton"; import { MenuContext } from "@scm-manager/ui-components"; import DiffExpander, { ExpandableHunk } from "./DiffExpander"; +import HunkExpandLink from "./HunkExpandLink"; const EMPTY_ANNOTATION_FACTORY = {}; @@ -51,6 +52,7 @@ type State = Collapsible & { file: File; sideBySide?: boolean; diffExpander: DiffExpander; + expansionError?: any; }; const DiffFilePanel = styled.div` @@ -145,17 +147,13 @@ class DiffFile extends React.Component { }); }; - diffExpanded = (newFile: File) => { - this.setState({ file: newFile, diffExpander: new DiffExpander(newFile) }); - }; - createHunkHeader = (expandableHunk: ExpandableHunk) => { if (expandableHunk.maxExpandHeadRange > 0) { if (expandableHunk.maxExpandHeadRange <= 10) { return ( - expandableHunk.expandHead(expandableHunk.maxExpandHeadRange).then(this.diffExpanded)}> + {this.props.t("diff.expandHeadComplete", { count: expandableHunk.maxExpandHeadRange })} @@ -165,10 +163,10 @@ class DiffFile extends React.Component { return ( - expandableHunk.expandHead(10).then(this.diffExpanded)}> + {this.props.t("diff.expandHeadByLines", { count: 10 })} {" "} - expandableHunk.expandHead(expandableHunk.maxExpandHeadRange).then(this.diffExpanded)}> + {this.props.t("diff.expandHeadComplete", { count: expandableHunk.maxExpandHeadRange })} @@ -196,10 +194,10 @@ class DiffFile extends React.Component { return ( - expandableHunk.expandBottom(10).then(this.diffExpanded)}> + {this.props.t("diff.expandBottomByLines", { count: 10 })} {" "} - expandableHunk.expandBottom(expandableHunk.maxExpandBottomRange).then(this.diffExpanded)}> + {this.props.t("diff.expandBottomComplete", { count: expandableHunk.maxExpandBottomRange })} @@ -207,7 +205,7 @@ class DiffFile extends React.Component { ); } } - // hunk header must be defined + // hunk footer must be defined return ; }; @@ -216,10 +214,8 @@ class DiffFile extends React.Component { return ( - expandableHunk.expandBottom(10).then(this.diffExpanded)}> - {this.props.t("diff.expandLastBottomByLines")} - {" "} - expandableHunk.expandBottom(expandableHunk.maxExpandBottomRange).then(this.diffExpanded)}> + {this.props.t("diff.expandLastBottomByLines")}{" "} + {this.props.t("diff.expandLastBottomComplete")} @@ -230,6 +226,33 @@ class DiffFile extends React.Component { return ; }; + expandHead = (expandableHunk: ExpandableHunk, count: number) => { + return () => { + expandableHunk + .expandHead(count) + .then(this.diffExpanded) + .catch(this.diffExpansionFailed); + }; + }; + + expandBottom = (expandableHunk: ExpandableHunk, count: number) => { + return () => { + expandableHunk + .expandBottom(count) + .then(this.diffExpanded) + .catch(this.diffExpansionFailed); + }; + }; + + diffExpanded = (newFile: File) => { + this.setState({ file: newFile, diffExpander: new DiffExpander(newFile) }); + }; + + diffExpansionFailed = (err: any) => { + console.log(err); + this.setState({ expansionError: err }); + }; + collectHunkAnnotations = (hunk: HunkType) => { const { annotationFactory } = this.props; const { file } = this.state; @@ -354,7 +377,7 @@ class DiffFile extends React.Component { render() { const { fileControlFactory, fileAnnotationFactory, t } = this.props; - const { file, collapsed, sideBySide, diffExpander } = this.state; + const { file, collapsed, sideBySide, diffExpander, expansionError } = this.state; const viewType = sideBySide ? "split" : "unified"; let body = null; From 35c78b7ad93c24604e9649eb4b20192c09d73567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 10 Jun 2020 10:34:55 +0200 Subject: [PATCH 25/38] Handle errors and show loading state --- scm-ui/ui-components/src/repos/DiffFile.tsx | 73 +++++++++++++------ .../src/repos/HunkExpandLink.tsx | 44 +++++++++++ scm-ui/ui-webapp/public/locales/en/repos.json | 4 +- 3 files changed, 96 insertions(+), 25 deletions(-) create mode 100644 scm-ui/ui-components/src/repos/HunkExpandLink.tsx diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx index 4f6fceef93..96af23d51a 100644 --- a/scm-ui/ui-components/src/repos/DiffFile.tsx +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -36,6 +36,8 @@ import DiffButton from "./DiffButton"; import { MenuContext } from "@scm-manager/ui-components"; import DiffExpander, { ExpandableHunk } from "./DiffExpander"; import HunkExpandLink from "./HunkExpandLink"; +import { Modal } from "../modals"; +import ErrorNotification from "../ErrorNotification"; const EMPTY_ANNOTATION_FACTORY = {}; @@ -153,9 +155,10 @@ class DiffFile extends React.Component { return ( - - {this.props.t("diff.expandHeadComplete", { count: expandableHunk.maxExpandHeadRange })} - + ); @@ -163,12 +166,14 @@ class DiffFile extends React.Component { return ( - - {this.props.t("diff.expandHeadByLines", { count: 10 })} - {" "} - - {this.props.t("diff.expandHeadComplete", { count: expandableHunk.maxExpandHeadRange })} - + {" "} + ); @@ -184,9 +189,10 @@ class DiffFile extends React.Component { return ( - expandableHunk.expandBottom(expandableHunk.maxExpandBottomRange).then(this.diffExpanded)}> - {this.props.t("diff.expandBottomComplete", { count: expandableHunk.maxExpandBottomRange })} - + ); @@ -194,12 +200,14 @@ class DiffFile extends React.Component { return ( - - {this.props.t("diff.expandBottomByLines", { count: 10 })} - {" "} - - {this.props.t("diff.expandBottomComplete", { count: expandableHunk.maxExpandBottomRange })} - + {" "} + ); @@ -214,10 +222,14 @@ class DiffFile extends React.Component { return ( - {this.props.t("diff.expandLastBottomByLines")}{" "} - - {this.props.t("diff.expandLastBottomComplete")} - + {" "} + ); @@ -228,7 +240,7 @@ class DiffFile extends React.Component { expandHead = (expandableHunk: ExpandableHunk, count: number) => { return () => { - expandableHunk + return expandableHunk .expandHead(count) .then(this.diffExpanded) .catch(this.diffExpansionFailed); @@ -237,7 +249,7 @@ class DiffFile extends React.Component { expandBottom = (expandableHunk: ExpandableHunk, count: number) => { return () => { - expandableHunk + return expandableHunk .expandBottom(count) .then(this.diffExpanded) .catch(this.diffExpansionFailed); @@ -424,8 +436,21 @@ class DiffFile extends React.Component { ) : null; + let errorModal; + if (expansionError) { + errorModal = ( + this.setState({ expansionError: undefined })} + body={} + active={true} + /> + ); + } + return ( + {errorModal}
Promise; +}; + +const HunkExpandLink: FC = ({ text, onClick }) => { + const [t] = useTranslation("repos"); + const [loading, setLoading] = useState(false); + + const onClickWithLoadingMarker = () => { + setLoading(true); + onClick().then(() => setLoading(false)); + }; + + return {loading? t("diff.expanding"): text}; +}; + +export default HunkExpandLink; diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index fa97c7d6cf..aaddfb4f12 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -208,7 +208,9 @@ "expandBottomComplete": ">> load {{count}} line", "expandBottomComplete_plural": ">> load all {{count}} lines", "expandLastBottomByLines": "> load up to 10 more lines", - "expandLastBottomComplete": ">> load all remaining lines" + "expandLastBottomComplete": ">> load all remaining lines", + "expanding": "loading lines ...", + "expansionFailed": "Error loading additional content" }, "fileUpload": { "clickHere": "Click here to select your file", From f6916fc7e13884ad62b1def62845294a537e27bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 10 Jun 2020 10:45:04 +0200 Subject: [PATCH 26/38] Use font awesome angles --- scm-ui/ui-components/src/repos/DiffFile.tsx | 8 ++++++++ .../src/repos/HunkExpandLink.tsx | 9 +++++++-- scm-ui/ui-webapp/public/locales/en/repos.json | 20 +++++++++---------- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx index 96af23d51a..f2f137a8df 100644 --- a/scm-ui/ui-components/src/repos/DiffFile.tsx +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -156,6 +156,7 @@ class DiffFile extends React.Component { @@ -167,10 +168,12 @@ class DiffFile extends React.Component { {" "} @@ -190,6 +193,7 @@ class DiffFile extends React.Component { @@ -201,10 +205,12 @@ class DiffFile extends React.Component { {" "} @@ -223,10 +229,12 @@ class DiffFile extends React.Component { {" "} diff --git a/scm-ui/ui-components/src/repos/HunkExpandLink.tsx b/scm-ui/ui-components/src/repos/HunkExpandLink.tsx index 375f074705..7779519f55 100644 --- a/scm-ui/ui-components/src/repos/HunkExpandLink.tsx +++ b/scm-ui/ui-components/src/repos/HunkExpandLink.tsx @@ -23,22 +23,27 @@ */ import React, { FC, useState } from "react"; import { useTranslation } from "react-i18next"; +import classNames from "classnames"; type Props = { + icon: string; text: string; onClick: () => Promise; }; -const HunkExpandLink: FC = ({ text, onClick }) => { +const HunkExpandLink: FC = ({ icon, text, onClick }) => { const [t] = useTranslation("repos"); const [loading, setLoading] = useState(false); const onClickWithLoadingMarker = () => { + if (loading) { + return; + } setLoading(true); onClick().then(() => setLoading(false)); }; - return {loading? t("diff.expanding"): text}; + return {" "}{loading? t("diff.expanding"): text}; }; export default HunkExpandLink; diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index aaddfb4f12..6f142b067a 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -199,16 +199,16 @@ "sideBySide": "Switch to side-by-side view", "combined": "Switch to combined view", "noDiffFound": "No Diff between the selected branches found.", - "expandHeadByLines": "> load {{count}} more line", - "expandHeadByLines_plural": "> load {{count}} more lines", - "expandHeadComplete": ">> load {{count}} line", - "expandHeadComplete_plural": ">> load all {{count}} lines", - "expandBottomByLines": "> load {{count}} more line", - "expandBottomByLines_plural": "> load {{count}} more lines", - "expandBottomComplete": ">> load {{count}} line", - "expandBottomComplete_plural": ">> load all {{count}} lines", - "expandLastBottomByLines": "> load up to 10 more lines", - "expandLastBottomComplete": ">> load all remaining lines", + "expandHeadByLines": "load {{count}} more line", + "expandHeadByLines_plural": "load {{count}} more lines", + "expandHeadComplete": "load {{count}} line", + "expandHeadComplete_plural": "load all {{count}} lines", + "expandBottomByLines": "load {{count}} more line", + "expandBottomByLines_plural": "load {{count}} more lines", + "expandBottomComplete": "load {{count}} line", + "expandBottomComplete_plural": "load all {{count}} lines", + "expandLastBottomByLines": "load up to 10 more lines", + "expandLastBottomComplete": "load all remaining lines", "expanding": "loading lines ...", "expansionFailed": "Error loading additional content" }, From cb37e7b0697691252a7d2152ad4f08e920757365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 10 Jun 2020 10:54:14 +0200 Subject: [PATCH 27/38] Add german translation --- scm-ui/ui-components/src/repos/DiffFile.tsx | 2 +- scm-ui/ui-webapp/public/locales/de/repos.json | 14 +++++++++++++- scm-ui/ui-webapp/public/locales/en/repos.json | 4 ++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx index f2f137a8df..55c4036d2f 100644 --- a/scm-ui/ui-components/src/repos/DiffFile.tsx +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -231,7 +231,7 @@ class DiffFile extends React.Component { {" "} Date: Wed, 10 Jun 2020 10:58:52 +0200 Subject: [PATCH 28/38] Remove redundant translations --- scm-ui/ui-components/src/repos/DiffFile.tsx | 12 ++++++------ scm-ui/ui-webapp/public/locales/de/repos.json | 14 +++++--------- scm-ui/ui-webapp/public/locales/en/repos.json | 12 ++++-------- 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx index 55c4036d2f..3ac50271f0 100644 --- a/scm-ui/ui-components/src/repos/DiffFile.tsx +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -158,7 +158,7 @@ class DiffFile extends React.Component { @@ -170,12 +170,12 @@ class DiffFile extends React.Component { {" "} @@ -195,7 +195,7 @@ class DiffFile extends React.Component { @@ -207,12 +207,12 @@ class DiffFile extends React.Component { {" "} diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index 2d30ccd5fa..a25020f0cb 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -192,16 +192,12 @@ "sideBySide": "Zur zweispaltigen Ansicht wechseln", "combined": "Zur kombinierten Ansicht wechseln", "noDiffFound": "Kein Diff zwischen den ausgewählten Branches gefunden.", - "expandHeadByLines": "{{count}} weitere Zeile laden", - "expandHeadByLines_plural": "{{count}} weitere Zeilen laden", - "expandHeadComplete": "{{count}} verbleibende weitere Zeile laden", - "expandHeadComplete_plural": "Alle {{count}} weiteren Zeilen laden", - "expandBottomByLines": "{{count}} verbleibende weitere Zeile laden", - "expandBottomByLines_plural": "Alle {{count}} weiteren Zeilen laden", - "expandBottomComplete": "{{count}} verbleibende weitere Zeile laden", - "expandBottomComplete_plural": "Alle {{count}} weiteren Zeilen laden", + "expandByLines": "{{count}} weitere Zeile laden", + "expandByLines_plural": "{{count}} weitere Zeilen laden", + "expandComplete": "{{count}} verbleibende Zeile laden", + "expandComplete_plural": "Alle {{count}} verbleibenden Zeilen laden", "expandLastBottomByLines": "Bis zu {{count}} weitere Zeilen laden", - "expandLastBottomComplete": "Alle weiteren Zeilen laden", + "expandLastBottomComplete": "Alle verbleibenden Zeilen laden", "expanding": "Zeilen werden geladen ...", "expansionFailed": "Fehler beim Laden der zusätzlichen Zeilen" }, diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index 230fb270c5..3ae77c8781 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -199,14 +199,10 @@ "sideBySide": "Switch to side-by-side view", "combined": "Switch to combined view", "noDiffFound": "No Diff between the selected branches found.", - "expandHeadByLines": "load {{count}} more line", - "expandHeadByLines_plural": "load {{count}} more lines", - "expandHeadComplete": "load {{count}} line", - "expandHeadComplete_plural": "load all {{count}} lines", - "expandBottomByLines": "load {{count}} more line", - "expandBottomByLines_plural": "load {{count}} more lines", - "expandBottomComplete": "load {{count}} line", - "expandBottomComplete_plural": "load all {{count}} lines", + "expandByLines": "load {{count}} more line", + "expandByLines_plural": "load {{count}} more lines", + "expandComplete": "load {{count}} remaining line", + "expandComplete_plural": "load all {{count}} remaining lines", "expandLastBottomByLines": "load up to {{count}} more lines", "expandLastBottomComplete": "load all remaining lines", "expanding": "loading lines ...", From 56a6792826990e9b2644ac869969cc0704fbf1cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 10 Jun 2020 11:02:10 +0200 Subject: [PATCH 29/38] Fix angle --- scm-ui/ui-components/src/repos/DiffFile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx index 3ac50271f0..9ff6ef58a1 100644 --- a/scm-ui/ui-components/src/repos/DiffFile.tsx +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -156,7 +156,7 @@ class DiffFile extends React.Component { From 1fcc47743e09347a41b3f6aa82067c2ff2c79f1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 10 Jun 2020 11:22:00 +0200 Subject: [PATCH 30/38] Log change --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index beb81c4566..db5da7a8ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added - Option to configure jvm parameter of docker container with env JAVA_OPTS or with arguments ([#1175](https://github.com/scm-manager/scm-manager/pull/1175)) +- Added links in diff views to expand the gaps between "hunks" ([#1178](https://github.com/scm-manager/scm-manager/pull/1178)) ### Fixed - Avoid caching of detected browser language ([#1176](https://github.com/scm-manager/scm-manager/pull/1176)) From 76ee9467df2201611cad2635f7f57b61493e948e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 10 Jun 2020 12:04:51 +0200 Subject: [PATCH 31/38] Fix formatting --- scm-ui/ui-components/src/repos/HunkExpandLink.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scm-ui/ui-components/src/repos/HunkExpandLink.tsx b/scm-ui/ui-components/src/repos/HunkExpandLink.tsx index 7779519f55..dfff80160c 100644 --- a/scm-ui/ui-components/src/repos/HunkExpandLink.tsx +++ b/scm-ui/ui-components/src/repos/HunkExpandLink.tsx @@ -43,7 +43,11 @@ const HunkExpandLink: FC = ({ icon, text, onClick }) => { onClick().then(() => setLoading(false)); }; - return {" "}{loading? t("diff.expanding"): text}; + return ( + + {loading ? t("diff.expanding") : text} + + ); }; export default HunkExpandLink; From 86282e570f0fba497eb2d67eeee34cd10fac2923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 10 Jun 2020 12:52:13 +0200 Subject: [PATCH 32/38] Fix layout and pointer hand for expand link --- scm-ui/ui-components/src/repos/DiffFile.tsx | 119 ++++++++---------- .../src/repos/HunkExpandDivider.tsx | 42 +++++++ .../src/repos/HunkExpandLink.tsx | 9 +- 3 files changed, 101 insertions(+), 69 deletions(-) create mode 100644 scm-ui/ui-components/src/repos/HunkExpandDivider.tsx diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx index 9ff6ef58a1..4a8291646c 100644 --- a/scm-ui/ui-components/src/repos/DiffFile.tsx +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -26,7 +26,7 @@ import { withTranslation, WithTranslation } from "react-i18next"; import classNames from "classnames"; import styled from "styled-components"; // @ts-ignore -import { Decoration, getChangeKey, Hunk } from "react-diff-view"; +import { getChangeKey, Hunk } from "react-diff-view"; import { ButtonGroup } from "../buttons"; import Tag from "../Tag"; import Icon from "../Icon"; @@ -38,6 +38,7 @@ import DiffExpander, { ExpandableHunk } from "./DiffExpander"; import HunkExpandLink from "./HunkExpandLink"; import { Modal } from "../modals"; import ErrorNotification from "../ErrorNotification"; +import HunkExpandDivider from "./HunkExpandDivider"; const EMPTY_ANNOTATION_FACTORY = {}; @@ -81,11 +82,6 @@ const ButtonWrapper = styled.div` margin-left: auto; `; -const HunkDivider = styled.div` - background: #98d8f3; - font-size: 0.7rem; -`; - const ChangeTypeTag = styled(Tag)` margin-left: 0.75rem; `; @@ -153,32 +149,28 @@ class DiffFile extends React.Component { if (expandableHunk.maxExpandHeadRange > 0) { if (expandableHunk.maxExpandHeadRange <= 10) { return ( - - - - - + + + ); } else { return ( - - - {" "} - - - + + {" "} + + ); } } @@ -190,32 +182,28 @@ class DiffFile extends React.Component { if (expandableHunk.maxExpandBottomRange > 0) { if (expandableHunk.maxExpandBottomRange <= 10) { return ( - - - - - + + + ); } else { return ( - - - {" "} - - - + + {" "} + + ); } } @@ -226,20 +214,18 @@ class DiffFile extends React.Component { createLastHunkFooter = (expandableHunk: ExpandableHunk) => { if (expandableHunk.maxExpandBottomRange !== 0) { return ( - - - {" "} - - - + + {" "} + + ); } // hunk header must be defined @@ -269,7 +255,6 @@ class DiffFile extends React.Component { }; diffExpansionFailed = (err: any) => { - console.log(err); this.setState({ expansionError: err }); }; diff --git a/scm-ui/ui-components/src/repos/HunkExpandDivider.tsx b/scm-ui/ui-components/src/repos/HunkExpandDivider.tsx new file mode 100644 index 0000000000..f30d248dd9 --- /dev/null +++ b/scm-ui/ui-components/src/repos/HunkExpandDivider.tsx @@ -0,0 +1,42 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import React, { FC } from "react"; +import { Decoration } from "react-diff-view"; +import styled from "styled-components"; + +const HunkDivider = styled.div` + background: #98d8f3; + font-size: 0.7rem; + padding-left: 1.78em; +`; + +const HunkExpandDivider: FC = ({ children }) => { + return ( + + {children} + + ); +}; + +export default HunkExpandDivider; diff --git a/scm-ui/ui-components/src/repos/HunkExpandLink.tsx b/scm-ui/ui-components/src/repos/HunkExpandLink.tsx index dfff80160c..0b337c5bd2 100644 --- a/scm-ui/ui-components/src/repos/HunkExpandLink.tsx +++ b/scm-ui/ui-components/src/repos/HunkExpandLink.tsx @@ -24,6 +24,7 @@ import React, { FC, useState } from "react"; import { useTranslation } from "react-i18next"; import classNames from "classnames"; +import styled from "styled-components"; type Props = { icon: string; @@ -31,6 +32,10 @@ type Props = { onClick: () => Promise; }; +const ExpandLink = styled.span` + cursor: pointer; +`; + const HunkExpandLink: FC = ({ icon, text, onClick }) => { const [t] = useTranslation("repos"); const [loading, setLoading] = useState(false); @@ -44,9 +49,9 @@ const HunkExpandLink: FC = ({ icon, text, onClick }) => { }; return ( - + {loading ? t("diff.expanding") : text} - + ); }; From e6deecca17766dfa91a27d663e5f7aeec737fd31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 10 Jun 2020 13:11:44 +0200 Subject: [PATCH 33/38] Add divider for not parsed hunks --- .../src/__snapshots__/storyshots.test.ts.snap | 336 ++++++++++++++++++ scm-ui/ui-components/src/repos/DiffFile.tsx | 8 +- 2 files changed, 343 insertions(+), 1 deletion(-) diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index 09c693ba41..05d83b9b14 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -1784,6 +1784,20 @@ exports[`Storyshots Diff CollapsingWithFunction 1`] = ` + + + +
+ + + @@ -4166,6 +4180,20 @@ exports[`Storyshots Diff Default 1`] = ` + + + +
+ + + @@ -6181,6 +6209,20 @@ exports[`Storyshots Diff Default 1`] = ` + + + +
+ + + @@ -6381,6 +6423,20 @@ exports[`Storyshots Diff Default 1`] = ` + + + +
+ + + @@ -7968,6 +8024,20 @@ exports[`Storyshots Diff File Annotation 1`] = ` + + + +
+ + + @@ -9995,6 +10065,20 @@ exports[`Storyshots Diff File Annotation 1`] = ` + + + +
+ + + @@ -10195,6 +10279,20 @@ exports[`Storyshots Diff File Annotation 1`] = ` + + + +
+ + + @@ -11814,6 +11912,20 @@ exports[`Storyshots Diff File Controls 1`] = ` + + + +
+ + + @@ -13883,6 +13995,20 @@ exports[`Storyshots Diff File Controls 1`] = ` + + + +
+ + + @@ -14083,6 +14209,20 @@ exports[`Storyshots Diff File Controls 1`] = ` + + + +
+ + + @@ -15140,6 +15280,20 @@ exports[`Storyshots Diff Hunks 1`] = ` + + + +
+ + + @@ -15317,6 +15471,20 @@ exports[`Storyshots Diff Hunks 1`] = ` + + + +
+ + + @@ -16501,6 +16669,20 @@ exports[`Storyshots Diff Line Annotation 1`] = ` + + + +
+ + + @@ -18516,6 +18698,20 @@ exports[`Storyshots Diff Line Annotation 1`] = ` + + + +
+ + + @@ -18716,6 +18912,20 @@ exports[`Storyshots Diff Line Annotation 1`] = ` + + + +
+ + + @@ -20361,6 +20571,20 @@ exports[`Storyshots Diff OnClick 1`] = ` + + + +
+ + + @@ -22524,6 +22748,20 @@ exports[`Storyshots Diff OnClick 1`] = ` + + + +
+ + + @@ -22740,6 +22978,20 @@ exports[`Storyshots Diff OnClick 1`] = ` + + + +
+ + + @@ -24512,6 +24764,20 @@ exports[`Storyshots Diff Side-By-Side 1`] = ` + + + +
+ + + @@ -26788,6 +27054,20 @@ exports[`Storyshots Diff Side-By-Side 1`] = ` + + + +
+ + + @@ -27016,6 +27296,20 @@ exports[`Storyshots Diff Side-By-Side 1`] = ` + + + +
+ + + @@ -28698,6 +28992,20 @@ exports[`Storyshots Diff SyntaxHighlighting 1`] = ` + + + +
+ + + @@ -30713,6 +31021,20 @@ exports[`Storyshots Diff SyntaxHighlighting 1`] = ` + + + +
+ + + @@ -30913,6 +31235,20 @@ exports[`Storyshots Diff SyntaxHighlighting 1`] = ` + + + +
+ + + diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx index 4a8291646c..98fa9959e3 100644 --- a/scm-ui/ui-components/src/repos/DiffFile.tsx +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -26,7 +26,7 @@ import { withTranslation, WithTranslation } from "react-i18next"; import classNames from "classnames"; import styled from "styled-components"; // @ts-ignore -import { getChangeKey, Hunk } from "react-diff-view"; +import { Decoration, getChangeKey, Hunk } from "react-diff-view"; import { ButtonGroup } from "../buttons"; import Tag from "../Tag"; import Icon from "../Icon"; @@ -82,6 +82,10 @@ const ButtonWrapper = styled.div` margin-left: auto; `; +const HunkDivider = styled.hr` + margin: 0.5rem 0; +`; + const ChangeTypeTag = styled(Tag)` margin-left: 0.75rem; `; @@ -304,6 +308,8 @@ class DiffFile extends React.Component { const items = []; if (file._links?.lines) { items.push(this.createHunkHeader(expandableHunk)); + } else if (i > 0) { + items.push(); } items.push( From 4e69d0367891fb127170f65d32c7130f1105ed36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 10 Jun 2020 13:24:42 +0200 Subject: [PATCH 34/38] Document query parameters --- .../sonia/scm/api/v2/resources/ContentResource.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java index ed5ebe6e57..74d1d2deeb 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java @@ -27,6 +27,7 @@ package sonia.scm.api.v2.resources; import com.github.sdorra.spotter.ContentType; import com.github.sdorra.spotter.ContentTypes; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -97,6 +98,17 @@ public class ContentResource { mediaType = VndMediaType.ERROR_TYPE, schema = @Schema(implementation = ErrorDto.class) )) + @Parameter( + name = "start", + description = "If set, the content will be returned from this line on. The first line is line number 0. " + + "If omitted, the output will start with the first line." + ) + @Parameter( + name = "end", + description = "If set, the content will be returned excluding the given line number and following." + + "The first line ist line number 0. " + + "If set to -1, no lines will be excluded (this equivalent to omitting this parameter" + ) public Response get( @PathParam("namespace") String namespace, @PathParam("name") String name, From fc72cb8d5ff04f5ac4848b65bf03cc2818522b5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 10 Jun 2020 13:32:06 +0200 Subject: [PATCH 35/38] Add story for expandable diffs --- .../src/__snapshots__/storyshots.test.ts.snap | 4306 +++++++++++++++++ .../ui-components/src/repos/Diff.stories.tsx | 9 +- 2 files changed, 4314 insertions(+), 1 deletion(-) diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index 05d83b9b14..f0486525f0 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -7200,6 +7200,4312 @@ exports[`Storyshots Diff Default 1`] = `
`; +exports[`Storyshots Diff Expandable 1`] = ` +
+
+
+
+
+ + + src/main/java/com/cloudogu/scm/review/events/EventListener.java + + + modify + +
+
+
+
+ + + + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 1 + + 1 + + package com.cloudogu.scm.review.events; +
+ 2 + + 2 + + +
+ 3 + + + import com.cloudogu.scm.review.comment.service.BasicComment; +
+ 4 + + + import com.cloudogu.scm.review.comment.service.BasicCommentEvent; +
+ 5 + + + import com.cloudogu.scm.review.comment.service.CommentEvent; +
+ 6 + + + import com.cloudogu.scm.review.comment.service.ReplyEvent; +
+ 7 + + 3 + + import com.cloudogu.scm.review.pullrequest.service.BasicPullRequestEvent; +
+ 8 + + 4 + + import com.cloudogu.scm.review.pullrequest.service.PullRequest; +
+ 9 + + + import com.cloudogu.scm.review.pullrequest.service.PullRequestEvent; +
+ 10 + + 5 + + import com.github.legman.Subscribe; +
+ 11 + + + import lombok.Data; +
+ 12 + + 6 + + import org.apache.shiro.SecurityUtils; +
+ 13 + + 7 + + import org.apache.shiro.subject.PrincipalCollection; +
+ 14 + + 8 + + import org.apache.shiro.subject.Subject; +
+ 15 + + 9 + + import sonia.scm.EagerSingleton; +
+ 16 + + + import sonia.scm.HandlerEventType; +
+ 17 + + + import sonia.scm.event.HandlerEvent; +
+ 18 + + 10 + + import sonia.scm.plugin.Extension; +
+ 19 + + 11 + + import sonia.scm.repository.Repository; +
+ 20 + + 12 + + import sonia.scm.security.SessionId; +
+
+ + + + diff.expandLastBottomByLines + + + + + + diff.expandLastBottomComplete + +
+
+
+
+
+
+
+
+ + + src/main/js/ChangeNotification.tsx + + + modify + +
+
+
+
+ + + + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + diff.expandComplete + +
+
+ 2 + + 2 + + import { Link } from "@scm-manager/ui-types"; +
+ 3 + + 3 + + import { apiClient, Toast, ToastButtons, ToastButton } from "@scm-manager/ui-components"; +
+ 4 + + 4 + + import { PullRequest } from "./types/PullRequest"; +
+ + 5 + + import { useTranslation } from "react-i18next"; +
+ 5 + + 6 + + +
+ 6 + + 7 + + type HandlerProps = { +
+ 7 + + 8 + + url: string; +
+
+ + + + diff.expandComplete + +
+
+
+ + + + diff.expandComplete + +
+
+ 15 + + 16 + + pullRequest: setEvent +
+ 16 + + 17 + + }); +
+ 17 + + 18 + + }, [url]); +
+ + 19 + + const { t } = useTranslation("plugins"); +
+ 18 + + 20 + + if (event) { +
+ 19 + + 21 + + return ( +
+ 20 + + + <Toast type="warning" title="New Changes"> +
+ 21 + + + <p>The underlying Pull-Request has changed. Press reload to see the changes.</p> +
+ 22 + + + <p>Warning: Non saved modification will be lost.</p> +
+ + 22 + + <Toast type="warning" title={t("scm-review-plugin.changeNotification.title")}> +
+ + 23 + + <p>{t("scm-review-plugin.changeNotification.description")}</p> +
+ + 24 + + <p>{t("scm-review-plugin.changeNotification.modificationWarning")}</p> +
+ 23 + + 25 + + <ToastButtons> +
+ 24 + + + <ToastButton icon="redo" onClick={reload}>Reload</ToastButton> +
+ 25 + + + <ToastButton icon="times" onClick={() => setEvent(undefined)}>Ignore</ToastButton> +
+ + 26 + + <ToastButton icon="redo" onClick={reload}> +
+ + 27 + + {t("scm-review-plugin.changeNotification.buttons.reload")} +
+ + 28 + + </ToastButton> +
+ + 29 + + <ToastButton icon="times" onClick={() => setEvent(undefined)}> +
+ + 30 + + {t("scm-review-plugin.changeNotification.buttons.ignore")} +
+ + 31 + + </ToastButton> +
+ 26 + + 32 + + </ToastButtons> +
+ 27 + + 33 + + </Toast> +
+ 28 + + 34 + + ); +
+
+ + + + diff.expandLastBottomByLines + + + + + + diff.expandLastBottomComplete + +
+
+
+
+
+
+
+
+ + + src/main/resources/locales/de/plugins.json + + + modify + +
+
+
+
+ + + + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + diff.expandByLines + + + + + + diff.expandComplete + +
+
+ 181 + + 181 + + "titleClickable": "Der Kommentar bezieht sich auf eine ältere Version des Source- oder Target-Branches. Klicken Sie hier, um den ursprünglichen Kontext zu sehen." +
+ 182 + + 182 + + } +
+ 183 + + 183 + + } +
+ + 184 + + }, +
+ + 185 + + "changeNotification": { +
+ + 186 + + "title": "Neue Änderungen", +
+ + 187 + + "description": "An diesem Pull Request wurden Änderungen vorgenommen. Laden Sie die Seite neu um diese anzuzeigen.", +
+ + 188 + + "modificationWarning": "Warnung: Nicht gespeicherte Eingaben gehen verloren.", +
+ + 189 + + "buttons": { +
+ + 190 + + "reload": "Neu laden", +
+ + 191 + + "ignore": "Ignorieren" +
+ + 192 + + } +
+ 184 + + 193 + + } +
+ 185 + + 194 + + }, +
+ 186 + + 195 + + "permissions": { +
+
+ + + + diff.expandLastBottomByLines + + + + + + diff.expandLastBottomComplete + +
+
+
+
+
+
+
+
+ + + src/main/resources/locales/en/plugins.json + + + modify + +
+
+
+
+ + + + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + diff.expandByLines + + + + + + diff.expandComplete + +
+
+ 181 + + 181 + + "titleClickable": "The comment is related to an older of the source or target branch. Click here to see the original context." +
+ 182 + + 182 + + } +
+ 183 + + 183 + + } +
+ + 184 + + }, +
+ + 185 + + "changeNotification": { +
+ + 186 + + "title": "New Changes", +
+ + 187 + + "description": "The underlying Pull-Request has changed. Press reload to see the changes.", +
+ + 188 + + "modificationWarning": "Warning: Non saved modification will be lost.", +
+ + 189 + + "buttons": { +
+ + 190 + + "reload": "Reload", +
+ + 191 + + "ignore": "Ignore" +
+ + 192 + + } +
+ 184 + + 193 + + } +
+ 185 + + 194 + + }, +
+ 186 + + 195 + + "permissions": { +
+
+ + + + diff.expandLastBottomByLines + + + + + + diff.expandLastBottomComplete + +
+
+
+
+
+
+
+
+ + + src/test/java/com/cloudogu/scm/review/events/ClientTest.java + + + modify + +
+
+
+
+ + + + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + diff.expandComplete + +
+
+ 7 + + 7 + + import org.mockito.Mock; +
+ 8 + + 8 + + import org.mockito.junit.jupiter.MockitoExtension; +
+ 9 + + 9 + + import sonia.scm.security.SessionId; +
+ + 10 + + +
+ 10 + + 11 + + import javax.ws.rs.sse.OutboundSseEvent; +
+ 11 + + 12 + + import javax.ws.rs.sse.SseEventSink; +
+ 12 + + + +
+ 13 + + 13 + + import java.time.Clock; +
+ 14 + + 14 + + import java.time.Instant; +
+ 15 + + 15 + + import java.time.LocalDateTime; +
+ 16 + + 16 + + import java.time.ZoneOffset; +
+ 17 + + 17 + + import java.time.temporal.ChronoField; +
+ 18 + + + import java.time.temporal.ChronoUnit; +
+ 19 + + + import java.time.temporal.TemporalField; +
+ 20 + + 18 + + import java.util.concurrent.CompletableFuture; +
+ 21 + + 19 + + import java.util.concurrent.CompletionStage; +
+ 22 + + + import java.util.concurrent.atomic.AtomicLong; +
+ 23 + + 20 + + import java.util.concurrent.atomic.AtomicReference; +
+ 24 + + 21 + + +
+ 25 + + 22 + + import static java.time.temporal.ChronoUnit.MINUTES; +
+
+ + + + diff.expandByLines + + + + + + diff.expandComplete + +
+
+
+ + + + diff.expandByLines + + + + + + diff.expandComplete + +
+
+ 83 + + 80 + + +
+ 84 + + 81 + + @Test +
+ 85 + + 82 + + @SuppressWarnings("unchecked") +
+ 86 + + + void shouldCloseEventSinkOnFailure() throws InterruptedException { +
+ + 83 + + void shouldCloseEventSinkOnFailure() { +
+ 87 + + 84 + + CompletionStage future = CompletableFuture.supplyAsync(() -> { +
+ 88 + + 85 + + throw new RuntimeException("failed to send message"); +
+ 89 + + 86 + + }); +
+
+ + + + diff.expandComplete + +
+
+
+ + + + diff.expandComplete + +
+
+ 91 + + 88 + + +
+ 92 + + 89 + + client.send(message); +
+ 93 + + 90 + + +
+ 94 + + + Thread.sleep(50L); +
+ 95 + + + +
+ 96 + + + verify(eventSink).close(); +
+ + 91 + + verify(eventSink, timeout(50L)).close(); +
+ 97 + + 92 + + } +
+ 98 + + 93 + + +
+ 99 + + 94 + + @Test +
+
+ + + + diff.expandLastBottomByLines + + + + + + diff.expandLastBottomComplete + +
+
+
+
+
+
+
+
+ + + Main.java + + + modify + +
+
+
+
+ + + + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + 1 + + import java.io.PrintStream; +
+ 1 + + 2 + + import java.util.Arrays; +
+ 2 + + 3 + + +
+ 3 + + 4 + + class Main { +
+ + 5 + + private static final PrintStream OUT = System.out; +
+ + 6 + + +
+ 4 + + 7 + + public static void main(String[] args) { +
+ + 8 + + <<<<<<< HEAD +
+ 5 + + 9 + + System.out.println("Expect nothing more to happen."); +
+ 6 + + 10 + + System.out.println("The command line parameters are:"); +
+ 7 + + 11 + + Arrays.stream(args).map(arg -> "- " + arg).forEach(System.out::println); +
+ + 12 + + ======= +
+ + 13 + + OUT.println("Expect nothing more to happen."); +
+ + 14 + + OUT.println("Parameters:"); +
+ + 15 + + Arrays.stream(args).map(arg -> "- " + arg).forEach(OUT::println); +
+ + 16 + + >>>>>>> feature/use_constant +
+ 8 + + 17 + + } +
+ 9 + + 18 + + } +
+
+ + + + diff.expandLastBottomByLines + + + + + + diff.expandLastBottomComplete + +
+
+
+
+
+`; + exports[`Storyshots Diff File Annotation 1`] = `
( oldPath.endsWith(".java")} /> - )); + )) + .add("Expandable", () => { + const filesWithLanguage = diffFiles.map((file: File) => { + file._links = { lines: { href: "http://example.com/" } }; + return file; + }); + return ; + }); From 0a03d4c136ddf2778b86ab7dd40b0e0e2fb30c28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 10 Jun 2020 13:36:50 +0200 Subject: [PATCH 36/38] Fix import --- scm-ui/ui-components/src/repos/DiffExpander.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/ui-components/src/repos/DiffExpander.ts b/scm-ui/ui-components/src/repos/DiffExpander.ts index cd348908fe..63ec2b6c6d 100644 --- a/scm-ui/ui-components/src/repos/DiffExpander.ts +++ b/scm-ui/ui-components/src/repos/DiffExpander.ts @@ -24,7 +24,7 @@ import { apiClient } from "@scm-manager/ui-components"; import { Change, File, Hunk } from "./DiffTypes"; -import { Link } from "@scm-manager/ui-types/src"; +import { Link } from "@scm-manager/ui-types"; class DiffExpander { file: File; From 4bbc06f30c9f78403c45681c99d82ca0b8b6b867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 10 Jun 2020 13:56:38 +0200 Subject: [PATCH 37/38] Fix type check error --- scm-ui/ui-components/src/repos/HunkExpandDivider.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/scm-ui/ui-components/src/repos/HunkExpandDivider.tsx b/scm-ui/ui-components/src/repos/HunkExpandDivider.tsx index f30d248dd9..db2da6c247 100644 --- a/scm-ui/ui-components/src/repos/HunkExpandDivider.tsx +++ b/scm-ui/ui-components/src/repos/HunkExpandDivider.tsx @@ -22,6 +22,7 @@ * SOFTWARE. */ import React, { FC } from "react"; +// @ts-ignore import { Decoration } from "react-diff-view"; import styled from "styled-components"; From b8dc613c780abe83ce56e0bd8bb5795a12a6774a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 10 Jun 2020 14:31:32 +0200 Subject: [PATCH 38/38] Add simple performance optimization --- .../scm/api/v2/resources/LineFilteredOutputStream.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/LineFilteredOutputStream.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/LineFilteredOutputStream.java index 7d42e8ed94..1a2411c643 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/LineFilteredOutputStream.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/LineFilteredOutputStream.java @@ -71,6 +71,14 @@ class LineFilteredOutputStream extends OutputStream { } } + @Override + public void write(byte[] b, int off, int len) throws IOException { + if (currentLine > end) { + return; + } + super.write(b, off, len); + } + public void keepLineBreakInMind(char b) { lastLineBreakCharacter = b; ++currentLine;