From 6cf58d6d342b9c0afff392fbca29238d64f52d0e Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Thu, 22 Oct 2026 20:51:43 +0800 Subject: [PATCH 01/10] Add EnableDistancePenalty to Patcher Move match options for Patcher into inner class FuzzyMatchOptions Matches 0f4d100d43411d20747b4f696faa64994e12fef4 --- .../diffpatch/cli/PatchOperation.java | 35 ++++++------ .../diffpatch/match/FuzzyMatchOptions.java | 55 +++++++++++++++++++ .../codechicken/diffpatch/patch/Patcher.java | 27 +++++---- .../diffpatch/cli/DiffPatchCliTests.java | 22 +++----- 4 files changed, 95 insertions(+), 44 deletions(-) create mode 100644 src/main/java/io/codechicken/diffpatch/match/FuzzyMatchOptions.java diff --git a/src/main/java/io/codechicken/diffpatch/cli/PatchOperation.java b/src/main/java/io/codechicken/diffpatch/cli/PatchOperation.java index 5566598..25f4aa0 100644 --- a/src/main/java/io/codechicken/diffpatch/cli/PatchOperation.java +++ b/src/main/java/io/codechicken/diffpatch/cli/PatchOperation.java @@ -1,6 +1,7 @@ package io.codechicken.diffpatch.cli; import io.codechicken.diffpatch.match.FuzzyLineMatcher; +import io.codechicken.diffpatch.match.FuzzyMatchOptions; import io.codechicken.diffpatch.patch.Patcher; import io.codechicken.diffpatch.util.*; import io.codechicken.diffpatch.util.FileCollector.CollectedEntry; @@ -39,14 +40,13 @@ public class PatchOperation extends CliOperation final Output patchedOutput; final @Nullable Output rejectsOutput; final boolean rejectsAsPatches; - final float minFuzz; - final int maxOffset; + final FuzzyMatchOptions fuzzyOptions; final PatchMode mode; final String patchesPrefix; final String lineEnding; final String[] ignorePrefixes; - private PatchOperation(PrintStream logger, LogLevel level, Consumer helpCallback, boolean summary, Input baseInput, Input patchesInput, String aPrefix, String bPrefix, Output patchedOutput, @Nullable Output rejectsOutput, boolean rejectsAsPatches, float minFuzz, int maxOffset, PatchMode mode, String patchesPrefix, String lineEnding, String[] ignorePrefixes) { + private PatchOperation(PrintStream logger, LogLevel level, Consumer helpCallback, boolean summary, Input baseInput, Input patchesInput, String aPrefix, String bPrefix, Output patchedOutput, @Nullable Output rejectsOutput, boolean rejectsAsPatches, FuzzyMatchOptions fuzzyOptions, PatchMode mode, String patchesPrefix, String lineEnding, String[] ignorePrefixes) { super(logger, level, helpCallback); this.summary = summary; this.baseInput = baseInput; @@ -56,8 +56,7 @@ private PatchOperation(PrintStream logger, LogLevel level, Consumer this.patchedOutput = patchedOutput; this.rejectsOutput = rejectsOutput; this.rejectsAsPatches = rejectsAsPatches; - this.minFuzz = minFuzz; - this.maxOffset = maxOffset; + this.fuzzyOptions = fuzzyOptions; this.mode = mode; this.patchesPrefix = patchesPrefix; this.lineEnding = lineEnding; @@ -107,7 +106,7 @@ public Result operate() throws IOException { SingleOutput rejects = (SingleOutput) rejectsOutput; PatchFile patchFile = PatchFile.fromLines(patches.name(), patches.readLines(), true); - boolean success = doPatch(outputCollector, rejectCollector, summary, base.name(), base.readLines(), patchFile, minFuzz, maxOffset, mode); + boolean success = doPatch(outputCollector, rejectCollector, summary, base.name(), base.readLines(), patchFile, fuzzyOptions, mode); CollectedEntry outputEntry = outputCollector.getSingleFile(); CollectedEntry rejectEntry = rejectCollector.getSingleFile(); try (OutputStream os = output.open()) { @@ -144,7 +143,7 @@ public Result operate() throws IOException { patches.open(patchesPrefix); Set baseIndex = filterPrefixed(base.index(), ignorePrefixes); Set patchesIndex = patches.index(); - patchSuccess = doPatch(outputCollector, rejectCollector, summary, baseIndex, patchesIndex, base, patches, minFuzz, maxOffset, mode); + patchSuccess = doPatch(outputCollector, rejectCollector, summary, baseIndex, patchesIndex, base, patches, fuzzyOptions, mode); } try (MultiOutput output = (MultiOutput) patchedOutput) { @@ -168,7 +167,7 @@ public Result operate() throws IOException { return new Result<>(patchSuccess ? 0 : 1, summary); } - private boolean doPatch(FileCollector oCollector, FileCollector rCollector, PatchesSummary summary, Set bEntries, Set pEntries, MultiInput baseInput, MultiInput patchesInput, float minFuzz, int maxOffset, PatchMode mode) throws IOException { + private boolean doPatch(FileCollector oCollector, FileCollector rCollector, PatchesSummary summary, Set bEntries, Set pEntries, MultiInput baseInput, MultiInput patchesInput, FuzzyMatchOptions fuzzyOptions, PatchMode mode) throws IOException { Map patchFiles = FastStream.of(pEntries) .map(e -> { try { @@ -218,7 +217,7 @@ private boolean doPatch(FileCollector oCollector, FileCollector rCollector, Patc summary.changedFiles++; PatchFile patchFile = patchFiles.get(file); List baseLines = baseInput.readLines(file); - result &= doPatch(oCollector, rCollector, summary, file, baseLines, patchFile, minFuzz, maxOffset, mode); + result &= doPatch(oCollector, rCollector, summary, file, baseLines, patchFile, fuzzyOptions, mode); } for (String file : missingFiles) { @@ -234,8 +233,8 @@ private boolean doPatch(FileCollector oCollector, FileCollector rCollector, Patc return result; } - private boolean doPatch(FileCollector outputCollector, FileCollector rejectCollector, PatchesSummary summary, String baseName, List base, PatchFile patchFile, float minFuzz, int maxOffset, PatchMode mode) { - Patcher patcher = new Patcher(patchFile, base, minFuzz, maxOffset); + private boolean doPatch(FileCollector outputCollector, FileCollector rejectCollector, PatchesSummary summary, String baseName, List base, PatchFile patchFile, FuzzyMatchOptions options, PatchMode mode) { + Patcher patcher = new Patcher(patchFile, base, options); log(DEBUG, "Patching: " + baseName); List results = patcher.patch(mode); List rejectedHunks = new ArrayList<>(); @@ -402,8 +401,7 @@ public static class Builder { private @Nullable Output patchedOutput; private @Nullable Output rejectsOutput; private boolean rejectsAsPatches = false; - private float minFuzz = FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE; - private int maxOffset = FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET; + private FuzzyMatchOptions fuzzyOptions = FuzzyMatchOptions.DEFAULT; private PatchMode mode = PatchMode.EXACT; private String patchesPrefix = ""; @@ -480,12 +478,17 @@ public Builder rejectsAsPatches(boolean rejectsAsPatches) { } public Builder minFuzz(float minFuzz) { - this.minFuzz = minFuzz; + this.fuzzyOptions = fuzzyOptions.minMatchScore(minFuzz); return this; } public Builder maxOffset(int maxOffset) { - this.maxOffset = maxOffset; + this.fuzzyOptions = fuzzyOptions.maxMatchOffset(maxOffset); + return this; + } + + public Builder enableDistancePenalty(boolean enableDistancePenalty) { + this.fuzzyOptions = fuzzyOptions.enableDistancePenalty(enableDistancePenalty); return this; } @@ -514,7 +517,7 @@ public PatchOperation build() { if (patchesInput == null) throw new IllegalStateException("patchesInput is required."); if (patchedOutput == null) throw new IllegalStateException("patchedOutput is required."); - return new PatchOperation(logger, level, helpCallback, summary, baseInput, patchesInput, aPrefix, bPrefix, patchedOutput, rejectsOutput, rejectsAsPatches, minFuzz, maxOffset, mode, patchesPrefix, lineEnding, ignorePrefixes.toArray(new String[0])); + return new PatchOperation(logger, level, helpCallback, summary, baseInput, patchesInput, aPrefix, bPrefix, patchedOutput, rejectsOutput, rejectsAsPatches, fuzzyOptions, mode, patchesPrefix, lineEnding, ignorePrefixes.toArray(new String[0])); } } } diff --git a/src/main/java/io/codechicken/diffpatch/match/FuzzyMatchOptions.java b/src/main/java/io/codechicken/diffpatch/match/FuzzyMatchOptions.java new file mode 100644 index 0000000..c9e8dd3 --- /dev/null +++ b/src/main/java/io/codechicken/diffpatch/match/FuzzyMatchOptions.java @@ -0,0 +1,55 @@ +package io.codechicken.diffpatch.match; + +import java.util.Objects; + +public class FuzzyMatchOptions { + public static final FuzzyMatchOptions DEFAULT = new FuzzyMatchOptions(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, true); + + private final int maxMatchOffset; + private final float minMatchScore; + private final boolean enableDistancePenalty; + + public FuzzyMatchOptions(int maxMatchOffset, float minMatchScore, boolean enableDistancePenalty) { + this.maxMatchOffset = maxMatchOffset; + this.minMatchScore = minMatchScore; + this.enableDistancePenalty = enableDistancePenalty; + } + + public int maxMatchOffset() { + return maxMatchOffset; + } + + public float minMatchScore() { + return minMatchScore; + } + + public boolean enableDistancePenalty() { + return enableDistancePenalty; + } + + public FuzzyMatchOptions maxMatchOffset(int maxMatchOffset) { + return new FuzzyMatchOptions(maxMatchOffset, this.minMatchScore, this.enableDistancePenalty); + } + + public FuzzyMatchOptions minMatchScore(float minMatchScore) { + return new FuzzyMatchOptions(this.maxMatchOffset, minMatchScore, this.enableDistancePenalty); + } + + public FuzzyMatchOptions enableDistancePenalty(boolean enableDistancePenalty) { + return new FuzzyMatchOptions(this.maxMatchOffset, this.minMatchScore, enableDistancePenalty); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof FuzzyMatchOptions)) return false; + FuzzyMatchOptions other = (FuzzyMatchOptions) obj; + return this.maxMatchOffset == other.maxMatchOffset + && this.minMatchScore == other.minMatchScore + && this.enableDistancePenalty == other.enableDistancePenalty; + } + + @Override + public int hashCode() { + return Objects.hash(this.maxMatchOffset, this.minMatchScore, this.enableDistancePenalty); + } +} diff --git a/src/main/java/io/codechicken/diffpatch/patch/Patcher.java b/src/main/java/io/codechicken/diffpatch/patch/Patcher.java index 4adc5ff..60172f5 100644 --- a/src/main/java/io/codechicken/diffpatch/patch/Patcher.java +++ b/src/main/java/io/codechicken/diffpatch/patch/Patcher.java @@ -1,6 +1,7 @@ package io.codechicken.diffpatch.patch; import io.codechicken.diffpatch.match.FuzzyLineMatcher; +import io.codechicken.diffpatch.match.FuzzyMatchOptions; import io.codechicken.diffpatch.util.*; import net.covers1624.quack.collection.ColUtils; import net.covers1624.quack.collection.FastStream; @@ -31,26 +32,24 @@ public class Patcher { private @Nullable String lmText; private @Nullable List wmLines; - public final int maxMatchOffset; - public final float minMatchScore; + private final FuzzyMatchOptions fuzzyOptions; public Patcher(PatchFile patchFile, List lines) { - this(patchFile, lines, null, FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET); + this(patchFile, lines, null, FuzzyMatchOptions.DEFAULT); } - public Patcher(PatchFile patchFile, List lines, float minFuzz, int maxOffset) { - this(patchFile, lines, null, minFuzz, maxOffset); + public Patcher(PatchFile patchFile, List lines, FuzzyMatchOptions fuzzyOptions) { + this(patchFile, lines, null, fuzzyOptions); } - public Patcher(PatchFile patchFile, List lines, @Nullable CharRepresenter charRep, float minFuzz, int maxOffset) { + public Patcher(PatchFile patchFile, List lines, @Nullable CharRepresenter charRep, FuzzyMatchOptions fuzzyOptions) { this.patches = FastStream.of(patchFile.patches).map(WorkingPatch::new).toList(); this.lines = new ArrayList<>(lines); if (charRep == null) { charRep = new CharRepresenter(); } this.charRep = charRep; - this.minMatchScore = minFuzz; - this.maxMatchOffset = maxOffset; + this.fuzzyOptions = fuzzyOptions; } public List patch(PatchMode mode) { @@ -356,32 +355,32 @@ private Pair findMatch(int loc, List wmContext) { // parts of file to search in List ranges = LineRange.fromStartLen(0, wmLines.size()).except(keepoutRanges); - return fuzzyMatch(wmContext, wmLines, loc, maxMatchOffset, minMatchScore, ranges); + return fuzzyMatch(wmContext, wmLines, loc, fuzzyOptions, ranges); } - public static Pair fuzzyMatch(List wmPattern, List wmText, int loc, int maxMatchOffset, float minMatchScore, @Nullable List ranges) { + public static Pair fuzzyMatch(List wmPattern, List wmText, int loc, FuzzyMatchOptions fuzzyOptions, @Nullable List ranges) { if (ranges == null) { ranges = Collections.singletonList(LineRange.fromStartLen(0, wmText.size())); } // we're creating twice as many MatchMatrix objects as we need, incurring some wasted allocation and setup time, but it reads easier than trying to precompute all the edge cases List fwdMatchers = FastStream.of(ranges) - .map(r -> new FuzzyLineMatcher.MatchMatrix(wmPattern, wmText, maxMatchOffset, r)) + .map(r -> new FuzzyLineMatcher.MatchMatrix(wmPattern, wmText, fuzzyOptions.maxMatchOffset(), r)) .filter(m -> loc < m.workingRange.getLast()) .toList(); List revMatchers = FastStream.of(ranges) .reversed() - .map(r -> new FuzzyLineMatcher.MatchMatrix(wmPattern, wmText, maxMatchOffset, r)) + .map(r -> new FuzzyLineMatcher.MatchMatrix(wmPattern, wmText, fuzzyOptions.maxMatchOffset(), r)) .filter(m -> loc > m.workingRange.getFirst()) .toList(); int warnDist = offsetWarnDistance(wmPattern.size(), wmText.size()); - float penaltyPerLine = 1f / (10 * warnDist); + float penaltyPerLine = fuzzyOptions.enableDistancePenalty() ? 1f / (10 * warnDist) : 0; MatchRunner fwd = new MatchRunner(loc, 1, fwdMatchers, penaltyPerLine); MatchRunner rev = new MatchRunner(loc, -1, revMatchers, penaltyPerLine); - AtomicReference bestScore = new AtomicReference<>(minMatchScore); + AtomicReference bestScore = new AtomicReference<>(fuzzyOptions.minMatchScore()); AtomicReference bestMatch = new AtomicReference<>(null); while (fwd.step(bestScore, bestMatch) | rev.step(bestScore, bestMatch)) { // Empty body. diff --git a/src/test/java/io/codechicken/diffpatch/cli/DiffPatchCliTests.java b/src/test/java/io/codechicken/diffpatch/cli/DiffPatchCliTests.java index 3d8e752..660db7d 100644 --- a/src/test/java/io/codechicken/diffpatch/cli/DiffPatchCliTests.java +++ b/src/test/java/io/codechicken/diffpatch/cli/DiffPatchCliTests.java @@ -2,6 +2,7 @@ import io.codechicken.diffpatch.diff.Differ; import io.codechicken.diffpatch.match.FuzzyLineMatcher; +import io.codechicken.diffpatch.match.FuzzyMatchOptions; import io.codechicken.diffpatch.test.TestBase; import io.codechicken.diffpatch.util.Input; import io.codechicken.diffpatch.util.Output; @@ -220,8 +221,7 @@ public void testPatch() throws IOException { assertTrue(op.patchedOutput instanceof Output.SingleOutput.ToStream); assertNull(op.rejectsOutput); assertFalse(op.rejectsAsPatches); - assertEquals(FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, op.minFuzz); - assertEquals(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, op.maxOffset); + assertEquals(new FuzzyMatchOptions(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, true), op.fuzzyOptions); assertEquals(PatchMode.EXACT, op.mode); assertEquals("", op.patchesPrefix); assertEquals(System.lineSeparator(), op.lineEnding); @@ -242,8 +242,7 @@ public void testPatchOptions() throws IOException { assertTrue(op.patchedOutput instanceof Output.SingleOutput.ToStream); assertNull(op.rejectsOutput); assertTrue(op.rejectsAsPatches); - assertEquals(69.0F, op.minFuzz); - assertEquals(32, op.maxOffset); + assertEquals(new FuzzyMatchOptions(32, 69.0F, true), op.fuzzyOptions); assertEquals(PatchMode.FUZZY, op.mode); assertEquals("asdf/", op.patchesPrefix); assertEquals(DiffPatchCli.LineEnding.CR.chars, op.lineEnding); @@ -281,8 +280,7 @@ public void testPatchToFile() throws IOException { assertTrue(op.patchedOutput instanceof Output.SingleOutput.ToPath); assertTrue(op.rejectsOutput instanceof Output.SingleOutput.ToPath); assertFalse(op.rejectsAsPatches); - assertEquals(FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, op.minFuzz); - assertEquals(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, op.maxOffset); + assertEquals(new FuzzyMatchOptions(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, true), op.fuzzyOptions); assertEquals(PatchMode.EXACT, op.mode); assertEquals("", op.patchesPrefix); assertEquals(System.lineSeparator(), op.lineEnding); @@ -312,8 +310,7 @@ public void testPatchToArchives() throws IOException { assertTrue(op.patchedOutput instanceof Output.SingleOutput.PathArchiveMultiOutput); assertTrue(op.rejectsOutput instanceof Output.SingleOutput.PathArchiveMultiOutput); assertFalse(op.rejectsAsPatches); - assertEquals(FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, op.minFuzz); - assertEquals(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, op.maxOffset); + assertEquals(new FuzzyMatchOptions(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, true), op.fuzzyOptions); assertEquals(PatchMode.EXACT, op.mode); assertEquals("", op.patchesPrefix); assertEquals(System.lineSeparator(), op.lineEnding); @@ -339,8 +336,7 @@ public void testPatchToArchives3() throws IOException { assertTrue(op.patchedOutput instanceof Output.SingleOutput.PipeArchiveMultiOutput); assertTrue(op.rejectsOutput instanceof Output.SingleOutput.PathArchiveMultiOutput); assertFalse(op.rejectsAsPatches); - assertEquals(FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, op.minFuzz); - assertEquals(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, op.maxOffset); + assertEquals(new FuzzyMatchOptions(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, true), op.fuzzyOptions); assertEquals(PatchMode.EXACT, op.mode); assertEquals("", op.patchesPrefix); assertEquals(System.lineSeparator(), op.lineEnding); @@ -366,8 +362,7 @@ public void testPatchToArchives2() throws IOException { assertTrue(op.patchedOutput instanceof Output.SingleOutput.PathArchiveMultiOutput); assertTrue(op.rejectsOutput instanceof Output.SingleOutput.PathArchiveMultiOutput); assertFalse(op.rejectsAsPatches); - assertEquals(FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, op.minFuzz); - assertEquals(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, op.maxOffset); + assertEquals(new FuzzyMatchOptions(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, true), op.fuzzyOptions); assertEquals(PatchMode.EXACT, op.mode); assertEquals("", op.patchesPrefix); assertEquals(System.lineSeparator(), op.lineEnding); @@ -393,8 +388,7 @@ public void testPatchToArchives2(@TempDir Path folder) throws IOException { assertTrue(op.patchedOutput instanceof Output.FolderMultiOutput); assertTrue(op.rejectsOutput instanceof Output.FolderMultiOutput); assertFalse(op.rejectsAsPatches); - assertEquals(FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, op.minFuzz); - assertEquals(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, op.maxOffset); + assertEquals(new FuzzyMatchOptions(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, true), op.fuzzyOptions); assertEquals(PatchMode.EXACT, op.mode); assertEquals("", op.patchesPrefix); assertEquals(System.lineSeparator(), op.lineEnding); From 8b5296d92abfad5d35ab8e9fa3a74ead5ee4e953 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Thu, 22 Oct 2026 21:06:03 +0800 Subject: [PATCH 02/10] Allow retrying failed patches Matches #2f93ca9d53e8f307a81ef6457671fdb9bcb25c0d --- .../codechicken/diffpatch/patch/Patcher.java | 52 +++++++++---------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/src/main/java/io/codechicken/diffpatch/patch/Patcher.java b/src/main/java/io/codechicken/diffpatch/patch/Patcher.java index 60172f5..0a905ee 100644 --- a/src/main/java/io/codechicken/diffpatch/patch/Patcher.java +++ b/src/main/java/io/codechicken/diffpatch/patch/Patcher.java @@ -53,28 +53,23 @@ public Patcher(PatchFile patchFile, List lines, @Nullable CharRepresente } public List patch(PatchMode mode) { - if (applied) { - throw new RuntimeException("Already applied"); - } - applied = true; + // searchOffset is the offset of the last patch (applied location - expected location) + // this way if a line is inserted, and all patches are offset by 1, only the first patch is reported as offset + // normally this is equivalent to `lastAppliedPatch?.AppliedOffset` but if a patch fails, we subtract its length delta from the search offset + int searchOffset = 0; for (WorkingPatch patch : patches) { - if (applyExact(patch)) { - continue; - } - if (mode.ordinal() >= PatchMode.ACCESS.ordinal() && applyAccess(patch)) { - continue; - } - if (mode.ordinal() >= PatchMode.OFFSET.ordinal() && applyOffset(patch)) { - continue; + if (patch.result != null && patch.result.success + || applyExact(patch, searchOffset) + || mode.ordinal() >= PatchMode.ACCESS.ordinal() && applyAccess(patch, searchOffset) + || mode.ordinal() >= PatchMode.OFFSET.ordinal() && applyOffset(patch, searchOffset) + || mode.ordinal() >= PatchMode.FUZZY.ordinal() && applyFuzzy(patch, searchOffset)) { + + searchOffset = patch.result.appliedPatch.start2 - patch.start2; + } else { + patch.fail(searchOffset); + searchOffset -= patch.length2 - patch.length1; } - if (mode.ordinal() >= PatchMode.FUZZY.ordinal() && applyFuzzy(patch)) { - continue; - } - - patch.fail(); - patch.result.searchOffset = searchOffset; - searchOffset -= patch.length2 - patch.length1; } return FastStream.of(patches).map(e -> e.result).toList(); @@ -160,7 +155,7 @@ private boolean canApplySafelyAt(int loc, Patch patch) { }); } - private boolean applyExact(WorkingPatch patch) { + private boolean applyExact(WorkingPatch patch, int searchOffset) { int loc = patch.start2 + searchOffset; if (loc + patch.length1 > lines.size()) { return false; @@ -174,7 +169,7 @@ private boolean applyExact(WorkingPatch patch) { return true; } - private boolean applyOffset(WorkingPatch patch) { + private boolean applyOffset(WorkingPatch patch, int searchOffset) { if (lmText == null) { linesToChars(); } @@ -211,7 +206,7 @@ private boolean applyOffset(WorkingPatch patch) { return true; } - private boolean applyAccess(WorkingPatch patch) { + private boolean applyAccess(WorkingPatch patch, int searchOffset) { if (wmLines == null) { wordsToChars(); } @@ -268,7 +263,7 @@ private boolean applyAccess(WorkingPatch patch) { return true; } - private boolean applyFuzzy(WorkingPatch patch) { + private boolean applyFuzzy(WorkingPatch patch, int searchOffset) { if (wmLines == null) { wordsToChars(); } @@ -475,12 +470,12 @@ public WorkingPatch(Patch other) { super(other); } - public void fail() { - result = new Result(this, false, null); + public void fail(int searchOffset) { + result = new Result(this, false, null, searchOffset); } public void succeed(PatchMode mode, Patch appliedPatch) { - result = new Result(this, true, mode); + result = new Result(this, true, mode, 0); result.appliedPatch = appliedPatch; } @@ -522,7 +517,7 @@ public OptionalInt getAppliedDelta() { public static class Result { public final Patch patch; - public final boolean success; + public final boolean success; public final @Nullable PatchMode mode; public int searchOffset; @@ -532,10 +527,11 @@ public static class Result { public boolean offsetWarning; public float fuzzyQuality; - public Result(Patch patch, boolean success, @Nullable PatchMode mode) { + public Result(Patch patch, boolean success, @Nullable PatchMode mode, int searchOffset) { this.patch = patch; this.success = success; this.mode = mode; + this.searchOffset = searchOffset; } public String summary() { From 6b5f33b102ba908dcf433953c74228cfd6e1ec98 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Thu, 22 Oct 2026 21:09:36 +0800 Subject: [PATCH 03/10] Make header verification errors a bit more helpful Matches 607d4c63f3f3dd6648dddbaf209d9d0ada85adf8 --- .../io/codechicken/diffpatch/util/PatchFile.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/io/codechicken/diffpatch/util/PatchFile.java b/src/main/java/io/codechicken/diffpatch/util/PatchFile.java index 599a222..2072754 100644 --- a/src/main/java/io/codechicken/diffpatch/util/PatchFile.java +++ b/src/main/java/io/codechicken/diffpatch/util/PatchFile.java @@ -99,6 +99,18 @@ public static PatchFile fromLines(String name, List lines, boolean verif throw new IllegalArgumentException(String.format("Invalid patch line in '%s' at %s:'%s'", line, i, line)); } } + + if (verifyHeaders) { + for (Patch p : patchFile.patches) { + int cLen = p.getContextLines().size(); + if (p.length1 != cLen) + throw new IllegalArgumentException(String.format("Context length (%s) doesn't match header: %s", cLen, p.getHeader())); + int pLen = p.getPatchedLines().size(); + if (p.length2 != pLen) + throw new IllegalArgumentException(String.format("Patched length (%s) doesn't match header: %s", pLen, p.getHeader())); + } + } + return patchFile; } From e4109eaba072268a74abce4a8de61870d0178f89 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Wed, 21 Oct 2026 19:12:45 +0800 Subject: [PATCH 04/10] Delete useless collate parameter Uncollate still exists as a utility, but there is no point providing a parameter which just undoes the work of LineMatchedDiffer Matches 7f996aa433c6e49536be18df2480ff5f0ee90e54 --- .../io/codechicken/diffpatch/cli/DiffOperation.java | 2 +- .../java/io/codechicken/diffpatch/diff/Differ.java | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/main/java/io/codechicken/diffpatch/cli/DiffOperation.java b/src/main/java/io/codechicken/diffpatch/cli/DiffOperation.java index 295c0cb..543de87 100644 --- a/src/main/java/io/codechicken/diffpatch/cli/DiffOperation.java +++ b/src/main/java/io/codechicken/diffpatch/cli/DiffOperation.java @@ -235,7 +235,7 @@ private List doDiff(DiffSummary summary, @Nullable String aName, @Nullab } else if (bLines.isEmpty()) { patchFile.patches = Differ.makeFileRemoved(aLines); } else { - patchFile.patches = differFactory.get().makePatches(aLines, bLines, context, true); + patchFile.patches = differFactory.get().makePatches(aLines, bLines, context); } if (patchFile.patches.isEmpty()) { log(DEBUG, "%s -> %s\n No changes.", aName, bName); diff --git a/src/main/java/io/codechicken/diffpatch/diff/Differ.java b/src/main/java/io/codechicken/diffpatch/diff/Differ.java index 35a8c29..6db031b 100644 --- a/src/main/java/io/codechicken/diffpatch/diff/Differ.java +++ b/src/main/java/io/codechicken/diffpatch/diff/Differ.java @@ -35,11 +35,11 @@ public List diff(List lines1, List lines2) { } public List makePatches(List lines1, List lines2) { - return makePatches(lines1, lines2, DEFAULT_CONTEXT, true); + return makePatches(lines1, lines2, DEFAULT_CONTEXT); } - public List makePatches(List lines1, List lines2, int numContextLines, boolean collate) { - return makePatches(diff(lines1, lines2), numContextLines, collate); + public List makePatches(List lines1, List lines2, int numContextLines) { + return makePatches(diff(lines1, lines2), numContextLines); } public static List makeFileAdded(List lines) { @@ -52,7 +52,7 @@ public static List makeFileRemoved(List lines) { return patch.length1 == 0 ? Collections.emptyList() : Collections.singletonList(patch); } - public static List makePatches(List diffs, int numContextLines, boolean collate) { + public static List makePatches(List diffs, int numContextLines) { Patch p = new Patch(); p.diffs = diffs; p.recalculateLength(); @@ -60,9 +60,6 @@ public static List makePatches(List diffs, int numContextLines, boo if (p.length1 == 0) { return Collections.emptyList(); } - if (!collate) { - p.uncollate(); - } return p.split(numContextLines); } From c722814ed4faa9f4513176d70335e7ddeb1fe337 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Wed, 21 Oct 2026 19:18:49 +0800 Subject: [PATCH 05/10] Improve MatchMatrix.Visualize Matches 37ba6402114eca017fd6c5a240c664ea7226a1d7 --- .../diffpatch/match/FuzzyLineMatcher.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/codechicken/diffpatch/match/FuzzyLineMatcher.java b/src/main/java/io/codechicken/diffpatch/match/FuzzyLineMatcher.java index 2f7f787..496587d 100644 --- a/src/main/java/io/codechicken/diffpatch/match/FuzzyLineMatcher.java +++ b/src/main/java/io/codechicken/diffpatch/match/FuzzyLineMatcher.java @@ -316,7 +316,7 @@ public int[] path() { return path; } - public String visualise() { + public String visualize() { int[] path = path(); StringBuilder sb = new StringBuilder(); for (int j = 0; j <= maxOffset; j++) { @@ -331,6 +331,22 @@ public String visualise() { } sb.append("\n"); } + + sb.append("-:"); + sb.append(" "); + for (int i = 1; i < patternLength; i++) { + int offset = path[i] - path[i-1] - 1; + int penalty = Math.round(-0.5f * offset * 100); + if (path[i] < 0 || penalty == 0) { + sb.append(" "); + } else { + sb.append(String.format("%-4d", penalty)); + } + } + sb.append("\n"); + + float sum = matches[firstNode].nodes[0].sum * 100; + sb.append("Total Score: ").append(Math.round(sum)).append(" / ").append(patternLength * 100).append(" = ").append(Math.round(sum / patternLength)).append("%\n"); return sb.toString(); } From ae3762eedc46dd310dea83f8678d9cbaf355bd6b Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Thu, 22 Oct 2026 21:53:44 +0800 Subject: [PATCH 06/10] Add tests Matches 052028c3938693f4d9c8c1fa0be4fd44556d2f97 --- .../codechicken/diffpatch/test/DiffTests.java | 39 ++++ .../diffpatch/test/PatchTests.java | 186 ++++++++++++++++++ .../codechicken/diffpatch/test/TestBase.java | 108 ++++++++++ .../files/ConfigInsertedBulkLines_a.cs | 11 ++ .../files/ConfigInsertedBulkLines_b.cs | 11 ++ .../ConfigInsertedLines_LineMatched_b.cs | 12 ++ .../ConfigInsertedLines_MaxMatchOffset1_b.cs | 12 ++ .../ConfigInsertedLines_MaxMatchOffset2_b.cs | 12 ++ .../files/ConfigInsertedLines_Patience_b.cs | 10 + .../resources/files/ConfigInsertedLines_a.cs | 12 ++ src/test/resources/files/Config_a.cs | 8 + src/test/resources/files/Config_b.cs | 8 + .../resources/files/DistancePenaltyOff_b.cs | 129 ++++++++++++ src/test/resources/files/DistancePenalty_a.cs | 124 ++++++++++++ src/test/resources/files/DistancePenalty_b.cs | 129 ++++++++++++ .../files/MaxMatchOffsetLocation0_b.cs | 18 ++ .../files/MaxMatchOffsetLocation_a.cs | 18 ++ .../files/MaxMatchOffsetLocation_b.cs | 18 ++ src/test/resources/files/MergedContext_a.cs | 14 ++ src/test/resources/files/MergedContext_b.cs | 16 ++ src/test/resources/files/SimpleInsert_a.cs | 14 ++ src/test/resources/files/SimpleInsert_b.cs | 19 ++ src/test/resources/patches/AutoOffset.patch | 12 ++ .../patches/Config_LineMatched.patch | 11 ++ .../resources/patches/Config_Patience.patch | 11 ++ .../resources/patches/DistancePenalty.patch | 12 ++ src/test/resources/patches/FailedPatch.patch | 12 ++ src/test/resources/patches/FuzzyPatch.patch | 12 ++ .../resources/patches/MergedContext.patch | 15 ++ src/test/resources/patches/OffsetPatch.patch | 12 ++ src/test/resources/patches/SimpleInsert.patch | 12 ++ src/test/resources/patches/SplitContext.patch | 12 ++ 32 files changed, 1049 insertions(+) create mode 100644 src/test/java/io/codechicken/diffpatch/test/DiffTests.java create mode 100644 src/test/java/io/codechicken/diffpatch/test/PatchTests.java create mode 100644 src/test/resources/files/ConfigInsertedBulkLines_a.cs create mode 100644 src/test/resources/files/ConfigInsertedBulkLines_b.cs create mode 100644 src/test/resources/files/ConfigInsertedLines_LineMatched_b.cs create mode 100644 src/test/resources/files/ConfigInsertedLines_MaxMatchOffset1_b.cs create mode 100644 src/test/resources/files/ConfigInsertedLines_MaxMatchOffset2_b.cs create mode 100644 src/test/resources/files/ConfigInsertedLines_Patience_b.cs create mode 100644 src/test/resources/files/ConfigInsertedLines_a.cs create mode 100644 src/test/resources/files/Config_a.cs create mode 100644 src/test/resources/files/Config_b.cs create mode 100644 src/test/resources/files/DistancePenaltyOff_b.cs create mode 100644 src/test/resources/files/DistancePenalty_a.cs create mode 100644 src/test/resources/files/DistancePenalty_b.cs create mode 100644 src/test/resources/files/MaxMatchOffsetLocation0_b.cs create mode 100644 src/test/resources/files/MaxMatchOffsetLocation_a.cs create mode 100644 src/test/resources/files/MaxMatchOffsetLocation_b.cs create mode 100644 src/test/resources/files/MergedContext_a.cs create mode 100644 src/test/resources/files/MergedContext_b.cs create mode 100644 src/test/resources/files/SimpleInsert_a.cs create mode 100644 src/test/resources/files/SimpleInsert_b.cs create mode 100644 src/test/resources/patches/AutoOffset.patch create mode 100644 src/test/resources/patches/Config_LineMatched.patch create mode 100644 src/test/resources/patches/Config_Patience.patch create mode 100644 src/test/resources/patches/DistancePenalty.patch create mode 100644 src/test/resources/patches/FailedPatch.patch create mode 100644 src/test/resources/patches/FuzzyPatch.patch create mode 100644 src/test/resources/patches/MergedContext.patch create mode 100644 src/test/resources/patches/OffsetPatch.patch create mode 100644 src/test/resources/patches/SimpleInsert.patch create mode 100644 src/test/resources/patches/SplitContext.patch diff --git a/src/test/java/io/codechicken/diffpatch/test/DiffTests.java b/src/test/java/io/codechicken/diffpatch/test/DiffTests.java new file mode 100644 index 0000000..d658687 --- /dev/null +++ b/src/test/java/io/codechicken/diffpatch/test/DiffTests.java @@ -0,0 +1,39 @@ +package io.codechicken.diffpatch.test; + +import java.io.IOException; + +import io.codechicken.diffpatch.diff.LineMatchedDiffer; +import io.codechicken.diffpatch.diff.PatienceDiffer; +import org.junit.jupiter.api.Test; + +public class DiffTests extends TestBase { + @Test + public void SimpleInsert() throws IOException { + assertDiff(null, null, null, -1, false); + } + + @Test + public void AutoOffset() throws IOException { + assertDiff(null, "SimpleInsert", null, -1, true); + } + + @Test + public void LineMatched() throws IOException { + assertDiff(new LineMatchedDiffer(), "Config", "Config_LineMatched", -1, false); + } + + @Test + public void LineMatchedWithPatience() throws IOException { + assertDiff(new PatienceDiffer(), "Config", "Config_Patience", -1, false); + } + + @Test + public void MergedContext() throws IOException { + assertDiff(null, null, null, 2, false); + } + + @Test + public void SplitContext() throws IOException { + assertDiff(null, "MergedContext", null, 1, false); + } +} diff --git a/src/test/java/io/codechicken/diffpatch/test/PatchTests.java b/src/test/java/io/codechicken/diffpatch/test/PatchTests.java new file mode 100644 index 0000000..049ff3f --- /dev/null +++ b/src/test/java/io/codechicken/diffpatch/test/PatchTests.java @@ -0,0 +1,186 @@ +package io.codechicken.diffpatch.test; + +import io.codechicken.diffpatch.match.FuzzyMatchOptions; +import io.codechicken.diffpatch.patch.Patcher; +import io.codechicken.diffpatch.util.PatchMode; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + +public class PatchTests extends TestBase { + @Test + public void SimpleInsert() throws IOException { + assertPatch(null, null, null, null, null); + } + + @Test + public void OffsetPatch() throws IOException { + Patcher.Result result = assertPatch(PatchMode.OFFSET, null, "SimpleInsert", null, null); + assertEquals(PatchMode.OFFSET, result.mode); + assertEquals(2, result.offset); + } + + @Test + public void FuzzyPatch() throws IOException { + Patcher.Result result = assertPatch(PatchMode.FUZZY, null, "SimpleInsert", null, null); + assertEquals(PatchMode.FUZZY, result.mode); + assertTrue(result.fuzzyQuality > 0.8f, "Expected quality > 80%, got " + result.fuzzyQuality * 100); + } + + @Test + public void FuzzyPatchHighThreshold() throws IOException { + Patcher.Result result = patch(PatchMode.FUZZY, FuzzyMatchOptions.DEFAULT.minMatchScore(0.95f), "SimpleInsert", "FuzzyPatch"); + assertFalse(result.success, "Patch should fail when min match score exceeds actual quality"); + } + + @Test + public void FailedPatch() throws IOException { + Patcher.Result result = patch(PatchMode.FUZZY, null, "SimpleInsert", null); + assertFalse(result.success, "Patch should fail against a completely different file"); + } + + @Test + public void FuzzyInsertedLines_LineMatched() throws IOException { + Patcher.Result result = assertPatch(PatchMode.FUZZY, null, "ConfigInsertedLines", "Config_LineMatched", "ConfigInsertedLines_LineMatched"); + assertEquals(PatchMode.FUZZY, result.mode); + // Patch is 7 lines long which perfectly match (Quality 100%) + // Context has 4 inserted lines, with penalty 0.5 each + assertEquals((7 - 0.5f * 4) / 7, result.fuzzyQuality, "Expected quality"); + } + + @Test + public void FuzzyInsertedLines_Patience() throws IOException { + Patcher.Result result = assertPatch(PatchMode.FUZZY, null, "ConfigInsertedLines", "Config_Patience", "ConfigInsertedLines_Patience"); + assertEquals(PatchMode.FUZZY, result.mode); + assertEquals((7 - 0.5f * 4) / 7, result.fuzzyQuality, "Expected quality"); + } + + @Test + public void FuzzyInsertedBulkLines() throws IOException { + Patcher.Result result = assertPatch(PatchMode.FUZZY, null, "ConfigInsertedBulkLines", "Config_LineMatched", "ConfigInsertedBulkLines"); + assertEquals(PatchMode.FUZZY, result.mode); + // 0:[%%][%%][%%][%%] 45 00 00 + // 1: 00 00 00 64 00 00 00 + // 2: 00 00 00 00 00 64 00 + // 3: 00 00 00 00 [%%][%%][%%] + // 4: 00 00 00 64 64 00 00 + // 5: 00 00 00 64 00 00 00 + // -: -150 + // Total Score: 550 / 700 = 79% + assertEquals((7 - 0.5f * 3) / 7, result.fuzzyQuality, "Expected quality"); + } + + @Test + public void MaxMatchOffset1() throws IOException { + // With MaxMatchOffset = 1 we can only skip line 4 (public int Zero = 0; // extra tokens make this an extra bad match) + + // 0:[%%][%%][%%] 00 64 20 00 + // 1: 00 00 00 [%%][20][64] 00 + // -: -50 + // Total Score: 434 / 700 = 62% + + // 20% = Score("public int Alpha2 = Alpha * Alpha;", "public int Beta = 2;") + // 64% = Score("public int Beta = 2;", "public int Gamma = 3;") + + Patcher.Result result = assertPatch( + PatchMode.FUZZY, + FuzzyMatchOptions.DEFAULT.maxMatchOffset(1), + "ConfigInsertedLines", + "Config_LineMatched", + "ConfigInsertedLines_MaxMatchOffset1" + ); + + assertEquals(0.62f, result.fuzzyQuality, 0.01f, "Expected quality"); + } + + @Test + public void MaxMatchOffset2() throws IOException { + // With MaxMatchOffset = 2, the results are the same as with MaxMatchOffset = 1 + // The added line penalty is too high to get the better match on "public int Beta = 2;" + // If the score for mismatching Beta and Gamma lines was lower than 50%, or the inserted line penalty was lower, we would have a different output + + // 0:[%%][%%][%%] 00 64 20 00 + // 1: 00 00 00 [%%][20][64] 00 + // 2: 00 00 00 20 %% 20 00 + // -: -50 + // Total Score: 434 / 700 = 62% + Patcher.Result result = assertPatch( + PatchMode.FUZZY, + FuzzyMatchOptions.DEFAULT.maxMatchOffset(2), + "ConfigInsertedLines", + "Config_LineMatched", + "ConfigInsertedLines_MaxMatchOffset2" + ); + assertEquals(0.62f, result.fuzzyQuality, 0.01f, "Expected quality"); + } + + @Test + public void MaxMatchOffsetLocation() throws IOException { + // 0: 00 00 00 00 00 64 00 + // 1: 00 00 00 00 64 20 00 + // 2:[%%][%%][%%][%%] 20 64 00 + // 3: 00 00 00 20 [%%] 20 00 + // 4: 00 00 00 64 20 [%%] 00 + // 5: 00 00 00 20 64 20 [%%] + // -: -50 -50 -50 + // Total Score: 550 / 700 = 79% + Patcher.Result defaultOffset = assertPatch( + PatchMode.FUZZY, + null, + "MaxMatchOffsetLocation", + "Config_LineMatched", + null + ); + assertEquals(0.79f, defaultOffset.fuzzyQuality, 0.01f, "Expected quality"); + + // 0:[%%][00][%%][64][64][64][%%] + // -: + // Total Score: 491 / 700 = 70% + Patcher.Result zeroOffset = assertPatch( + PatchMode.FUZZY, + FuzzyMatchOptions.DEFAULT.maxMatchOffset(0), + "MaxMatchOffsetLocation", + "Config_LineMatched", + "MaxMatchOffsetLocation0" + ); + assertEquals(0.70f, zeroOffset.fuzzyQuality, 0.01f, "Expected quality"); + } + + @Test + public void DistancePenalty() throws IOException { + // File has two fuzzy matches: a close one and a distant better one + // With distance penalty, the close match wins despite lower quality + // Without penalty, the far match wins due to higher quality + + // We use MaxMatchOffset = 0 to get a more precise test, because the MaxOffset of the matcher also gets subtracted from the warn distance when matching forward + + // 0:[%%][78][%%][%%][50][%%] + // Total Score: 528 / 600 = + Patcher.Result withPenalty = assertPatch(PatchMode.FUZZY, FuzzyMatchOptions.DEFAULT.maxMatchOffset(0), null, null, null); + assertEquals(0.88f, withPenalty.fuzzyQuality, 0.01f, "Expected quality"); + + // 0:[%%][%%][%%][%%][75][%%] + // Total Score: 575 / 600 = 96% + Patcher.Result withoutPenalty = assertPatch( + PatchMode.FUZZY, + FuzzyMatchOptions.DEFAULT.maxMatchOffset(0).enableDistancePenalty(false), + null, + null, + "DistancePenaltyOff" + ); + assertEquals(0.96f, withoutPenalty.fuzzyQuality, 0.01f, "Expected quality"); + + // OffsetWarnDistance impl + // Max(patchLength * 10, fileLength / 10) = Max(6 * 10, 136 / 10) + // + // The penalty is 10% for every 60 lines, after the first 10% + // For penalty to be large enough to beat the quality difference (8%) we need to be offset at least (60 + 60*0.8) = 108 lines + double minOffset = 60 / 0.1 * (withoutPenalty.fuzzyQuality - withPenalty.fuzzyQuality + 0.1); + assertNotNull(withPenalty.appliedPatch); + assertNotNull(withoutPenalty.appliedPatch); + + assertTrue(withPenalty.appliedPatch.start2 + minOffset > withoutPenalty.appliedPatch.start2, "Patch without penalty should apply at a much higher offset; expected value greater than " + withoutPenalty.appliedPatch.start2 + ", got " + withPenalty.appliedPatch.start2); + } +} diff --git a/src/test/java/io/codechicken/diffpatch/test/TestBase.java b/src/test/java/io/codechicken/diffpatch/test/TestBase.java index 7300564..a88455e 100644 --- a/src/test/java/io/codechicken/diffpatch/test/TestBase.java +++ b/src/test/java/io/codechicken/diffpatch/test/TestBase.java @@ -1,11 +1,26 @@ package io.codechicken.diffpatch.test; +import io.codechicken.diffpatch.diff.Differ; +import io.codechicken.diffpatch.diff.PatienceDiffer; +import io.codechicken.diffpatch.match.FuzzyMatchOptions; +import io.codechicken.diffpatch.patch.Patcher; import io.codechicken.diffpatch.util.ConsumingOutputStream; +import io.codechicken.diffpatch.util.Patch; +import io.codechicken.diffpatch.util.PatchFile; +import java.net.URISyntaxException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import io.codechicken.diffpatch.util.PatchMode; import net.covers1624.quack.io.IOUtils; import java.io.*; import java.nio.charset.StandardCharsets; import java.util.*; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Assertions; /** * Created by covers1624 on 30/5/24. @@ -79,4 +94,97 @@ public static byte[] testResource(String resource) throws IOException { public static InputStream stream(byte[] bytes) { return new ByteArrayInputStream(bytes); } + + public static Path testResourceDirectory(String directory) { + try { + return Paths.get(Objects.requireNonNull(TestBase.class.getResource("/" + directory)).toURI()); + } catch (URISyntaxException e) { + throw new RuntimeException("Failed to convert test resource", e); + } + } + + public static Path testFile(String directory, String file) throws IOException { + try (DirectoryStream stream = Files.newDirectoryStream(testResourceDirectory(directory), file)) { + return single(stream); + } catch (NoSuchElementException e) { + throw new IllegalStateException("Failed to find single file '" + file + "' under " + testResourceDirectory(directory), e); + } + } + + public static void assertDiff(@Nullable Differ differ, @Nullable String testName, @Nullable String patchName, int numContextLines, boolean autoOffset) throws IOException { + // Get the name of the caller + // To get the caller member name, get a stack trace and step back 1 + 'offset' times + // An offset of 1 would mean the caller of whoever called this method + patchName = patchName != null ? patchName : new Throwable().getStackTrace()[1].getMethodName(); + differ = differ != null ? differ : new PatienceDiffer(); + testName = testName != null ? testName : patchName; + numContextLines = numContextLines > 0 ? numContextLines : Differ.DEFAULT_CONTEXT; + + List lines1 = Files.readAllLines(testFile("files", testName + "_a.cs")); + List lines2 = Files.readAllLines(testFile("files", testName + "_b.cs")); + List patches = differ.makePatches(lines1, lines2, numContextLines); + PatchFile patchFile = new PatchFile(); + patchFile.patches = patches; + List actual = patchFile.toLines(autoOffset); + + Path patchPath = testFile("patches", patchName + ".patch"); + List expected = Files.exists(patchPath) ? Files.readAllLines(patchPath) : Collections.emptyList(); + // Files.readAllLines already strips out the newlines + Assertions.assertLinesMatch(expected, actual); + } + + public static T single(Iterable iterable) { + Iterator it = iterable.iterator(); + T value = it.next(); + if (it.hasNext()) { + throw new IllegalStateException("More than one element found"); + } + return value; + } + + private static class PatcherAndResult { + Patcher patcher; + List results; + + PatcherAndResult(Patcher patcher, List results) { + this.patcher = patcher; + this.results = results; + } + } + + private static PatcherAndResult runPatcher(String testName, String patchName, PatchMode mode, @Nullable FuzzyMatchOptions fuzzyOptions) throws IOException { + List linesA = Files.readAllLines(testFile("files", testName + "_a.cs")); + PatchFile patchFile = PatchFile.fromLines(patchName, Files.readAllLines(testFile("patches", patchName + ".patch")), true); + + fuzzyOptions = fuzzyOptions != null ? fuzzyOptions : FuzzyMatchOptions.DEFAULT; + Patcher patcher = new Patcher(patchFile, linesA, fuzzyOptions); + return new PatcherAndResult(patcher, patcher.patch(mode)); + } + + public static Patcher.Result patch(@Nullable PatchMode mode, @Nullable FuzzyMatchOptions fuzzyOptions, @Nullable String testName, @Nullable String patchName) throws IOException { + mode = mode != null ? mode : PatchMode.EXACT; + // Get the name of the caller + // To get the caller member name, get a stack trace and step back 1 + 'offset' times + // An offset of 1 would mean the caller of whoever called this method + patchName = patchName != null ? patchName : new Throwable().getStackTrace()[1].getMethodName(); + testName = testName != null ? testName : patchName; + + return single(runPatcher(testName, patchName, mode, fuzzyOptions).results); + } + + public static Patcher.Result assertPatch(@Nullable PatchMode mode, @Nullable FuzzyMatchOptions fuzzyOptions, @Nullable String testName, @Nullable String patchName, @Nullable String outputName) throws IOException { + mode = mode != null ? mode : PatchMode.EXACT; + // Get the name of the caller + // To get the caller member name, get a stack trace and step back 1 + 'offset' times + // An offset of 1 would mean the caller of whoever called this method + patchName = patchName != null ? patchName : new Throwable().getStackTrace()[1].getMethodName(); + testName = testName != null ? testName : patchName; + outputName = outputName != null ? outputName : testName; + PatcherAndResult actual = runPatcher(testName, patchName, mode, fuzzyOptions); + + List expected = Files.readAllLines(testFile("files", outputName + "_b.cs")); + Assertions.assertLinesMatch(expected, actual.patcher.lines); + + return single(actual.results); + } } diff --git a/src/test/resources/files/ConfigInsertedBulkLines_a.cs b/src/test/resources/files/ConfigInsertedBulkLines_a.cs new file mode 100644 index 0000000..12bc940 --- /dev/null +++ b/src/test/resources/files/ConfigInsertedBulkLines_a.cs @@ -0,0 +1,11 @@ +namespace TestData; + +public class Config +{ + public int Alpha = 1; + public int NewLines = 1 + + 2 + + 3; + public int Beta = 2; + public int Gamma = 3; +} \ No newline at end of file diff --git a/src/test/resources/files/ConfigInsertedBulkLines_b.cs b/src/test/resources/files/ConfigInsertedBulkLines_b.cs new file mode 100644 index 0000000..3f7fee4 --- /dev/null +++ b/src/test/resources/files/ConfigInsertedBulkLines_b.cs @@ -0,0 +1,11 @@ +namespace TestData; + +public class Config +{ + public int NewLines = 1 + + 2 + + 3; + public int Alpha = 10; + public int Beta = 20; + public int Gamma = 30; +} \ No newline at end of file diff --git a/src/test/resources/files/ConfigInsertedLines_LineMatched_b.cs b/src/test/resources/files/ConfigInsertedLines_LineMatched_b.cs new file mode 100644 index 0000000..ae35406 --- /dev/null +++ b/src/test/resources/files/ConfigInsertedLines_LineMatched_b.cs @@ -0,0 +1,12 @@ +namespace TestData; + +public class Config +{ + public int Zero = 0; // extra tokens make this an extra bad match + public int Alpha2 = Alpha * Alpha; + public int Alpha = 10; + public int Beta2 = Beta * Beta; + public int Beta = 20; + public int Gamma2 = Gamma * Gamma; + public int Gamma = 30; +} \ No newline at end of file diff --git a/src/test/resources/files/ConfigInsertedLines_MaxMatchOffset1_b.cs b/src/test/resources/files/ConfigInsertedLines_MaxMatchOffset1_b.cs new file mode 100644 index 0000000..c4f357e --- /dev/null +++ b/src/test/resources/files/ConfigInsertedLines_MaxMatchOffset1_b.cs @@ -0,0 +1,12 @@ +namespace TestData; + +public class Config +{ + public int Zero = 0; // extra tokens make this an extra bad match + public int Alpha = 10; + public int Beta = 20; + public int Gamma = 30; + public int Beta2 = Beta * Beta; + public int Gamma = 3; + public int Gamma2 = Gamma * Gamma; +} \ No newline at end of file diff --git a/src/test/resources/files/ConfigInsertedLines_MaxMatchOffset2_b.cs b/src/test/resources/files/ConfigInsertedLines_MaxMatchOffset2_b.cs new file mode 100644 index 0000000..c4f357e --- /dev/null +++ b/src/test/resources/files/ConfigInsertedLines_MaxMatchOffset2_b.cs @@ -0,0 +1,12 @@ +namespace TestData; + +public class Config +{ + public int Zero = 0; // extra tokens make this an extra bad match + public int Alpha = 10; + public int Beta = 20; + public int Gamma = 30; + public int Beta2 = Beta * Beta; + public int Gamma = 3; + public int Gamma2 = Gamma * Gamma; +} \ No newline at end of file diff --git a/src/test/resources/files/ConfigInsertedLines_Patience_b.cs b/src/test/resources/files/ConfigInsertedLines_Patience_b.cs new file mode 100644 index 0000000..2b8ac52 --- /dev/null +++ b/src/test/resources/files/ConfigInsertedLines_Patience_b.cs @@ -0,0 +1,10 @@ +namespace TestData; + +public class Config +{ + public int Zero = 0; // extra tokens make this an extra bad match + public int Gamma2 = Gamma * Gamma; + public int Alpha = 10; + public int Beta = 20; + public int Gamma = 30; +} \ No newline at end of file diff --git a/src/test/resources/files/ConfigInsertedLines_a.cs b/src/test/resources/files/ConfigInsertedLines_a.cs new file mode 100644 index 0000000..9c6ed61 --- /dev/null +++ b/src/test/resources/files/ConfigInsertedLines_a.cs @@ -0,0 +1,12 @@ +namespace TestData; + +public class Config +{ + public int Zero = 0; // extra tokens make this an extra bad match + public int Alpha = 1; + public int Alpha2 = Alpha * Alpha; + public int Beta = 2; + public int Beta2 = Beta * Beta; + public int Gamma = 3; + public int Gamma2 = Gamma * Gamma; +} \ No newline at end of file diff --git a/src/test/resources/files/Config_a.cs b/src/test/resources/files/Config_a.cs new file mode 100644 index 0000000..fded3a6 --- /dev/null +++ b/src/test/resources/files/Config_a.cs @@ -0,0 +1,8 @@ +namespace TestData; + +public class Config +{ + public int Alpha = 1; + public int Beta = 2; + public int Gamma = 3; +} \ No newline at end of file diff --git a/src/test/resources/files/Config_b.cs b/src/test/resources/files/Config_b.cs new file mode 100644 index 0000000..6fb2b70 --- /dev/null +++ b/src/test/resources/files/Config_b.cs @@ -0,0 +1,8 @@ +namespace TestData; + +public class Config +{ + public int Alpha = 10; + public int Beta = 20; + public int Gamma = 30; +} \ No newline at end of file diff --git a/src/test/resources/files/DistancePenaltyOff_b.cs b/src/test/resources/files/DistancePenaltyOff_b.cs new file mode 100644 index 0000000..871eb0b --- /dev/null +++ b/src/test/resources/files/DistancePenaltyOff_b.cs @@ -0,0 +1,129 @@ +namespace TestData; + +class Calculator +{ + public int Add(int a, int b) + { + return a + b; + } + + public void NewLongFunction() + { + var nums = [ + 01, + 02, + 03, + 04, + 05, + 06, + 07, + 08, + 09, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + ] + } + + public int Subtract(int a, int b) + { + return a - b; + } + + public int Multiply(int a, int b) + { + return a * b; + } + + public void Run() + { + // impl + } +} \ No newline at end of file diff --git a/src/test/resources/files/DistancePenalty_a.cs b/src/test/resources/files/DistancePenalty_a.cs new file mode 100644 index 0000000..131e84f --- /dev/null +++ b/src/test/resources/files/DistancePenalty_a.cs @@ -0,0 +1,124 @@ +namespace TestData; + +class Calculator +{ + public int Add(int a, int b) + { + return a + b; + } + + public void NewLongFunction() + { + var nums = [ + 01, + 02, + 03, + 04, + 05, + 06, + 07, + 08, + 09, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + ] + } + + public int Subtract(int a, int b) + { + return a - b; + } + + public void Run() + { + // impl + } +} \ No newline at end of file diff --git a/src/test/resources/files/DistancePenalty_b.cs b/src/test/resources/files/DistancePenalty_b.cs new file mode 100644 index 0000000..2a66f73 --- /dev/null +++ b/src/test/resources/files/DistancePenalty_b.cs @@ -0,0 +1,129 @@ +namespace TestData; + +class Calculator +{ + public int Add(int a, int b) + { + return a + b; + } + + public int Multiply(int a, int b) + { + return a * b; + } + + public void NewLongFunction() + { + var nums = [ + 01, + 02, + 03, + 04, + 05, + 06, + 07, + 08, + 09, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + ] + } + + public int Subtract(int a, int b) + { + return a - b; + } + + public void Run() + { + // impl + } +} \ No newline at end of file diff --git a/src/test/resources/files/MaxMatchOffsetLocation0_b.cs b/src/test/resources/files/MaxMatchOffsetLocation0_b.cs new file mode 100644 index 0000000..c78fc7f --- /dev/null +++ b/src/test/resources/files/MaxMatchOffsetLocation0_b.cs @@ -0,0 +1,18 @@ +namespace TestData; + +public class Bonfig // extra tokens make this an extra bad match +{ + public int Alpha = 10; + public int Beta = 20; + public int Gamma = 30; +} + +public class Config +{ + public int Alpha = 1; + public int Alpha2 = Alpha * Alpha; + public int Beta = 2; + public int Beta2 = Beta * Beta; + public int Gamma = 3; + public int Gamma2 = Gamma * Gamma; +} \ No newline at end of file diff --git a/src/test/resources/files/MaxMatchOffsetLocation_a.cs b/src/test/resources/files/MaxMatchOffsetLocation_a.cs new file mode 100644 index 0000000..d2d3b3b --- /dev/null +++ b/src/test/resources/files/MaxMatchOffsetLocation_a.cs @@ -0,0 +1,18 @@ +namespace TestData; + +public class Bonfig // extra tokens make this an extra bad match +{ + public int Width = 100; + public int Height = 200; + public int Depth = 300; +} + +public class Config +{ + public int Alpha = 1; + public int Alpha2 = Alpha * Alpha; + public int Beta = 2; + public int Beta2 = Beta * Beta; + public int Gamma = 3; + public int Gamma2 = Gamma * Gamma; +} \ No newline at end of file diff --git a/src/test/resources/files/MaxMatchOffsetLocation_b.cs b/src/test/resources/files/MaxMatchOffsetLocation_b.cs new file mode 100644 index 0000000..1aeeebc --- /dev/null +++ b/src/test/resources/files/MaxMatchOffsetLocation_b.cs @@ -0,0 +1,18 @@ +namespace TestData; + +public class Bonfig // extra tokens make this an extra bad match +{ + public int Width = 100; + public int Height = 200; + public int Depth = 300; +} + +public class Config +{ + public int Alpha2 = Alpha * Alpha; + public int Alpha = 10; + public int Beta2 = Beta * Beta; + public int Beta = 20; + public int Gamma2 = Gamma * Gamma; + public int Gamma = 30; +} \ No newline at end of file diff --git a/src/test/resources/files/MergedContext_a.cs b/src/test/resources/files/MergedContext_a.cs new file mode 100644 index 0000000..171ed44 --- /dev/null +++ b/src/test/resources/files/MergedContext_a.cs @@ -0,0 +1,14 @@ +namespace TestData; + +public class Calculator +{ + public int Add(int a, int b) + { + return a + b; + } + + public int Multiply(int a, int b) + { + return a * b; + } +} \ No newline at end of file diff --git a/src/test/resources/files/MergedContext_b.cs b/src/test/resources/files/MergedContext_b.cs new file mode 100644 index 0000000..9a10797 --- /dev/null +++ b/src/test/resources/files/MergedContext_b.cs @@ -0,0 +1,16 @@ +namespace TestData; + +public class Calculator +{ + public int Add(int a, int b) + { + var result = a + b; + return result; + } + + public int Multiply(int a, int b) + { + var result = a * b; + return result; + } +} \ No newline at end of file diff --git a/src/test/resources/files/SimpleInsert_a.cs b/src/test/resources/files/SimpleInsert_a.cs new file mode 100644 index 0000000..171ed44 --- /dev/null +++ b/src/test/resources/files/SimpleInsert_a.cs @@ -0,0 +1,14 @@ +namespace TestData; + +public class Calculator +{ + public int Add(int a, int b) + { + return a + b; + } + + public int Multiply(int a, int b) + { + return a * b; + } +} \ No newline at end of file diff --git a/src/test/resources/files/SimpleInsert_b.cs b/src/test/resources/files/SimpleInsert_b.cs new file mode 100644 index 0000000..b95a03c --- /dev/null +++ b/src/test/resources/files/SimpleInsert_b.cs @@ -0,0 +1,19 @@ +namespace TestData; + +public class Calculator +{ + public int Add(int a, int b) + { + return a + b; + } + + public int Subtract(int a, int b) + { + return a - b; + } + + public int Multiply(int a, int b) + { + return a * b; + } +} \ No newline at end of file diff --git a/src/test/resources/patches/AutoOffset.patch b/src/test/resources/patches/AutoOffset.patch new file mode 100644 index 0000000..f15d9c6 --- /dev/null +++ b/src/test/resources/patches/AutoOffset.patch @@ -0,0 +1,12 @@ +@@ -7,6 +_,11 @@ + return a + b; + } + ++ public int Subtract(int a, int b) ++ { ++ return a - b; ++ } ++ + public int Multiply(int a, int b) + { + return a * b; \ No newline at end of file diff --git a/src/test/resources/patches/Config_LineMatched.patch b/src/test/resources/patches/Config_LineMatched.patch new file mode 100644 index 0000000..9388400 --- /dev/null +++ b/src/test/resources/patches/Config_LineMatched.patch @@ -0,0 +1,11 @@ +@@ -2,7 +2,7 @@ + + public class Config + { +- public int Alpha = 1; ++ public int Alpha = 10; +- public int Beta = 2; ++ public int Beta = 20; +- public int Gamma = 3; ++ public int Gamma = 30; + } \ No newline at end of file diff --git a/src/test/resources/patches/Config_Patience.patch b/src/test/resources/patches/Config_Patience.patch new file mode 100644 index 0000000..f9c10b4 --- /dev/null +++ b/src/test/resources/patches/Config_Patience.patch @@ -0,0 +1,11 @@ +@@ -2,7 +2,7 @@ + + public class Config + { +- public int Alpha = 1; +- public int Beta = 2; +- public int Gamma = 3; ++ public int Alpha = 10; ++ public int Beta = 20; ++ public int Gamma = 30; + } \ No newline at end of file diff --git a/src/test/resources/patches/DistancePenalty.patch b/src/test/resources/patches/DistancePenalty.patch new file mode 100644 index 0000000..754111b --- /dev/null +++ b/src/test/resources/patches/DistancePenalty.patch @@ -0,0 +1,12 @@ +@@ -6,6 +6,11 @@ + { + return a - b; + } ++ ++ public int Multiply(int a, int b) ++ { ++ return a * b; ++ } + + public int Run() + { \ No newline at end of file diff --git a/src/test/resources/patches/FailedPatch.patch b/src/test/resources/patches/FailedPatch.patch new file mode 100644 index 0000000..421b95a --- /dev/null +++ b/src/test/resources/patches/FailedPatch.patch @@ -0,0 +1,12 @@ +@@ -1,7 +1,7 @@ + namespace TestData; + + public class Unrelated + { +- public string Foo = "bar"; +- public string Baz = "qux"; +- public string Quux = "corge"; ++ public string Foo = "BAR"; ++ public string Baz = "QUX"; ++ public string Quux = "CORGE"; + } \ No newline at end of file diff --git a/src/test/resources/patches/FuzzyPatch.patch b/src/test/resources/patches/FuzzyPatch.patch new file mode 100644 index 0000000..5931845 --- /dev/null +++ b/src/test/resources/patches/FuzzyPatch.patch @@ -0,0 +1,12 @@ +@@ -7,6 +7,11 @@ + return first + second; + } + ++ public int Subtract(int a, int b) ++ { ++ return a - b; ++ } ++ + public int Multiply(int a, int b) + { + return a * b; \ No newline at end of file diff --git a/src/test/resources/patches/MergedContext.patch b/src/test/resources/patches/MergedContext.patch new file mode 100644 index 0000000..de411c2 --- /dev/null +++ b/src/test/resources/patches/MergedContext.patch @@ -0,0 +1,15 @@ +@@ -5,10 +5,12 @@ + public int Add(int a, int b) + { +- return a + b; ++ var result = a + b; ++ return result; + } + + public int Multiply(int a, int b) + { +- return a * b; ++ var result = a * b; ++ return result; + } + } \ No newline at end of file diff --git a/src/test/resources/patches/OffsetPatch.patch b/src/test/resources/patches/OffsetPatch.patch new file mode 100644 index 0000000..5e23ff1 --- /dev/null +++ b/src/test/resources/patches/OffsetPatch.patch @@ -0,0 +1,12 @@ +@@ -5,6 +5,11 @@ + return a + b; + } + ++ public int Subtract(int a, int b) ++ { ++ return a - b; ++ } ++ + public int Multiply(int a, int b) + { + return a * b; \ No newline at end of file diff --git a/src/test/resources/patches/SimpleInsert.patch b/src/test/resources/patches/SimpleInsert.patch new file mode 100644 index 0000000..1d3373e --- /dev/null +++ b/src/test/resources/patches/SimpleInsert.patch @@ -0,0 +1,12 @@ +@@ -7,6 +7,11 @@ + return a + b; + } + ++ public int Subtract(int a, int b) ++ { ++ return a - b; ++ } ++ + public int Multiply(int a, int b) + { + return a * b; diff --git a/src/test/resources/patches/SplitContext.patch b/src/test/resources/patches/SplitContext.patch new file mode 100644 index 0000000..d910fc5 --- /dev/null +++ b/src/test/resources/patches/SplitContext.patch @@ -0,0 +1,12 @@ +@@ -6,3 +6,4 @@ + { +- return a + b; ++ var result = a + b; ++ return result; + } +@@ -11,3 +12,4 @@ + { +- return a * b; ++ var result = a * b; ++ return result; + } \ No newline at end of file From 005335cfb60552bee1057067541463aef9e2a52f Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Thu, 22 Oct 2026 21:55:57 +0800 Subject: [PATCH 07/10] Fix fuzzy patches reordering inserted context lines Matches fc967ed6bd7225c2b71a616ef89f5e011d2622f3 --- .../java/io/codechicken/diffpatch/patch/Patcher.java | 10 +++++----- src/test/resources/files/ConfigInsertedBulkLines_b.cs | 2 +- .../files/ConfigInsertedLines_LineMatched_b.cs | 6 +++--- .../resources/files/ConfigInsertedLines_Patience_b.cs | 2 +- src/test/resources/files/MaxMatchOffsetLocation_b.cs | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/java/io/codechicken/diffpatch/patch/Patcher.java b/src/main/java/io/codechicken/diffpatch/patch/Patcher.java index 0a905ee..219d0ac 100644 --- a/src/main/java/io/codechicken/diffpatch/patch/Patcher.java +++ b/src/main/java/io/codechicken/diffpatch/patch/Patcher.java @@ -312,6 +312,11 @@ public static Patch adjustPatchToMatchedLines(Patch patch, int[] match, List= 0 && ploc >= 0 && mloc - ploc > 1) { // delete an unmatched target line if the surrounding diffs are also DELETE, otherwise use it as context @@ -323,11 +328,6 @@ public static Patch adjustPatchToMatchedLines(Patch patch, int[] match, List Date: Fri, 23 Oct 2026 09:43:49 +0800 Subject: [PATCH 08/10] Fix fuzzy patches missing offset data in Result Matches cdc0b0070c5c5b6cd0400dd0680a40aee0c7f8fa --- .../codechicken/diffpatch/patch/Patcher.java | 2 +- .../diffpatch/test/PatchTests.java | 23 +++++++++++++- .../codechicken/diffpatch/test/TestBase.java | 30 ++++++++++++++++--- .../resources/files/MultiPatchOffset_a.cs | 21 +++++++++++++ .../resources/files/MultiPatchOffset_b.cs | 21 +++++++++++++ .../resources/patches/MultiPatchOffset.patch | 15 ++++++++++ 6 files changed, 106 insertions(+), 6 deletions(-) create mode 100644 src/test/resources/files/MultiPatchOffset_a.cs create mode 100644 src/test/resources/files/MultiPatchOffset_b.cs create mode 100644 src/test/resources/patches/MultiPatchOffset.patch diff --git a/src/main/java/io/codechicken/diffpatch/patch/Patcher.java b/src/main/java/io/codechicken/diffpatch/patch/Patcher.java index 219d0ac..985fbd0 100644 --- a/src/main/java/io/codechicken/diffpatch/patch/Patcher.java +++ b/src/main/java/io/codechicken/diffpatch/patch/Patcher.java @@ -296,7 +296,7 @@ private boolean applyFuzzy(WorkingPatch patch, int searchOffset) { } } patch.succeed(PatchMode.FUZZY, applyExactAt(at, fuzzyPatch)); - patch.addOffsetResult(fuzzyPatch.start2 - loc, lines.size()); + patch.addOffsetResult(at - loc, lines.size()); patch.addFuzzyResult(pair.getRight()); return true; } diff --git a/src/test/java/io/codechicken/diffpatch/test/PatchTests.java b/src/test/java/io/codechicken/diffpatch/test/PatchTests.java index 049ff3f..a01d735 100644 --- a/src/test/java/io/codechicken/diffpatch/test/PatchTests.java +++ b/src/test/java/io/codechicken/diffpatch/test/PatchTests.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test; import java.io.IOException; +import java.util.List; import static org.junit.jupiter.api.Assertions.*; @@ -181,6 +182,26 @@ public void DistancePenalty() throws IOException { assertNotNull(withPenalty.appliedPatch); assertNotNull(withoutPenalty.appliedPatch); - assertTrue(withPenalty.appliedPatch.start2 + minOffset > withoutPenalty.appliedPatch.start2, "Patch without penalty should apply at a much higher offset; expected value greater than " + withoutPenalty.appliedPatch.start2 + ", got " + withPenalty.appliedPatch.start2); + assertTrue(withoutPenalty.appliedPatch.start2 > withPenalty.appliedPatch.start2 + minOffset, "Patch without penalty should apply at a much higher offset; expected value greater than " + withoutPenalty.appliedPatch.start2 + ", got " + withPenalty.appliedPatch.start2); + } + + @Test + public void MultiPatchOffset() throws IOException { + // 3 hunks. The patch header positions are outdated due to inserted lines: + // Hunk 1: header says line 5, actual at line 7 → offset 2 + // Hunk 2: header says line 9, searchOffset=2 → loc=11, actual at line 14 → offset 3 + // Hunk 3: header says line 13, searchOffset=5 → loc=18, actual at line 18 → offset 0 (EXACT via accumulated searchOffset) + List results = assertMultiPatch(PatchMode.OFFSET, null, null, null, null); + + assertEquals(3, results.size()); + + assertEquals(PatchMode.OFFSET, results.get(0).mode); + assertEquals(2, results.get(0).offset); + + assertEquals(PatchMode.OFFSET, results.get(1).mode); + assertEquals(3, results.get(1).offset); + + assertEquals(PatchMode.EXACT, results.get(2).mode); + assertEquals(0, results.get(2).offset); } } diff --git a/src/test/java/io/codechicken/diffpatch/test/TestBase.java b/src/test/java/io/codechicken/diffpatch/test/TestBase.java index a88455e..3c2502a 100644 --- a/src/test/java/io/codechicken/diffpatch/test/TestBase.java +++ b/src/test/java/io/codechicken/diffpatch/test/TestBase.java @@ -22,6 +22,9 @@ import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.Assertions; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + /** * Created by covers1624 on 30/5/24. */ @@ -158,7 +161,17 @@ private static PatcherAndResult runPatcher(String testName, String patchName, Pa fuzzyOptions = fuzzyOptions != null ? fuzzyOptions : FuzzyMatchOptions.DEFAULT; Patcher patcher = new Patcher(patchFile, linesA, fuzzyOptions); - return new PatcherAndResult(patcher, patcher.patch(mode)); + List results = patcher.patch(mode); + + int lastOffset = 0; + for (Patcher.Result r : results) { + if (!r.success) continue; + int totalOffset = r.appliedPatch.start2 - r.patch.start1; + assertEquals(totalOffset - lastOffset, r.offset); + lastOffset = totalOffset; + } + + return new PatcherAndResult(patcher, results); } public static Patcher.Result patch(@Nullable PatchMode mode, @Nullable FuzzyMatchOptions fuzzyOptions, @Nullable String testName, @Nullable String patchName) throws IOException { @@ -171,8 +184,8 @@ public static Patcher.Result patch(@Nullable PatchMode mode, @Nullable FuzzyMatc return single(runPatcher(testName, patchName, mode, fuzzyOptions).results); } - - public static Patcher.Result assertPatch(@Nullable PatchMode mode, @Nullable FuzzyMatchOptions fuzzyOptions, @Nullable String testName, @Nullable String patchName, @Nullable String outputName) throws IOException { + + public static List assertMultiPatch(@Nullable PatchMode mode, @Nullable FuzzyMatchOptions fuzzyOptions, @Nullable String testName, @Nullable String patchName, @Nullable String outputName) throws IOException { mode = mode != null ? mode : PatchMode.EXACT; // Get the name of the caller // To get the caller member name, get a stack trace and step back 1 + 'offset' times @@ -185,6 +198,15 @@ public static Patcher.Result assertPatch(@Nullable PatchMode mode, @Nullable Fuz List expected = Files.readAllLines(testFile("files", outputName + "_b.cs")); Assertions.assertLinesMatch(expected, actual.patcher.lines); - return single(actual.results); + return actual.results; + } + + public static Patcher.Result assertPatch(@Nullable PatchMode mode, @Nullable FuzzyMatchOptions fuzzyOptions, @Nullable String testName, @Nullable String patchName, @Nullable String outputName) throws IOException { + // Get the name of the caller + // To get the caller member name, get a stack trace and step back 1 + 'offset' times + // An offset of 1 would mean the caller of whoever called this method + patchName = patchName != null ? patchName : new Throwable().getStackTrace()[1].getMethodName(); + + return single(assertMultiPatch(mode, fuzzyOptions, testName, patchName, outputName)); } } diff --git a/src/test/resources/files/MultiPatchOffset_a.cs b/src/test/resources/files/MultiPatchOffset_a.cs new file mode 100644 index 0000000..ba9a9d6 --- /dev/null +++ b/src/test/resources/files/MultiPatchOffset_a.cs @@ -0,0 +1,21 @@ +namespace TestData; + +public class Config +{ + // extra 1 + // extra 2 + public int Alpha = 1; + public int Beta = 2; + public int Gamma = 3; + + // extra 3 + // extra 4 + // extra 5 + public int Delta = 4; + public int Epsilon = 5; + public int Zeta = 6; + + public int Eta = 7; + public int Theta = 8; + public int Iota = 9; +} \ No newline at end of file diff --git a/src/test/resources/files/MultiPatchOffset_b.cs b/src/test/resources/files/MultiPatchOffset_b.cs new file mode 100644 index 0000000..aa0b4de --- /dev/null +++ b/src/test/resources/files/MultiPatchOffset_b.cs @@ -0,0 +1,21 @@ +namespace TestData; + +public class Config +{ + // extra 1 + // extra 2 + public int Alpha = 1; + public int Beta = 22; + public int Gamma = 3; + + // extra 3 + // extra 4 + // extra 5 + public int Delta = 4; + public int Epsilon = 55; + public int Zeta = 6; + + public int Eta = 7; + public int Theta = 88; + public int Iota = 9; +} \ No newline at end of file diff --git a/src/test/resources/patches/MultiPatchOffset.patch b/src/test/resources/patches/MultiPatchOffset.patch new file mode 100644 index 0000000..18d40fd --- /dev/null +++ b/src/test/resources/patches/MultiPatchOffset.patch @@ -0,0 +1,15 @@ +@@ -5,3 +5,3 @@ + public int Alpha = 1; +- public int Beta = 2; ++ public int Beta = 22; + public int Gamma = 3; +@@ -9,3 +9,3 @@ + public int Delta = 4; +- public int Epsilon = 5; ++ public int Epsilon = 55; + public int Zeta = 6; +@@ -13,3 +13,3 @@ + public int Eta = 7; +- public int Theta = 8; ++ public int Theta = 88; + public int Iota = 9; \ No newline at end of file From f13c1fcdca56b28c2281f05e17629af32dc3a162 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Fri, 23 Oct 2026 20:15:54 +0800 Subject: [PATCH 09/10] Add InsertedLinePenalty to FuzzyMatchOptions Matches 6df884284814c969db6241e8b7d5ac6984fadde8 --- .../diffpatch/match/FuzzyLineMatcher.java | 14 +++++++---- .../diffpatch/match/FuzzyMatchOptions.java | 25 +++++++++++++------ .../codechicken/diffpatch/patch/Patcher.java | 4 +-- .../diffpatch/cli/DiffPatchCliTests.java | 14 +++++------ .../diffpatch/test/PatchTests.java | 20 +++++++++++++++ .../files/ConfigInsertedLines_LowPenalty_b.cs | 12 +++++++++ 6 files changed, 68 insertions(+), 21 deletions(-) create mode 100644 src/test/resources/files/ConfigInsertedLines_LowPenalty_b.cs diff --git a/src/main/java/io/codechicken/diffpatch/match/FuzzyLineMatcher.java b/src/main/java/io/codechicken/diffpatch/match/FuzzyLineMatcher.java index 496587d..b75b040 100644 --- a/src/main/java/io/codechicken/diffpatch/match/FuzzyLineMatcher.java +++ b/src/main/java/io/codechicken/diffpatch/match/FuzzyLineMatcher.java @@ -14,6 +14,7 @@ public class FuzzyLineMatcher { public int maxMatchOffset = MatchMatrix.DEFAULT_MAX_OFFSET; public float minMatchScore = DEFAULT_MIN_MATCH_SCORE; + public float insertedLinePenalty = MatchMatrix.DEFAULT_INSERTED_LINE_PENALTY; public void matchLinesByWords(int[] matches, List wmLines1, List wmLines2) { for (Pair entry : LineMatching.unmatchedRanges(matches, wmLines2.size())) { @@ -54,7 +55,7 @@ public int[] match(List pattern, List search) { float bestScore = minMatchScore; int[] bestMatch = null; - MatchMatrix mm = new MatchMatrix(pattern, search, maxMatchOffset, null); + MatchMatrix mm = new MatchMatrix(pattern, search, maxMatchOffset, insertedLinePenalty, null); for (int i = mm.workingRange.getFirst(); ; i++) { Pair pair = mm.match(i); if (!pair.getLeft()) { @@ -142,11 +143,13 @@ public static int levenshteinDistance(String s, String t) { public static class MatchMatrix { public static final int DEFAULT_MAX_OFFSET = 5; + public static final float DEFAULT_INSERTED_LINE_PENALTY = 0.5f; private final int patternLength; private final LineRange range; // maximum offset between line matches in a run private final int maxOffset; + private final float insertedLinePenalty; public LineRange workingRange; @@ -159,16 +162,17 @@ public static class MatchMatrix { private int firstNode; public MatchMatrix(List pattern, List search) { - this(pattern, search, DEFAULT_MAX_OFFSET, null); + this(pattern, search, DEFAULT_MAX_OFFSET, DEFAULT_INSERTED_LINE_PENALTY, null); } - public MatchMatrix(List pattern, List search, int maxOffset, @Nullable LineRange range) { + public MatchMatrix(List pattern, List search, int maxOffset, float insertedLinePenalty, @Nullable LineRange range) { if (range == null) { range = LineRange.fromStartLen(0, search.size()); } patternLength = pattern.size(); this.range = range; this.maxOffset = maxOffset; + this.insertedLinePenalty = insertedLinePenalty; workingRange = LineRange.fromFirstLast(range.getStart() - maxOffset, range.getEnd() - patternLength); matches = new StraightMatch[maxOffset + 1]; @@ -255,7 +259,7 @@ private float recalculate() { float sum = matches[k].nodes[l].sum; if (k > j) { - sum -= 0.5f * (k - j); // penalty for skipping lines in search text + sum -= insertedLinePenalty * (k - j); // penalty for skipping lines in search text } if (sum > maxsum) { @@ -336,7 +340,7 @@ public String visualize() { sb.append(" "); for (int i = 1; i < patternLength; i++) { int offset = path[i] - path[i-1] - 1; - int penalty = Math.round(-0.5f * offset * 100); + int penalty = Math.round(-insertedLinePenalty * offset * 100); if (path[i] < 0 || penalty == 0) { sb.append(" "); } else { diff --git a/src/main/java/io/codechicken/diffpatch/match/FuzzyMatchOptions.java b/src/main/java/io/codechicken/diffpatch/match/FuzzyMatchOptions.java index c9e8dd3..56545dd 100644 --- a/src/main/java/io/codechicken/diffpatch/match/FuzzyMatchOptions.java +++ b/src/main/java/io/codechicken/diffpatch/match/FuzzyMatchOptions.java @@ -3,16 +3,18 @@ import java.util.Objects; public class FuzzyMatchOptions { - public static final FuzzyMatchOptions DEFAULT = new FuzzyMatchOptions(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, true); + public static final FuzzyMatchOptions DEFAULT = new FuzzyMatchOptions(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, true, FuzzyLineMatcher.MatchMatrix.DEFAULT_INSERTED_LINE_PENALTY); private final int maxMatchOffset; private final float minMatchScore; private final boolean enableDistancePenalty; + private final float insertedLinePenalty; - public FuzzyMatchOptions(int maxMatchOffset, float minMatchScore, boolean enableDistancePenalty) { + public FuzzyMatchOptions(int maxMatchOffset, float minMatchScore, boolean enableDistancePenalty, float insertedLinePenalty) { this.maxMatchOffset = maxMatchOffset; this.minMatchScore = minMatchScore; this.enableDistancePenalty = enableDistancePenalty; + this.insertedLinePenalty = insertedLinePenalty; } public int maxMatchOffset() { @@ -27,16 +29,24 @@ public boolean enableDistancePenalty() { return enableDistancePenalty; } + public float insertedLinePenalty() { + return insertedLinePenalty; + } + public FuzzyMatchOptions maxMatchOffset(int maxMatchOffset) { - return new FuzzyMatchOptions(maxMatchOffset, this.minMatchScore, this.enableDistancePenalty); + return new FuzzyMatchOptions(maxMatchOffset, this.minMatchScore, this.enableDistancePenalty, this.insertedLinePenalty); } public FuzzyMatchOptions minMatchScore(float minMatchScore) { - return new FuzzyMatchOptions(this.maxMatchOffset, minMatchScore, this.enableDistancePenalty); + return new FuzzyMatchOptions(this.maxMatchOffset, minMatchScore, this.enableDistancePenalty, this.insertedLinePenalty); } public FuzzyMatchOptions enableDistancePenalty(boolean enableDistancePenalty) { - return new FuzzyMatchOptions(this.maxMatchOffset, this.minMatchScore, enableDistancePenalty); + return new FuzzyMatchOptions(this.maxMatchOffset, this.minMatchScore, enableDistancePenalty, this.insertedLinePenalty); + } + + public FuzzyMatchOptions insertedLinePenalty(float insertedLinePenalty) { + return new FuzzyMatchOptions(this.maxMatchOffset, this.minMatchScore, this.enableDistancePenalty, insertedLinePenalty); } @Override @@ -45,11 +55,12 @@ public boolean equals(Object obj) { FuzzyMatchOptions other = (FuzzyMatchOptions) obj; return this.maxMatchOffset == other.maxMatchOffset && this.minMatchScore == other.minMatchScore - && this.enableDistancePenalty == other.enableDistancePenalty; + && this.enableDistancePenalty == other.enableDistancePenalty + && this.insertedLinePenalty == other.insertedLinePenalty; } @Override public int hashCode() { - return Objects.hash(this.maxMatchOffset, this.minMatchScore, this.enableDistancePenalty); + return Objects.hash(this.maxMatchOffset, this.minMatchScore, this.enableDistancePenalty, this.insertedLinePenalty); } } diff --git a/src/main/java/io/codechicken/diffpatch/patch/Patcher.java b/src/main/java/io/codechicken/diffpatch/patch/Patcher.java index 985fbd0..bd665ab 100644 --- a/src/main/java/io/codechicken/diffpatch/patch/Patcher.java +++ b/src/main/java/io/codechicken/diffpatch/patch/Patcher.java @@ -360,12 +360,12 @@ public static Pair fuzzyMatch(List wmPattern, List // we're creating twice as many MatchMatrix objects as we need, incurring some wasted allocation and setup time, but it reads easier than trying to precompute all the edge cases List fwdMatchers = FastStream.of(ranges) - .map(r -> new FuzzyLineMatcher.MatchMatrix(wmPattern, wmText, fuzzyOptions.maxMatchOffset(), r)) + .map(r -> new FuzzyLineMatcher.MatchMatrix(wmPattern, wmText, fuzzyOptions.maxMatchOffset(), fuzzyOptions.insertedLinePenalty(), r)) .filter(m -> loc < m.workingRange.getLast()) .toList(); List revMatchers = FastStream.of(ranges) .reversed() - .map(r -> new FuzzyLineMatcher.MatchMatrix(wmPattern, wmText, fuzzyOptions.maxMatchOffset(), r)) + .map(r -> new FuzzyLineMatcher.MatchMatrix(wmPattern, wmText, fuzzyOptions.maxMatchOffset(), fuzzyOptions.insertedLinePenalty(), r)) .filter(m -> loc > m.workingRange.getFirst()) .toList(); diff --git a/src/test/java/io/codechicken/diffpatch/cli/DiffPatchCliTests.java b/src/test/java/io/codechicken/diffpatch/cli/DiffPatchCliTests.java index 660db7d..ae2d738 100644 --- a/src/test/java/io/codechicken/diffpatch/cli/DiffPatchCliTests.java +++ b/src/test/java/io/codechicken/diffpatch/cli/DiffPatchCliTests.java @@ -221,7 +221,7 @@ public void testPatch() throws IOException { assertTrue(op.patchedOutput instanceof Output.SingleOutput.ToStream); assertNull(op.rejectsOutput); assertFalse(op.rejectsAsPatches); - assertEquals(new FuzzyMatchOptions(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, true), op.fuzzyOptions); + assertEquals(new FuzzyMatchOptions(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, true, FuzzyLineMatcher.MatchMatrix.DEFAULT_INSERTED_LINE_PENALTY), op.fuzzyOptions); assertEquals(PatchMode.EXACT, op.mode); assertEquals("", op.patchesPrefix); assertEquals(System.lineSeparator(), op.lineEnding); @@ -242,7 +242,7 @@ public void testPatchOptions() throws IOException { assertTrue(op.patchedOutput instanceof Output.SingleOutput.ToStream); assertNull(op.rejectsOutput); assertTrue(op.rejectsAsPatches); - assertEquals(new FuzzyMatchOptions(32, 69.0F, true), op.fuzzyOptions); + assertEquals(new FuzzyMatchOptions(32, 69.0F, true, FuzzyLineMatcher.MatchMatrix.DEFAULT_INSERTED_LINE_PENALTY), op.fuzzyOptions); assertEquals(PatchMode.FUZZY, op.mode); assertEquals("asdf/", op.patchesPrefix); assertEquals(DiffPatchCli.LineEnding.CR.chars, op.lineEnding); @@ -280,7 +280,7 @@ public void testPatchToFile() throws IOException { assertTrue(op.patchedOutput instanceof Output.SingleOutput.ToPath); assertTrue(op.rejectsOutput instanceof Output.SingleOutput.ToPath); assertFalse(op.rejectsAsPatches); - assertEquals(new FuzzyMatchOptions(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, true), op.fuzzyOptions); + assertEquals(new FuzzyMatchOptions(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, true, FuzzyLineMatcher.MatchMatrix.DEFAULT_INSERTED_LINE_PENALTY), op.fuzzyOptions); assertEquals(PatchMode.EXACT, op.mode); assertEquals("", op.patchesPrefix); assertEquals(System.lineSeparator(), op.lineEnding); @@ -310,7 +310,7 @@ public void testPatchToArchives() throws IOException { assertTrue(op.patchedOutput instanceof Output.SingleOutput.PathArchiveMultiOutput); assertTrue(op.rejectsOutput instanceof Output.SingleOutput.PathArchiveMultiOutput); assertFalse(op.rejectsAsPatches); - assertEquals(new FuzzyMatchOptions(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, true), op.fuzzyOptions); + assertEquals(new FuzzyMatchOptions(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, true, FuzzyLineMatcher.MatchMatrix.DEFAULT_INSERTED_LINE_PENALTY), op.fuzzyOptions); assertEquals(PatchMode.EXACT, op.mode); assertEquals("", op.patchesPrefix); assertEquals(System.lineSeparator(), op.lineEnding); @@ -336,7 +336,7 @@ public void testPatchToArchives3() throws IOException { assertTrue(op.patchedOutput instanceof Output.SingleOutput.PipeArchiveMultiOutput); assertTrue(op.rejectsOutput instanceof Output.SingleOutput.PathArchiveMultiOutput); assertFalse(op.rejectsAsPatches); - assertEquals(new FuzzyMatchOptions(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, true), op.fuzzyOptions); + assertEquals(new FuzzyMatchOptions(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, true, FuzzyLineMatcher.MatchMatrix.DEFAULT_INSERTED_LINE_PENALTY), op.fuzzyOptions); assertEquals(PatchMode.EXACT, op.mode); assertEquals("", op.patchesPrefix); assertEquals(System.lineSeparator(), op.lineEnding); @@ -362,7 +362,7 @@ public void testPatchToArchives2() throws IOException { assertTrue(op.patchedOutput instanceof Output.SingleOutput.PathArchiveMultiOutput); assertTrue(op.rejectsOutput instanceof Output.SingleOutput.PathArchiveMultiOutput); assertFalse(op.rejectsAsPatches); - assertEquals(new FuzzyMatchOptions(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, true), op.fuzzyOptions); + assertEquals(new FuzzyMatchOptions(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, true, FuzzyLineMatcher.MatchMatrix.DEFAULT_INSERTED_LINE_PENALTY), op.fuzzyOptions); assertEquals(PatchMode.EXACT, op.mode); assertEquals("", op.patchesPrefix); assertEquals(System.lineSeparator(), op.lineEnding); @@ -388,7 +388,7 @@ public void testPatchToArchives2(@TempDir Path folder) throws IOException { assertTrue(op.patchedOutput instanceof Output.FolderMultiOutput); assertTrue(op.rejectsOutput instanceof Output.FolderMultiOutput); assertFalse(op.rejectsAsPatches); - assertEquals(new FuzzyMatchOptions(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, true), op.fuzzyOptions); + assertEquals(new FuzzyMatchOptions(FuzzyLineMatcher.MatchMatrix.DEFAULT_MAX_OFFSET, FuzzyLineMatcher.DEFAULT_MIN_MATCH_SCORE, true, FuzzyLineMatcher.MatchMatrix.DEFAULT_INSERTED_LINE_PENALTY), op.fuzzyOptions); assertEquals(PatchMode.EXACT, op.mode); assertEquals("", op.patchesPrefix); assertEquals(System.lineSeparator(), op.lineEnding); diff --git a/src/test/java/io/codechicken/diffpatch/test/PatchTests.java b/src/test/java/io/codechicken/diffpatch/test/PatchTests.java index a01d735..ae27fdc 100644 --- a/src/test/java/io/codechicken/diffpatch/test/PatchTests.java +++ b/src/test/java/io/codechicken/diffpatch/test/PatchTests.java @@ -117,6 +117,26 @@ public void MaxMatchOffset2() throws IOException { assertEquals(0.62f, result.fuzzyQuality, 0.01f, "Expected quality"); } + @Test + public void InsertedLinePenalty() throws IOException { + // Same setup as MaxMatchOffset2, but with lower inserted line penalty (0.25 instead of 0.5) + // The lower penalty makes it worthwhile to skip lines in the search text to reach better matches at offset 2 + // + // 0:[%%][%%][%%] 00 64 20 00 + // 1: 00 00 00 [%%] 20 64 00 + // 2: 00 00 00 20 [%%][20] 00 + // -: -25 -25 + // Total Score: 470 / 700 = 67% + Patcher.Result result = assertPatch( + PatchMode.FUZZY, + FuzzyMatchOptions.DEFAULT.maxMatchOffset(2).insertedLinePenalty(0.25f), + "ConfigInsertedLines", + "Config_LineMatched", + "ConfigInsertedLines_LowPenalty" + ); + assertEquals(0.67f, result.fuzzyQuality, 0.01f, "Expected quality"); + } + @Test public void MaxMatchOffsetLocation() throws IOException { // 0: 00 00 00 00 00 64 00 diff --git a/src/test/resources/files/ConfigInsertedLines_LowPenalty_b.cs b/src/test/resources/files/ConfigInsertedLines_LowPenalty_b.cs new file mode 100644 index 0000000..4033460 --- /dev/null +++ b/src/test/resources/files/ConfigInsertedLines_LowPenalty_b.cs @@ -0,0 +1,12 @@ +namespace TestData; + +public class Config +{ + public int Zero = 0; // extra tokens make this an extra bad match + public int Alpha = 10; + public int Alpha2 = Alpha * Alpha; + public int Beta = 20; + public int Gamma = 30; + public int Gamma = 3; + public int Gamma2 = Gamma * Gamma; +} \ No newline at end of file From 6e9f73345db738c9aecccb90c80a40bff60be591 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Fri, 23 Oct 2026 20:20:13 +0800 Subject: [PATCH 10/10] Fix letters and underscores breaking words starting with digits 1f and 1_000 are now single 'words', as is 1a2f2af2 Matches 107ce07951cbf9260d0f6055bfdd2a323452b5d7 --- .../codechicken/diffpatch/util/CharRepresenter.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/codechicken/diffpatch/util/CharRepresenter.java b/src/main/java/io/codechicken/diffpatch/util/CharRepresenter.java index f4264d8..83d1c7f 100644 --- a/src/main/java/io/codechicken/diffpatch/util/CharRepresenter.java +++ b/src/main/java/io/codechicken/diffpatch/util/CharRepresenter.java @@ -52,15 +52,19 @@ public char addWord(String word) { private char[] buf = new char[4096]; + private boolean isIdentifierPart(char c) { + return Character.isLetterOrDigit(c) || c == '_'; + } + public String wordsToChars(String line) { int b = 0; for (int i = 0, len; i < line.length(); i += len) { char c = line.charAt(i); - //identify word + // identify word len = 1; - if (Character.isLetter(c)) { - while (i + len < line.length() && Character.isLetterOrDigit(line.charAt(i + len))) { + if (isIdentifierPart(c)) { + while (i + len < line.length() && isIdentifierPart(line.charAt(i + len))) { len++; } } else if (Character.isDigit(c)) { @@ -96,5 +100,4 @@ public int getMaxLineChar() { public int getMaxWordChar() { return charToWord.size(); } - }