diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java index 1ac64c1b5e..d6203f2a1b 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java @@ -33,13 +33,18 @@ package sonia.scm.repository.spi; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffFormatter; +import org.eclipse.jgit.util.QuotedString; import sonia.scm.repository.Repository; import sonia.scm.repository.api.DiffCommandBuilder; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.OutputStream; + +import static java.nio.charset.StandardCharsets.UTF_8; /** - * * @author Sebastian Sdorra */ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand { @@ -51,12 +56,12 @@ 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(output)) { + try (DiffFormatter formatter = new DiffFormatter(new DequoteOutputStream(output))) { formatter.setRepository(repository); for (DiffEntry e : diff.getEntries()) { @@ -70,4 +75,115 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand { }; } + static class DequoteOutputStream extends OutputStream { + + private static final String[] DEQUOTE_STARTS = { + "--- ", + "+++ ", + "diff --git " + }; + + private final OutputStream target; + + private boolean afterNL = true; + private boolean writeToBuffer = false; + private int numberOfPotentialBeginning = -1; + private int potentialBeginningCharCount = 0; + private boolean inPotentialQuotedLine = false; + + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + DequoteOutputStream(OutputStream target) { + this.target = new BufferedOutputStream(target); + } + + @Override + public void write(int i) throws IOException { + if (i == (int) '\n') { + handleNewLine(i); + return; + } + + if (afterNL) { + afterNL = false; + if (foundPotentialBeginning(i)) { + return; + } + numberOfPotentialBeginning = -1; + } + + if (inPotentialQuotedLine && i == '"') { + handleQuote(); + return; + } + + if (numberOfPotentialBeginning > -1 && checkForFurtherBeginning(i)) { + return; + } + + if (writeToBuffer) { + buffer.write(i); + } else { + target.write(i); + } + } + + private boolean checkForFurtherBeginning(int i) throws IOException { + if (i == DEQUOTE_STARTS[numberOfPotentialBeginning].charAt(potentialBeginningCharCount)) { + if (potentialBeginningCharCount + 1 < DEQUOTE_STARTS[numberOfPotentialBeginning].length()) { + ++potentialBeginningCharCount; + } else { + inPotentialQuotedLine = true; + } + target.write(i); + return true; + } else { + numberOfPotentialBeginning = -1; + } + return false; + } + + private boolean foundPotentialBeginning(int i) throws IOException { + for (int n = 0; n < DEQUOTE_STARTS.length; ++n) { + if (i == DEQUOTE_STARTS[n].charAt(0)) { + numberOfPotentialBeginning = n; + potentialBeginningCharCount = 1; + target.write(i); + return true; + } + } + return false; + } + + private void handleQuote() throws IOException { + if (writeToBuffer) { + buffer.write('"'); + dequoteBuffer(); + } else { + writeToBuffer = true; + buffer.reset(); + buffer.write('"'); + } + } + + private void handleNewLine(int i) throws IOException { + afterNL = true; + if (writeToBuffer) { + dequoteBuffer(); + } + target.write(i); + } + + private void dequoteBuffer() throws IOException { + byte[] bytes = buffer.toByteArray(); + String dequote = QuotedString.GIT_PATH.dequote(bytes, 0, bytes.length); + target.write(dequote.getBytes(UTF_8)); + writeToBuffer = false; + } + + @Override + public void flush() throws IOException { + target.flush(); + } + } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommand_DequoteOutputStreamTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommand_DequoteOutputStreamTest.java new file mode 100644 index 0000000000..8eaab4fc16 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommand_DequoteOutputStreamTest.java @@ -0,0 +1,35 @@ +package sonia.scm.repository.spi; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class GitDiffCommand_DequoteOutputStreamTest { + + @Test + void shouldDequoteText() throws IOException { + String s = "diff --git \"a/file \\303\\272\\303\\274\\303\\276\\303\\253\\303\\251\\303\\245\\303\\253\\303\\245\\303\\251 a\" \"b/file \\303\\272\\303\\274\\303\\276\\303\\253\\303\\251\\303\\245\\303\\253\\303\\245\\303\\251 b\"\n" + + "new file mode 100644\n" + + "index 0000000..8cb0607\n" + + "--- /dev/null\n" + + "+++ \"b/\\303\\272\\303\\274\\303\\276\\303\\253\\303\\251\\303\\245\\303\\253\\303\\245\\303\\251 \\303\\245g\\303\\260f\\303\\237\"\n" + + "@@ -0,0 +1 @@\n" + + "+rthms"; + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + GitDiffCommand.DequoteOutputStream stream = new GitDiffCommand.DequoteOutputStream(buffer); + byte[] bytes = s.getBytes(); + stream.write(bytes, 0, bytes.length); + stream.flush(); + + Assertions.assertThat(buffer.toString()).isEqualTo("diff --git a/file úüþëéåëåé a b/file úüþëéåëåé b\n" + + "new file mode 100644\n" + + "index 0000000..8cb0607\n" + + "--- /dev/null\n" + + "+++ b/úüþëéåëåé ågðfß\n" + + "@@ -0,0 +1 @@\n" + + "+rthms"); + } +}