Add option to hide whitepace changes in diffs

Co-authored-by: Florian Scholdei<florian.scholdei@cloudogu.com>
Co-authored-by: René Pfeuffer<rene.pfeuffer@cloudogu.com>


Reviewed-by: Florian Scholdei <florian.scholdei@cloudogu.com>
This commit is contained in:
Viktor Egorov
2024-05-14 11:33:48 +02:00
parent e5e2fd151c
commit c8ef99cf07
37 changed files with 688 additions and 498 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 130 KiB

View File

@@ -52,11 +52,15 @@ Die Diffs können durch Klicken auf den blauen Balken schrittweise oder vollstä
Falls sich Commit Links im Format "namespace/name@commitId" in der Changeset Beschreibung befinden, werden die zu relativen SCM-Manager Links erweitert.
Beispielsweise wird der Text hitchhiker/HeartOfGold@1a2b3c4 zu einem Link zu dem Commit 1a2b3c4 im Repository hitchhiker/HeartOfGold umgewandelt.
In der Diff Ansicht gibt es mehrere Buttons.
- Der erste Button von Links ermöglicht eine direkte Gegenüberstellung von den Änderungen.
- Mit der Lupe kann man die Änderungen auf den ganzen Bildschirm betrachten.
- Der nächste Button schaltet Leerzeichen und Tabs ein bzw. aus.
- Der letzte Button bringt Sie zur Quelldatei.
Für das Changeset gibt es zwei Buttons:
- Mit dem ersten Button können die Whitespaces-Änderungen ein- und ausgeblendet werden.
- Der zweite ermöglicht das Ein- und Ausblenden aller Changesets.
Jeder Changeset Diff hat mehrere Buttons:
- Der erste Button von Links ermöglicht einen direkten Vergleich der Änderungen.
- Mit der Lupe können die Änderungen über die gesamte Breite des Fensters betrachtet werden.
- Der nächste Button schaltet Leerzeichen und Tabs ein und aus.
- Der letzte Button führt zur Quelldatei.
![Repository-Code-Changesets](assets/repository-code-changesetDetails.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 130 KiB

View File

@@ -54,10 +54,14 @@ You can expand the diffs gradually or completely by clicking on the blue bars.
If commit links formatted like "namespace/name@commitId" are used in the changeset description they will be rendered to internal links.
For example the text hitchhiker/HeartOfGold@1a2b3c4 will be transformed to a link directing to the commit 1a2b3c4 of the repository hitchhiker/heartOfGold.
The details view has several buttons.
There are two buttons for the changeset:
- The first button show and hide the whitespace changes.
- The second button can collapse all changesets.
Every changeset diff has several buttons:
- The first button on the left allows a direct comparison of the changes.
- With the magnifying glass you can view the changes on the entire screen.
- The next button turns whitespaces and tabs on or off.
- With the magnifying glass you can view the changes across the width of the window.
- The next button switches spaces and tabs on and off.
- The last button takes you to the source file.
![Repository-Code-Changesets](assets/repository-code-changesetDetails.png)

View File

@@ -0,0 +1,2 @@
- type: added
description: Global button to ignore whitespaces and tabs inside of diffs

View File

@@ -91,4 +91,9 @@ abstract class AbstractDiffCommandBuilder <T extends AbstractDiffCommandBuilder,
abstract T self();
abstract R createRequest();
public T setIgnoreWhitespace(IgnoreWhitespaceLevel ignoreWhitespace) {
request.setIgnoreWhitespaceLevel(ignoreWhitespace);
return self();
}
}

View File

@@ -45,4 +45,8 @@ public interface DiffResult extends Iterable<DiffFile> {
default Optional<Integer> getLimit() {
return empty();
}
default IgnoreWhitespaceLevel getIgnoreWhitespace() {
return IgnoreWhitespaceLevel.NONE;
}
}

View File

@@ -0,0 +1,31 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.api;
public enum IgnoreWhitespaceLevel {
ALL,
NONE;
}

View File

@@ -29,6 +29,7 @@ import com.google.common.base.Strings;
import lombok.EqualsAndHashCode;
import sonia.scm.Validateable;
import sonia.scm.repository.api.DiffFormat;
import sonia.scm.repository.api.IgnoreWhitespaceLevel;
/**
*
@@ -44,6 +45,8 @@ public class DiffCommandRequest extends FileBaseCommandRequest
private String ancestorChangeset;
private IgnoreWhitespaceLevel ignoreWhitespace;
@Override
public DiffCommandRequest clone()
{
@@ -100,4 +103,10 @@ public class DiffCommandRequest extends FileBaseCommandRequest
return ancestorChangeset;
}
public IgnoreWhitespaceLevel getIgnoreWhitespaceLevel() { return ignoreWhitespace; }
public void setIgnoreWhitespaceLevel(IgnoreWhitespaceLevel ignoreWhitespace) {
this.ignoreWhitespace = ignoreWhitespace;
}
}

View File

@@ -28,8 +28,10 @@ import com.google.inject.assistedinject.Assisted;
import jakarta.inject.Inject;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.diff.RawTextComparator;
import org.eclipse.jgit.util.QuotedString;
import sonia.scm.repository.api.DiffCommandBuilder;
import sonia.scm.repository.api.IgnoreWhitespaceLevel;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
@@ -49,13 +51,16 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand {
@Override
public DiffCommandBuilder.OutputStreamConsumer getDiffResult(DiffCommandRequest request) throws IOException {
@SuppressWarnings("squid:S2095") // repository will be closed with the RepositoryService
org.eclipse.jgit.lib.Repository repository = open();
org.eclipse.jgit.lib.Repository repository = open();
Differ.Diff diff = Differ.diff(repository, request);
return output -> {
try (DiffFormatter formatter = new DiffFormatter(new DequoteOutputStream(output))) {
formatter.setRepository(repository);
if (request.getIgnoreWhitespaceLevel() == IgnoreWhitespaceLevel.ALL) {
formatter.setDiffComparator(RawTextComparator.WS_IGNORE_ALL);
}
for (DiffEntry e : diff.getEntries()) {
if (idOrPathChanged(e)) {

View File

@@ -26,6 +26,7 @@ package sonia.scm.repository.spi;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.diff.RawTextComparator;
import org.eclipse.jgit.lib.ObjectId;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException;
@@ -33,6 +34,7 @@ import sonia.scm.repository.Repository;
import sonia.scm.repository.api.DiffFile;
import sonia.scm.repository.api.DiffResult;
import sonia.scm.repository.api.Hunk;
import sonia.scm.repository.api.IgnoreWhitespaceLevel;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -51,14 +53,21 @@ public class GitDiffResult implements DiffResult {
private final Differ.Diff diff;
private final List<DiffEntry> diffEntries;
private final IgnoreWhitespaceLevel ignoreWhitespaceLevel;
private final int offset;
private final Integer limit;
public GitDiffResult(Repository scmRepository, org.eclipse.jgit.lib.Repository repository, Differ.Diff diff, int offset, Integer limit) {
public GitDiffResult(Repository scmRepository,
org.eclipse.jgit.lib.Repository repository,
Differ.Diff diff,
IgnoreWhitespaceLevel ignoreWhitespaceLevel,
int offset,
Integer limit) {
this.scmRepository = scmRepository;
this.repository = repository;
this.diff = diff;
this.offset = offset;
this.ignoreWhitespaceLevel = ignoreWhitespaceLevel;
this.limit = limit;
this.diffEntries = diff.getEntries();
}
@@ -163,6 +172,9 @@ public class GitDiffResult implements DiffResult {
private String format(org.eclipse.jgit.lib.Repository repository, DiffEntry entry) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); DiffFormatter formatter = new DiffFormatter(baos)) {
if (ignoreWhitespaceLevel == IgnoreWhitespaceLevel.ALL) {
formatter.setDiffComparator(RawTextComparator.WS_IGNORE_ALL);
}
formatter.setRepository(repository);
formatter.format(entry);
return baos.toString(StandardCharsets.UTF_8);
@@ -170,6 +182,10 @@ public class GitDiffResult implements DiffResult {
throw new InternalRepositoryException(scmRepository, "failed to format diff entry", ex);
}
}
}
@Override
public IgnoreWhitespaceLevel getIgnoreWhitespace() {
return ignoreWhitespaceLevel;
}
}

View File

@@ -40,9 +40,16 @@ public class GitDiffResultCommand extends AbstractGitCommand implements DiffResu
super(context);
}
public DiffResult getDiffResult(DiffCommandRequest diffCommandRequest) throws IOException {
public DiffResult getDiffResult(DiffCommandRequest request) throws IOException {
org.eclipse.jgit.lib.Repository repository = open();
return new GitDiffResult(this.repository, repository, Differ.diff(repository, diffCommandRequest), 0, null);
return new GitDiffResult(
this.repository,
repository,
Differ.diff(repository, request),
request.getIgnoreWhitespaceLevel(),
0,
null
);
}
@Override
@@ -50,7 +57,14 @@ public class GitDiffResultCommand extends AbstractGitCommand implements DiffResu
org.eclipse.jgit.lib.Repository repository = open();
int offset = request.getOffset() == null ? 0 : request.getOffset();
try {
return new GitDiffResult(this.repository, repository, Differ.diff(repository, request), offset, request.getLimit());
return new GitDiffResult(
this.repository,
repository,
Differ.diff(repository, request),
request.getIgnoreWhitespaceLevel(),
offset,
request.getLimit()
);
} catch (AmbiguousObjectException ex) {
throw new NotUniqueRevisionException(Repository.class, context.getRepository().getId());
}

View File

@@ -41,6 +41,7 @@ import sonia.scm.repository.Modifications;
import sonia.scm.repository.Modified;
import sonia.scm.repository.Removed;
import sonia.scm.repository.Renamed;
import sonia.scm.repository.api.IgnoreWhitespaceLevel;
import java.io.IOException;
import java.text.MessageFormat;

View File

@@ -25,6 +25,7 @@
package sonia.scm.repository.spi;
import org.junit.Test;
import sonia.scm.repository.api.IgnoreWhitespaceLevel;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -77,6 +78,20 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase {
" b\n" +
"+change\n";
public static final String DIFF_IGNORE_WHITESPACE = "diff --git a/a.txt b/a.txt\n" +
"index 2f8bc28..fc3f0ba 100644\n" +
"--- a/a.txt\n" +
"+++ b/a.txt\n";
public static final String DIFF_WITH_WHITESPACE = "diff --git a/a.txt b/a.txt\n" +
"index 2f8bc28..fc3f0ba 100644\n" +
"--- a/a.txt\n" +
"+++ b/a.txt\n" +
"@@ -1,2 +1,2 @@\n" +
" a\n" +
"-line for blame\n" +
"+line for blame\n";
@Test
public void diffForOneRevisionShouldCreateDiff() throws IOException {
GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext());
@@ -97,6 +112,28 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase {
assertEquals(DIFF_FILE_A + DIFF_FILE_B, output.toString());
}
@Test
public void shouldIgnoreWhiteSpace() throws IOException {
GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext());
DiffCommandRequest diffCommandRequest = new DiffCommandRequest();
diffCommandRequest.setIgnoreWhitespaceLevel(IgnoreWhitespaceLevel.ALL);
diffCommandRequest.setRevision("whitespace");
ByteArrayOutputStream output = new ByteArrayOutputStream();
gitDiffCommand.getDiffResult(diffCommandRequest).accept(output);
assertEquals(DIFF_IGNORE_WHITESPACE, output.toString());
}
@Test
public void shouldNotIgnoreWhiteSpace() throws IOException {
GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext());
DiffCommandRequest diffCommandRequest = new DiffCommandRequest();
diffCommandRequest.setIgnoreWhitespaceLevel(IgnoreWhitespaceLevel.NONE);
diffCommandRequest.setRevision("whitespace");
ByteArrayOutputStream output = new ByteArrayOutputStream();
gitDiffCommand.getDiffResult(diffCommandRequest).accept(output);
assertEquals(DIFF_WITH_WHITESPACE, output.toString());
}
@Test
public void diffForPathShouldCreateLimitedDiff() throws IOException {
GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext());
@@ -156,4 +193,9 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase {
.contains("rename from b.txt")
.contains("rename to b-copy.txt");
}
@Override
protected String getZippedRepositoryResource() {
return "sonia/scm/repository/spi/scm-git-spi-whitespace-test.zip";
}
}

View File

@@ -28,11 +28,14 @@ import org.junit.Test;
import sonia.scm.repository.api.DiffFile;
import sonia.scm.repository.api.DiffResult;
import sonia.scm.repository.api.Hunk;
import sonia.scm.repository.api.IgnoreWhitespaceLevel;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Iterator;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
public class GitDiffResultCommandTest extends AbstractGitCommandTestBase {
@@ -162,6 +165,48 @@ public class GitDiffResultCommandTest extends AbstractGitCommandTestBase {
assertThat(diffResult.getOffset()).isZero();
}
@Test
public void shouldIgnoreWhiteSpace() throws IOException {
GitDiffResultCommand gitDiffResultCommand = new GitDiffResultCommand(createContext());
DiffResultCommandRequest diffCommandRequest = new DiffResultCommandRequest();
diffCommandRequest.setRevision("whitespace");
diffCommandRequest.setIgnoreWhitespaceLevel(IgnoreWhitespaceLevel.ALL);
DiffResult diffResult = gitDiffResultCommand.getDiffResult(diffCommandRequest);
Iterator<DiffFile> iterator = diffResult.iterator();
DiffFile a = iterator.next();
Iterator<Hunk> hunks = a.iterator();
assertThat(hunks).isExhausted();
}
@Test
public void shouldNotIgnoreWhiteSpace() throws IOException {
GitDiffResultCommand gitDiffResultCommand = new GitDiffResultCommand(createContext());
DiffResultCommandRequest diffCommandRequest = new DiffResultCommandRequest();
diffCommandRequest.setRevision("whitespace");
diffCommandRequest.setIgnoreWhitespaceLevel(IgnoreWhitespaceLevel.NONE);
DiffResult diffResult = gitDiffResultCommand.getDiffResult(diffCommandRequest);
Iterator<DiffFile> iterator = diffResult.iterator();
DiffFile a = iterator.next();
Iterator<Hunk> hunks = a.iterator();
Hunk hunk = hunks.next();
assertThat(hunk.getOldStart()).isEqualTo(1);
assertThat(hunk.getOldLineCount()).isEqualTo(2);
assertThat(hunk.iterator())
.toIterable()
.extracting("content")
.containsExactly(
"a",
"line for blame",
"line for blame"
);
}
private DiffResult createDiffResult(String s) throws IOException {
return createDiffResult(s, null, null);
}
@@ -175,4 +220,9 @@ public class GitDiffResultCommandTest extends AbstractGitCommandTestBase {
return gitDiffResultCommand.getDiffResult(diffCommandRequest);
}
@Override
protected String getZippedRepositoryResource() {
return "sonia/scm/repository/spi/scm-git-spi-whitespace-test.zip";
}
}

View File

@@ -32,6 +32,7 @@ import jakarta.inject.Inject;
import org.javahg.Repository;
import sonia.scm.repository.api.DiffCommandBuilder;
import sonia.scm.repository.api.DiffFormat;
import sonia.scm.repository.api.IgnoreWhitespaceLevel;
import sonia.scm.repository.spi.javahg.HgDiffInternalCommand;
import sonia.scm.web.HgUtil;
@@ -82,6 +83,9 @@ public class HgDiffCommand extends AbstractCommand implements DiffCommand {
cmd.git();
}
String revision = HgUtil.getRevision(request.getRevision());
if (request.getIgnoreWhitespaceLevel() == IgnoreWhitespaceLevel.ALL) {
cmd.cmdAppend("-w");
}
if (request.getAncestorChangeset() != null) {
String ancestor = HgUtil.getRevision(request.getAncestorChangeset());
cmd.cmdAppend(String.format("-r ancestor(%s,%s):%s", ancestor, revision, revision));

View File

@@ -27,6 +27,7 @@ package sonia.scm.repository.spi;
import org.junit.Test;
import sonia.scm.repository.api.DiffCommandBuilder;
import sonia.scm.repository.api.DiffFormat;
import sonia.scm.repository.api.IgnoreWhitespaceLevel;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -66,7 +67,7 @@ public class HgDiffCommandTest extends AbstractHgCommandTestBase {
}
@Test
public void shouldNotCloseInternalStream() throws IOException {
public void shouldNotCloseInternalStream() {
HgCommandContext context = spy(cmdContext);
DiffCommandRequest request = new DiffCommandRequest();
request.setRevision("3049df33fdbbded08b707bac3eccd0f7b453c58b");
@@ -106,6 +107,25 @@ public class HgDiffCommandTest extends AbstractHgCommandTestBase {
verify(context).close();
}
@Test
public void shouldNotIgnoreWhitespaceInDefaultDiff() throws IOException {
String content = diff(cmdContext, "2b6f8a90b33f");
assertThat(content).contains("""
@@ -1,2 +1,2 @@
a
-line for blame
+line for blame""");
}
@Test
public void shouldIgnoreWhitespaceInDiff() throws IOException {
DiffCommandRequest request = new DiffCommandRequest();
request.setIgnoreWhitespaceLevel(IgnoreWhitespaceLevel.ALL);
request.setRevision("2b6f8a90b33f");
String content = diff(cmdContext, request);
assertThat(content).isEmpty();
}
private String diff(HgCommandContext context, String revision) throws IOException {
DiffCommandRequest request = new DiffCommandRequest();
request.setRevision(revision);
@@ -124,4 +144,8 @@ public class HgDiffCommandTest extends AbstractHgCommandTestBase {
return baos.toString(UTF_8);
}
@Override
protected String getZippedRepositoryResource() {
return "sonia/scm/repository/spi/scm-hg-spi-diff-test.zip";
}
}

View File

@@ -34,17 +34,18 @@ import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.internal.wc2.ng.SvnNewDiffGenerator;
import org.tmatesoft.svn.core.wc.SVNClientManager;
import org.tmatesoft.svn.core.wc.SVNDiffClient;
import org.tmatesoft.svn.core.wc.SVNDiffOptions;
import org.tmatesoft.svn.core.wc.SVNRevision;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.SvnUtil;
import sonia.scm.repository.api.DiffCommandBuilder;
import sonia.scm.repository.api.DiffFormat;
import sonia.scm.repository.api.IgnoreWhitespaceLevel;
import sonia.scm.util.Util;
public class SvnDiffCommand extends AbstractSvnCommand implements DiffCommand {
private static final Logger logger =
LoggerFactory.getLogger(SvnDiffCommand.class);
@@ -67,8 +68,11 @@ public class SvnDiffCommand extends AbstractSvnCommand implements DiffCommand {
}
clientManager = SVNClientManager.newInstance();
SVNDiffClient diffClient = clientManager.getDiffClient();
diffClient.setDiffGenerator(new SvnNewDiffGenerator(new SCMSvnDiffGenerator()));
SCMSvnDiffGenerator generator = new SCMSvnDiffGenerator();
diffClient.setDiffGenerator(new SvnNewDiffGenerator(generator));
if (request.getIgnoreWhitespaceLevel() == IgnoreWhitespaceLevel.ALL) {
generator.setDiffOptions(new SVNDiffOptions(true, true, true));
}
long currentRev = SvnUtil.getRevisionNumber(request.getRevision(), repository);
diffClient.setGitDiffFormat(request.getFormat() == DiffFormat.GIT);

View File

@@ -38,10 +38,12 @@ import org.tmatesoft.svn.core.wc.SVNClientManager;
import org.tmatesoft.svn.core.wc.SVNRevision;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.repository.api.DiffFormat;
import sonia.scm.repository.api.IgnoreWhitespaceLevel;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;
import java.util.Map;
@@ -64,6 +66,47 @@ class SvnDiffCommandTest {
workingCopy = directory.resolve("working-copy").toFile();
}
@Test
void shouldCreateDiffForSimpleFile() throws SVNException, IOException {
createRepository();
Path newFile = workingCopy.toPath().resolve("a.txt");
Files.write(newFile, "Some nice content\n".getBytes());
client.getWCClient().doAdd(newFile.toFile(), false, false, false, SVNDepth.INFINITY, false, false);
commit("add a.txt");
Files.write(newFile, "Some more content\n".getBytes());
commit("modify a.txt");
String diff = gitDiff("2");
assertThat(diff).isEqualTo("""
diff --git a/a.txt b/a.txt
--- a/a.txt
+++ b/a.txt
@@ -1 +1 @@
-Some nice content
+Some more content
""");
}
@Test
void shouldIgnoreWhitespaceChanges() throws SVNException, IOException {
createRepository();
Path newFile = workingCopy.toPath().resolve("a.txt");
Files.write(newFile, "Some nice content\n".getBytes());
client.getWCClient().doAdd(newFile.toFile(), false, false, false, SVNDepth.INFINITY, false, false);
commit("add a.txt");
Files.write(newFile, "Some nice content \n".getBytes());
commit("modify a.txt");
DiffCommandRequest request = createSimpleDiffRequest("2");
request.setIgnoreWhitespaceLevel(IgnoreWhitespaceLevel.ALL);
String diff = executeDiff(request);
assertThat(diff).isEqualTo("diff --git a/a.txt b/a.txt\n");
}
@Test
void shouldCreateGitCompatibleDiffForSinglePropChanges() throws SVNException, IOException {
createRepository();
@@ -150,16 +193,25 @@ class SvnDiffCommandTest {
@Nonnull
private String gitDiff(String revision) throws IOException {
DiffCommandRequest request = createSimpleDiffRequest(revision);
return executeDiff(request);
}
private String executeDiff(DiffCommandRequest request) throws IOException {
SvnDiffCommand command = createCommand();
DiffCommandRequest request = new DiffCommandRequest();
request.setFormat(DiffFormat.GIT);
request.setRevision(revision);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
command.getDiffResult(request).accept(baos);
return baos.toString();
}
private static DiffCommandRequest createSimpleDiffRequest(String revision) {
DiffCommandRequest request = new DiffCommandRequest();
request.setFormat(DiffFormat.GIT);
request.setRevision(revision);
return request;
}
private SvnDiffCommand createCommand() {
return new SvnDiffCommand(new SvnContext(RepositoryTestData.createHeartOfGold(), repository));
}

View File

@@ -221,11 +221,11 @@ describe("Test diff", () => {
});
it("should append query parameters to url which has already query params", async () => {
fetchMock.getOnce("/api/v2/diff?format=GIT&limit=25", {
fetchMock.getOnce("/api/v2/diff?format=GIT&limit=25&ignoreWhitespace=NONE", {
body: simpleDiff,
headers: { "Content-Type": "application/vnd.scmm-diffparsed+json;v=2" },
});
const { result, waitFor } = renderHook(() => useDiff("/diff?format=GIT", { limit: 25 }), {
const { result, waitFor } = renderHook(() => useDiff("/diff?format=GIT", { limit: 25, ignoreWhitespace: "NONE" }), {
wrapper: createWrapper(),
});
await waitFor(() => !!result.current.data);

View File

@@ -31,6 +31,7 @@ import { Diff, Link } from "@scm-manager/ui-types";
type UseDiffOptions = {
limit?: number;
refetchOnWindowFocus?: boolean;
ignoreWhitespace?: string;
};
const defaultOptions: UseDiffOptions = {
@@ -41,10 +42,10 @@ export const useDiff = (link: string, options: UseDiffOptions = defaultOptions)
let initialLink = link;
if (options.limit) {
const separator = initialLink.includes("?") ? "&" : "?";
initialLink = `${initialLink}${separator}limit=${options.limit}`;
initialLink = `${initialLink}${separator}limit=${options.limit}&ignoreWhitespace=${options.ignoreWhitespace}`;
}
const { isLoading, error, data, isFetchingNextPage, fetchNextPage } = useInfiniteQuery<Diff, Error, Diff>(
["link", link],
["link", link, options.ignoreWhitespace],
({ pageParam }) => {
return apiClient.get(pageParam || initialLink).then((response) => {
const contentType = response.headers.get("Content-Type");

View File

@@ -22830,11 +22830,9 @@ exports[`Storyshots Repositories/Diff Binaries 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -23129,11 +23127,9 @@ exports[`Storyshots Repositories/Diff Changing Content 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -23766,11 +23762,9 @@ exports[`Storyshots Repositories/Diff Changing Content 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -24740,11 +24734,9 @@ exports[`Storyshots Repositories/Diff Changing Content 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -25285,11 +25277,9 @@ exports[`Storyshots Repositories/Diff Changing Content 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -25830,11 +25820,9 @@ exports[`Storyshots Repositories/Diff Changing Content 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -27065,11 +27053,9 @@ exports[`Storyshots Repositories/Diff Changing Content 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -27660,11 +27646,9 @@ exports[`Storyshots Repositories/Diff Collapsed 1`] = `
alt="diff.showContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-right"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-right"
/>
</span>
<h4
@@ -27790,11 +27774,9 @@ exports[`Storyshots Repositories/Diff Collapsed 1`] = `
alt="diff.showContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-right"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-right"
/>
</span>
<h4
@@ -27920,11 +27902,9 @@ exports[`Storyshots Repositories/Diff Collapsed 1`] = `
alt="diff.showContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-right"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-right"
/>
</span>
<h4
@@ -28050,11 +28030,9 @@ exports[`Storyshots Repositories/Diff Collapsed 1`] = `
alt="diff.showContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-right"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-right"
/>
</span>
<h4
@@ -28180,11 +28158,9 @@ exports[`Storyshots Repositories/Diff Collapsed 1`] = `
alt="diff.showContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-right"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-right"
/>
</span>
<h4
@@ -28310,11 +28286,9 @@ exports[`Storyshots Repositories/Diff Collapsed 1`] = `
alt="diff.showContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-right"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-right"
/>
</span>
<h4
@@ -28449,11 +28423,9 @@ exports[`Storyshots Repositories/Diff CollapsingWithFunction 1`] = `
alt="diff.showContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-right"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-right"
/>
</span>
<h4
@@ -28543,11 +28515,9 @@ exports[`Storyshots Repositories/Diff CollapsingWithFunction 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -29420,11 +29390,9 @@ exports[`Storyshots Repositories/Diff CollapsingWithFunction 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -29893,11 +29861,9 @@ exports[`Storyshots Repositories/Diff CollapsingWithFunction 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -30366,11 +30332,9 @@ exports[`Storyshots Repositories/Diff CollapsingWithFunction 1`] = `
alt="diff.showContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-right"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-right"
/>
</span>
<h4
@@ -30460,11 +30424,9 @@ exports[`Storyshots Repositories/Diff CollapsingWithFunction 1`] = `
alt="diff.showContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-right"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-right"
/>
</span>
<h4
@@ -30644,11 +30606,9 @@ exports[`Storyshots Repositories/Diff Expandable 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -31281,11 +31241,9 @@ exports[`Storyshots Repositories/Diff Expandable 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -32255,11 +32213,9 @@ exports[`Storyshots Repositories/Diff Expandable 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -32800,11 +32756,9 @@ exports[`Storyshots Repositories/Diff Expandable 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -33345,11 +33299,9 @@ exports[`Storyshots Repositories/Diff Expandable 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -34580,11 +34532,9 @@ exports[`Storyshots Repositories/Diff Expandable 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -35174,11 +35124,9 @@ exports[`Storyshots Repositories/Diff External state management 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -35811,11 +35759,9 @@ exports[`Storyshots Repositories/Diff External state management 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -36785,11 +36731,9 @@ exports[`Storyshots Repositories/Diff External state management 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -37330,11 +37274,9 @@ exports[`Storyshots Repositories/Diff External state management 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -37875,11 +37817,9 @@ exports[`Storyshots Repositories/Diff External state management 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -39110,11 +39050,9 @@ exports[`Storyshots Repositories/Diff External state management 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -39704,11 +39642,9 @@ exports[`Storyshots Repositories/Diff File Annotation 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -40308,11 +40244,9 @@ exports[`Storyshots Repositories/Diff File Annotation 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -41189,11 +41123,9 @@ exports[`Storyshots Repositories/Diff File Annotation 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -41666,11 +41598,9 @@ exports[`Storyshots Repositories/Diff File Annotation 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -42143,11 +42073,9 @@ exports[`Storyshots Repositories/Diff File Annotation 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -43227,11 +43155,9 @@ exports[`Storyshots Repositories/Diff File Annotation 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -43788,11 +43714,9 @@ exports[`Storyshots Repositories/Diff File Controls 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -44403,11 +44327,9 @@ exports[`Storyshots Repositories/Diff File Controls 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -45295,11 +45217,9 @@ exports[`Storyshots Repositories/Diff File Controls 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -45783,11 +45703,9 @@ exports[`Storyshots Repositories/Diff File Controls 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -46271,11 +46189,9 @@ exports[`Storyshots Repositories/Diff File Controls 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -47366,11 +47282,9 @@ exports[`Storyshots Repositories/Diff File Controls 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -47938,11 +47852,9 @@ exports[`Storyshots Repositories/Diff Highlight line on hover 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -48802,11 +48714,9 @@ exports[`Storyshots Repositories/Diff Hunk gutter hover icon 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -49666,11 +49576,9 @@ exports[`Storyshots Repositories/Diff Hunks 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -50740,11 +50648,9 @@ exports[`Storyshots Repositories/Diff Line Annotation 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -51352,11 +51258,9 @@ exports[`Storyshots Repositories/Diff Line Annotation 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -52241,11 +52145,9 @@ exports[`Storyshots Repositories/Diff Line Annotation 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -52714,11 +52616,9 @@ exports[`Storyshots Repositories/Diff Line Annotation 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -53187,11 +53087,9 @@ exports[`Storyshots Repositories/Diff Line Annotation 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -54267,11 +54165,9 @@ exports[`Storyshots Repositories/Diff Line Annotation 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -54836,11 +54732,9 @@ exports[`Storyshots Repositories/Diff OnClick 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -55476,11 +55370,9 @@ exports[`Storyshots Repositories/Diff OnClick 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -56415,11 +56307,9 @@ exports[`Storyshots Repositories/Diff OnClick 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -56918,11 +56808,9 @@ exports[`Storyshots Repositories/Diff OnClick 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -57421,11 +57309,9 @@ exports[`Storyshots Repositories/Diff OnClick 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -58577,11 +58463,9 @@ exports[`Storyshots Repositories/Diff OnClick 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -59170,11 +59054,9 @@ exports[`Storyshots Repositories/Diff Side-By-Side 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -59863,11 +59745,9 @@ exports[`Storyshots Repositories/Diff Side-By-Side 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -60830,11 +60710,9 @@ exports[`Storyshots Repositories/Diff Side-By-Side 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -61355,11 +61233,9 @@ exports[`Storyshots Repositories/Diff Side-By-Side 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -61880,11 +61756,9 @@ exports[`Storyshots Repositories/Diff Side-By-Side 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -63129,11 +63003,9 @@ exports[`Storyshots Repositories/Diff Side-By-Side 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -63759,11 +63631,9 @@ exports[`Storyshots Repositories/Diff SyntaxHighlighting (Markdown) 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -64141,11 +64011,9 @@ exports[`Storyshots Repositories/Diff SyntaxHighlighting 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -64741,11 +64609,9 @@ exports[`Storyshots Repositories/Diff SyntaxHighlighting 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -65618,11 +65484,9 @@ exports[`Storyshots Repositories/Diff SyntaxHighlighting 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -66091,11 +65955,9 @@ exports[`Storyshots Repositories/Diff SyntaxHighlighting 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -66564,11 +66426,9 @@ exports[`Storyshots Repositories/Diff SyntaxHighlighting 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -67644,11 +67504,9 @@ exports[`Storyshots Repositories/Diff SyntaxHighlighting 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -68201,11 +68059,9 @@ exports[`Storyshots Repositories/Diff Whitespace 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -68801,11 +68657,9 @@ exports[`Storyshots Repositories/Diff Whitespace 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -69678,11 +69532,9 @@ exports[`Storyshots Repositories/Diff Whitespace 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -70151,11 +70003,9 @@ exports[`Storyshots Repositories/Diff Whitespace 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -70624,11 +70474,9 @@ exports[`Storyshots Repositories/Diff Whitespace 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -71704,11 +71552,9 @@ exports[`Storyshots Repositories/Diff Whitespace 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -72261,11 +72107,9 @@ exports[`Storyshots Repositories/Diff WithLinkToFile 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -72898,11 +72742,9 @@ exports[`Storyshots Repositories/Diff WithLinkToFile 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -73872,11 +73714,9 @@ exports[`Storyshots Repositories/Diff WithLinkToFile 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -74417,11 +74257,9 @@ exports[`Storyshots Repositories/Diff WithLinkToFile 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -74962,11 +74800,9 @@ exports[`Storyshots Repositories/Diff WithLinkToFile 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4
@@ -76197,11 +76033,9 @@ exports[`Storyshots Repositories/Diff WithLinkToFile 1`] = `
alt="diff.hideContent"
aria-hidden="true"
className="icon"
color="inherit"
name="angle-down"
>
<i
className="fas fa-fw fa-undefined"
className="fas fa-fw fa-angle-down"
/>
</span>
<h4

View File

@@ -34,17 +34,20 @@ import useScrollToElement from "../useScrollToElement";
type Props = DiffObjectProps & {
diff: FileDiff[];
fileControlFactory?: FileControlFactory;
ignoreWhitespace?: string;
};
const createKey = (file: FileDiff) => {
return `${file.oldPath}@${file.oldRevision}/${file.newPath}@${file.newRevision}`;
const createKey = (file: FileDiff, ignoreWhitespace?: string) => {
// we need to include the information about hidden whitespace in the key, because otherwise the diff might not be
// rendered correctly, if the user toggles the ignore whitespace button
return `${file.oldPath}@${file.oldRevision}/${file.newPath}@${file.newRevision}?${ignoreWhitespace}`;
};
const getAnchorSelector = (uriHashContent: string) => {
return "#" + escapeWhitespace(decodeURIComponent(uriHashContent));
};
const Diff: FC<Props> = ({ diff, ...fileProps }) => {
const Diff: FC<Props> = ({ diff, ignoreWhitespace, ...fileProps }) => {
const [t] = useTranslation("repos");
const [contentRef, setContentRef] = useState<HTMLElement | null>();
const { hash } = useLocation();
@@ -64,14 +67,14 @@ const Diff: FC<Props> = ({ diff, ...fileProps }) => {
{diff.length === 0 ? (
<Notification type="info">{t("diff.noDiffFound")}</Notification>
) : (
diff.map(file => <DiffFile key={createKey(file)} file={file} {...fileProps} />)
diff.map((file) => <DiffFile key={createKey(file, ignoreWhitespace)} file={file} {...fileProps} />)
)}
</div>
);
};
Diff.defaultProps = {
sideBySide: false
sideBySide: false,
};
export default Diff;

View File

@@ -35,6 +35,7 @@ type Props = DiffObjectProps & {
url: string;
limit?: number;
refetchOnWindowFocus?: boolean;
ignoreWhitespace?: string;
};
type NotificationProps = {
@@ -54,8 +55,12 @@ const PartialNotification: FC<NotificationProps> = ({ fetchNextPage, isFetchingN
);
};
const LoadingDiff: FC<Props> = ({ url, limit, refetchOnWindowFocus, ...props }) => {
const { error, isLoading, data, fetchNextPage, isFetchingNextPage } = useDiff(url, { limit, refetchOnWindowFocus });
const LoadingDiff: FC<Props> = ({ url, limit, refetchOnWindowFocus, ignoreWhitespace, ...props }) => {
const { error, isLoading, data, fetchNextPage, isFetchingNextPage } = useDiff(url, {
limit,
refetchOnWindowFocus,
ignoreWhitespace,
});
const [t] = useTranslation("repos");
if (error) {
@@ -70,7 +75,7 @@ const LoadingDiff: FC<Props> = ({ url, limit, refetchOnWindowFocus, ...props })
} else {
return (
<>
<Diff diff={data.files} {...props} />
<Diff diff={data.files} ignoreWhitespace={ignoreWhitespace} {...props} />
{data.partial ? (
<PartialNotification fetchNextPage={fetchNextPage} isFetchingNextPage={isFetchingNextPage} />
) : null}

View File

@@ -31,6 +31,7 @@ import { FileControlFactory } from "../DiffTypes";
type Props = WithTranslation & {
changeset: Changeset;
defaultCollapse?: boolean;
ignoreWhitespace?: string;
fileControlFactory?: FileControlFactory;
};
@@ -49,7 +50,7 @@ export const createUrl = (changeset: HalRepresentation) => {
class ChangesetDiff extends React.Component<Props> {
render() {
const { changeset, fileControlFactory, defaultCollapse, t } = this.props;
const { changeset, fileControlFactory, defaultCollapse, ignoreWhitespace, t } = this.props;
if (!isDiffSupported(changeset)) {
return <Notification type="danger">{t("changeset.diffNotSupported")}</Notification>;
} else {
@@ -58,6 +59,7 @@ class ChangesetDiff extends React.Component<Props> {
<LoadingDiff
url={url}
defaultCollapse={defaultCollapse}
ignoreWhitespace={ignoreWhitespace}
sideBySide={false}
fileControlFactory={fileControlFactory}
stickyHeader={true}

View File

@@ -281,9 +281,9 @@ const DiffFile: FC<Props> = ({
const collapseIcon = useMemo(
() =>
isCollapsed ? (
<Icon name="angle-right" color="inherit" alt={t("diff.showContent")} />
<Icon alt={t("diff.showContent")}>angle-right</Icon>
) : (
<Icon name="angle-down" color="inherit" alt={t("diff.hideContent")} />
<Icon alt={t("diff.hideContent")}>angle-down</Icon>
),
[isCollapsed, t]
);

View File

@@ -263,6 +263,8 @@
"noChangesets": "Keine Changesets in diesem Branch gefunden. Die Commits könnten gelöscht worden sein.",
"branchSelectorLabel": "Branches",
"collapseDiffs": "Auf-/Zuklappen",
"ignoreWhitespace": "Whitespace-Änderungen ignorieren",
"activateWhitespace": "Whitespace-Änderungen einblenden",
"moreDiffsAvailable": "Es sind weitere Diffs verfügbar",
"loadMore": "Weitere laden"
},

View File

@@ -263,6 +263,8 @@
"noChangesets": "No changesets found for this branch. The commits could have been removed.",
"branchSelectorLabel": "Branches",
"collapseDiffs": "Collapse",
"ignoreWhitespace": "Ignore whitespaces changes",
"activateWhitespace": "Show whitespaces changes",
"moreDiffsAvailable": "There are more diffs available",
"loadMore": "Load more"
},

View File

@@ -30,7 +30,6 @@ import { Changeset, ParentChangeset, Repository } from "@scm-manager/ui-types";
import {
AvatarImage,
AvatarWrapper,
Button,
ChangesetAuthor,
ChangesetDescription,
ChangesetDiff,
@@ -39,12 +38,10 @@ import {
ChangesetTags,
DateFromNow,
FileControlFactory,
Icon,
Level,
SignatureIcon,
SubSubtitle,
Tooltip,
} from "@scm-manager/ui-components";
import { Tooltip, SubSubtitle } from "@scm-manager/ui-core";
import { Button, Icon } from "@scm-manager/ui-buttons";
import ContributorTable from "./ContributorTable";
import { Link, Link as ReactLink } from "react-router-dom";
import CreateTagModal from "./CreateTagModal";
@@ -109,8 +106,8 @@ const Contributors: FC<{ changeset: Changeset }> = ({ changeset }) => {
return (
<div className="is-flex is-flex-direction-column mb-4">
<div className="is-flex">
<p className="is-ellipsis-overflow is-clickable mb-2" onClick={(e) => setOpen(!open)}>
<Icon name="angle-down" alt={t("changeset.contributors.hideList")} /> {t("changeset.contributors.list")}
<p className="is-ellipsis-overflow is-clickable mb-2" onClick={() => setOpen(!open)}>
<Icon alt={t("changeset.contributors.hideList")}>angle-down</Icon> {t("changeset.contributors.list")}
</p>
{signatureIcon}
</div>
@@ -120,9 +117,9 @@ const Contributors: FC<{ changeset: Changeset }> = ({ changeset }) => {
}
return (
<div className="is-flex is-clickable" onClick={(e) => setOpen(!open)}>
<div className="is-flex is-clickable" onClick={() => setOpen(!open)}>
<ContributorColumn className="is-ellipsis-overflow">
<Icon name="angle-right" alt={t("changeset.contributors.showList")} /> <ChangesetAuthor changeset={changeset} />
<Icon alt={t("changeset.contributors.showList")}>angle-right</Icon> <ChangesetAuthor changeset={changeset} />
</ContributorColumn>
{signatureIcon}
<CountColumn className="is-hidden-mobile is-hidden-tablet-only is-hidden-desktop-only">
@@ -147,15 +144,15 @@ const ContainedInTags: FC<{ changeset: Changeset; repository: Repository }> = ({
return (
<div className="is-flex is-flex-direction-column mb-4">
<div className="is-flex">
<p className="is-ellipsis-overflow is-clickable mb-2" onClick={(e) => setOpen(!open)}>
<Icon name="angle-down" alt={t("changeset.containedInTags.hideAllTags")} />{" "}
<p className="is-ellipsis-overflow is-clickable mb-2" onClick={() => setOpen(!open)}>
<Icon alt={t("changeset.containedInTags.hideAllTags")}>angle-down</Icon>{" "}
{t("changeset.containedInTags.allTags")}
</p>
</div>
<div>
{" "}
{tags.map((tag) => (
<span className="tag is-info is-normal m-1">
<span className="tag is-info is-normal m-1" key={tag.name}>
<Link
to={`/repo/${repository.namespace}/${repository.name}/tag/${tag.name}`}
className="has-text-inherit"
@@ -172,7 +169,7 @@ const ContainedInTags: FC<{ changeset: Changeset; repository: Repository }> = ({
return (
<div className="is-flex is-clickable" onClick={() => setOpen(!open)}>
<ContributorColumn className="is-ellipsis-overflow">
<Icon name="angle-right" alt={t("changeset.containedInTags.showAllTags")} />{" "}
<Icon alt={t("changeset.containedInTags.showAllTags")}>angle-right</Icon>{" "}
{t("changeset.containedInTags.containedInTag", { count: tags.length })}
</ContributorColumn>
</div>
@@ -181,6 +178,7 @@ const ContainedInTags: FC<{ changeset: Changeset; repository: Repository }> = ({
const ChangesetDetails: FC<Props> = ({ changeset, repository, fileControlFactory }) => {
const [collapsed, setCollapsed] = useState(false);
const [ignoreWhitespace, setIgnoreWhitespace] = useState(false);
const [isTagCreationModalVisible, setTagCreationModalVisible] = useState(false);
const [t] = useTranslation("repos");
@@ -198,6 +196,10 @@ const ChangesetDetails: FC<Props> = ({ changeset, repository, fileControlFactory
setCollapsed(!collapsed);
};
const ignoreWhitespaces = () => {
setIgnoreWhitespace(!ignoreWhitespace);
};
return (
<>
<div className={classNames("content", "m-0")}>
@@ -240,14 +242,11 @@ const ChangesetDetails: FC<Props> = ({ changeset, repository, fileControlFactory
{showCreateButton && (
<div className="media-right">
<Tooltip message={t("changeset.tag.create")} location="top">
<Button
color="success"
className="tag"
label={(changeset._embedded?.tags?.length === 0 && t("changeset.tag.create")) || ""}
icon="plus"
action={() => setTagCreationModalVisible(true)}
/>
<Tooltip message={t("changeset.tag.create")}>
<Button className="tag is-success has-gap-1" onClick={() => setTagCreationModalVisible(true)}>
<Icon>plus</Icon>
{(changeset._embedded?.tags?.length === 0 && t("changeset.tag.create")) || ""}
</Button>
</Tooltip>
</div>
)}
@@ -280,20 +279,23 @@ const ChangesetDetails: FC<Props> = ({ changeset, repository, fileControlFactory
</p>
</div>
<div>
<Level
className="mb-4"
right={
<Button
action={collapseDiffs}
color="default"
icon={collapsed ? "eye" : "eye-slash"}
label={t("changesets.collapseDiffs")}
title={t("changesets.collapseDiffs")}
reducedMobile={true}
/>
}
<div className="is-flex has-gap-4 mb-4 is-justify-content-flex-end">
<Button onClick={ignoreWhitespaces}>
<Icon className="mr-1">{ignoreWhitespace ? "laptop" : "laptop-code"}</Icon>
{t(ignoreWhitespace ? "changesets.activateWhitespace" : "changesets.ignoreWhitespace")}
</Button>
<Button onClick={collapseDiffs}>
<Icon className="mr-1">{collapsed ? "eye" : "eye-slash"}</Icon>
{t("changesets.collapseDiffs")}
</Button>
</div>
<ChangesetDiff
changeset={changeset}
fileControlFactory={fileControlFactory}
defaultCollapse={collapsed}
ignoreWhitespace={ignoreWhitespace ? "ALL" : "NONE"}
/>
<ChangesetDiff changeset={changeset} fileControlFactory={fileControlFactory} defaultCollapse={collapsed} />
</div>
</>
);

View File

@@ -58,7 +58,7 @@ class DiffResultToDiffResultDtoMapper {
}
public DiffResultDto mapForIncoming(Repository repository, DiffResult result, String source, String target) {
String baseLink = resourceLinks.incoming().diffParsed(repository.getNamespace(), repository.getName(), source, target);
String baseLink = resourceLinks.incoming().diffParsed(repository.getNamespace(), repository.getName(), source, target) + "?ignoreWhitespace=" + result.getIgnoreWhitespace().name();
Links.Builder links = linkingTo().self(createSelfLink(result, baseLink));
appendNextChunkLinkIfNeeded(links, result, baseLink);
DiffResultDto dto = new DiffResultDto(links.build());
@@ -67,7 +67,7 @@ class DiffResultToDiffResultDtoMapper {
}
public DiffResultDto mapForRevision(Repository repository, DiffResult result, String revision) {
String baseLink = resourceLinks.diff().parsed(repository.getNamespace(), repository.getName(), revision);
String baseLink = resourceLinks.diff().parsed(repository.getNamespace(), repository.getName(), revision) + "?ignoreWhitespace=" + result.getIgnoreWhitespace().name();
Links.Builder links = linkingTo().self(createSelfLink(result, baseLink));
appendNextChunkLinkIfNeeded(links, result, baseLink);
DiffResultDto dto = new DiffResultDto(links.build());
@@ -96,25 +96,29 @@ class DiffResultToDiffResultDtoMapper {
private String createLinkWithLimitAndOffset(String baseLink, int offset, Integer limit) {
if (limit == null) {
return String.format("%s?offset=%s", baseLink, offset);
return String.format("%s&offset=%s", baseLink, offset);
} else {
return String.format("%s?offset=%s&limit=%s", baseLink, offset, limit);
return String.format("%s&offset=%s&limit=%s", baseLink, offset, limit);
}
}
private void setFiles(DiffResult result, DiffResultDto dto, Repository repository, String revision) {
List<DiffResultDto.FileDto> files = new ArrayList<>();
for (DiffFile file : result) {
files.add(mapFile(file, repository, revision));
files.add(mapFile(file, result, repository, revision));
}
dto.setFiles(files);
dto.setPartial(result.isPartial());
}
private DiffResultDto.FileDto mapFile(DiffFile file, Repository repository, String revision) {
private DiffResultDto.FileDto mapFile(DiffFile file, DiffResult result, 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()) + "?ignoreWhitespace=" + result.getIgnoreWhitespace().name() + "&start={start}&end={end}").build()
);
}
if (!file.getChangeType().equals(DiffFile.ChangeType.ADD)) {
links.single(linkBuilder("oldFile", resourceLinks.source().content(repository.getNamespace(), repository.getName(), file.getOldRevision(), file.getOldPath())).build());

View File

@@ -44,6 +44,7 @@ import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.api.DiffCommandBuilder;
import sonia.scm.repository.api.DiffFormat;
import sonia.scm.repository.api.DiffResult;
import sonia.scm.repository.api.IgnoreWhitespaceLevel;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.util.HttpUtil;
@@ -99,13 +100,18 @@ public class DiffRootResource {
schema = @Schema(implementation = ErrorDto.class)
)
)
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @Pattern(regexp = DIFF_FORMAT_VALUES_REGEX) @DefaultValue("NATIVE") @QueryParam("format") String format) throws IOException {
public Response get(@PathParam("namespace") String namespace,
@PathParam("name") String name,
@PathParam("revision") String revision,
@QueryParam("format") @Pattern(regexp = DIFF_FORMAT_VALUES_REGEX) @DefaultValue("NATIVE") String format,
@QueryParam("ignoreWhitespace") @DefaultValue("NONE") IgnoreWhitespaceLevel ignoreWhitespace) throws IOException {
HttpUtil.checkForCRLFInjection(revision);
DiffFormat diffFormat = DiffFormat.valueOf(format);
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
DiffCommandBuilder.OutputStreamConsumer outputStreamConsumer = repositoryService.getDiffCommand()
.setRevision(revision)
.setFormat(diffFormat)
.setIgnoreWhitespace(ignoreWhitespace)
.retrieveContent();
return Response.ok((StreamingOutput) outputStreamConsumer::accept)
.header(HEADER_CONTENT_DISPOSITION, HttpUtil.createContentDispositionAttachmentHeader(String.format("%s-%s.diff", name, revision)))
@@ -147,6 +153,7 @@ public class DiffRootResource {
@PathParam("name") String name,
@PathParam("revision") String revision,
@QueryParam("limit") @Min(1) Integer limit,
@QueryParam("ignoreWhitespace") @DefaultValue("NONE") IgnoreWhitespaceLevel ignoreWhitespace,
@QueryParam("offset") @Min(0) Integer offset) throws IOException {
HttpUtil.checkForCRLFInjection(revision);
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
@@ -154,6 +161,7 @@ public class DiffRootResource {
.setRevision(revision)
.setLimit(limit)
.setOffset(offset)
.setIgnoreWhitespace(ignoreWhitespace)
.getDiffResult();
return parsedDiffMapper.mapForRevision(repositoryService.getRepository(), diffResult, revision);
}

View File

@@ -48,6 +48,7 @@ import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.api.DiffCommandBuilder;
import sonia.scm.repository.api.DiffFormat;
import sonia.scm.repository.api.DiffResult;
import sonia.scm.repository.api.IgnoreWhitespaceLevel;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.util.HttpUtil;
@@ -192,9 +193,9 @@ public class IncomingRootResource {
@PathParam("name") String name,
@PathParam("source") String source,
@PathParam("target") String target,
@QueryParam("ignoreWhitespace") @DefaultValue("NONE") IgnoreWhitespaceLevel ignoreWhitespace,
@Pattern(regexp = DIFF_FORMAT_VALUES_REGEX) @DefaultValue("NATIVE") @QueryParam("format") String format) throws IOException {
HttpUtil.checkForCRLFInjection(source);
HttpUtil.checkForCRLFInjection(target);
DiffFormat diffFormat = DiffFormat.valueOf(format);
@@ -203,6 +204,7 @@ public class IncomingRootResource {
.setRevision(source)
.setAncestorChangeset(target)
.setFormat(diffFormat)
.setIgnoreWhitespace(ignoreWhitespace)
.retrieveContent();
return Response.ok((StreamingOutput) outputStreamConsumer::accept)
@@ -245,6 +247,7 @@ public class IncomingRootResource {
@PathParam("source") String source,
@PathParam("target") String target,
@QueryParam("limit") @Min(1) Integer limit,
@QueryParam("ignoreWhitespace") @DefaultValue("NONE") IgnoreWhitespaceLevel ignoreWhitespace,
@QueryParam("offset") @Min(0) Integer offset) throws IOException {
HttpUtil.checkForCRLFInjection(source);
HttpUtil.checkForCRLFInjection(target);
@@ -253,6 +256,7 @@ public class IncomingRootResource {
.setRevision(source)
.setAncestorChangeset(target)
.setLimit(limit)
.setIgnoreWhitespace(ignoreWhitespace)
.setOffset(offset)
.getDiffResult();
return Response.ok(parsedDiffMapper.mapForIncoming(repositoryService.getRepository(), diffResult, source, target)).build();

View File

@@ -24,7 +24,6 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Links;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
@@ -34,13 +33,15 @@ import org.apache.shiro.util.ThreadContext;
import org.apache.shiro.util.ThreadState;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import sonia.scm.NotFoundException;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
@@ -48,33 +49,35 @@ import sonia.scm.repository.api.DiffCommandBuilder;
import sonia.scm.repository.api.DiffFormat;
import sonia.scm.repository.api.DiffResult;
import sonia.scm.repository.api.DiffResultCommandBuilder;
import sonia.scm.repository.api.IgnoreWhitespaceLevel;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.util.CRLFInjectionException;
import sonia.scm.web.RestDispatcher;
import sonia.scm.web.VndMediaType;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Arrays;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.quality.Strictness.LENIENT;
@RunWith(MockitoJUnitRunner.Silent.class)
@Slf4j
public class DiffResourceTest extends RepositoryTestBase {
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = LENIENT)
class DiffResourceTest extends RepositoryTestBase {
public static final String DIFF_PATH = "space/repo/diff/";
public static final String DIFF_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + DIFF_PATH;
public static final Repository REPOSITORY = new Repository("repoId", "git", "space", "repo");
private RestDispatcher dispatcher = new RestDispatcher();
private final RestDispatcher dispatcher = new RestDispatcher();
@Mock
private RepositoryServiceFactory serviceFactory;
@@ -93,15 +96,14 @@ public class DiffResourceTest extends RepositoryTestBase {
private final Subject subject = mock(Subject.class);
private final ThreadState subjectThreadState = new SubjectThreadState(subject);
@Before
public void prepareEnvironment() {
@BeforeEach
void prepareEnvironment() {
diffRootResource = new DiffRootResource(serviceFactory, diffResultToDiffResultDtoMapper);
dispatcher.addSingletonResource(getRepositoryRootResource());
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service);
when(serviceFactory.create(any(Repository.class))).thenReturn(service);
when(service.getRepository()).thenReturn(REPOSITORY);
ExceptionWithContextToErrorDtoMapperImpl mapper = new ExceptionWithContextToErrorDtoMapperImpl();
// ExceptionWithContextToErrorDtoMapperImpl mapper = new ExceptionWithContextToErrorDtoMapperImpl();
dispatcher.registerException(CRLFInjectionException.class, Response.Status.BAD_REQUEST);
when(service.getDiffCommand()).thenReturn(diffCommandBuilder);
when(service.getDiffResultCommand()).thenReturn(diffResultCommandBuilder);
@@ -110,13 +112,13 @@ public class DiffResourceTest extends RepositoryTestBase {
when(subject.isPermitted(any(String.class))).thenReturn(true);
}
@After
@AfterEach
public void cleanupContext() {
ThreadContext.unbindSubject();
}
@Test
public void shouldGetDiffs() throws Exception {
void shouldGetDiffs() throws Exception {
when(diffCommandBuilder.retrieveContent()).thenReturn(output -> {});
MockHttpRequest request = MockHttpRequest
.get(DIFF_URL + "revision")
@@ -129,75 +131,103 @@ public class DiffResourceTest extends RepositoryTestBase {
.isEqualTo(200);
String expectedHeader = "Content-Disposition";
String expectedValue = "attachment; filename=\"repo-revision.diff\"; filename*=utf-8''repo-revision.diff";
assertThat(response.getOutputHeaders().containsKey(expectedHeader)).isTrue();
assertThat(response.getOutputHeaders()).containsKey(expectedHeader);
assertThat((String) response.getOutputHeaders().get("Content-Disposition").get(0))
.contains(expectedValue);
}
@Test
public void shouldGetParsedDiffs() 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()));
void shouldGetDiffsWithIgnoredWhitespaceChanges() throws Exception {
when(diffCommandBuilder.retrieveContent()).thenReturn(output -> {});
MockHttpRequest request = MockHttpRequest
.get(DIFF_URL + "revision/parsed")
.accept(VndMediaType.DIFF_PARSED);
.get(DIFF_URL + "revision?ignoreWhitespace=ALL")
.accept(VndMediaType.DIFF);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertThat(response.getStatus())
.isEqualTo(200);
assertThat(response.getContentAsString())
.contains("\"self\":{\"href\":\"http://self\"}");
verify(diffCommandBuilder).setIgnoreWhitespace(IgnoreWhitespaceLevel.ALL);
}
@Nested
class WithParsedDiff {
@BeforeEach
void prepareForParsedDiff() throws IOException {
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()));
}
@Test
void shouldGetParsedDiffs() throws Exception {
MockHttpRequest request = MockHttpRequest
.get(DIFF_URL + "revision/parsed")
.accept(VndMediaType.DIFF_PARSED);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertThat(response.getStatus())
.isEqualTo(200);
assertThat(response.getContentAsString())
.contains("\"self\":{\"href\":\"http://self\"}");
}
@Test
void shouldGetParsedDiffsWithIgnoredWhitespaceChanges() throws Exception {
MockHttpRequest request = MockHttpRequest
.get(DIFF_URL + "revision/parsed?ignoreWhitespace=ALL")
.accept(VndMediaType.DIFF_PARSED);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertThat(response.getStatus())
.isEqualTo(200);
verify(diffResultCommandBuilder).setIgnoreWhitespace(IgnoreWhitespaceLevel.ALL);
}
@Test
void shouldGetParsedDiffsWithOffset() throws Exception {
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
void shouldGetParsedDiffsWithLimit() throws Exception {
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
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
public void shouldGet404OnMissingRepository() throws URISyntaxException {
void shouldGet404OnMissingRepository() throws URISyntaxException {
when(serviceFactory.create(any(NamespaceAndName.class))).thenThrow(new NotFoundException("Text", "x"));
MockHttpRequest request = MockHttpRequest
.get(DIFF_URL + "revision")
.accept(VndMediaType.DIFF);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(404, response.getStatus());
assertThat(response.getStatus()).isEqualTo(404);
}
@Test
public void shouldGet404OnMissingRevision() throws Exception {
void shouldGet404OnMissingRevision() throws Exception {
when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Text", "x"));
MockHttpRequest request = MockHttpRequest
@@ -207,11 +237,11 @@ public class DiffResourceTest extends RepositoryTestBase {
dispatcher.invoke(request, response);
assertEquals(404, response.getStatus());
assertThat(response.getStatus()).isEqualTo(404);
}
@Test
public void shouldGet400OnCrlfInjection() throws Exception {
void shouldGet400OnCrlfInjection() throws Exception {
when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Text", "x"));
MockHttpRequest request = MockHttpRequest
@@ -219,11 +249,11 @@ public class DiffResourceTest extends RepositoryTestBase {
.accept(VndMediaType.DIFF);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(400, response.getStatus());
assertThat(response.getStatus()).isEqualTo(400);
}
@Test
public void shouldGet400OnUnknownFormat() throws Exception {
void shouldGet400OnUnknownFormat() throws Exception {
when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Test", "test"));
MockHttpRequest request = MockHttpRequest
@@ -231,11 +261,11 @@ public class DiffResourceTest extends RepositoryTestBase {
.accept(VndMediaType.DIFF);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(400, response.getStatus());
assertThat(response.getStatus()).isEqualTo(400);
}
@Test
public void shouldAcceptDiffFormats() throws Exception {
void shouldAcceptDiffFormats() throws Exception {
when(diffCommandBuilder.retrieveContent()).thenReturn(output -> {});
Arrays.stream(DiffFormat.values()).map(DiffFormat::name).forEach(
@@ -250,7 +280,6 @@ public class DiffResourceTest extends RepositoryTestBase {
.get(DIFF_URL + "revision?format=" + format)
.accept(VndMediaType.DIFF);
} catch (URISyntaxException e) {
e.printStackTrace();
fail("got exception: " + e);
}
MockHttpResponse response = new MockHttpResponse();

View File

@@ -34,6 +34,7 @@ import sonia.scm.repository.api.DiffFile;
import sonia.scm.repository.api.DiffLine;
import sonia.scm.repository.api.DiffResult;
import sonia.scm.repository.api.Hunk;
import sonia.scm.repository.api.IgnoreWhitespaceLevel;
import java.util.Arrays;
import java.util.Collections;
@@ -90,7 +91,7 @@ class DiffResultToDiffResultDtoMapperTest {
.isPresent()
.get()
.extracting("href")
.isEqualTo("/scm/api/v2/repositories/space/X/diff/123/parsed");
.isEqualTo("/scm/api/v2/repositories/space/X/diff/123/parsed?ignoreWhitespace=ALL");
}
@Test
@@ -105,7 +106,7 @@ class DiffResultToDiffResultDtoMapperTest {
.isPresent()
.get()
.extracting("href")
.isEqualTo("/scm/api/v2/repositories/space/X/diff/123/parsed?offset=30&limit=10");
.isEqualTo("/scm/api/v2/repositories/space/X/diff/123/parsed?ignoreWhitespace=ALL&offset=30&limit=10");
}
@Test
@@ -118,7 +119,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?ignoreWhitespace=ALL&start={start}&end={end}");
}
@Test
@@ -130,7 +131,7 @@ class DiffResultToDiffResultDtoMapperTest {
.isPresent()
.get()
.extracting("href")
.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?ignoreWhitespace=ALL");
}
@Test
@@ -144,7 +145,7 @@ class DiffResultToDiffResultDtoMapperTest {
.isPresent()
.get()
.extracting("href")
.isEqualTo("/scm/api/v2/repositories/space/X/incoming/feature%2Fsome/master/diff/parsed?offset=25");
.isEqualTo("/scm/api/v2/repositories/space/X/incoming/feature%2Fsome/master/diff/parsed?ignoreWhitespace=ALL&offset=25");
}
@Test
@@ -158,7 +159,7 @@ class DiffResultToDiffResultDtoMapperTest {
.isPresent()
.get()
.extracting("href")
.isEqualTo("/scm/api/v2/repositories/space/X/incoming/feature%2Fsome/master/diff/parsed?offset=0&limit=25");
.isEqualTo("/scm/api/v2/repositories/space/X/incoming/feature%2Fsome/master/diff/parsed?ignoreWhitespace=ALL&offset=0&limit=25");
}
@Test
@@ -173,7 +174,7 @@ class DiffResultToDiffResultDtoMapperTest {
.isPresent()
.get()
.extracting("href")
.isEqualTo("/scm/api/v2/repositories/space/X/incoming/feature%2Fsome/master/diff/parsed?offset=30&limit=10");
.isEqualTo("/scm/api/v2/repositories/space/X/incoming/feature%2Fsome/master/diff/parsed?ignoreWhitespace=ALL&offset=30&limit=10");
}
private void mockPartialResult(DiffResult result) {
@@ -271,6 +272,7 @@ class DiffResultToDiffResultDtoMapperTest {
private DiffResult result(DiffFile... files) {
DiffResult result = mock(DiffResult.class);
when(result.getIgnoreWhitespace()).thenReturn(IgnoreWhitespaceLevel.ALL);
when(result.iterator()).thenReturn(Arrays.asList(files).iterator());
return result;
}

View File

@@ -24,7 +24,6 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Links;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
@@ -35,14 +34,16 @@ import org.apache.shiro.util.ThreadState;
import org.assertj.core.util.Lists;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import sonia.scm.NotFoundException;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult;
@@ -52,6 +53,7 @@ import sonia.scm.repository.Repository;
import sonia.scm.repository.api.DiffCommandBuilder;
import sonia.scm.repository.api.DiffResult;
import sonia.scm.repository.api.DiffResultCommandBuilder;
import sonia.scm.repository.api.IgnoreWhitespaceLevel;
import sonia.scm.repository.api.LogCommandBuilder;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
@@ -59,32 +61,33 @@ import sonia.scm.util.CRLFInjectionException;
import sonia.scm.web.RestDispatcher;
import sonia.scm.web.VndMediaType;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.quality.Strictness.LENIENT;
import static sonia.scm.repository.api.DiffFormat.NATIVE;
@RunWith(MockitoJUnitRunner.Silent.class)
@Slf4j
public class IncomingRootResourceTest extends RepositoryTestBase {
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = LENIENT)
class IncomingRootResourceTest extends RepositoryTestBase {
public static final String INCOMING_PATH = "space/repo/incoming/";
public static final String INCOMING_CHANGESETS_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + INCOMING_PATH;
public static final String INCOMING_DIFF_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + INCOMING_PATH;
public static final Repository REPOSITORY = new Repository("repoId", "git", "space", "repo");
private RestDispatcher dispatcher = new RestDispatcher();
private final RestDispatcher dispatcher = new RestDispatcher();
private final URI baseUri = URI.create("/");
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
@@ -106,18 +109,15 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
@Mock
private DiffResultToDiffResultDtoMapper diffResultToDiffResultDtoMapper;
private IncomingChangesetCollectionToDtoMapper incomingChangesetCollectionToDtoMapper;
@InjectMocks
private DefaultChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper;
private final Subject subject = mock(Subject.class);
private final ThreadState subjectThreadState = new SubjectThreadState(subject);
@Before
public void prepareEnvironment() {
incomingChangesetCollectionToDtoMapper = new IncomingChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks);
@BeforeEach
void prepareEnvironment() {
IncomingChangesetCollectionToDtoMapper incomingChangesetCollectionToDtoMapper = new IncomingChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks);
incomingRootResource = new IncomingRootResource(serviceFactory, incomingChangesetCollectionToDtoMapper, diffResultToDiffResultDtoMapper);
dispatcher.addSingletonResource(getRepositoryRootResource());
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService);
@@ -132,13 +132,13 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
when(subject.isPermitted(any(String.class))).thenReturn(true);
}
@After
public void cleanupContext() {
@AfterEach
void cleanupContext() {
ThreadContext.unbindSubject();
}
@Test
public void shouldGetIncomingChangesets() throws Exception {
void shouldGetIncomingChangesets() throws Exception {
String id = "revision_123";
Instant creationDate = Instant.now();
String authorName = "name";
@@ -160,16 +160,16 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
dispatcher.invoke(request, response);
assertEquals(200, response.getStatus());
assertThat(response.getStatus()).isEqualTo(200);
log.info("Response :{}", response.getContentAsString());
assertTrue(response.getContentAsString().contains(String.format("\"id\":\"%s\"", id)));
assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", authorName)));
assertTrue(response.getContentAsString().contains(String.format("\"mail\":\"%s\"", authorEmail)));
assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit)));
assertThat(response.getContentAsString()).contains(String.format("\"id\":\"%s\"", id));
assertThat(response.getContentAsString()).contains(String.format("\"name\":\"%s\"", authorName));
assertThat(response.getContentAsString()).contains(String.format("\"mail\":\"%s\"", authorEmail));
assertThat(response.getContentAsString()).contains(String.format("\"description\":\"%s\"", commit));
}
@Test
public void shouldGetSinglePageOfIncomingChangesets() throws Exception {
void shouldGetSinglePageOfIncomingChangesets() throws Exception {
String id = "revision_123";
Instant creationDate = Instant.now();
String authorName = "name";
@@ -191,16 +191,17 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
dispatcher.invoke(request, response);
assertEquals(200, response.getStatus());
assertTrue(response.getContentAsString().contains(String.format("\"id\":\"%s\"", id)));
assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", authorName)));
assertTrue(response.getContentAsString().contains(String.format("\"mail\":\"%s\"", authorEmail)));
assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit)));
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentAsString()).contains(String.format("\"id\":\"%s\"", id));
assertThat(response.getContentAsString()).contains(String.format("\"name\":\"%s\"", authorName));
assertThat(response.getContentAsString()).contains(String.format("\"mail\":\"%s\"", authorEmail));
assertThat(response.getContentAsString()).contains(String.format("\"description\":\"%s\"", commit));
}
@Test
public void shouldGetDiffs() throws Exception {
when(diffCommandBuilder.retrieveContent()).thenReturn(output -> {});
void shouldGetDiffs() throws Exception {
when(diffCommandBuilder.retrieveContent()).thenReturn(output -> {
});
MockHttpRequest request = MockHttpRequest
.get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff")
.accept(VndMediaType.DIFF);
@@ -212,7 +213,7 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
.isEqualTo(200);
String expectedHeader = "Content-Disposition";
String expectedValue = "attachment; filename=\"repo-src_changeset_id.diff\"; filename*=utf-8''repo-src_changeset_id.diff";
assertThat(response.getOutputHeaders().containsKey(expectedHeader)).isTrue();
assertThat(response.getOutputHeaders()).containsKey(expectedHeader);
assertThat((String) response.getOutputHeaders().get("Content-Disposition").get(0))
.contains(expectedValue);
verify(diffCommandBuilder).setRevision("src_changeset_id");
@@ -221,63 +222,85 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
}
@Test
public void shouldGetParsedDiffs() 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()));
void shouldGetDiffsWithIgnoredWhitespace() throws Exception {
when(diffCommandBuilder.retrieveContent()).thenReturn(output -> {
});
MockHttpRequest request = MockHttpRequest
.get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff/parsed")
.accept(VndMediaType.DIFF_PARSED);
.get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff?ignoreWhitespace=ALL")
.accept(VndMediaType.DIFF);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertThat(response.getStatus())
.isEqualTo(200);
assertThat(response.getContentAsString())
.contains("\"self\":{\"href\":\"http://self\"}");
verify(diffResultCommandBuilder).setRevision("src_changeset_id");
verify(diffResultCommandBuilder).setAncestorChangeset("target_changeset_id");
verify(diffCommandBuilder).setIgnoreWhitespace(IgnoreWhitespaceLevel.ALL);
}
@Nested
class WithParsedDiff {
@BeforeEach
void prepareResult() throws IOException {
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()));
}
@Test
void shouldGetParsedDiffs() throws Exception {
MockHttpRequest request = MockHttpRequest
.get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff/parsed")
.accept(VndMediaType.DIFF_PARSED);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertThat(response.getStatus())
.isEqualTo(200);
assertThat(response.getContentAsString())
.contains("\"self\":{\"href\":\"http://self\"}");
verify(diffResultCommandBuilder).setRevision("src_changeset_id");
verify(diffResultCommandBuilder).setAncestorChangeset("target_changeset_id");
}
@Test
void shouldGetParsedDiffsWithLimit() throws Exception {
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
void shouldGetParsedDiffsWithOffset() throws Exception {
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
void shouldGetParsedDiffsWithIgnoredWhitespace() throws Exception {
MockHttpRequest request = MockHttpRequest
.get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff/parsed?ignoreWhitespace=ALL")
.accept(VndMediaType.DIFF_PARSED);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
verify(diffResultCommandBuilder).setIgnoreWhitespace(IgnoreWhitespaceLevel.ALL);
}
}
@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
public void shouldGet404OnMissingRepository() throws URISyntaxException {
void shouldGet404OnMissingRepository() throws URISyntaxException {
when(serviceFactory.create(any(NamespaceAndName.class))).thenThrow(new NotFoundException("Text", "x"));
MockHttpRequest request = MockHttpRequest
.get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff")
@@ -286,11 +309,11 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
dispatcher.invoke(request, response);
assertEquals(404, response.getStatus());
assertThat(response.getStatus()).isEqualTo(404);
}
@Test
public void shouldGet404OnMissingRevision() throws Exception {
void shouldGet404OnMissingRevision() throws Exception {
when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Text", "x"));
MockHttpRequest request = MockHttpRequest
@@ -300,11 +323,11 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
dispatcher.invoke(request, response);
assertEquals(404, response.getStatus());
assertThat(response.getStatus()).isEqualTo(404);
}
@Test
public void shouldGet400OnCrlfInjection() throws Exception {
void shouldGet400OnCrlfInjection() throws Exception {
when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Text", "x"));
MockHttpRequest request = MockHttpRequest
.get(INCOMING_DIFF_URL + "ny%0D%0ASet-cookie:%20Tamper=3079675143472450634/ny%0D%0ASet-cookie:%20Tamper=3079675143472450634/diff")
@@ -313,12 +336,12 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
dispatcher.invoke(request, response);
assertEquals(400, response.getStatus());
assertThat(response.getStatus()).isEqualTo(400);
assertThat(response.getContentAsString()).contains("parameter contains an illegal character");
}
@Test
public void shouldGet400OnUnknownFormat() throws Exception {
void shouldGet400OnUnknownFormat() throws Exception {
when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Test", "test"));
MockHttpRequest request = MockHttpRequest
.get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff?format=Unknown")
@@ -327,8 +350,6 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
dispatcher.invoke(request, response);
assertEquals(400, response.getStatus());
assertThat(response.getStatus()).isEqualTo(400);
}
}