Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ private List<String> 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);
Expand Down
35 changes: 19 additions & 16 deletions src/main/java/io/codechicken/diffpatch/cli/PatchOperation.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -39,14 +40,13 @@ public class PatchOperation extends CliOperation<PatchOperation.PatchesSummary>
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<PrintStream> 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<PrintStream> 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;
Expand All @@ -56,8 +56,7 @@ private PatchOperation(PrintStream logger, LogLevel level, Consumer<PrintStream>
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;
Expand Down Expand Up @@ -107,7 +106,7 @@ public Result<PatchesSummary> 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()) {
Expand Down Expand Up @@ -144,7 +143,7 @@ public Result<PatchesSummary> operate() throws IOException {
patches.open(patchesPrefix);
Set<String> baseIndex = filterPrefixed(base.index(), ignorePrefixes);
Set<String> 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) {
Expand All @@ -168,7 +167,7 @@ public Result<PatchesSummary> operate() throws IOException {
return new Result<>(patchSuccess ? 0 : 1, summary);
}

private boolean doPatch(FileCollector oCollector, FileCollector rCollector, PatchesSummary summary, Set<String> bEntries, Set<String> pEntries, MultiInput baseInput, MultiInput patchesInput, float minFuzz, int maxOffset, PatchMode mode) throws IOException {
private boolean doPatch(FileCollector oCollector, FileCollector rCollector, PatchesSummary summary, Set<String> bEntries, Set<String> pEntries, MultiInput baseInput, MultiInput patchesInput, FuzzyMatchOptions fuzzyOptions, PatchMode mode) throws IOException {
Map<String, PatchFile> patchFiles = FastStream.of(pEntries)
.map(e -> {
try {
Expand Down Expand Up @@ -218,7 +217,7 @@ private boolean doPatch(FileCollector oCollector, FileCollector rCollector, Patc
summary.changedFiles++;
PatchFile patchFile = patchFiles.get(file);
List<String> 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) {
Expand All @@ -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<String> 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<String> base, PatchFile patchFile, FuzzyMatchOptions options, PatchMode mode) {
Patcher patcher = new Patcher(patchFile, base, options);
log(DEBUG, "Patching: " + baseName);
List<Patcher.Result> results = patcher.patch(mode);
List<RejectedHunk> rejectedHunks = new ArrayList<>();
Expand Down Expand Up @@ -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 = "";

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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]));
}
}
}
11 changes: 4 additions & 7 deletions src/main/java/io/codechicken/diffpatch/diff/Differ.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ public List<Diff> diff(List<String> lines1, List<String> lines2) {
}

public List<Patch> makePatches(List<String> lines1, List<String> lines2) {
return makePatches(lines1, lines2, DEFAULT_CONTEXT, true);
return makePatches(lines1, lines2, DEFAULT_CONTEXT);
}

public List<Patch> makePatches(List<String> lines1, List<String> lines2, int numContextLines, boolean collate) {
return makePatches(diff(lines1, lines2), numContextLines, collate);
public List<Patch> makePatches(List<String> lines1, List<String> lines2, int numContextLines) {
return makePatches(diff(lines1, lines2), numContextLines);
}

public static List<Patch> makeFileAdded(List<String> lines) {
Expand All @@ -52,17 +52,14 @@ public static List<Patch> makeFileRemoved(List<String> lines) {
return patch.length1 == 0 ? Collections.emptyList() : Collections.singletonList(patch);
}

public static List<Patch> makePatches(List<Diff> diffs, int numContextLines, boolean collate) {
public static List<Patch> makePatches(List<Diff> diffs, int numContextLines) {
Patch p = new Patch();
p.diffs = diffs;
p.recalculateLength();
p.trim(numContextLines);
if (p.length1 == 0) {
return Collections.emptyList();
}
if (!collate) {
p.uncollate();
}
return p.split(numContextLines);
}

Expand Down
30 changes: 25 additions & 5 deletions src/main/java/io/codechicken/diffpatch/match/FuzzyLineMatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> wmLines1, List<String> wmLines2) {
for (Pair<LineRange, LineRange> entry : LineMatching.unmatchedRanges(matches, wmLines2.size())) {
Expand Down Expand Up @@ -54,7 +55,7 @@ public int[] match(List<String> pattern, List<String> 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<Boolean, Float> pair = mm.match(i);
if (!pair.getLeft()) {
Expand Down Expand Up @@ -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;

Expand All @@ -159,16 +162,17 @@ public static class MatchMatrix {
private int firstNode;

public MatchMatrix(List<String> pattern, List<String> search) {
this(pattern, search, DEFAULT_MAX_OFFSET, null);
this(pattern, search, DEFAULT_MAX_OFFSET, DEFAULT_INSERTED_LINE_PENALTY, null);
}

public MatchMatrix(List<String> pattern, List<String> search, int maxOffset, @Nullable LineRange range) {
public MatchMatrix(List<String> pattern, List<String> 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];
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -316,7 +320,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++) {
Expand All @@ -331,6 +335,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(-insertedLinePenalty * 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();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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, 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, float insertedLinePenalty) {
this.maxMatchOffset = maxMatchOffset;
this.minMatchScore = minMatchScore;
this.enableDistancePenalty = enableDistancePenalty;
this.insertedLinePenalty = insertedLinePenalty;
}

public int maxMatchOffset() {
return maxMatchOffset;
}

public float minMatchScore() {
return minMatchScore;
}

public boolean enableDistancePenalty() {
return enableDistancePenalty;
}

public float insertedLinePenalty() {
return insertedLinePenalty;
}

public FuzzyMatchOptions maxMatchOffset(int maxMatchOffset) {
return new FuzzyMatchOptions(maxMatchOffset, this.minMatchScore, this.enableDistancePenalty, this.insertedLinePenalty);
}

public FuzzyMatchOptions minMatchScore(float minMatchScore) {
return new FuzzyMatchOptions(this.maxMatchOffset, minMatchScore, this.enableDistancePenalty, this.insertedLinePenalty);
}

public FuzzyMatchOptions enableDistancePenalty(boolean 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
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
&& this.insertedLinePenalty == other.insertedLinePenalty;
}

@Override
public int hashCode() {
return Objects.hash(this.maxMatchOffset, this.minMatchScore, this.enableDistancePenalty, this.insertedLinePenalty);
}
}
Loading