Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions jgit/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ plugins {
dependencies {
implementation("com.googlecode.javaewah:JavaEWAH:1.1.13")
compileOnly("org.slf4j:slf4j-api:1.7.36")
testRuntimeOnly("org.slf4j:slf4j-simple:1.7.36")
}

java {
Expand Down
32 changes: 31 additions & 1 deletion jgit/src/main/java/org/openrewrite/jgit/api/ApplyCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -760,7 +760,8 @@ private boolean canApplyAt(List<ByteBuffer> hunkLines,
case ' ':
case '-':
if (pos >= limit
|| !newLines.get(pos).equals(slice(hunkLine, 1))) {
|| !equalsIgnoringTrailingWhitespace(
newLines.get(pos), slice(hunkLine, 1))) {
return false;
}
pos++;
Expand All @@ -777,6 +778,35 @@ private ByteBuffer slice(ByteBuffer b, int off) {
return ByteBuffer.wrap(b.array(), newOffset, b.limit() - newOffset);
}

private static boolean equalsIgnoringTrailingWhitespace(ByteBuffer a,
ByteBuffer b) {
int aEnd = a.limit();
while (aEnd > a.position()
&& isWhitespace(a.array()[aEnd - 1])) {
aEnd--;
}
int bEnd = b.limit();
while (bEnd > b.position()
&& isWhitespace(b.array()[bEnd - 1])) {
bEnd--;
}
int aLen = aEnd - a.position();
int bLen = bEnd - b.position();
if (aLen != bLen) {
return false;
}
for (int i = 0; i < aLen; i++) {
if (a.array()[a.position() + i] != b.array()[b.position() + i]) {
return false;
}
}
return true;
}

private static boolean isWhitespace(byte b) {
return b == ' ' || b == '\t' || b == '\r';
}

private boolean isNoNewlineAtEndOfFile(FileHeader fh) {
List<? extends HunkHeader> hunks = fh.getHunks();
if (hunks == null || hunks.isEmpty()) {
Expand Down
3 changes: 3 additions & 0 deletions jgit/src/main/java/org/openrewrite/jgit/patch/FileHeader.java
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,9 @@ private String parseName(String expect, int ptr, int end) {
r = decode(UTF_8, buf, ptr, tab - 1);
}

if (r.endsWith("\r")) {
r = r.substring(0, r.length() - 1);
}
if (r.equals(DEV_NULL))
r = DEV_NULL;
return r;
Expand Down
274 changes: 274 additions & 0 deletions jgit/src/test/java/org/openrewrite/jgit/api/ApplyCommandTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
/*
* Copyright (C) 2026, OpenRewrite and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.openrewrite.jgit.api;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openrewrite.jgit.lib.Repository;
import org.openrewrite.jgit.util.FileUtils;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;

import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* Tests for {@link ApplyCommand}, focusing on multi-hunk patch application.
*/
public class ApplyCommandTest {

private Repository db;
private Git git;
private File trash;

@BeforeEach
public void setUp() throws Exception {
trash = Files.createTempDirectory("jgit-apply-test").toFile();
git = Git.init().setDirectory(trash).call();
db = git.getRepository();
}

@AfterEach
public void tearDown() throws Exception {
if (db != null) {
db.close();
}
if (trash != null) {
FileUtils.delete(trash, FileUtils.RECURSIVE | FileUtils.RETRY);
}
}

/**
* Test applying a multi-hunk patch where both the file and patch use LF line endings.
*/
@Test
public void testMultiHunkPatchWithLF() throws Exception {
// Create a file with 150 lines using LF endings
StringBuilder fileContent = new StringBuilder();
for (int i = 1; i <= 150; i++) {
fileContent.append("Line ").append(i).append('\n');
}
writeFileAndCommit("test.txt", fileContent.toString());

// Create a patch with 2 hunks:
// Hunk 1: adds 6 lines around line 10 (net +6)
// Hunk 2: adds 1 line around line 118 (old) / 124 (new)
String patch = "diff --git a/test.txt b/test.txt\n"
+ "--- a/test.txt\n"
+ "+++ b/test.txt\n"
+ "@@ -10,6 +10,12 @@\n"
+ " Line 10\n"
+ " Line 11\n"
+ " Line 12\n"
+ "+New Line A1\n"
+ "+New Line A2\n"
+ "+New Line A3\n"
+ "+New Line A4\n"
+ "+New Line A5\n"
+ "+New Line A6\n"
+ " Line 13\n"
+ " Line 14\n"
+ " Line 15\n"
+ "@@ -118,6 +124,7 @@\n"
+ " Line 118\n"
+ " Line 119\n"
+ " Line 120\n"
+ "+New Line B\n"
+ " Line 121\n"
+ " Line 122\n"
+ " Line 123\n";

ApplyResult result = applyPatch(patch);
assertTrue(result.getUpdatedFiles().size() > 0);

String resultContent = readFile("test.txt");
assertTrue(resultContent.contains("New Line A1"));
assertTrue(resultContent.contains("New Line A6"));
assertTrue(resultContent.contains("New Line B"));
}

/**
* Test applying a patch with CRLF line endings to a file with LF line
* endings. The patch context lines will have trailing \r after
* getRawString strips the \n, causing mismatch with the LF-only file
* lines.
*/
@Test
public void testMultiHunkPatchWithCRLFPatchAndLFFile() throws Exception {
// Create a file with 150 lines using LF endings
StringBuilder fileContent = new StringBuilder();
for (int i = 1; i <= 150; i++) {
fileContent.append("Line ").append(i).append('\n');
}
writeFileAndCommit("test.txt", fileContent.toString());

// Create the same patch but with CRLF line endings
String patch = "diff --git a/test.txt b/test.txt\r\n"
+ "--- a/test.txt\r\n"
+ "+++ b/test.txt\r\n"
+ "@@ -10,6 +10,12 @@\r\n"
+ " Line 10\r\n"
+ " Line 11\r\n"
+ " Line 12\r\n"
+ "+New Line A1\r\n"
+ "+New Line A2\r\n"
+ "+New Line A3\r\n"
+ "+New Line A4\r\n"
+ "+New Line A5\r\n"
+ "+New Line A6\r\n"
+ " Line 13\r\n"
+ " Line 14\r\n"
+ " Line 15\r\n"
+ "@@ -118,6 +124,7 @@\r\n"
+ " Line 118\r\n"
+ " Line 119\r\n"
+ " Line 120\r\n"
+ "+New Line B\r\n"
+ " Line 121\r\n"
+ " Line 122\r\n"
+ " Line 123\r\n";

ApplyResult result = applyPatch(patch);
assertTrue(result.getUpdatedFiles().size() > 0);

String resultContent = readFile("test.txt");
assertTrue(resultContent.contains("New Line A1"));
assertTrue(resultContent.contains("New Line B"));
}

/**
* Test applying a patch with LF line endings to a file with CRLF line
* endings. The needsCrLfConversion logic should handle this case by
* converting the file to LF before comparison.
*/
@Test
public void testMultiHunkPatchWithLFPatchAndCRLFFile() throws Exception {
// Create a file with 150 lines using CRLF endings
StringBuilder fileContent = new StringBuilder();
for (int i = 1; i <= 150; i++) {
fileContent.append("Line ").append(i).append("\r\n");
}
writeFileAndCommit("test.txt", fileContent.toString());

// Create a patch with LF line endings
String patch = "diff --git a/test.txt b/test.txt\n"
+ "--- a/test.txt\n"
+ "+++ b/test.txt\n"
+ "@@ -10,6 +10,12 @@\n"
+ " Line 10\n"
+ " Line 11\n"
+ " Line 12\n"
+ "+New Line A1\n"
+ "+New Line A2\n"
+ "+New Line A3\n"
+ "+New Line A4\n"
+ "+New Line A5\n"
+ "+New Line A6\n"
+ " Line 13\n"
+ " Line 14\n"
+ " Line 15\n"
+ "@@ -118,6 +124,7 @@\n"
+ " Line 118\n"
+ " Line 119\n"
+ " Line 120\n"
+ "+New Line B\n"
+ " Line 121\n"
+ " Line 122\n"
+ " Line 123\n";

ApplyResult result = applyPatch(patch);
assertTrue(result.getUpdatedFiles().size() > 0);

String resultContent = readFile("test.txt");
assertTrue(resultContent.contains("New Line A1"));
assertTrue(resultContent.contains("New Line B"));
}

/**
* Test applying a patch where the file has trailing whitespace on some
* context lines but the patch does not. The trailing-whitespace-tolerant
* comparison should handle this.
*/
@Test
public void testMultiHunkPatchWithTrailingWhitespaceDifference()
throws Exception {
// Create a file where some lines have trailing spaces
StringBuilder fileContent = new StringBuilder();
for (int i = 1; i <= 150; i++) {
if (i == 118 || i == 119 || i == 120) {
fileContent.append("Line ").append(i).append(" \n");
} else {
fileContent.append("Line ").append(i).append('\n');
}
}
writeFileAndCommit("test.txt", fileContent.toString());

// Patch context lines do NOT have trailing spaces
String patch = "diff --git a/test.txt b/test.txt\n"
+ "--- a/test.txt\n"
+ "+++ b/test.txt\n"
+ "@@ -10,6 +10,12 @@\n"
+ " Line 10\n"
+ " Line 11\n"
+ " Line 12\n"
+ "+New Line A1\n"
+ "+New Line A2\n"
+ "+New Line A3\n"
+ "+New Line A4\n"
+ "+New Line A5\n"
+ "+New Line A6\n"
+ " Line 13\n"
+ " Line 14\n"
+ " Line 15\n"
+ "@@ -118,6 +124,7 @@\n"
+ " Line 118\n"
+ " Line 119\n"
+ " Line 120\n"
+ "+New Line B\n"
+ " Line 121\n"
+ " Line 122\n"
+ " Line 123\n";

ApplyResult result = applyPatch(patch);
assertTrue(result.getUpdatedFiles().size() > 0);

String resultContent = readFile("test.txt");
assertTrue(resultContent.contains("New Line A1"));
assertTrue(resultContent.contains("New Line B"));
}

private void writeFileAndCommit(String name, String content)
throws Exception {
File f = new File(trash, name);
try (FileOutputStream fos = new FileOutputStream(f)) {
fos.write(content.getBytes(StandardCharsets.UTF_8));
}
git.add().addFilepattern(name).call();
git.commit().setMessage("initial").call();
}

private ApplyResult applyPatch(String patch) throws Exception {
InputStream in = new ByteArrayInputStream(
patch.getBytes(StandardCharsets.UTF_8));
return git.apply().setPatch(in).call();
}

private String readFile(String name) throws IOException {
File f = new File(trash, name);
return new String(Files.readAllBytes(f.toPath()),
StandardCharsets.UTF_8);
}
}
Loading