mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-08 22:45:45 +01:00
Feature Partial Diff (#1581)
With this pull request, diffs for Git are loaded in chunks. This means, that for diffs with a lot of files only a part of them are loaded. In the UI a button will be displayed to load more. In the REST API, the number of files can be specified. This only works for diffs, that are delivered as "parsed" diffs. Currently, this is only available for Git. Co-authored-by: Sebastian Sdorra <sebastian.sdorra@cloudogu.com>
This commit is contained in:
2
gradle/changelog/partial_diff.yaml
Normal file
2
gradle/changelog/partial_diff.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
- type: added
|
||||||
|
description: Partial diff ([#1581](https://github.com/scm-manager/scm-manager/issues/1581))
|
||||||
@@ -30,11 +30,11 @@ import sonia.scm.repository.spi.DiffCommandRequest;
|
|||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
abstract class AbstractDiffCommandBuilder <T extends AbstractDiffCommandBuilder> {
|
abstract class AbstractDiffCommandBuilder <T extends AbstractDiffCommandBuilder, R extends DiffCommandRequest> {
|
||||||
|
|
||||||
|
|
||||||
/** request for the diff command implementation */
|
/** request for the diff command implementation */
|
||||||
final DiffCommandRequest request = new DiffCommandRequest();
|
final R request = createRequest();
|
||||||
|
|
||||||
private final Set<Feature> supportedFeatures;
|
private final Set<Feature> supportedFeatures;
|
||||||
|
|
||||||
@@ -89,4 +89,6 @@ abstract class AbstractDiffCommandBuilder <T extends AbstractDiffCommandBuilder>
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract T self();
|
abstract T self();
|
||||||
|
|
||||||
|
abstract R createRequest();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import sonia.scm.repository.Feature;
|
import sonia.scm.repository.Feature;
|
||||||
import sonia.scm.repository.spi.DiffCommand;
|
import sonia.scm.repository.spi.DiffCommand;
|
||||||
|
import sonia.scm.repository.spi.DiffCommandRequest;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -54,13 +55,10 @@ import java.util.Set;
|
|||||||
* System.out.println(content);
|
* System.out.println(content);
|
||||||
* </code></pre>
|
* </code></pre>
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* TODO check current behavior.
|
|
||||||
*
|
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
* @since 1.17
|
* @since 1.17
|
||||||
*/
|
*/
|
||||||
public final class DiffCommandBuilder extends AbstractDiffCommandBuilder<DiffCommandBuilder>
|
public final class DiffCommandBuilder extends AbstractDiffCommandBuilder<DiffCommandBuilder, DiffCommandRequest>
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -161,6 +159,11 @@ public final class DiffCommandBuilder extends AbstractDiffCommandBuilder<DiffCom
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
DiffCommandRequest createRequest() {
|
||||||
|
return new DiffCommandRequest();
|
||||||
|
}
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface OutputStreamConsumer {
|
public interface OutputStreamConsumer {
|
||||||
void accept(OutputStream outputStream) throws IOException;
|
void accept(OutputStream outputStream) throws IOException;
|
||||||
|
|||||||
@@ -24,9 +24,25 @@
|
|||||||
|
|
||||||
package sonia.scm.repository.api;
|
package sonia.scm.repository.api;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static java.util.Optional.empty;
|
||||||
|
|
||||||
public interface DiffResult extends Iterable<DiffFile> {
|
public interface DiffResult extends Iterable<DiffFile> {
|
||||||
|
|
||||||
String getOldRevision();
|
String getOldRevision();
|
||||||
|
|
||||||
String getNewRevision();
|
String getNewRevision();
|
||||||
|
|
||||||
|
default boolean isPartial() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
default int getOffset() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
default Optional<Integer> getLimit() {
|
||||||
|
return empty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,11 +29,12 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import sonia.scm.repository.Feature;
|
import sonia.scm.repository.Feature;
|
||||||
import sonia.scm.repository.spi.DiffResultCommand;
|
import sonia.scm.repository.spi.DiffResultCommand;
|
||||||
|
import sonia.scm.repository.spi.DiffResultCommandRequest;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public class DiffResultCommandBuilder extends AbstractDiffCommandBuilder<DiffResultCommandBuilder> {
|
public class DiffResultCommandBuilder extends AbstractDiffCommandBuilder<DiffResultCommandBuilder, DiffResultCommandRequest> {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(DiffResultCommandBuilder.class);
|
private static final Logger LOG = LoggerFactory.getLogger(DiffResultCommandBuilder.class);
|
||||||
|
|
||||||
@@ -44,6 +45,31 @@ public class DiffResultCommandBuilder extends AbstractDiffCommandBuilder<DiffRes
|
|||||||
this.diffResultCommand = diffResultCommand;
|
this.diffResultCommand = diffResultCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets an offset for the first file diff entry that will be created in the result. If there are less entries than the
|
||||||
|
* given offset, an empty result will be created.
|
||||||
|
*
|
||||||
|
* @param offset The number of the first diff file entry that will be added to the result.
|
||||||
|
* @return This builder instance.
|
||||||
|
* @since 2.15.0
|
||||||
|
*/
|
||||||
|
public DiffResultCommandBuilder setOffset(Integer offset) {
|
||||||
|
request.setOffset(offset);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a limit for the file diff entries that will be created.
|
||||||
|
*
|
||||||
|
* @param limit The maximum number of file diff entries that will be created in the result.
|
||||||
|
* @return This builder instance.
|
||||||
|
* @since 2.15.0
|
||||||
|
*/
|
||||||
|
public DiffResultCommandBuilder setLimit(Integer limit) {
|
||||||
|
request.setLimit(limit);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the content of the difference as parsed objects.
|
* Returns the content of the difference as parsed objects.
|
||||||
*
|
*
|
||||||
@@ -62,4 +88,9 @@ public class DiffResultCommandBuilder extends AbstractDiffCommandBuilder<DiffRes
|
|||||||
DiffResultCommandBuilder self() {
|
DiffResultCommandBuilder self() {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
DiffResultCommandRequest createRequest() {
|
||||||
|
return new DiffResultCommandRequest();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import sonia.scm.repository.api.DiffFormat;
|
|||||||
* @since 1.17
|
* @since 1.17
|
||||||
*/
|
*/
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public final class DiffCommandRequest extends FileBaseCommandRequest
|
public class DiffCommandRequest extends FileBaseCommandRequest
|
||||||
implements Validateable {
|
implements Validateable {
|
||||||
|
|
||||||
private static final long serialVersionUID = 4026911212676859626L;
|
private static final long serialVersionUID = 4026911212676859626L;
|
||||||
|
|||||||
@@ -30,4 +30,8 @@ import java.io.IOException;
|
|||||||
|
|
||||||
public interface DiffResultCommand {
|
public interface DiffResultCommand {
|
||||||
DiffResult getDiffResult(DiffCommandRequest request) throws IOException;
|
DiffResult getDiffResult(DiffCommandRequest request) throws IOException;
|
||||||
|
|
||||||
|
default DiffResult getDiffResult(DiffResultCommandRequest request) throws IOException {
|
||||||
|
return getDiffResult((DiffCommandRequest) request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class DiffResultCommandRequest extends DiffCommandRequest {
|
||||||
|
|
||||||
|
private Integer offset;
|
||||||
|
private Integer limit;
|
||||||
|
}
|
||||||
@@ -36,7 +36,11 @@ import javax.inject.Inject;
|
|||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.stream.Collectors;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static java.util.Optional.ofNullable;
|
||||||
|
|
||||||
public class GitDiffResultCommand extends AbstractGitCommand implements DiffResultCommand {
|
public class GitDiffResultCommand extends AbstractGitCommand implements DiffResultCommand {
|
||||||
|
|
||||||
@@ -47,17 +51,31 @@ public class GitDiffResultCommand extends AbstractGitCommand implements DiffResu
|
|||||||
|
|
||||||
public DiffResult getDiffResult(DiffCommandRequest diffCommandRequest) throws IOException {
|
public DiffResult getDiffResult(DiffCommandRequest diffCommandRequest) throws IOException {
|
||||||
org.eclipse.jgit.lib.Repository repository = open();
|
org.eclipse.jgit.lib.Repository repository = open();
|
||||||
return new GitDiffResult(repository, Differ.diff(repository, diffCommandRequest));
|
return new GitDiffResult(repository, Differ.diff(repository, diffCommandRequest), 0, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DiffResult getDiffResult(DiffResultCommandRequest request) throws IOException {
|
||||||
|
org.eclipse.jgit.lib.Repository repository = open();
|
||||||
|
int offset = request.getOffset() == null ? 0 : request.getOffset();
|
||||||
|
return new GitDiffResult(repository, Differ.diff(repository, request), offset, request.getLimit());
|
||||||
}
|
}
|
||||||
|
|
||||||
private class GitDiffResult implements DiffResult {
|
private class GitDiffResult implements DiffResult {
|
||||||
|
|
||||||
private final org.eclipse.jgit.lib.Repository repository;
|
private final org.eclipse.jgit.lib.Repository repository;
|
||||||
private final Differ.Diff diff;
|
private final Differ.Diff diff;
|
||||||
|
private final List<DiffEntry> diffEntries;
|
||||||
|
|
||||||
private GitDiffResult(org.eclipse.jgit.lib.Repository repository, Differ.Diff diff) {
|
private final int offset;
|
||||||
|
private final Integer limit;
|
||||||
|
|
||||||
|
private GitDiffResult(org.eclipse.jgit.lib.Repository repository, Differ.Diff diff, int offset, Integer limit) {
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
this.diff = diff;
|
this.diff = diff;
|
||||||
|
this.offset = offset;
|
||||||
|
this.limit = limit;
|
||||||
|
this.diffEntries = diff.getEntries();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -70,12 +88,32 @@ public class GitDiffResultCommand extends AbstractGitCommand implements DiffResu
|
|||||||
return GitUtil.getId(diff.getCommit().getId());
|
return GitUtil.getId(diff.getCommit().getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPartial() {
|
||||||
|
return limit != null && limit + offset < diffEntries.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOffset() {
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Integer> getLimit() {
|
||||||
|
return ofNullable(limit);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Iterator<DiffFile> iterator() {
|
public Iterator<DiffFile> iterator() {
|
||||||
return diff.getEntries()
|
Stream<DiffEntry> diffEntryStream = diffEntries
|
||||||
.stream()
|
.stream()
|
||||||
|
.skip(offset);
|
||||||
|
if (limit != null) {
|
||||||
|
diffEntryStream = diffEntryStream.limit(limit);
|
||||||
|
}
|
||||||
|
return diffEntryStream
|
||||||
.map(diffEntry -> new GitDiffFile(repository, diffEntry))
|
.map(diffEntry -> new GitDiffFile(repository, diffEntry))
|
||||||
.collect(Collectors.<DiffFile>toList())
|
.map(DiffFile.class::cast)
|
||||||
.iterator();
|
.iterator();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ public class GitDiffResultCommandTest extends AbstractGitCommandTestBase {
|
|||||||
DiffFile b = iterator.next();
|
DiffFile b = iterator.next();
|
||||||
assertThat(b.getOldPath()).isEqualTo("b.txt");
|
assertThat(b.getOldPath()).isEqualTo("b.txt");
|
||||||
assertThat(b.getNewPath()).isEqualTo("/dev/null");
|
assertThat(b.getNewPath()).isEqualTo("/dev/null");
|
||||||
|
|
||||||
|
assertThat(diffResult.isPartial()).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -69,6 +71,8 @@ public class GitDiffResultCommandTest extends AbstractGitCommandTestBase {
|
|||||||
DiffFile b = iterator.next();
|
DiffFile b = iterator.next();
|
||||||
assertThat(b.getOldRevision()).isEqualTo("61780798228d17af2d34fce4cfbdf35556832472");
|
assertThat(b.getOldRevision()).isEqualTo("61780798228d17af2d34fce4cfbdf35556832472");
|
||||||
assertThat(b.getNewRevision()).isEqualTo("0000000000000000000000000000000000000000");
|
assertThat(b.getNewRevision()).isEqualTo("0000000000000000000000000000000000000000");
|
||||||
|
|
||||||
|
assertThat(iterator.hasNext()).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -119,10 +123,55 @@ public class GitDiffResultCommandTest extends AbstractGitCommandTestBase {
|
|||||||
assertThat(renameB.iterator().hasNext()).isFalse();
|
assertThat(renameB.iterator().hasNext()).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldLimitResult() throws IOException {
|
||||||
|
DiffResult diffResult = createDiffResult("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4", null, 1);
|
||||||
|
Iterator<DiffFile> iterator = diffResult.iterator();
|
||||||
|
|
||||||
|
DiffFile a = iterator.next();
|
||||||
|
assertThat(a.getNewPath()).isEqualTo("a.txt");
|
||||||
|
assertThat(a.getOldPath()).isEqualTo("a.txt");
|
||||||
|
|
||||||
|
assertThat(iterator.hasNext()).isFalse();
|
||||||
|
|
||||||
|
assertThat(diffResult.isPartial()).isTrue();
|
||||||
|
assertThat(diffResult.getLimit()).get().isEqualTo(1);
|
||||||
|
assertThat(diffResult.getOffset()).isZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldSetOffsetForResult() throws IOException {
|
||||||
|
DiffResult diffResult = createDiffResult("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4", 1, null);
|
||||||
|
Iterator<DiffFile> iterator = diffResult.iterator();
|
||||||
|
|
||||||
|
DiffFile b = iterator.next();
|
||||||
|
assertThat(b.getOldPath()).isEqualTo("b.txt");
|
||||||
|
assertThat(b.getNewPath()).isEqualTo("/dev/null");
|
||||||
|
|
||||||
|
assertThat(iterator.hasNext()).isFalse();
|
||||||
|
|
||||||
|
assertThat(diffResult.isPartial()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldNotBePartialWhenResultCountMatchesLimit() throws IOException {
|
||||||
|
DiffResult diffResult = createDiffResult("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4", 0, 2);
|
||||||
|
|
||||||
|
assertThat(diffResult.isPartial()).isFalse();
|
||||||
|
assertThat(diffResult.getLimit()).get().isEqualTo(2);
|
||||||
|
assertThat(diffResult.getOffset()).isZero();
|
||||||
|
}
|
||||||
|
|
||||||
private DiffResult createDiffResult(String s) throws IOException {
|
private DiffResult createDiffResult(String s) throws IOException {
|
||||||
|
return createDiffResult(s, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DiffResult createDiffResult(String s, Integer offset, Integer limit) throws IOException {
|
||||||
GitDiffResultCommand gitDiffResultCommand = new GitDiffResultCommand(createContext());
|
GitDiffResultCommand gitDiffResultCommand = new GitDiffResultCommand(createContext());
|
||||||
DiffCommandRequest diffCommandRequest = new DiffCommandRequest();
|
DiffResultCommandRequest diffCommandRequest = new DiffResultCommandRequest();
|
||||||
diffCommandRequest.setRevision(s);
|
diffCommandRequest.setRevision(s);
|
||||||
|
diffCommandRequest.setOffset(offset);
|
||||||
|
diffCommandRequest.setLimit(limit);
|
||||||
|
|
||||||
return gitDiffResultCommand.getDiffResult(diffCommandRequest);
|
return gitDiffResultCommand.getDiffResult(diffCommandRequest);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,8 @@
|
|||||||
"fetch-mock-jest": "^1.5.1",
|
"fetch-mock-jest": "^1.5.1",
|
||||||
"query-string": "5",
|
"query-string": "5",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-query": "^3.5.16"
|
"react-query": "^3.5.16",
|
||||||
|
"gitdiff-parser": "^0.1.2"
|
||||||
},
|
},
|
||||||
"babel": {
|
"babel": {
|
||||||
"presets": [
|
"presets": [
|
||||||
|
|||||||
225
scm-ui/ui-api/src/diff.test.ts
Normal file
225
scm-ui/ui-api/src/diff.test.ts
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
/*
|
||||||
|
* 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 fetchMock from "fetch-mock-jest";
|
||||||
|
import { Diff } from "@scm-manager/ui-types";
|
||||||
|
import { act, renderHook } from "@testing-library/react-hooks";
|
||||||
|
import { useDiff } from "./diff";
|
||||||
|
import createWrapper from "./tests/createWrapper";
|
||||||
|
|
||||||
|
describe("Test diff", () => {
|
||||||
|
const simpleDiff: Diff = {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
oldPath: "/dev/null",
|
||||||
|
newPath: "0.txt",
|
||||||
|
oldEndingNewLine: true,
|
||||||
|
newEndingNewLine: true,
|
||||||
|
oldRevision: "0000000000000000000000000000000000000000",
|
||||||
|
newRevision: "573541ac9702dd3969c9bc859d2b91ec1f7e6e56",
|
||||||
|
type: "add",
|
||||||
|
language: "text",
|
||||||
|
hunks: [
|
||||||
|
{
|
||||||
|
content: "@@ -0,0 +1 @@",
|
||||||
|
newStart: 1,
|
||||||
|
newLines: 1,
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
content: "0",
|
||||||
|
type: "insert",
|
||||||
|
lineNumber: 1,
|
||||||
|
isInsert: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
_links: {
|
||||||
|
lines: {
|
||||||
|
href:
|
||||||
|
"/api/v2/repositories/scmadmin/HeartOfGold-git/content/one_to_onehundred/0.txt?start={start}&end={end}",
|
||||||
|
templated: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
partial: false,
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: "/api/v2/diff"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const partialDiff1: Diff = {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
oldPath: "/dev/null",
|
||||||
|
newPath: "0.txt",
|
||||||
|
oldEndingNewLine: true,
|
||||||
|
newEndingNewLine: true,
|
||||||
|
oldRevision: "0000000000000000000000000000000000000000",
|
||||||
|
newRevision: "573541ac9702dd3969c9bc859d2b91ec1f7e6e56",
|
||||||
|
type: "add",
|
||||||
|
language: "text",
|
||||||
|
hunks: [
|
||||||
|
{
|
||||||
|
content: "@@ -0,0 +1 @@",
|
||||||
|
newStart: 1,
|
||||||
|
newLines: 1,
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
content: "0",
|
||||||
|
type: "insert",
|
||||||
|
lineNumber: 1,
|
||||||
|
isInsert: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
_links: {
|
||||||
|
lines: {
|
||||||
|
href:
|
||||||
|
"/api/v2/repositories/scmadmin/HeartOfGold-git/content/one_to_onehundred/0.txt?start={start}&end={end}",
|
||||||
|
templated: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
partial: true,
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: "/diff"
|
||||||
|
},
|
||||||
|
next: {
|
||||||
|
href: "/diff?offset=1&limit=1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const partialDiff2: Diff = {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
oldPath: "/dev/null",
|
||||||
|
newPath: "1.txt",
|
||||||
|
oldEndingNewLine: true,
|
||||||
|
newEndingNewLine: true,
|
||||||
|
oldRevision: "0000000000000000000000000000000000000000",
|
||||||
|
newRevision: "573541ac9702dd3969c9bc859d2b91ec1f7e6e56",
|
||||||
|
type: "add",
|
||||||
|
language: "text",
|
||||||
|
hunks: [
|
||||||
|
{
|
||||||
|
content: "@@ -0,0 +1 @@",
|
||||||
|
newStart: 1,
|
||||||
|
newLines: 1,
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
content: "1",
|
||||||
|
type: "insert",
|
||||||
|
lineNumber: 1,
|
||||||
|
isInsert: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
_links: {
|
||||||
|
lines: {
|
||||||
|
href:
|
||||||
|
"/api/v2/repositories/scmadmin/HeartOfGold-git/content/one_to_onehundred/1.txt?start={start}&end={end}",
|
||||||
|
templated: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
partial: false,
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: "/diff"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fetchMock.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return simple parsed diff", async () => {
|
||||||
|
fetchMock.getOnce("/api/v2/diff", {
|
||||||
|
body: simpleDiff,
|
||||||
|
headers: { "Content-Type": "application/vnd.scmm-diffparsed+json;v=2" }
|
||||||
|
});
|
||||||
|
const { result, waitFor } = renderHook(() => useDiff("/diff"), {
|
||||||
|
wrapper: createWrapper()
|
||||||
|
});
|
||||||
|
await waitFor(() => !!result.current.data);
|
||||||
|
expect(result.current.data).toEqual(simpleDiff);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse and return textual diff", async () => {
|
||||||
|
fetchMock.getOnce("/api/v2/diff", {
|
||||||
|
body: `diff --git a/new.txt b/new.txt
|
||||||
|
--- a/new.txt
|
||||||
|
+++ b/new.txt
|
||||||
|
@@ -1,1 +1,1 @@
|
||||||
|
-i am old!
|
||||||
|
\\ No newline at end of file
|
||||||
|
+i am new!
|
||||||
|
\\ No newline at end of file
|
||||||
|
`,
|
||||||
|
headers: { "Content-Type": "text/plain" }
|
||||||
|
});
|
||||||
|
const { result, waitFor } = renderHook(() => useDiff("/diff"), {
|
||||||
|
wrapper: createWrapper()
|
||||||
|
});
|
||||||
|
await waitFor(() => !!result.current.data);
|
||||||
|
expect(result.current.data?.files).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return parsed diff in multiple chunks", async () => {
|
||||||
|
fetchMock.getOnce("/api/v2/diff?limit=1", {
|
||||||
|
body: partialDiff1,
|
||||||
|
headers: { "Content-Type": "application/vnd.scmm-diffparsed+json;v=2" }
|
||||||
|
});
|
||||||
|
fetchMock.getOnce("/api/v2/diff?offset=1&limit=1", {
|
||||||
|
body: partialDiff2,
|
||||||
|
headers: { "Content-Type": "application/vnd.scmm-diffparsed+json;v=2" }
|
||||||
|
});
|
||||||
|
const { result, waitFor, waitForNextUpdate } = renderHook(() => useDiff("/diff?limit=1"), {
|
||||||
|
wrapper: createWrapper()
|
||||||
|
});
|
||||||
|
await waitFor(() => !!result.current.data);
|
||||||
|
expect(result.current.data).toEqual(partialDiff1);
|
||||||
|
|
||||||
|
await act(() => {
|
||||||
|
const { fetchNextPage } = result.current;
|
||||||
|
fetchNextPage();
|
||||||
|
return waitForNextUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => !result.current.isFetchingNextPage);
|
||||||
|
|
||||||
|
expect(result.current.data).toEqual({ ...partialDiff2, files: [partialDiff1.files[0], partialDiff2.files[0]] });
|
||||||
|
});
|
||||||
|
});
|
||||||
86
scm-ui/ui-api/src/diff.ts
Normal file
86
scm-ui/ui-api/src/diff.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* 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 parser from "gitdiff-parser";
|
||||||
|
|
||||||
|
import { useInfiniteQuery } from "react-query";
|
||||||
|
import { apiClient } from "./apiclient";
|
||||||
|
import { Diff, Link } from "@scm-manager/ui-types";
|
||||||
|
|
||||||
|
type UseDiffOptions = {
|
||||||
|
limit?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDiff = (link: string, options: UseDiffOptions = {}) => {
|
||||||
|
let initialLink = link;
|
||||||
|
if (options.limit) {
|
||||||
|
initialLink = `${initialLink}?limit=${options.limit}`;
|
||||||
|
}
|
||||||
|
const { isLoading, error, data, isFetchingNextPage, fetchNextPage } = useInfiniteQuery<Diff, Error, Diff>(
|
||||||
|
["link", link],
|
||||||
|
({ pageParam }) => {
|
||||||
|
return apiClient.get(pageParam || initialLink).then(response => {
|
||||||
|
const contentType = response.headers.get("Content-Type");
|
||||||
|
if (contentType && contentType.toLowerCase() === "application/vnd.scmm-diffparsed+json;v=2") {
|
||||||
|
return response.json();
|
||||||
|
} else {
|
||||||
|
return response
|
||||||
|
.text()
|
||||||
|
.then(parser.parse)
|
||||||
|
.then(data => {
|
||||||
|
return {
|
||||||
|
files: data,
|
||||||
|
partial: false,
|
||||||
|
_links: {}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
getNextPageParam: lastPage => (lastPage._links.next as Link)?.href
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
isFetchingNextPage,
|
||||||
|
fetchNextPage: () => {
|
||||||
|
fetchNextPage();
|
||||||
|
},
|
||||||
|
data: merge(data?.pages)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const merge = (diffs?: Diff[]): Diff | undefined => {
|
||||||
|
if (!diffs || diffs.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const joinedFiles = diffs.flatMap(diff => diff.files);
|
||||||
|
return {
|
||||||
|
...diffs[diffs.length - 1],
|
||||||
|
files: joinedFiles
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -44,6 +44,7 @@ export * from "./repository-roles";
|
|||||||
export * from "./permissions";
|
export * from "./permissions";
|
||||||
export * from "./sources";
|
export * from "./sources";
|
||||||
export * from "./import";
|
export * from "./import";
|
||||||
|
export * from "./diff";
|
||||||
|
|
||||||
export { default as ApiProvider } from "./ApiProvider";
|
export { default as ApiProvider } from "./ApiProvider";
|
||||||
export * from "./ApiProvider";
|
export * from "./ApiProvider";
|
||||||
|
|||||||
@@ -52,7 +52,8 @@
|
|||||||
"react-test-renderer": "^17.0.1",
|
"react-test-renderer": "^17.0.1",
|
||||||
"storybook-addon-i18next": "^1.3.0",
|
"storybook-addon-i18next": "^1.3.0",
|
||||||
"to-camel-case": "^1.0.0",
|
"to-camel-case": "^1.0.0",
|
||||||
"worker-plugin": "^3.2.0"
|
"worker-plugin": "^3.2.0",
|
||||||
|
"gitdiff-parser": "^0.1.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@scm-manager/ui-api": "^2.14.2-SNAPSHOT",
|
"@scm-manager/ui-api": "^2.14.2-SNAPSHOT",
|
||||||
@@ -60,7 +61,6 @@
|
|||||||
"@scm-manager/ui-types": "^2.14.2-SNAPSHOT",
|
"@scm-manager/ui-types": "^2.14.2-SNAPSHOT",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"date-fns": "^2.4.1",
|
"date-fns": "^2.4.1",
|
||||||
"gitdiff-parser": "^0.1.2",
|
|
||||||
"lowlight": "^1.13.0",
|
"lowlight": "^1.13.0",
|
||||||
"prism-themes": "^1.4.0",
|
"prism-themes": "^1.4.0",
|
||||||
"query-string": "5",
|
"query-string": "5",
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ import simpleDiff from "../__resources__/Diff.simple";
|
|||||||
import hunksDiff from "../__resources__/Diff.hunks";
|
import hunksDiff from "../__resources__/Diff.hunks";
|
||||||
import binaryDiff from "../__resources__/Diff.binary";
|
import binaryDiff from "../__resources__/Diff.binary";
|
||||||
import markdownDiff from "../__resources__/Diff.markdown";
|
import markdownDiff from "../__resources__/Diff.markdown";
|
||||||
import { DiffEventContext, File, FileControlFactory } from "./DiffTypes";
|
import { DiffEventContext, FileControlFactory } from "./DiffTypes";
|
||||||
import Toast from "../toast/Toast";
|
import Toast from "../toast/Toast";
|
||||||
import { getPath } from "./diffs";
|
import { getPath } from "./diffs";
|
||||||
import DiffButton from "./DiffButton";
|
import DiffButton from "./DiffButton";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { MemoryRouter } from "react-router-dom";
|
import { MemoryRouter } from "react-router-dom";
|
||||||
import { one, two } from "../__resources__/changesets";
|
import { one, two } from "../__resources__/changesets";
|
||||||
import { Changeset } from "@scm-manager/ui-types";
|
import { Changeset, FileDiff } from "@scm-manager/ui-types";
|
||||||
import JumpToFileButton from "./JumpToFileButton";
|
import JumpToFileButton from "./JumpToFileButton";
|
||||||
|
|
||||||
const diffFiles = parser.parse(simpleDiff);
|
const diffFiles = parser.parse(simpleDiff);
|
||||||
@@ -141,7 +141,7 @@ storiesOf("Diff", module)
|
|||||||
return <Diff diff={binaryDiffFiles} />;
|
return <Diff diff={binaryDiffFiles} />;
|
||||||
})
|
})
|
||||||
.add("SyntaxHighlighting", () => {
|
.add("SyntaxHighlighting", () => {
|
||||||
const filesWithLanguage = diffFiles.map((file: File) => {
|
const filesWithLanguage = diffFiles.map((file: FileDiff) => {
|
||||||
const ext = getPath(file).split(".")[1];
|
const ext = getPath(file).split(".")[1];
|
||||||
if (ext === "tsx") {
|
if (ext === "tsx") {
|
||||||
file.language = "typescript";
|
file.language = "typescript";
|
||||||
@@ -160,7 +160,7 @@ storiesOf("Diff", module)
|
|||||||
<Diff diff={diffFiles} defaultCollapse={(oldPath, newPath) => oldPath.endsWith(".java")} />
|
<Diff diff={diffFiles} defaultCollapse={(oldPath, newPath) => oldPath.endsWith(".java")} />
|
||||||
))
|
))
|
||||||
.add("Expandable", () => {
|
.add("Expandable", () => {
|
||||||
const filesWithLanguage = diffFiles.map((file: File) => {
|
const filesWithLanguage = diffFiles.map((file: FileDiff) => {
|
||||||
file._links = { lines: { href: "http://example.com/" } };
|
file._links = { lines: { href: "http://example.com/" } };
|
||||||
return file;
|
return file;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,7 +23,8 @@
|
|||||||
*/
|
*/
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import DiffFile from "./DiffFile";
|
import DiffFile from "./DiffFile";
|
||||||
import { DiffObjectProps, File, FileControlFactory } from "./DiffTypes";
|
import { DiffObjectProps, FileControlFactory } from "./DiffTypes";
|
||||||
|
import { FileDiff } from "@scm-manager/ui-types";
|
||||||
import { escapeWhitespace } from "./diffs";
|
import { escapeWhitespace } from "./diffs";
|
||||||
import Notification from "../Notification";
|
import Notification from "../Notification";
|
||||||
import { WithTranslation, withTranslation } from "react-i18next";
|
import { WithTranslation, withTranslation } from "react-i18next";
|
||||||
@@ -32,7 +33,7 @@ import { RouteComponentProps, withRouter } from "react-router-dom";
|
|||||||
type Props = RouteComponentProps &
|
type Props = RouteComponentProps &
|
||||||
WithTranslation &
|
WithTranslation &
|
||||||
DiffObjectProps & {
|
DiffObjectProps & {
|
||||||
diff: File[];
|
diff: FileDiff[];
|
||||||
fileControlFactory?: FileControlFactory;
|
fileControlFactory?: FileControlFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
*/
|
*/
|
||||||
import fetchMock from "fetch-mock";
|
import fetchMock from "fetch-mock";
|
||||||
import DiffExpander from "./DiffExpander";
|
import DiffExpander from "./DiffExpander";
|
||||||
import { File, Hunk } from "./DiffTypes";
|
import { FileDiff, Hunk } from "@scm-manager/ui-types";
|
||||||
|
|
||||||
const HUNK_0: Hunk = {
|
const HUNK_0: Hunk = {
|
||||||
content: "@@ -1,8 +1,8 @@",
|
content: "@@ -1,8 +1,8 @@",
|
||||||
@@ -94,7 +94,7 @@ const HUNK_3: Hunk = {
|
|||||||
{ content: "line", type: "normal", oldLineNumber: 38, newLineNumber: 40, isNormal: true }
|
{ content: "line", type: "normal", oldLineNumber: 38, newLineNumber: 40, isNormal: true }
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
const TEST_CONTENT_WITH_HUNKS: File = {
|
const TEST_CONTENT_WITH_HUNKS: FileDiff = {
|
||||||
hunks: [HUNK_0, HUNK_1, HUNK_2, HUNK_3],
|
hunks: [HUNK_0, HUNK_1, HUNK_2, HUNK_3],
|
||||||
newEndingNewLine: true,
|
newEndingNewLine: true,
|
||||||
newPath: "src/main/js/CommitMessage.js",
|
newPath: "src/main/js/CommitMessage.js",
|
||||||
@@ -112,7 +112,7 @@ const TEST_CONTENT_WITH_HUNKS: File = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const TEST_CONTENT_WITH_NEW_BINARY_FILE: File = {
|
const TEST_CONTENT_WITH_NEW_BINARY_FILE: FileDiff = {
|
||||||
oldPath: "/dev/null",
|
oldPath: "/dev/null",
|
||||||
newPath: "src/main/fileUploadV2.png",
|
newPath: "src/main/fileUploadV2.png",
|
||||||
oldEndingNewLine: true,
|
oldEndingNewLine: true,
|
||||||
@@ -122,7 +122,7 @@ const TEST_CONTENT_WITH_NEW_BINARY_FILE: File = {
|
|||||||
type: "add"
|
type: "add"
|
||||||
};
|
};
|
||||||
|
|
||||||
const TEST_CONTENT_WITH_NEW_TEXT_FILE: File = {
|
const TEST_CONTENT_WITH_NEW_TEXT_FILE: FileDiff = {
|
||||||
oldPath: "/dev/null",
|
oldPath: "/dev/null",
|
||||||
newPath: "src/main/markdown/README.md",
|
newPath: "src/main/markdown/README.md",
|
||||||
oldEndingNewLine: true,
|
oldEndingNewLine: true,
|
||||||
@@ -151,7 +151,7 @@ const TEST_CONTENT_WITH_NEW_TEXT_FILE: File = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const TEST_CONTENT_WITH_DELETED_TEXT_FILE: File = {
|
const TEST_CONTENT_WITH_DELETED_TEXT_FILE: FileDiff = {
|
||||||
oldPath: "README.md",
|
oldPath: "README.md",
|
||||||
newPath: "/dev/null",
|
newPath: "/dev/null",
|
||||||
oldEndingNewLine: true,
|
oldEndingNewLine: true,
|
||||||
@@ -171,7 +171,7 @@ const TEST_CONTENT_WITH_DELETED_TEXT_FILE: File = {
|
|||||||
_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 } }
|
||||||
};
|
};
|
||||||
|
|
||||||
const TEST_CONTENT_WITH_DELETED_LINES_AT_END: File = {
|
const TEST_CONTENT_WITH_DELETED_LINES_AT_END: FileDiff = {
|
||||||
oldPath: "pom.xml",
|
oldPath: "pom.xml",
|
||||||
newPath: "pom.xml",
|
newPath: "pom.xml",
|
||||||
oldEndingNewLine: true,
|
oldEndingNewLine: true,
|
||||||
@@ -214,7 +214,7 @@ const TEST_CONTENT_WITH_DELETED_LINES_AT_END: File = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const TEST_CONTENT_WITH_ALL_LINES_REMOVED_FROM_FILE: File = {
|
const TEST_CONTENT_WITH_ALL_LINES_REMOVED_FROM_FILE: FileDiff = {
|
||||||
oldPath: "pom.xml",
|
oldPath: "pom.xml",
|
||||||
newPath: "pom.xml",
|
newPath: "pom.xml",
|
||||||
oldEndingNewLine: true,
|
oldEndingNewLine: true,
|
||||||
@@ -281,7 +281,7 @@ describe("with hunks the diff expander", () => {
|
|||||||
const expandedHunk = diffExpander.getHunk(1).hunk;
|
const expandedHunk = diffExpander.getHunk(1).hunk;
|
||||||
const subsequentHunk = diffExpander.getHunk(2).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");
|
fetchMock.get("http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=20&end=21", "new line 1");
|
||||||
let newFile: File;
|
let newFile: FileDiff;
|
||||||
await diffExpander
|
await diffExpander
|
||||||
.getHunk(1)
|
.getHunk(1)
|
||||||
.expandBottom(1)
|
.expandBottom(1)
|
||||||
@@ -306,7 +306,7 @@ describe("with hunks the diff expander", () => {
|
|||||||
"http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=8&end=13",
|
"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"
|
"new line 9\nnew line 10\nnew line 11\nnew line 12\nnew line 13"
|
||||||
);
|
);
|
||||||
let newFile: File;
|
let newFile: FileDiff;
|
||||||
await diffExpander
|
await diffExpander
|
||||||
.getHunk(1)
|
.getHunk(1)
|
||||||
.expandHead(5)
|
.expandHead(5)
|
||||||
@@ -335,7 +335,7 @@ describe("with hunks the diff expander", () => {
|
|||||||
"http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=40&end=50",
|
"http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=40&end=50",
|
||||||
"new line 40\nnew line 41\nnew line 42"
|
"new line 40\nnew line 41\nnew line 42"
|
||||||
);
|
);
|
||||||
let newFile: File;
|
let newFile: FileDiff;
|
||||||
await diffExpander
|
await diffExpander
|
||||||
.getHunk(3)
|
.getHunk(3)
|
||||||
.expandBottom(10)
|
.expandBottom(10)
|
||||||
@@ -348,7 +348,7 @@ describe("with hunks the diff expander", () => {
|
|||||||
"http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=40&end=-1",
|
"http://localhost:8081/scm/api/v2/content/abc/CommitMessage.js?start=40&end=-1",
|
||||||
"new line 40\nnew line 41\nnew line 42"
|
"new line 40\nnew line 41\nnew line 42"
|
||||||
);
|
);
|
||||||
let newFile: File;
|
let newFile: FileDiff;
|
||||||
await diffExpander
|
await diffExpander
|
||||||
.getHunk(3)
|
.getHunk(3)
|
||||||
.expandBottom(-1)
|
.expandBottom(-1)
|
||||||
|
|||||||
@@ -23,13 +23,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { apiClient } from "@scm-manager/ui-components";
|
import { apiClient } from "@scm-manager/ui-components";
|
||||||
import { Change, File, Hunk } from "./DiffTypes";
|
import { Change, FileDiff, Hunk, Link } from "@scm-manager/ui-types";
|
||||||
import { Link } from "@scm-manager/ui-types";
|
|
||||||
|
|
||||||
class DiffExpander {
|
class DiffExpander {
|
||||||
file: File;
|
file: FileDiff;
|
||||||
|
|
||||||
constructor(file: File) {
|
constructor(file: FileDiff) {
|
||||||
this.file = file;
|
this.file = file;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +72,7 @@ class DiffExpander {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
expandHead: (n: number, count: number) => Promise<File> = (n, count) => {
|
expandHead: (n: number, count: number) => Promise<FileDiff> = (n, count) => {
|
||||||
const start = this.minLineNumber(n) - Math.min(count, this.computeMaxExpandHeadRange(n)) - 1;
|
const start = this.minLineNumber(n) - Math.min(count, this.computeMaxExpandHeadRange(n)) - 1;
|
||||||
const end = this.minLineNumber(n) - 1;
|
const end = this.minLineNumber(n) - 1;
|
||||||
return this.loadLines(start, end).then(lines => {
|
return this.loadLines(start, end).then(lines => {
|
||||||
@@ -90,7 +89,7 @@ class DiffExpander {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
expandBottom: (n: number, count: number) => Promise<File> = (n, count) => {
|
expandBottom: (n: number, count: number) => Promise<FileDiff> = (n, count) => {
|
||||||
const maxExpandBottomRange = this.computeMaxExpandBottomRange(n);
|
const maxExpandBottomRange = this.computeMaxExpandBottomRange(n);
|
||||||
const start = this.maxLineNumber(n);
|
const start = this.maxLineNumber(n);
|
||||||
const end =
|
const end =
|
||||||
@@ -191,8 +190,8 @@ export type ExpandableHunk = {
|
|||||||
hunk: Hunk;
|
hunk: Hunk;
|
||||||
maxExpandHeadRange: number;
|
maxExpandHeadRange: number;
|
||||||
maxExpandBottomRange: number;
|
maxExpandBottomRange: number;
|
||||||
expandHead: (count: number) => Promise<File>;
|
expandHead: (count: number) => Promise<FileDiff>;
|
||||||
expandBottom: (count: number) => Promise<File>;
|
expandBottom: (count: number) => Promise<FileDiff>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DiffExpander;
|
export default DiffExpander;
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ import { Decoration, getChangeKey, Hunk } from "react-diff-view";
|
|||||||
import { ButtonGroup } from "../buttons";
|
import { ButtonGroup } from "../buttons";
|
||||||
import Tag from "../Tag";
|
import Tag from "../Tag";
|
||||||
import Icon from "../Icon";
|
import Icon from "../Icon";
|
||||||
import { Change, ChangeEvent, DiffObjectProps, File, Hunk as HunkType } from "./DiffTypes";
|
import { Change, FileDiff, Hunk as HunkType } from "@scm-manager/ui-types";
|
||||||
|
import { ChangeEvent, DiffObjectProps } from "./DiffTypes";
|
||||||
import TokenizedDiffView from "./TokenizedDiffView";
|
import TokenizedDiffView from "./TokenizedDiffView";
|
||||||
import DiffButton from "./DiffButton";
|
import DiffButton from "./DiffButton";
|
||||||
import { MenuContext, OpenInFullscreenButton } from "@scm-manager/ui-components";
|
import { MenuContext, OpenInFullscreenButton } from "@scm-manager/ui-components";
|
||||||
@@ -45,7 +46,7 @@ const EMPTY_ANNOTATION_FACTORY = {};
|
|||||||
|
|
||||||
type Props = DiffObjectProps &
|
type Props = DiffObjectProps &
|
||||||
WithTranslation & {
|
WithTranslation & {
|
||||||
file: File;
|
file: FileDiff;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Collapsible = {
|
type Collapsible = {
|
||||||
@@ -53,7 +54,7 @@ type Collapsible = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type State = Collapsible & {
|
type State = Collapsible & {
|
||||||
file: File;
|
file: FileDiff;
|
||||||
sideBySide?: boolean;
|
sideBySide?: boolean;
|
||||||
diffExpander: DiffExpander;
|
diffExpander: DiffExpander;
|
||||||
expansionError?: any;
|
expansionError?: any;
|
||||||
@@ -257,7 +258,7 @@ class DiffFile extends React.Component<Props, State> {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
diffExpanded = (newFile: File) => {
|
diffExpanded = (newFile: FileDiff) => {
|
||||||
this.setState({ file: newFile, diffExpander: new DiffExpander(newFile) });
|
this.setState({ file: newFile, diffExpander: new DiffExpander(newFile) });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -303,7 +304,7 @@ class DiffFile extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
renderHunk = (file: File, expandableHunk: ExpandableHunk, i: number) => {
|
renderHunk = (file: FileDiff, expandableHunk: ExpandableHunk, i: number) => {
|
||||||
const hunk = expandableHunk.hunk;
|
const hunk = expandableHunk.hunk;
|
||||||
if (this.props.markConflicts && hunk.changes) {
|
if (this.props.markConflicts && hunk.changes) {
|
||||||
this.markConflicts(hunk);
|
this.markConflicts(hunk);
|
||||||
@@ -353,7 +354,7 @@ class DiffFile extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
getAnchorId(file: File) {
|
getAnchorId(file: FileDiff) {
|
||||||
let path: string;
|
let path: string;
|
||||||
if (file.type === "delete") {
|
if (file.type === "delete") {
|
||||||
path = file.oldPath;
|
path = file.oldPath;
|
||||||
@@ -363,7 +364,7 @@ class DiffFile extends React.Component<Props, State> {
|
|||||||
return escapeWhitespace(path);
|
return escapeWhitespace(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFileTitle = (file: File) => {
|
renderFileTitle = (file: FileDiff) => {
|
||||||
if (file.oldPath !== file.newPath && (file.type === "copy" || file.type === "rename")) {
|
if (file.oldPath !== file.newPath && (file.type === "copy" || file.type === "rename")) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -376,7 +377,7 @@ class DiffFile extends React.Component<Props, State> {
|
|||||||
return file.newPath;
|
return file.newPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
hoverFileTitle = (file: File): string => {
|
hoverFileTitle = (file: FileDiff): string => {
|
||||||
if (file.oldPath !== file.newPath && (file.type === "copy" || file.type === "rename")) {
|
if (file.oldPath !== file.newPath && (file.type === "copy" || file.type === "rename")) {
|
||||||
return `${file.oldPath} > ${file.newPath}`;
|
return `${file.oldPath} > ${file.newPath}`;
|
||||||
} else if (file.type === "delete") {
|
} else if (file.type === "delete") {
|
||||||
@@ -385,7 +386,7 @@ class DiffFile extends React.Component<Props, State> {
|
|||||||
return file.newPath;
|
return file.newPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
renderChangeTag = (file: File) => {
|
renderChangeTag = (file: FileDiff) => {
|
||||||
const { t } = this.props;
|
const { t } = this.props;
|
||||||
if (!file.type) {
|
if (!file.type) {
|
||||||
return;
|
return;
|
||||||
@@ -401,7 +402,7 @@ class DiffFile extends React.Component<Props, State> {
|
|||||||
return <ChangeTypeTag className={classNames("is-rounded", "has-text-weight-normal")} color={color} label={value} />;
|
return <ChangeTypeTag className={classNames("is-rounded", "has-text-weight-normal")} color={color} label={value} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
hasContent = (file: File) => file && !file.isBinary && file.hunks && file.hunks.length > 0;
|
hasContent = (file: FileDiff) => file && !file.isBinary && file.hunks && file.hunks.length > 0;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { fileControlFactory, fileAnnotationFactory, t } = this.props;
|
const { fileControlFactory, fileAnnotationFactory, t } = this.props;
|
||||||
|
|||||||
@@ -24,55 +24,7 @@
|
|||||||
|
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { DefaultCollapsed } from "./defaultCollapsed";
|
import { DefaultCollapsed } from "./defaultCollapsed";
|
||||||
import { Links } from "@scm-manager/ui-types";
|
import { Change, Hunk, FileDiff as File } 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.
|
|
||||||
// This types represents only the required types for the Diff related components,
|
|
||||||
// such as every other component does with its Props.
|
|
||||||
|
|
||||||
export type FileChangeType = "add" | "modify" | "delete" | "copy" | "rename";
|
|
||||||
|
|
||||||
export type File = {
|
|
||||||
hunks?: Hunk[];
|
|
||||||
newEndingNewLine: boolean;
|
|
||||||
newMode?: string;
|
|
||||||
newPath: string;
|
|
||||||
newRevision?: string;
|
|
||||||
oldEndingNewLine: boolean;
|
|
||||||
oldMode?: string;
|
|
||||||
oldPath: string;
|
|
||||||
oldRevision?: string;
|
|
||||||
type: FileChangeType;
|
|
||||||
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;
|
|
||||||
fullyExpanded?: boolean;
|
|
||||||
expansion?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ChangeType = "insert" | "delete" | "normal" | "conflict";
|
|
||||||
|
|
||||||
export type Change = {
|
|
||||||
content: string;
|
|
||||||
isNormal?: boolean;
|
|
||||||
isInsert?: boolean;
|
|
||||||
isDelete?: boolean;
|
|
||||||
lineNumber?: number;
|
|
||||||
newLineNumber?: number;
|
|
||||||
oldLineNumber?: number;
|
|
||||||
type: ChangeType;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ChangeEvent = {
|
export type ChangeEvent = {
|
||||||
change: Change;
|
change: Change;
|
||||||
|
|||||||
@@ -21,93 +21,72 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import React from "react";
|
import React, { FC } from "react";
|
||||||
import { apiClient, NotFoundError } from "@scm-manager/ui-api";
|
import { NotFoundError, useDiff } from "@scm-manager/ui-api";
|
||||||
import ErrorNotification from "../ErrorNotification";
|
import ErrorNotification from "../ErrorNotification";
|
||||||
// @ts-ignore
|
import Notification from "../Notification";
|
||||||
import parser from "gitdiff-parser";
|
|
||||||
|
|
||||||
import Loading from "../Loading";
|
import Loading from "../Loading";
|
||||||
import Diff from "./Diff";
|
import Diff from "./Diff";
|
||||||
import { DiffObjectProps, File } from "./DiffTypes";
|
import { DiffObjectProps } from "./DiffTypes";
|
||||||
import { Notification } from "../index";
|
import { useTranslation } from "react-i18next";
|
||||||
import { withTranslation, WithTranslation } from "react-i18next";
|
import Button from "../buttons/Button";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
type Props = WithTranslation &
|
type Props = DiffObjectProps & {
|
||||||
DiffObjectProps & {
|
url: string;
|
||||||
url: string;
|
limit?: number;
|
||||||
};
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
diff?: File[];
|
|
||||||
loading: boolean;
|
|
||||||
error?: Error;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class LoadingDiff extends React.Component<Props, State> {
|
type NotificationProps = {
|
||||||
static defaultProps = {
|
fetchNextPage: () => void;
|
||||||
sideBySide: false
|
isFetchingNextPage: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: Props) {
|
const StyledNotification = styled(Notification)`
|
||||||
super(props);
|
margin-top: 1.5rem;
|
||||||
this.state = {
|
`;
|
||||||
loading: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
const PartialNotification: FC<NotificationProps> = ({ fetchNextPage, isFetchingNextPage }) => {
|
||||||
this.fetchDiff();
|
const [t] = useTranslation("repos");
|
||||||
}
|
return (
|
||||||
|
<StyledNotification type="info">
|
||||||
|
<div className="columns is-centered">
|
||||||
|
<div className="column">{t("changesets.moreDiffsAvailable")}</div>
|
||||||
|
<Button label={t("changesets.loadMore")} action={fetchNextPage} loading={isFetchingNextPage} />
|
||||||
|
</div>
|
||||||
|
</StyledNotification>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
const LoadingDiff: FC<Props> = ({ url, limit, ...props }) => {
|
||||||
if (prevProps.url !== this.props.url) {
|
const { error, isLoading, data, fetchNextPage, isFetchingNextPage } = useDiff(url, { limit });
|
||||||
this.fetchDiff();
|
const [t] = useTranslation("repos");
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
if (error instanceof NotFoundError) {
|
||||||
|
return <Notification type="info">{t("changesets.noChangesets")}</Notification>;
|
||||||
}
|
}
|
||||||
|
return <ErrorNotification error={error} />;
|
||||||
|
} else if (isLoading) {
|
||||||
|
return <Loading />;
|
||||||
|
} else if (!data?.files) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Diff diff={data.files} {...props} />
|
||||||
|
{data.partial ? (
|
||||||
|
<PartialNotification fetchNextPage={fetchNextPage} isFetchingNextPage={isFetchingNextPage} />
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
fetchDiff = () => {
|
LoadingDiff.defaultProps = {
|
||||||
const { url } = this.props;
|
limit: 25,
|
||||||
this.setState({ loading: true });
|
sideBySide: false
|
||||||
apiClient
|
};
|
||||||
.get(url)
|
|
||||||
.then(response => {
|
|
||||||
const contentType = response.headers.get("Content-Type");
|
|
||||||
if (contentType && contentType.toLowerCase() === "application/vnd.scmm-diffparsed+json;v=2") {
|
|
||||||
return response.json().then(data => data.files);
|
|
||||||
} else {
|
|
||||||
return response.text().then(parser.parse);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then((diff: File[]) => {
|
|
||||||
this.setState({
|
|
||||||
loading: false,
|
|
||||||
diff: diff
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((error: Error) => {
|
|
||||||
this.setState({
|
|
||||||
loading: false,
|
|
||||||
error
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
export default LoadingDiff;
|
||||||
const { diff, loading, error } = this.state;
|
|
||||||
if (error) {
|
|
||||||
if (error instanceof NotFoundError) {
|
|
||||||
return <Notification type="info">{this.props.t("changesets.noChangesets")}</Notification>;
|
|
||||||
}
|
|
||||||
return <ErrorNotification error={error} />;
|
|
||||||
} else if (loading) {
|
|
||||||
return <Loading />;
|
|
||||||
} else if (!diff) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return <Diff diff={diff} {...this.props} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withTranslation("repos")(LoadingDiff);
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import React, { FC } from "react";
|
|||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
// @ts-ignore we have no typings for react-diff-view
|
// @ts-ignore we have no typings for react-diff-view
|
||||||
import { Diff, useTokenizeWorker } from "react-diff-view";
|
import { Diff, useTokenizeWorker } from "react-diff-view";
|
||||||
import { File } from "./DiffTypes";
|
import { FileDiff } from "@scm-manager/ui-types";
|
||||||
import { determineLanguage } from "../languages";
|
import { determineLanguage } from "../languages";
|
||||||
|
|
||||||
// @ts-ignore no types for css modules
|
// @ts-ignore no types for css modules
|
||||||
@@ -65,7 +65,7 @@ const tokenize = new Worker("./Tokenize.worker.ts", { name: "tokenizer", type: "
|
|||||||
tokenize.postMessage({ theme });
|
tokenize.postMessage({ theme });
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
file: File;
|
file: FileDiff;
|
||||||
viewType: "split" | "unified";
|
viewType: "split" | "unified";
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,11 +22,11 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { File, FileChangeType, Hunk } from "./DiffTypes";
|
import { FileChangeType, Hunk, FileDiff } from "@scm-manager/ui-types";
|
||||||
import { getPath, createHunkIdentifier, createHunkIdentifierFromContext, escapeWhitespace } from "./diffs";
|
import { getPath, createHunkIdentifier, createHunkIdentifierFromContext, escapeWhitespace } from "./diffs";
|
||||||
|
|
||||||
describe("tests for diff util functions", () => {
|
describe("tests for diff util functions", () => {
|
||||||
const file = (type: FileChangeType, oldPath: string, newPath: string): File => {
|
const file = (type: FileChangeType, oldPath: string, newPath: string): FileDiff => {
|
||||||
return {
|
return {
|
||||||
hunks: [],
|
hunks: [],
|
||||||
type: type,
|
type: type,
|
||||||
|
|||||||
@@ -22,16 +22,17 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { BaseContext, File, Hunk } from "./DiffTypes";
|
import { BaseContext } from "./DiffTypes";
|
||||||
|
import { FileDiff, Hunk } from "@scm-manager/ui-types";
|
||||||
|
|
||||||
export function getPath(file: File) {
|
export function getPath(file: FileDiff) {
|
||||||
if (file.type === "delete") {
|
if (file.type === "delete") {
|
||||||
return file.oldPath;
|
return file.oldPath;
|
||||||
}
|
}
|
||||||
return file.newPath;
|
return file.newPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createHunkIdentifier(file: File, hunk: Hunk) {
|
export function createHunkIdentifier(file: FileDiff, hunk: Hunk) {
|
||||||
const path = getPath(file);
|
const path = getPath(file);
|
||||||
return `${file.type}_${path}_${hunk.content}`;
|
return `${file.type}_${path}_${hunk.content}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,11 +25,6 @@
|
|||||||
import * as diffs from "./diffs";
|
import * as diffs from "./diffs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
File,
|
|
||||||
FileChangeType,
|
|
||||||
Hunk,
|
|
||||||
Change,
|
|
||||||
ChangeType,
|
|
||||||
BaseContext,
|
BaseContext,
|
||||||
AnnotationFactory,
|
AnnotationFactory,
|
||||||
AnnotationFactoryContext,
|
AnnotationFactoryContext,
|
||||||
@@ -37,6 +32,7 @@ import {
|
|||||||
DiffEventContext
|
DiffEventContext
|
||||||
} from "./DiffTypes";
|
} from "./DiffTypes";
|
||||||
|
|
||||||
|
import { FileDiff as File, FileChangeType, Hunk, Change, ChangeType } from "@scm-manager/ui-types";
|
||||||
export { diffs };
|
export { diffs };
|
||||||
|
|
||||||
export * from "./annotate";
|
export * from "./annotate";
|
||||||
|
|||||||
73
scm-ui/ui-types/src/Diff.ts
Normal file
73
scm-ui/ui-types/src/Diff.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* 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 { HalRepresentation, Links } from "./hal";
|
||||||
|
|
||||||
|
export type FileChangeType = "add" | "modify" | "delete" | "copy" | "rename";
|
||||||
|
|
||||||
|
export type Diff = HalRepresentation & {
|
||||||
|
files: FileDiff[];
|
||||||
|
partial: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FileDiff = {
|
||||||
|
hunks?: Hunk[];
|
||||||
|
newEndingNewLine: boolean;
|
||||||
|
newMode?: string;
|
||||||
|
newPath: string;
|
||||||
|
newRevision?: string;
|
||||||
|
oldEndingNewLine: boolean;
|
||||||
|
oldMode?: string;
|
||||||
|
oldPath: string;
|
||||||
|
oldRevision?: string;
|
||||||
|
type: FileChangeType;
|
||||||
|
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;
|
||||||
|
fullyExpanded?: boolean;
|
||||||
|
expansion?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ChangeType = "insert" | "delete" | "normal" | "conflict";
|
||||||
|
|
||||||
|
export type Change = {
|
||||||
|
content: string;
|
||||||
|
isNormal?: boolean;
|
||||||
|
isInsert?: boolean;
|
||||||
|
isDelete?: boolean;
|
||||||
|
lineNumber?: number;
|
||||||
|
newLineNumber?: number;
|
||||||
|
oldLineNumber?: number;
|
||||||
|
type: ChangeType;
|
||||||
|
};
|
||||||
@@ -64,3 +64,5 @@ export * from "./NamespaceStrategies";
|
|||||||
export * from "./LoginInfo";
|
export * from "./LoginInfo";
|
||||||
|
|
||||||
export * from "./Admin";
|
export * from "./Admin";
|
||||||
|
|
||||||
|
export * from "./Diff";
|
||||||
|
|||||||
@@ -203,7 +203,9 @@
|
|||||||
"errorSubtitle": "Changesets konnten nicht abgerufen werden",
|
"errorSubtitle": "Changesets konnten nicht abgerufen werden",
|
||||||
"noChangesets": "Keine Changesets in diesem Branch gefunden. Die Commits könnten gelöscht worden sein.",
|
"noChangesets": "Keine Changesets in diesem Branch gefunden. Die Commits könnten gelöscht worden sein.",
|
||||||
"branchSelectorLabel": "Branches",
|
"branchSelectorLabel": "Branches",
|
||||||
"collapseDiffs": "Auf-/Zuklappen"
|
"collapseDiffs": "Auf-/Zuklappen",
|
||||||
|
"moreDiffsAvailable": "Es sind weitere Diffs verfügbar",
|
||||||
|
"loadMore": "Weitere laden"
|
||||||
},
|
},
|
||||||
"changeset": {
|
"changeset": {
|
||||||
"label": "Changeset",
|
"label": "Changeset",
|
||||||
|
|||||||
@@ -203,7 +203,9 @@
|
|||||||
"errorSubtitle": "Could not fetch changesets",
|
"errorSubtitle": "Could not fetch changesets",
|
||||||
"noChangesets": "No changesets found for this branch. The commits could have been removed.",
|
"noChangesets": "No changesets found for this branch. The commits could have been removed.",
|
||||||
"branchSelectorLabel": "Branches",
|
"branchSelectorLabel": "Branches",
|
||||||
"collapseDiffs": "Collapse"
|
"collapseDiffs": "Collapse",
|
||||||
|
"moreDiffsAvailable": "There are more diffs available",
|
||||||
|
"loadMore": "Load more"
|
||||||
},
|
},
|
||||||
"changeset": {
|
"changeset": {
|
||||||
"label": "Changeset",
|
"label": "Changeset",
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ public class DiffResultDto extends HalRepresentation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<FileDto> files;
|
private List<FileDto> files;
|
||||||
|
private boolean partial;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = false)
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.OptionalInt;
|
import java.util.OptionalInt;
|
||||||
|
|
||||||
|
import static de.otto.edison.hal.Link.link;
|
||||||
import static de.otto.edison.hal.Link.linkBuilder;
|
import static de.otto.edison.hal.Link.linkBuilder;
|
||||||
import static de.otto.edison.hal.Links.linkingTo;
|
import static de.otto.edison.hal.Links.linkingTo;
|
||||||
|
|
||||||
@@ -55,23 +56,57 @@ class DiffResultToDiffResultDtoMapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public DiffResultDto mapForIncoming(Repository repository, DiffResult result, String source, String target) {
|
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());
|
String baseLink = resourceLinks.incoming().diffParsed(repository.getNamespace(), repository.getName(), source, target);
|
||||||
|
Links.Builder links = linkingTo().self(createSelfLink(result, baseLink));
|
||||||
|
appendNextChunkLinkIfNeeded(links, result, baseLink);
|
||||||
|
DiffResultDto dto = new DiffResultDto(links.build());
|
||||||
setFiles(result, dto, repository, source);
|
setFiles(result, dto, repository, source);
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DiffResultDto mapForRevision(Repository repository, DiffResult result, String revision) {
|
public DiffResultDto mapForRevision(Repository repository, DiffResult result, String revision) {
|
||||||
DiffResultDto dto = new DiffResultDto(linkingTo().self(resourceLinks.diff().parsed(repository.getNamespace(), repository.getName(), revision)).build());
|
String baseLink = resourceLinks.diff().parsed(repository.getNamespace(), repository.getName(), revision);
|
||||||
|
Links.Builder links = linkingTo().self(createSelfLink(result, baseLink));
|
||||||
|
appendNextChunkLinkIfNeeded(links, result, baseLink);
|
||||||
|
DiffResultDto dto = new DiffResultDto(links.build());
|
||||||
setFiles(result, dto, repository, revision);
|
setFiles(result, dto, repository, revision);
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String createSelfLink(DiffResult result, String baseLink) {
|
||||||
|
if (result.getOffset() > 0 || result.getLimit().isPresent()) {
|
||||||
|
return createLinkWithLimitAndOffset(baseLink, result.getOffset(), result.getLimit().orElse(null));
|
||||||
|
} else {
|
||||||
|
return baseLink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendNextChunkLinkIfNeeded(Links.Builder links, DiffResult result, String baseLink) {
|
||||||
|
if (result.isPartial()) {
|
||||||
|
Optional<Integer> limit = result.getLimit();
|
||||||
|
if (limit.isPresent()) {
|
||||||
|
links.single(link("next", createLinkWithLimitAndOffset(baseLink, result.getOffset() + limit.get(), limit.get())));
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("a result cannot be partial without a limit");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createLinkWithLimitAndOffset(String baseLink, int offset, Integer limit) {
|
||||||
|
if (limit == null) {
|
||||||
|
return String.format("%s?offset=%s", baseLink, offset);
|
||||||
|
} else {
|
||||||
|
return String.format("%s?offset=%s&limit=%s", baseLink, offset, limit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void setFiles(DiffResult result, DiffResultDto dto, Repository repository, String revision) {
|
private void setFiles(DiffResult result, DiffResultDto dto, Repository repository, String revision) {
|
||||||
List<DiffResultDto.FileDto> files = new ArrayList<>();
|
List<DiffResultDto.FileDto> files = new ArrayList<>();
|
||||||
for (DiffFile file : result) {
|
for (DiffFile file : result) {
|
||||||
files.add(mapFile(file, repository, revision));
|
files.add(mapFile(file, repository, revision));
|
||||||
}
|
}
|
||||||
dto.setFiles(files);
|
dto.setFiles(files);
|
||||||
|
dto.setPartial(result.isPartial());
|
||||||
}
|
}
|
||||||
|
|
||||||
private DiffResultDto.FileDto mapFile(DiffFile file, Repository repository, String revision) {
|
private DiffResultDto.FileDto mapFile(DiffFile file, Repository repository, String revision) {
|
||||||
@@ -119,7 +154,6 @@ class DiffResultToDiffResultDtoMapper {
|
|||||||
dto.setOldPath(oldPath);
|
dto.setOldPath(oldPath);
|
||||||
dto.setOldRevision(file.getOldRevision());
|
dto.setOldRevision(file.getOldRevision());
|
||||||
|
|
||||||
|
|
||||||
Optional<Language> language = ContentTypeResolver.resolve(path).getLanguage();
|
Optional<Language> language = ContentTypeResolver.resolve(path).getLanguage();
|
||||||
language.ifPresent(value -> dto.setLanguage(ProgrammingLanguages.getValue(value)));
|
language.ifPresent(value -> dto.setLanguage(ProgrammingLanguages.getValue(value)));
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import sonia.scm.util.HttpUtil;
|
|||||||
import sonia.scm.web.VndMediaType;
|
import sonia.scm.web.VndMediaType;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import javax.validation.constraints.Min;
|
||||||
import javax.validation.constraints.Pattern;
|
import javax.validation.constraints.Pattern;
|
||||||
import javax.ws.rs.DefaultValue;
|
import javax.ws.rs.DefaultValue;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
@@ -142,10 +143,18 @@ public class DiffRootResource {
|
|||||||
schema = @Schema(implementation = ErrorDto.class)
|
schema = @Schema(implementation = ErrorDto.class)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
public DiffResultDto getParsed(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) throws IOException {
|
public DiffResultDto getParsed(@PathParam("namespace") String namespace,
|
||||||
|
@PathParam("name") String name,
|
||||||
|
@PathParam("revision") String revision,
|
||||||
|
@QueryParam("limit") @Min(1) Integer limit,
|
||||||
|
@QueryParam("offset") @Min(0) Integer offset) throws IOException {
|
||||||
HttpUtil.checkForCRLFInjection(revision);
|
HttpUtil.checkForCRLFInjection(revision);
|
||||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||||
DiffResult diffResult = repositoryService.getDiffResultCommand().setRevision(revision).getDiffResult();
|
DiffResult diffResult = repositoryService.getDiffResultCommand()
|
||||||
|
.setRevision(revision)
|
||||||
|
.setLimit(limit)
|
||||||
|
.setOffset(offset)
|
||||||
|
.getDiffResult();
|
||||||
return parsedDiffMapper.mapForRevision(repositoryService.getRepository(), diffResult, revision);
|
return parsedDiffMapper.mapForRevision(repositoryService.getRepository(), diffResult, revision);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import sonia.scm.repository.api.RepositoryServiceFactory;
|
|||||||
import sonia.scm.util.HttpUtil;
|
import sonia.scm.util.HttpUtil;
|
||||||
import sonia.scm.web.VndMediaType;
|
import sonia.scm.web.VndMediaType;
|
||||||
|
|
||||||
|
import javax.validation.constraints.Min;
|
||||||
import javax.validation.constraints.Pattern;
|
import javax.validation.constraints.Pattern;
|
||||||
import javax.ws.rs.DefaultValue;
|
import javax.ws.rs.DefaultValue;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
@@ -240,15 +241,19 @@ public class IncomingRootResource {
|
|||||||
schema = @Schema(implementation = ErrorDto.class)
|
schema = @Schema(implementation = ErrorDto.class)
|
||||||
))
|
))
|
||||||
public Response incomingDiffParsed(@PathParam("namespace") String namespace,
|
public Response incomingDiffParsed(@PathParam("namespace") String namespace,
|
||||||
@PathParam("name") String name,
|
@PathParam("name") String name,
|
||||||
@PathParam("source") String source,
|
@PathParam("source") String source,
|
||||||
@PathParam("target") String target) throws IOException {
|
@PathParam("target") String target,
|
||||||
|
@QueryParam("limit") @Min(1) Integer limit,
|
||||||
|
@QueryParam("offset") @Min(0) Integer offset) throws IOException {
|
||||||
HttpUtil.checkForCRLFInjection(source);
|
HttpUtil.checkForCRLFInjection(source);
|
||||||
HttpUtil.checkForCRLFInjection(target);
|
HttpUtil.checkForCRLFInjection(target);
|
||||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||||
DiffResult diffResult = repositoryService.getDiffResultCommand()
|
DiffResult diffResult = repositoryService.getDiffResultCommand()
|
||||||
.setRevision(source)
|
.setRevision(source)
|
||||||
.setAncestorChangeset(target)
|
.setAncestorChangeset(target)
|
||||||
|
.setLimit(limit)
|
||||||
|
.setOffset(offset)
|
||||||
.getDiffResult();
|
.getDiffResult();
|
||||||
return Response.ok(parsedDiffMapper.mapForIncoming(repositoryService.getRepository(), diffResult, source, target)).build();
|
return Response.ok(parsedDiffMapper.mapForIncoming(repositoryService.getRepository(), diffResult, source, target)).build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ import static org.junit.Assert.assertEquals;
|
|||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@RunWith(MockitoJUnitRunner.Silent.class)
|
@RunWith(MockitoJUnitRunner.Silent.class)
|
||||||
@@ -153,6 +154,38 @@ public class DiffResourceTest extends RepositoryTestBase {
|
|||||||
.contains("\"self\":{\"href\":\"http://self\"}");
|
.contains("\"self\":{\"href\":\"http://self\"}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldGetParsedDiffsWithOffset() throws Exception {
|
||||||
|
DiffResult diffResult = mock(DiffResult.class);
|
||||||
|
when(diffResultCommandBuilder.getDiffResult()).thenReturn(diffResult);
|
||||||
|
when(diffResultToDiffResultDtoMapper.mapForRevision(REPOSITORY, diffResult, "revision"))
|
||||||
|
.thenReturn(new DiffResultDto(Links.linkingTo().self("http://self").build()));
|
||||||
|
MockHttpRequest request = MockHttpRequest
|
||||||
|
.get(DIFF_URL + "revision/parsed?offset=42")
|
||||||
|
.accept(VndMediaType.DIFF_PARSED);
|
||||||
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
|
|
||||||
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
verify(diffResultCommandBuilder).setOffset(42);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldGetParsedDiffsWithLimit() throws Exception {
|
||||||
|
DiffResult diffResult = mock(DiffResult.class);
|
||||||
|
when(diffResultCommandBuilder.getDiffResult()).thenReturn(diffResult);
|
||||||
|
when(diffResultToDiffResultDtoMapper.mapForRevision(REPOSITORY, diffResult, "revision"))
|
||||||
|
.thenReturn(new DiffResultDto(Links.linkingTo().self("http://self").build()));
|
||||||
|
MockHttpRequest request = MockHttpRequest
|
||||||
|
.get(DIFF_URL + "revision/parsed?limit=42")
|
||||||
|
.accept(VndMediaType.DIFF_PARSED);
|
||||||
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
|
|
||||||
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
verify(diffResultCommandBuilder).setLimit(42);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldGet404OnMissingRepository() throws URISyntaxException {
|
public void shouldGet404OnMissingRepository() throws URISyntaxException {
|
||||||
when(serviceFactory.create(any(NamespaceAndName.class))).thenThrow(new NotFoundException("Text", "x"));
|
when(serviceFactory.create(any(NamespaceAndName.class))).thenThrow(new NotFoundException("Text", "x"));
|
||||||
|
|||||||
@@ -35,13 +35,13 @@ import sonia.scm.repository.api.DiffResult;
|
|||||||
import sonia.scm.repository.api.Hunk;
|
import sonia.scm.repository.api.Hunk;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.OptionalInt;
|
import java.util.OptionalInt;
|
||||||
|
|
||||||
import static java.net.URI.create;
|
import static java.net.URI.create;
|
||||||
import static java.util.Collections.emptyIterator;
|
import static java.util.Collections.emptyIterator;
|
||||||
|
import static java.util.Optional.of;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
@@ -86,6 +86,21 @@ class DiffResultToDiffResultDtoMapperTest {
|
|||||||
.isEqualTo("/scm/api/v2/repositories/space/X/diff/123/parsed");
|
.isEqualTo("/scm/api/v2/repositories/space/X/diff/123/parsed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateNextLinkForRevision() {
|
||||||
|
DiffResult result = createResult();
|
||||||
|
mockPartialResult(result);
|
||||||
|
|
||||||
|
DiffResultDto dto = mapper.mapForRevision(REPOSITORY, result, "123");
|
||||||
|
|
||||||
|
Optional<Link> nextLink = dto.getLinks().getLinkBy("next");
|
||||||
|
assertThat(nextLink)
|
||||||
|
.isPresent()
|
||||||
|
.get()
|
||||||
|
.extracting("href")
|
||||||
|
.isEqualTo("/scm/api/v2/repositories/space/X/diff/123/parsed?offset=30&limit=10");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldCreateLinkToLoadMoreLinesForFilesWithHunks() {
|
void shouldCreateLinkToLoadMoreLinesForFilesWithHunks() {
|
||||||
DiffResultDto dto = mapper.mapForRevision(REPOSITORY, createResult(), "123");
|
DiffResultDto dto = mapper.mapForRevision(REPOSITORY, createResult(), "123");
|
||||||
@@ -111,6 +126,55 @@ class DiffResultToDiffResultDtoMapperTest {
|
|||||||
.isEqualTo("/scm/api/v2/repositories/space/X/incoming/feature%2Fsome/master/diff/parsed");
|
.isEqualTo("/scm/api/v2/repositories/space/X/incoming/feature%2Fsome/master/diff/parsed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateSelfLinkForIncomingWithOffset() {
|
||||||
|
DiffResult result = createResult();
|
||||||
|
when(result.getOffset()).thenReturn(25);
|
||||||
|
DiffResultDto dto = mapper.mapForIncoming(REPOSITORY, result, "feature/some", "master");
|
||||||
|
|
||||||
|
Optional<Link> selfLink = dto.getLinks().getLinkBy("self");
|
||||||
|
assertThat(selfLink)
|
||||||
|
.isPresent()
|
||||||
|
.get()
|
||||||
|
.extracting("href")
|
||||||
|
.isEqualTo("/scm/api/v2/repositories/space/X/incoming/feature%2Fsome/master/diff/parsed?offset=25");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateSelfLinkForIncomingWithLimit() {
|
||||||
|
DiffResult result = createResult();
|
||||||
|
when(result.getLimit()).thenReturn(of(25));
|
||||||
|
DiffResultDto dto = mapper.mapForIncoming(REPOSITORY, result, "feature/some", "master");
|
||||||
|
|
||||||
|
Optional<Link> selfLink = dto.getLinks().getLinkBy("self");
|
||||||
|
assertThat(selfLink)
|
||||||
|
.isPresent()
|
||||||
|
.get()
|
||||||
|
.extracting("href")
|
||||||
|
.isEqualTo("/scm/api/v2/repositories/space/X/incoming/feature%2Fsome/master/diff/parsed?offset=0&limit=25");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateNextLinkForIncoming() {
|
||||||
|
DiffResult result = createResult();
|
||||||
|
mockPartialResult(result);
|
||||||
|
|
||||||
|
DiffResultDto dto = mapper.mapForIncoming(REPOSITORY, result, "feature/some", "master");
|
||||||
|
|
||||||
|
Optional<Link> nextLink = dto.getLinks().getLinkBy("next");
|
||||||
|
assertThat(nextLink)
|
||||||
|
.isPresent()
|
||||||
|
.get()
|
||||||
|
.extracting("href")
|
||||||
|
.isEqualTo("/scm/api/v2/repositories/space/X/incoming/feature%2Fsome/master/diff/parsed?offset=30&limit=10");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mockPartialResult(DiffResult result) {
|
||||||
|
when(result.getLimit()).thenReturn(of(10));
|
||||||
|
when(result.getOffset()).thenReturn(20);
|
||||||
|
when(result.isPartial()).thenReturn(true);
|
||||||
|
}
|
||||||
|
|
||||||
private DiffResult createResult() {
|
private DiffResult createResult() {
|
||||||
return result(
|
return result(
|
||||||
addedFile("A.java", "abc"),
|
addedFile("A.java", "abc"),
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import org.junit.After;
|
|||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.Answers;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.MockitoJUnitRunner;
|
import org.mockito.junit.MockitoJUnitRunner;
|
||||||
@@ -71,6 +72,7 @@ import static org.junit.Assert.assertTrue;
|
|||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
import static sonia.scm.repository.api.DiffFormat.NATIVE;
|
import static sonia.scm.repository.api.DiffFormat.NATIVE;
|
||||||
|
|
||||||
@@ -97,9 +99,9 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
|
|||||||
@Mock
|
@Mock
|
||||||
private LogCommandBuilder logCommandBuilder;
|
private LogCommandBuilder logCommandBuilder;
|
||||||
|
|
||||||
@Mock
|
@Mock(answer = Answers.RETURNS_SELF)
|
||||||
private DiffCommandBuilder diffCommandBuilder;
|
private DiffCommandBuilder diffCommandBuilder;
|
||||||
@Mock
|
@Mock(answer = Answers.RETURNS_SELF)
|
||||||
private DiffResultCommandBuilder diffResultCommandBuilder;
|
private DiffResultCommandBuilder diffResultCommandBuilder;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
@@ -199,9 +201,6 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldGetDiffs() throws Exception {
|
public void shouldGetDiffs() throws Exception {
|
||||||
when(diffCommandBuilder.setRevision("src_changeset_id")).thenReturn(diffCommandBuilder);
|
|
||||||
when(diffCommandBuilder.setAncestorChangeset("target_changeset_id")).thenReturn(diffCommandBuilder);
|
|
||||||
when(diffCommandBuilder.setFormat(NATIVE)).thenReturn(diffCommandBuilder);
|
|
||||||
when(diffCommandBuilder.retrieveContent()).thenReturn(output -> {});
|
when(diffCommandBuilder.retrieveContent()).thenReturn(output -> {});
|
||||||
MockHttpRequest request = MockHttpRequest
|
MockHttpRequest request = MockHttpRequest
|
||||||
.get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff")
|
.get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff")
|
||||||
@@ -217,12 +216,13 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
|
|||||||
assertThat(response.getOutputHeaders().containsKey(expectedHeader)).isTrue();
|
assertThat(response.getOutputHeaders().containsKey(expectedHeader)).isTrue();
|
||||||
assertThat((String) response.getOutputHeaders().get("Content-Disposition").get(0))
|
assertThat((String) response.getOutputHeaders().get("Content-Disposition").get(0))
|
||||||
.contains(expectedValue);
|
.contains(expectedValue);
|
||||||
|
verify(diffCommandBuilder).setRevision("src_changeset_id");
|
||||||
|
verify(diffCommandBuilder).setAncestorChangeset("target_changeset_id");
|
||||||
|
verify(diffCommandBuilder).setFormat(NATIVE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldGetParsedDiffs() throws Exception {
|
public void shouldGetParsedDiffs() throws Exception {
|
||||||
when(diffResultCommandBuilder.setRevision("src_changeset_id")).thenReturn(diffResultCommandBuilder);
|
|
||||||
when(diffResultCommandBuilder.setAncestorChangeset("target_changeset_id")).thenReturn(diffResultCommandBuilder);
|
|
||||||
DiffResult diffResult = mock(DiffResult.class);
|
DiffResult diffResult = mock(DiffResult.class);
|
||||||
when(diffResultCommandBuilder.getDiffResult()).thenReturn(diffResult);
|
when(diffResultCommandBuilder.getDiffResult()).thenReturn(diffResult);
|
||||||
when(diffResultToDiffResultDtoMapper.mapForIncoming(REPOSITORY, diffResult, "src_changeset_id", "target_changeset_id"))
|
when(diffResultToDiffResultDtoMapper.mapForIncoming(REPOSITORY, diffResult, "src_changeset_id", "target_changeset_id"))
|
||||||
@@ -239,6 +239,42 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
|
|||||||
.isEqualTo(200);
|
.isEqualTo(200);
|
||||||
assertThat(response.getContentAsString())
|
assertThat(response.getContentAsString())
|
||||||
.contains("\"self\":{\"href\":\"http://self\"}");
|
.contains("\"self\":{\"href\":\"http://self\"}");
|
||||||
|
verify(diffResultCommandBuilder).setRevision("src_changeset_id");
|
||||||
|
verify(diffResultCommandBuilder).setAncestorChangeset("target_changeset_id");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldGetParsedDiffsWithLimit() throws Exception {
|
||||||
|
DiffResult diffResult = mock(DiffResult.class);
|
||||||
|
when(diffResultCommandBuilder.getDiffResult()).thenReturn(diffResult);
|
||||||
|
when(diffResultToDiffResultDtoMapper.mapForIncoming(REPOSITORY, diffResult, "src_changeset_id", "target_changeset_id"))
|
||||||
|
.thenReturn(new DiffResultDto(Links.linkingTo().self("http://self").build()));
|
||||||
|
|
||||||
|
MockHttpRequest request = MockHttpRequest
|
||||||
|
.get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff/parsed?limit=42")
|
||||||
|
.accept(VndMediaType.DIFF_PARSED);
|
||||||
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
|
|
||||||
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
verify(diffResultCommandBuilder).setLimit(42);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldGetParsedDiffsWithOffset() throws Exception {
|
||||||
|
DiffResult diffResult = mock(DiffResult.class);
|
||||||
|
when(diffResultCommandBuilder.getDiffResult()).thenReturn(diffResult);
|
||||||
|
when(diffResultToDiffResultDtoMapper.mapForIncoming(REPOSITORY, diffResult, "src_changeset_id", "target_changeset_id"))
|
||||||
|
.thenReturn(new DiffResultDto(Links.linkingTo().self("http://self").build()));
|
||||||
|
|
||||||
|
MockHttpRequest request = MockHttpRequest
|
||||||
|
.get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff/parsed?offset=42")
|
||||||
|
.accept(VndMediaType.DIFF_PARSED);
|
||||||
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
|
|
||||||
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
verify(diffResultCommandBuilder).setOffset(42);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -256,9 +292,6 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldGet404OnMissingRevision() throws Exception {
|
public void shouldGet404OnMissingRevision() throws Exception {
|
||||||
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
|
|
||||||
when(diffCommandBuilder.setAncestorChangeset(anyString())).thenReturn(diffCommandBuilder);
|
|
||||||
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
|
|
||||||
when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Text", "x"));
|
when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Text", "x"));
|
||||||
|
|
||||||
MockHttpRequest request = MockHttpRequest
|
MockHttpRequest request = MockHttpRequest
|
||||||
@@ -273,9 +306,6 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldGet400OnCrlfInjection() throws Exception {
|
public void shouldGet400OnCrlfInjection() throws Exception {
|
||||||
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
|
|
||||||
when(diffCommandBuilder.setAncestorChangeset(anyString())).thenReturn(diffCommandBuilder);
|
|
||||||
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
|
|
||||||
when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Text", "x"));
|
when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Text", "x"));
|
||||||
MockHttpRequest request = MockHttpRequest
|
MockHttpRequest request = MockHttpRequest
|
||||||
.get(INCOMING_DIFF_URL + "ny%0D%0ASet-cookie:%20Tamper=3079675143472450634/ny%0D%0ASet-cookie:%20Tamper=3079675143472450634/diff")
|
.get(INCOMING_DIFF_URL + "ny%0D%0ASet-cookie:%20Tamper=3079675143472450634/ny%0D%0ASet-cookie:%20Tamper=3079675143472450634/diff")
|
||||||
@@ -290,9 +320,6 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldGet400OnUnknownFormat() throws Exception {
|
public void shouldGet400OnUnknownFormat() throws Exception {
|
||||||
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
|
|
||||||
when(diffCommandBuilder.setAncestorChangeset(anyString())).thenReturn(diffCommandBuilder);
|
|
||||||
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
|
|
||||||
when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Test", "test"));
|
when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Test", "test"));
|
||||||
MockHttpRequest request = MockHttpRequest
|
MockHttpRequest request = MockHttpRequest
|
||||||
.get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff?format=Unknown")
|
.get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff?format=Unknown")
|
||||||
|
|||||||
Reference in New Issue
Block a user