From ff45a4c4fbbe7e82b8bbec69bac7fadadf4743e4 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Thu, 4 Jul 2024 18:24:48 +0100 Subject: [PATCH 01/33] Add stubs of three new tests --- test/patch/apply.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/patch/apply.js b/test/patch/apply.js index d8c0711f..46ff44d8 100755 --- a/test/patch/apply.js +++ b/test/patch/apply.js @@ -546,6 +546,18 @@ describe('patch/apply', function() { + 'line5\n'); }); + it("should fail if a line to delete doesn't match, even with fuzz factor", function() { + // TODO + }); + + it("should fail if lines immediately surrounding an insertion don't match, regardless of fuzz factor", function() { + // TODO + }); + + it("should fail if number of lines of context mismatch is greater than fuzz factor", function() { + // TODO + }); + it('should succeed when hunk needs a negative offset', function() { expect(applyPatch( 'line1\n' From 835eb75331d610e301b743092ef702115041ce1f Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Thu, 4 Jul 2024 19:15:58 +0100 Subject: [PATCH 02/33] Flesh out one test --- test/patch/apply.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/test/patch/apply.js b/test/patch/apply.js index 46ff44d8..d5cd1a52 100755 --- a/test/patch/apply.js +++ b/test/patch/apply.js @@ -547,14 +547,30 @@ describe('patch/apply', function() { }); it("should fail if a line to delete doesn't match, even with fuzz factor", function() { - // TODO + const patch = 'Index: foo.txt\n' + + '===================================================================\n' + + '--- foo.txt\n' + + '+++ foo.txt\n' + + '@@ -1,4 +1,3 @@\n' + + ' foo\n' + + '-bar\n' + + ' baz\n' + + ' qux\n'; + + // Sanity-check - patch should apply fine to this: + const result1 = applyPatch('foo\nbar\nbaz\nqux\n', patch, {fuzzFactor: 99}); + expect(result1).to.equal('foo\nbaz\nqux\n'); + + // ... but not to this: + const result2 = applyPatch('foo\nSOMETHING ENTIRELY DIFFERENT\nbaz\nqux\n', patch, {fuzzFactor: 99}); + expect(result2).to.equal(false); }); it("should fail if lines immediately surrounding an insertion don't match, regardless of fuzz factor", function() { // TODO }); - it("should fail if number of lines of context mismatch is greater than fuzz factor", function() { + it('should fail if number of lines of context mismatch is greater than fuzz factor', function() { // TODO }); From 2d637b0a01efc3cafd92083aa73ebec435e1be04 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Thu, 4 Jul 2024 19:20:24 +0100 Subject: [PATCH 03/33] Add more stubs --- test/patch/apply.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/test/patch/apply.js b/test/patch/apply.js index d5cd1a52..ff057c67 100755 --- a/test/patch/apply.js +++ b/test/patch/apply.js @@ -525,7 +525,9 @@ describe('patch/apply', function() { .to.equal(false); }); - it('should succeed within fuzz factor', function() { + it('should succeed when context lines are modified fuzz factor', function() { + // TODO: Surely this should fail? How do we know if line4 should go before or after the + // second line2 here? We cannot know, right? Wouldn't GNU patch, Git, etc reject this? expect(applyPatch( 'line2\n' + 'line2\n' @@ -574,6 +576,18 @@ describe('patch/apply', function() { // TODO }); + it('should, given a fuzz factor, allow mismatches caused by presence of extra lines', function() { + // TODO + }); + + it('should, given a fuzz factor, allow mismatches due to missing lines', function() { + // TODO + }); + + it('should, given a fuzz factor, allow mismatches caused by lines being changed', function() { + // TODO + }); + it('should succeed when hunk needs a negative offset', function() { expect(applyPatch( 'line1\n' From d0b5408ff13881937e6abe5aae56671ac83cce6b Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Fri, 12 Jul 2024 22:11:33 +0100 Subject: [PATCH 04/33] First draft of new applyPatch; seems to be fucked currently --- src/patch/apply.js | 260 ++++++++++++++++++++++++++++++++------------- 1 file changed, 184 insertions(+), 76 deletions(-) diff --git a/src/patch/apply.js b/src/patch/apply.js index 151ecfc1..95423284 100644 --- a/src/patch/apply.js +++ b/src/patch/apply.js @@ -29,104 +29,212 @@ export function applyPatch(source, uniDiff, options = {}) { hunks = uniDiff.hunks, compareLine = options.compareLine || ((lineNumber, line, operation, patchContent) => line === patchContent), - errorCount = 0, fuzzFactor = options.fuzzFactor || 0, - minLine = 0, - offset = 0, + minLine = -1; - removeEOFNL, - addEOFNL; + if (fuzzFactor < 0) { + throw new Error('fuzzFactor must be non-negative'); + } + + // Before anything else, handle EOFNL insertion/removal. If the patch tells us to make a change + // to the EOFNL that redundant/impossible - i.e. to remove a newline that's not there, or add a + // newline that already exists - then we either return false and fail to apply the patch (if + // fuzzFactor is 0) or simply ignore the problem and do nothing (if fuzzFactor is >0). + // If we do need to remove/add a newline at EOF, this will always be in the final hunk: + let prevLine = '', + removeEOFNL = false, + addEOFNL = false; + for (const line of hunks[hunks.length - 1].lines) { + if (line[0] == '\\') { + if (prevLine[0] == '+') { + removeEOFNL = true; + } else if (prevLine[1] == '-') { + addEOFNL = true; + } else { + throw new Error(`Unsure how to interpret line ${line} which does not follow an insertion or deletion`); + } + break; + } + prevLine = line; + } + if (removeEOFNL) { + if (lines[lines.length - 1] == '') { + lines.pop(); + } else if (!fuzzFactor) { + return false; + } + } else if (addEOFNL) { + if (lines[lines.length - 1] != '') { + lines.push(''); + } else if (!fuzzFactor) { + return false; + } + } /** - * Checks if the hunk exactly fits on the provided location + * Checks if the hunk can be made to fit at the provided location with at most `maxErrors` + * insertions, substitutions, or deletions, while ensuring also that: + * - lines deleted in the hunk match exactly, and + * - wherever an insertion operation or block of insertion operations appears in the hunk, the + * immediately preceding and following lines of context match exactly + * + * `toPos` should be set such that lines[toPos] is meant to match hunkLines[0]. + * + * If the hunk can be applied, returns an object with properties `oldLineLastI` and + * `replacementLines`. Otherwise, returns null. */ - function hunkFits(hunk, toPos) { - for (let j = 0; j < hunk.lines.length; j++) { - let line = hunk.lines[j], - operation = (line.length > 0 ? line[0] : ' '), - content = (line.length > 0 ? line.substr(1) : line); - - if (operation === ' ' || operation === '-') { - // Context sanity check - if (!compareLine(toPos + 1, lines[toPos], operation, content)) { - errorCount++; - - if (errorCount > fuzzFactor) { - return false; + function applyHunk( + hunkLines, + toPos, + maxErrors, + hunkLinesI = 0, + lastContextLineMatched = true, + patchedLines = [], + patchedLinesLength = 0, + nConsecutiveOldContextLines = 0, + ) { + let nextContextLineMustMatch = false; + for (; hunkLinesI < hunkLines.length; hunkLinesI++) { + let hunkLine = hunkLines[hunkLinesI], + operation = (hunkLine.length > 0 ? hunkLine[0] : ' '), + content = (hunkLine.length > 0 ? hunkLine.substr(1) : hunkLine); + + if (operation === '-') { + if (compareLine(toPos + 1, lines[toPos], operation, content)) { + toPos++; + } else { + if (!maxErrors || lines[toPos] == null) { + return null; } + patchedLines[patchedLinesLength] = lines[toPos]; + return applyHunk( + hunkLines, + toPos + 1, + maxErrors - 1, + hunkLinesI, + false, + patchedLines, + patchedLinesLength + 1, + ); } - toPos++; } - } - - return true; - } - // Search best fit offsets for each hunk based on the previous ones - for (let i = 0; i < hunks.length; i++) { - let hunk = hunks[i], - maxLine = lines.length - hunk.oldLines, - localOffset = 0, - toPos = offset + hunk.oldStart - 1; - - let iterator = distanceIterator(toPos, minLine, maxLine); - - for (; localOffset !== undefined; localOffset = iterator()) { - if (hunkFits(hunk, toPos + localOffset)) { - hunk.offset = offset += localOffset; - break; + if (operation === '+') { + if (!lastContextLineMatched) { + return null; + } + patchedLines[patchedLinesLength] = content; + patchedLinesLength++; + nextContextLineMustMatch = true; } - } - if (localOffset === undefined) { - return false; + if (operation === ' ') { + nConsecutiveOldContextLines++; + patchedLines[patchedLinesLength] = lines[toPos]; + if (compareLine(toPos + 1, lines[toPos], operation, content)) { + patchedLinesLength++; + lastContextLineMatched = true; + toPos++; + } else { + if (nextContextLineMustMatch) { + return null; + } + + // Consider 3 possibilities in sequence: + // 1. lines contains a *substitution* not included in the patch context, or + // 2. lines contains an *insertion* not included in the patch context, or + // 3. lines contains a *deletion* not included in the patch context + // The first two options are of course only possible if the line from lines is non-null - + // i.e. only option 3 is possible if we've overrun the end of the old file. + return ( + lines[toPos] && ( + applyHunk( + hunkLines, + toPos + 1, + maxErrors - 1, + hunkLinesI + 1, + false, + patchedLines, + patchedLinesLength + 1 + ) || applyHunk( + hunkLines, + toPos + 1, + maxErrors - 1, + hunkLinesI, + false, + patchedLines, + patchedLinesLength + 1 + ) + ) || applyHunk( + hunkLines, + toPos, + maxErrors - 1, + hunkLinesI + 1, + false, + patchedLines, + patchedLinesLength + ) + ); + } + } } - // Set lower text limit to end of the current hunk, so next ones don't try - // to fit over already patched text - minLine = hunk.offset + hunk.oldStart + hunk.oldLines; + // Before returning, trim any unmodified context lines off the end of patchedLines and reduce + // toPos (and thus oldLineLastI) accordingly. This allows later hunks to be applied to a region + // that starts in this hunk's trailing context. + patchedLinesLength -= nConsecutiveOldContextLines; + toPos -= nConsecutiveOldContextLines; + patchedLines.length = patchedLinesLength; + return { + patchedLines, + oldLineLastI: toPos + }; } - // Apply patch hunks - let diffOffset = 0; - for (let i = 0; i < hunks.length; i++) { - let hunk = hunks[i], - toPos = hunk.oldStart + hunk.offset + diffOffset - 1; - diffOffset += hunk.newLines - hunk.oldLines; + const resultLines = []; - for (let j = 0; j < hunk.lines.length; j++) { - let line = hunk.lines[j], - operation = (line.length > 0 ? line[0] : ' '), - content = (line.length > 0 ? line.substr(1) : line); + // Search best fit offsets for each hunk based on the previous ones + let prevHunkOffset = 0; + for (const hunk of hunks) { + for (let maxErrors = 0; maxErrors <= fuzzFactor; maxErrors++) { + let maxLine = lines.length - hunk.oldLines + fuzzFactor, + toPos = hunk.oldStart + prevHunkOffset - 1; - if (operation === ' ') { - toPos++; - } else if (operation === '-') { - lines.splice(toPos, 1); - /* istanbul ignore else */ - } else if (operation === '+') { - lines.splice(toPos, 0, content); - toPos++; - } else if (operation === '\\') { - let previousOperation = hunk.lines[j - 1] ? hunk.lines[j - 1][0] : null; - if (previousOperation === '+') { - removeEOFNL = true; - } else if (previousOperation === '-') { - addEOFNL = true; + let iterator = distanceIterator(toPos, minLine, maxLine); + let hunkResult; + + for (; toPos !== undefined; toPos = iterator()) { + hunkResult = applyHunk(hunk.lines, toPos, maxErrors); + if (hunkResult) { + break; } } - } - } - // Handle EOFNL insertion/removal - if (removeEOFNL) { - while (!lines[lines.length - 1]) { - lines.pop(); + if (!hunkResult) { + return false; + } + + // Copy everything from the end of where we applied the last hunk to the start of this hunk + for (let i = minLine + 1; i < toPos; i++) { + resultLines.push(lines[i]); + } + + // Add the lines produced by applying the hunk: + for (const line of hunkResult.patchedLines) { + resultLines.push(line); + } + + // Set lower text limit to end of the current hunk, so next ones don't try + // to fit over already patched text + minLine = hunkResult.oldLineLastI + 1; + + // Note the offset between where the patch said the hunk should've applied and where we + // applied it, so we can adjust future hunks accordingly: + prevHunkOffset = toPos + 1 - hunk.oldStart; } - } else if (addEOFNL) { - lines.push(''); } - return lines.join('\n'); + + return resultLines.join('\n'); } // Wrapper that supports multiple file patches via callbacks. From a25ea84bd78e504988e76d89e2ab3e69280f13f4 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Mon, 15 Jul 2024 21:22:43 +0100 Subject: [PATCH 05/33] Fix some bugs in my work --- src/patch/apply.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/patch/apply.js b/src/patch/apply.js index 95423284..f934caa5 100644 --- a/src/patch/apply.js +++ b/src/patch/apply.js @@ -30,7 +30,7 @@ export function applyPatch(source, uniDiff, options = {}) { compareLine = options.compareLine || ((lineNumber, line, operation, patchContent) => line === patchContent), fuzzFactor = options.fuzzFactor || 0, - minLine = -1; + minLine = 0; if (fuzzFactor < 0) { throw new Error('fuzzFactor must be non-negative'); @@ -102,6 +102,7 @@ export function applyPatch(source, uniDiff, options = {}) { if (operation === '-') { if (compareLine(toPos + 1, lines[toPos], operation, content)) { toPos++; + nConsecutiveOldContextLines = 0; } else { if (!maxErrors || lines[toPos] == null) { return null; @@ -125,6 +126,7 @@ export function applyPatch(source, uniDiff, options = {}) { } patchedLines[patchedLinesLength] = content; patchedLinesLength++; + nConsecutiveOldContextLines = 0; nextContextLineMustMatch = true; } @@ -187,7 +189,7 @@ export function applyPatch(source, uniDiff, options = {}) { patchedLines.length = patchedLinesLength; return { patchedLines, - oldLineLastI: toPos + oldLineLastI: toPos - 1 }; } @@ -215,7 +217,7 @@ export function applyPatch(source, uniDiff, options = {}) { } // Copy everything from the end of where we applied the last hunk to the start of this hunk - for (let i = minLine + 1; i < toPos; i++) { + for (let i = minLine; i < toPos; i++) { resultLines.push(lines[i]); } @@ -234,6 +236,11 @@ export function applyPatch(source, uniDiff, options = {}) { } } + // Copy over the rest of the lines from the old text + for (let i = minLine; i < lines.length; i++) { + resultLines.push(lines[i]); + } + return resultLines.join('\n'); } From b6f0bf150234829a97d7bee3fa3c8d186255afcc Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Mon, 15 Jul 2024 21:42:30 +0100 Subject: [PATCH 06/33] Fix more bugs --- src/patch/apply.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/patch/apply.js b/src/patch/apply.js index f934caa5..9d342c2f 100644 --- a/src/patch/apply.js +++ b/src/patch/apply.js @@ -37,7 +37,7 @@ export function applyPatch(source, uniDiff, options = {}) { } // Before anything else, handle EOFNL insertion/removal. If the patch tells us to make a change - // to the EOFNL that redundant/impossible - i.e. to remove a newline that's not there, or add a + // to the EOFNL that is redundant/impossible - i.e. to remove a newline that's not there, or add a // newline that already exists - then we either return false and fail to apply the patch (if // fuzzFactor is 0) or simply ignore the problem and do nothing (if fuzzFactor is >0). // If we do need to remove/add a newline at EOF, this will always be in the final hunk: @@ -48,10 +48,8 @@ export function applyPatch(source, uniDiff, options = {}) { if (line[0] == '\\') { if (prevLine[0] == '+') { removeEOFNL = true; - } else if (prevLine[1] == '-') { + } else if (prevLine[0] == '-') { addEOFNL = true; - } else { - throw new Error(`Unsure how to interpret line ${line} which does not follow an insertion or deletion`); } break; } From 28a8d331f6114be71f9ecfaebca027d9f5f1d631 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Mon, 15 Jul 2024 21:47:24 +0100 Subject: [PATCH 07/33] Fix another bug --- src/patch/apply.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/patch/apply.js b/src/patch/apply.js index 9d342c2f..83cfa1fd 100644 --- a/src/patch/apply.js +++ b/src/patch/apply.js @@ -36,6 +36,11 @@ export function applyPatch(source, uniDiff, options = {}) { throw new Error('fuzzFactor must be non-negative'); } + // Special case for empty patch. + if (!hunks.length) { + return source; + } + // Before anything else, handle EOFNL insertion/removal. If the patch tells us to make a change // to the EOFNL that is redundant/impossible - i.e. to remove a newline that's not there, or add a // newline that already exists - then we either return false and fail to apply the patch (if From 50ec72e927a9ce03661d5b42b5061d49a69deeb3 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Mon, 15 Jul 2024 22:19:58 +0100 Subject: [PATCH 08/33] Fix another bug --- src/patch/apply.js | 49 +++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/src/patch/apply.js b/src/patch/apply.js index 83cfa1fd..16916e79 100644 --- a/src/patch/apply.js +++ b/src/patch/apply.js @@ -201,42 +201,41 @@ export function applyPatch(source, uniDiff, options = {}) { // Search best fit offsets for each hunk based on the previous ones let prevHunkOffset = 0; for (const hunk of hunks) { - for (let maxErrors = 0; maxErrors <= fuzzFactor; maxErrors++) { - let maxLine = lines.length - hunk.oldLines + fuzzFactor, - toPos = hunk.oldStart + prevHunkOffset - 1; - + let hunkResult; + let maxLine = lines.length - hunk.oldLines + fuzzFactor; + let toPos; + maxErrorsLoop: for (let maxErrors = 0; maxErrors <= fuzzFactor; maxErrors++) { + toPos = hunk.oldStart + prevHunkOffset - 1 let iterator = distanceIterator(toPos, minLine, maxLine); - let hunkResult; - for (; toPos !== undefined; toPos = iterator()) { hunkResult = applyHunk(hunk.lines, toPos, maxErrors); if (hunkResult) { - break; + break maxErrorsLoop; } } + } - if (!hunkResult) { - return false; - } + if (!hunkResult) { + return false; + } - // Copy everything from the end of where we applied the last hunk to the start of this hunk - for (let i = minLine; i < toPos; i++) { - resultLines.push(lines[i]); - } + // Copy everything from the end of where we applied the last hunk to the start of this hunk + for (let i = minLine; i < toPos; i++) { + resultLines.push(lines[i]); + } - // Add the lines produced by applying the hunk: - for (const line of hunkResult.patchedLines) { - resultLines.push(line); - } + // Add the lines produced by applying the hunk: + for (const line of hunkResult.patchedLines) { + resultLines.push(line); + } - // Set lower text limit to end of the current hunk, so next ones don't try - // to fit over already patched text - minLine = hunkResult.oldLineLastI + 1; + // Set lower text limit to end of the current hunk, so next ones don't try + // to fit over already patched text + minLine = hunkResult.oldLineLastI + 1; - // Note the offset between where the patch said the hunk should've applied and where we - // applied it, so we can adjust future hunks accordingly: - prevHunkOffset = toPos + 1 - hunk.oldStart; - } + // Note the offset between where the patch said the hunk should've applied and where we + // applied it, so we can adjust future hunks accordingly: + prevHunkOffset = toPos + 1 - hunk.oldStart; } // Copy over the rest of the lines from the old text From 8bdb035f79e4edfd07da5708c6c0a78cc7a7df1f Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Mon, 15 Jul 2024 22:20:07 +0100 Subject: [PATCH 09/33] Fix a bad test --- test/patch/apply.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test/patch/apply.js b/test/patch/apply.js index ff057c67..6281a597 100755 --- a/test/patch/apply.js +++ b/test/patch/apply.js @@ -526,24 +526,22 @@ describe('patch/apply', function() { }); it('should succeed when context lines are modified fuzz factor', function() { - // TODO: Surely this should fail? How do we know if line4 should go before or after the - // second line2 here? We cannot know, right? Wouldn't GNU patch, Git, etc reject this? expect(applyPatch( 'line2\n' - + 'line2\n' + + 'line3\n' + 'line5\n', '--- test\theader1\n' + '+++ test\theader2\n' + '@@ -1,3 +1,4 @@\n' - + ' line2\n' + + ' line1\n' + ' line3\n' + '+line4\n' + ' line5\n', {fuzzFactor: 1})) .to.equal( - 'line2\n' - + 'line2\n' + 'line1\n' + + 'line3\n' + 'line4\n' + 'line5\n'); }); From 1fb2337dcd8c9d5014202b6d8a348c4cc7b0023c Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Mon, 15 Jul 2024 22:27:48 +0100 Subject: [PATCH 10/33] Fix another bug --- src/patch/apply.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/patch/apply.js b/src/patch/apply.js index 16916e79..fc77d9af 100644 --- a/src/patch/apply.js +++ b/src/patch/apply.js @@ -141,7 +141,7 @@ export function applyPatch(source, uniDiff, options = {}) { lastContextLineMatched = true; toPos++; } else { - if (nextContextLineMustMatch) { + if (nextContextLineMustMatch || !maxErrors) { return null; } From 16fd7baf83ecbffe924fcb0265769c8751568362 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Fri, 19 Jul 2024 08:21:52 +0100 Subject: [PATCH 11/33] Fix a test bug --- test/patch/apply.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/patch/apply.js b/test/patch/apply.js index 6281a597..690c694b 100755 --- a/test/patch/apply.js +++ b/test/patch/apply.js @@ -540,7 +540,7 @@ describe('patch/apply', function() { + ' line5\n', {fuzzFactor: 1})) .to.equal( - 'line1\n' + 'line2\n' + 'line3\n' + 'line4\n' + 'line5\n'); From 9aa7ca8a6cf25176ce34a78641a7d7151e6d5f84 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Fri, 19 Jul 2024 08:46:27 +0100 Subject: [PATCH 12/33] Make distance-iterator behaviour match its documentation --- src/util/distance-iterator.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util/distance-iterator.js b/src/util/distance-iterator.js index 73be1f80..1ffe6413 100644 --- a/src/util/distance-iterator.js +++ b/src/util/distance-iterator.js @@ -18,7 +18,7 @@ export default function(start, minLine, maxLine) { // Check if trying to fit beyond text length, and if not, check it fits // after offset location (or desired location on first iteration) if (start + localOffset <= maxLine) { - return localOffset; + return start + localOffset; } forwardExhausted = true; @@ -32,7 +32,7 @@ export default function(start, minLine, maxLine) { // Check if trying to fit before text beginning, and if not, check it fits // before offset location if (minLine <= start - localOffset) { - return -localOffset++; + return start - localOffset++; } backwardExhausted = true; From d35e72413e49c0b0b6bdfb535b6982ed80ce773f Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Fri, 19 Jul 2024 09:16:25 +0100 Subject: [PATCH 13/33] More tests --- test/patch/apply.js | 46 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/test/patch/apply.js b/test/patch/apply.js index 690c694b..c6197c25 100755 --- a/test/patch/apply.js +++ b/test/patch/apply.js @@ -586,6 +586,10 @@ describe('patch/apply', function() { // TODO }); + it('should adjust where it starts looking to apply the hunk based on offsets of prior hunks', function() { + // TODO: example where hunk context is replicated 3 times + }) + it('should succeed when hunk needs a negative offset', function() { expect(applyPatch( 'line1\n' @@ -607,6 +611,48 @@ describe('patch/apply', function() { + 'line5\n'); }); + it('can handle an insertion before the first line', function() { + expect(applyPatch( + 'line2\n' + + 'line3\n' + + 'line4\n' + + 'line5\n', + + '--- test\theader1\n' + + '+++ test\theader2\n' + + '@@ -1,2 +1,3 @@\n' + + '+line1\n' + + ' line2\n' + + ' line3\n')) + .to.equal( + 'line1\n' + + 'line2\n' + + 'line3\n' + + 'line4\n' + + 'line5\n'); + }); + + it('can handle an insertion after the first line', function() { + expect(applyPatch( + 'line1\n' + + 'line2\n' + + 'line3\n' + + 'line4\n', + + '--- test\theader1\n' + + '+++ test\theader2\n' + + '@@ -3,2 +3,3 @@\n' + + ' line3\n' + + ' line4\n' + + '+line5\n')) + .to.equal( + 'line1\n' + + 'line2\n' + + 'line3\n' + + 'line4\n' + + 'line5\n'); + }); + it('should succeed when hunk needs a positive offset', function() { expect(applyPatch( 'line1\n' From 0ca0cb1d8f8ced9af62d4d2f035b5583afa91710 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Fri, 19 Jul 2024 09:20:58 +0100 Subject: [PATCH 14/33] New test --- test/patch/apply.js | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/test/patch/apply.js b/test/patch/apply.js index c6197c25..83a08c81 100755 --- a/test/patch/apply.js +++ b/test/patch/apply.js @@ -566,8 +566,44 @@ describe('patch/apply', function() { expect(result2).to.equal(false); }); - it("should fail if lines immediately surrounding an insertion don't match, regardless of fuzz factor", function() { - // TODO + it("should fail if either line immediately next to an insertion doesn't match, regardless of fuzz factor", function() { + expect(applyPatch( + 'lineA\n' + + 'lineB\n' + + 'lineC\n' + + 'lineD\n' + + 'lineE\n', + + '--- test\theader1\n' + + '+++ test\theader2\n' + + '@@ -1,5 +1,6 @@\n' + + ' lineA\n' + + ' lineB\n' + + ' lineC\n' + + '+lineNEW\n' + + ' lineX\n' + + ' lineE\n', + {fuzzFactor: 10})) + .to.equal(false); + + expect(applyPatch( + 'lineA\n' + + 'lineB\n' + + 'lineC\n' + + 'lineD\n' + + 'lineE\n', + + '--- test\theader1\n' + + '+++ test\theader2\n' + + '@@ -1,5 +1,6 @@\n' + + ' lineA\n' + + ' lineB\n' + + ' lineX\n' + + '+lineNEW\n' + + ' lineD\n' + + ' lineE\n', + {fuzzFactor: 10})) + .to.equal(false); }); it('should fail if number of lines of context mismatch is greater than fuzz factor', function() { From 3a738f6daaa82f5bf85b3007e08aa533b95c78ed Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Fri, 19 Jul 2024 09:57:26 +0100 Subject: [PATCH 15/33] rearrange planned tests a bit --- test/patch/apply.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/patch/apply.js b/test/patch/apply.js index 83a08c81..a4ef70a9 100755 --- a/test/patch/apply.js +++ b/test/patch/apply.js @@ -525,6 +525,7 @@ describe('patch/apply', function() { .to.equal(false); }); + // TODO: Delete this test with incoherent description once obsoleted by new ones below it('should succeed when context lines are modified fuzz factor', function() { expect(applyPatch( 'line2\n' @@ -606,10 +607,6 @@ describe('patch/apply', function() { .to.equal(false); }); - it('should fail if number of lines of context mismatch is greater than fuzz factor', function() { - // TODO - }); - it('should, given a fuzz factor, allow mismatches caused by presence of extra lines', function() { // TODO }); @@ -622,6 +619,10 @@ describe('patch/apply', function() { // TODO }); + it('should fail if number of lines of context mismatch is greater than fuzz factor', function() { + // TODO + }); + it('should adjust where it starts looking to apply the hunk based on offsets of prior hunks', function() { // TODO: example where hunk context is replicated 3 times }) From 7db24678bd14257a99a4dfafb42b4ff65cc84613 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Fri, 19 Jul 2024 10:08:39 +0100 Subject: [PATCH 16/33] Add a test --- test/patch/apply.js | 80 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/test/patch/apply.js b/test/patch/apply.js index a4ef70a9..abb72f4e 100755 --- a/test/patch/apply.js +++ b/test/patch/apply.js @@ -608,7 +608,46 @@ describe('patch/apply', function() { }); it('should, given a fuzz factor, allow mismatches caused by presence of extra lines', function() { - // TODO + expect(applyPatch( + 'line1\n' + + 'line2\n' + + 'line2.5\n' + + 'line3\n' + + 'line4\n' + + 'line6\n' + + 'line7\n' + + 'line8\n' + + 'line8.5\n' + + 'line9\n' + + 'line10\n', + + '--- foo.txt\t2024-07-19 09:58:02.489059795 +0100\n' + + '+++ bar.txt\t2024-07-19 09:58:24.768153252 +0100\n' + + '@@ -2,8 +2,8 @@\n' + + ' line2\n' + + ' line3\n' + + ' line4\n' + + '+line5\n' + + ' line6\n' + + ' line7\n' + + ' line8\n' + + '-line9\n' + + ' line10\n', + + {fuzzFactor: 2} + )).to.equal( + 'line1\n' + + 'line2\n' + + 'line2.5\n' + + 'line3\n' + + 'line4\n' + + 'line5\n' + + 'line6\n' + + 'line7\n' + + 'line8\n' + + 'line8.5\n' + + 'line10\n', + ); }); it('should, given a fuzz factor, allow mismatches due to missing lines', function() { @@ -620,7 +659,44 @@ describe('patch/apply', function() { }); it('should fail if number of lines of context mismatch is greater than fuzz factor', function() { - // TODO + // 3 extra lines of context, but fuzzFactor: 2 + expect(applyPatch( + 'line1\n' + + 'line2\n' + + 'line2.5\n' + + 'line3\n' + + 'line4\n' + + 'line6\n' + + 'line6.5\n' + + 'line7\n' + + 'line8\n' + + 'line8.5\n' + + 'line9\n' + + 'line10\n', + + '--- foo.txt\t2024-07-19 09:58:02.489059795 +0100\n' + + '+++ bar.txt\t2024-07-19 09:58:24.768153252 +0100\n' + + '@@ -2,8 +2,8 @@\n' + + ' line2\n' + + ' line3\n' + + ' line4\n' + + '+line5\n' + + ' line6\n' + + ' line7\n' + + ' line8\n' + + '-line9\n' + + ' line10\n', + + {fuzzFactor: 2} + )).to.equal( + false + ); + + // TODO: missing lines + + // TODO: subbed lines + + // TODO: a mixture }); it('should adjust where it starts looking to apply the hunk based on offsets of prior hunks', function() { From 977f8a9af566e2ab9ea8e4109e4b8f7e5ccc456e Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Fri, 19 Jul 2024 10:19:02 +0100 Subject: [PATCH 17/33] Add failing test - a bug, I think? --- test/patch/apply.js | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/test/patch/apply.js b/test/patch/apply.js index abb72f4e..93e22c89 100755 --- a/test/patch/apply.js +++ b/test/patch/apply.js @@ -651,7 +651,38 @@ describe('patch/apply', function() { }); it('should, given a fuzz factor, allow mismatches due to missing lines', function() { - // TODO + expect(applyPatch( + 'line1\n' + + 'line2\n' + + 'line4\n' + + 'line6\n' + + 'line7\n' + + 'line9\n' + + 'line10\n', + + '--- foo.txt\t2024-07-19 09:58:02.489059795 +0100\n' + + '+++ bar.txt\t2024-07-19 09:58:24.768153252 +0100\n' + + '@@ -2,8 +2,8 @@\n' + + ' line2\n' + + ' line3\n' + + ' line4\n' + + '+line5\n' + + ' line6\n' + + ' line7\n' + + ' line8\n' + + '-line9\n' + + ' line10\n', + + {fuzzFactor: 2} + )).to.equal( + 'line1\n' + + 'line3\n' + + 'line4\n' + + 'line5\n' + + 'line6\n' + + 'line7\n' + + 'line10\n', + ); }); it('should, given a fuzz factor, allow mismatches caused by lines being changed', function() { From ab8eff01043b72542719cd97d830e79db2b761dc Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Fri, 19 Jul 2024 12:05:38 +0100 Subject: [PATCH 18/33] Fix bug and fix test --- src/patch/apply.js | 1 + test/patch/apply.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/patch/apply.js b/src/patch/apply.js index fc77d9af..53d90a8b 100644 --- a/src/patch/apply.js +++ b/src/patch/apply.js @@ -139,6 +139,7 @@ export function applyPatch(source, uniDiff, options = {}) { if (compareLine(toPos + 1, lines[toPos], operation, content)) { patchedLinesLength++; lastContextLineMatched = true; + nextContextLineMustMatch = false; toPos++; } else { if (nextContextLineMustMatch || !maxErrors) { diff --git a/test/patch/apply.js b/test/patch/apply.js index 93e22c89..57f01c3a 100755 --- a/test/patch/apply.js +++ b/test/patch/apply.js @@ -676,7 +676,7 @@ describe('patch/apply', function() { {fuzzFactor: 2} )).to.equal( 'line1\n' - + 'line3\n' + + 'line2\n' + 'line4\n' + 'line5\n' + 'line6\n' From db083ec9602f6a87a6945ee79d476e38fe2eafd4 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Fri, 19 Jul 2024 12:08:34 +0100 Subject: [PATCH 19/33] Add another test case --- test/patch/apply.js | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/test/patch/apply.js b/test/patch/apply.js index 57f01c3a..7a1ea12a 100755 --- a/test/patch/apply.js +++ b/test/patch/apply.js @@ -723,7 +723,31 @@ describe('patch/apply', function() { false ); - // TODO: missing lines + // 2 lines of context missing from file to patch, fuzz factor 1 + expect(applyPatch( + 'line1\n' + + 'line2\n' + + 'line4\n' + + 'line6\n' + + 'line7\n' + + 'line9\n' + + 'line10\n', + + '--- foo.txt\t2024-07-19 09:58:02.489059795 +0100\n' + + '+++ bar.txt\t2024-07-19 09:58:24.768153252 +0100\n' + + '@@ -2,8 +2,8 @@\n' + + ' line2\n' + + ' line3\n' + + ' line4\n' + + '+line5\n' + + ' line6\n' + + ' line7\n' + + ' line8\n' + + '-line9\n' + + ' line10\n', + + {fuzzFactor: 1} + )).to.equal(false); // TODO: subbed lines From e6784aa2d7cb5fa86a088d7ec53afc5a98ff7bad Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Fri, 19 Jul 2024 12:15:46 +0100 Subject: [PATCH 20/33] Add more tests --- test/patch/apply.js | 65 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/test/patch/apply.js b/test/patch/apply.js index 7a1ea12a..04fcfa8c 100755 --- a/test/patch/apply.js +++ b/test/patch/apply.js @@ -686,7 +686,42 @@ describe('patch/apply', function() { }); it('should, given a fuzz factor, allow mismatches caused by lines being changed', function() { - // TODO + expect(applyPatch( + 'line1\n' + + 'line2\n' + + 'lineTHREE\n' + + 'line4\n' + + 'line6\n' + + 'line7\n' + + 'lineEIGHT\n' + + 'line9\n' + + 'line10\n', + + '--- foo.txt\t2024-07-19 09:58:02.489059795 +0100\n' + + '+++ bar.txt\t2024-07-19 09:58:24.768153252 +0100\n' + + '@@ -2,8 +2,8 @@\n' + + ' line2\n' + + ' line3\n' + + ' line4\n' + + '+line5\n' + + ' line6\n' + + ' line7\n' + + ' line8\n' + + '-line9\n' + + ' line10\n', + + {fuzzFactor: 2} + )).to.equal( + 'line1\n' + + 'line2\n' + + 'lineTHREE\n' + + 'line4\n' + + 'line5\n' + + 'line6\n' + + 'line7\n' + + 'lineEIGHT\n' + + 'line10\n', + ); }); it('should fail if number of lines of context mismatch is greater than fuzz factor', function() { @@ -749,7 +784,33 @@ describe('patch/apply', function() { {fuzzFactor: 1} )).to.equal(false); - // TODO: subbed lines + // 3 changed context lines, but fuzzFactor of 2 + expect(applyPatch( + 'line1\n' + + 'lineTWO\n' + + 'lineTHREE\n' + + 'line4\n' + + 'line6\n' + + 'line7\n' + + 'lineEIGHT\n' + + 'line9\n' + + 'line10\n', + + '--- foo.txt\t2024-07-19 09:58:02.489059795 +0100\n' + + '+++ bar.txt\t2024-07-19 09:58:24.768153252 +0100\n' + + '@@ -2,8 +2,8 @@\n' + + ' line2\n' + + ' line3\n' + + ' line4\n' + + '+line5\n' + + ' line6\n' + + ' line7\n' + + ' line8\n' + + '-line9\n' + + ' line10\n', + + {fuzzFactor: 2} + )).to.equal(false); // TODO: a mixture }); From 5e4ac4603c377b316a023b4de73a416ba97d4f46 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Fri, 19 Jul 2024 12:16:01 +0100 Subject: [PATCH 21/33] Delete redundant test --- test/patch/apply.js | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/test/patch/apply.js b/test/patch/apply.js index 04fcfa8c..f6b27e12 100755 --- a/test/patch/apply.js +++ b/test/patch/apply.js @@ -525,28 +525,6 @@ describe('patch/apply', function() { .to.equal(false); }); - // TODO: Delete this test with incoherent description once obsoleted by new ones below - it('should succeed when context lines are modified fuzz factor', function() { - expect(applyPatch( - 'line2\n' - + 'line3\n' - + 'line5\n', - - '--- test\theader1\n' - + '+++ test\theader2\n' - + '@@ -1,3 +1,4 @@\n' - + ' line1\n' - + ' line3\n' - + '+line4\n' - + ' line5\n', - {fuzzFactor: 1})) - .to.equal( - 'line2\n' - + 'line3\n' - + 'line4\n' - + 'line5\n'); - }); - it("should fail if a line to delete doesn't match, even with fuzz factor", function() { const patch = 'Index: foo.txt\n' + '===================================================================\n' + From 0c0b99fda53d68acd44a49fb2aa4e1de7713dfc2 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Fri, 19 Jul 2024 12:18:01 +0100 Subject: [PATCH 22/33] Add another test --- test/patch/apply.js | 67 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/test/patch/apply.js b/test/patch/apply.js index f6b27e12..d94ed6a8 100755 --- a/test/patch/apply.js +++ b/test/patch/apply.js @@ -702,6 +702,45 @@ describe('patch/apply', function() { ); }); + it('should, given a fuzz factor, allow mismatches caused by a mixture of ins/sub/del', function() { + expect(applyPatch( + 'line1\n' + + 'line2\n' + + 'line2.5\n' + + 'lineTHREE\n' + + 'line4\n' + + 'line6\n' + + 'line7\n' + + 'line9\n' + + 'line10\n', + + '--- foo.txt\t2024-07-19 09:58:02.489059795 +0100\n' + + '+++ bar.txt\t2024-07-19 09:58:24.768153252 +0100\n' + + '@@ -2,8 +2,8 @@\n' + + ' line2\n' + + ' line3\n' + + ' line4\n' + + '+line5\n' + + ' line6\n' + + ' line7\n' + + ' line8\n' + + '-line9\n' + + ' line10\n', + + {fuzzFactor: 3} + )).to.equal( + 'line1\n' + + 'line2\n' + + 'line2.5\n' + + 'lineTHREE\n' + + 'line4\n' + + 'line5\n' + + 'line6\n' + + 'line7\n' + + 'line10\n', + ); + }); + it('should fail if number of lines of context mismatch is greater than fuzz factor', function() { // 3 extra lines of context, but fuzzFactor: 2 expect(applyPatch( @@ -790,7 +829,33 @@ describe('patch/apply', function() { {fuzzFactor: 2} )).to.equal(false); - // TODO: a mixture + // 3 total changes, fuzzFactor 2 + expect(applyPatch( + 'line1\n' + + 'line2\n' + + 'line2.5\n' + + 'lineTHREE\n' + + 'line4\n' + + 'line6\n' + + 'line7\n' + + 'line9\n' + + 'line10\n', + + '--- foo.txt\t2024-07-19 09:58:02.489059795 +0100\n' + + '+++ bar.txt\t2024-07-19 09:58:24.768153252 +0100\n' + + '@@ -2,8 +2,8 @@\n' + + ' line2\n' + + ' line3\n' + + ' line4\n' + + '+line5\n' + + ' line6\n' + + ' line7\n' + + ' line8\n' + + '-line9\n' + + ' line10\n', + + {fuzzFactor: 2} + )).to.equal(false); }); it('should adjust where it starts looking to apply the hunk based on offsets of prior hunks', function() { From 997126b9274c9c19b6ed5425017bd63036110152 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Fri, 19 Jul 2024 12:41:32 +0100 Subject: [PATCH 23/33] Add another test --- test/patch/apply.js | 313 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 311 insertions(+), 2 deletions(-) diff --git a/test/patch/apply.js b/test/patch/apply.js index d94ed6a8..bfcbd095 100755 --- a/test/patch/apply.js +++ b/test/patch/apply.js @@ -859,8 +859,317 @@ describe('patch/apply', function() { }); it('should adjust where it starts looking to apply the hunk based on offsets of prior hunks', function() { - // TODO: example where hunk context is replicated 3 times - }) + const patch = '--- foo.txt\t2024-07-19 12:28:25.056182029 +0100\n' + + '+++ bar.txt\t2024-07-19 12:28:13.036639136 +0100\n' + + '@@ -9,7 +9,6 @@\n' + + ' 1 2 3 introductory text\n' + + ' Baa oink moo introductory text\n' + + ' Probably enough introductory text\n' + + '-Incy wincy mincy introductory text\n' + + ' \n' + + ' Three repeated verses:\n' + + ' \n' + + '@@ -28,7 +27,7 @@\n' + + ' The wind came along and blew them in again\n' + + ' Poor old Michael Finnegan, begin again\n' + + ' \n' + + '-There was an old man named Michael Finnegan\n' + + '+There was an old man named Bob\n' + + ' He had whiskers on his chinnegan\n' + + ' The wind came along and blew them in again\n' + + ' Poor old Michael Finnegan, begin again\n' + + + // First we try applying the text to the original text I used to generate the patch. + // The patch was generated by modifying the fourth of the six occurrences of the repeated + // verse, and that's what we should see when we apply it... + expect(applyPatch( + 'Bla bla bla introductory text\n' + + 'Foo bar baz introductory text\n' + + 'Fworble worble glorble introductory text\n' + + 'Need to be at least 6 lines of introductory text\n' + + 'Jingle jangle jungle introductory text\n' + + 'Horgle worgle borgle introductory text\n' + + 'Wiggly jiggly piggly introductory text\n' + + 'A B C introductory text\n' + + '1 2 3 introductory text\n' + + 'Baa oink moo introductory text\n' + + 'Probably enough introductory text\n' + + 'Incy wincy mincy introductory text\n' + + '\n' + + 'Three repeated verses:\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n', + + patch + )).to.equal( + 'Bla bla bla introductory text\n' + + 'Foo bar baz introductory text\n' + + 'Fworble worble glorble introductory text\n' + + 'Need to be at least 6 lines of introductory text\n' + + 'Jingle jangle jungle introductory text\n' + + 'Horgle worgle borgle introductory text\n' + + 'Wiggly jiggly piggly introductory text\n' + + 'A B C introductory text\n' + + '1 2 3 introductory text\n' + + 'Baa oink moo introductory text\n' + + 'Probably enough introductory text\n' + + '\n' + + 'Three repeated verses:\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Bob\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + ); + + // But what if we apply the patch to a source file where the first 5 lines are deleted? + // Then we expect applyPatch to still modify the fourth occurrence of the repeated verse, + // NOT the fifth (which is now the one at the line number indicated by the hunk header). This + // is because it should be able to tell when it applied the previous hunk that 5 lines at the + // beginning of the file had been deleted, and to adjust where it tries to apply the second + // hunk accordingly. + expect(applyPatch( + 'Horgle worgle borgle introductory text\n' + + 'Wiggly jiggly piggly introductory text\n' + + 'A B C introductory text\n' + + '1 2 3 introductory text\n' + + 'Baa oink moo introductory text\n' + + 'Probably enough introductory text\n' + + 'Incy wincy mincy introductory text\n' + + '\n' + + 'Three repeated verses:\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n', + + patch + )).to.equal( + 'Horgle worgle borgle introductory text\n' + + 'Wiggly jiggly piggly introductory text\n' + + 'A B C introductory text\n' + + '1 2 3 introductory text\n' + + 'Baa oink moo introductory text\n' + + 'Probably enough introductory text\n' + + '\n' + + 'Three repeated verses:\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Bob\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + ); + + // What if we instead ADD five lines? Same thing - we still expect verse 4 to be the one + // changed + expect(applyPatch( + 'line1\n' + + 'line2\n' + + 'line3\n' + + 'line4\n' + + 'line5\n' + + 'Bla bla bla introductory text\n' + + 'Foo bar baz introductory text\n' + + 'Fworble worble glorble introductory text\n' + + 'Need to be at least 6 lines of introductory text\n' + + 'Jingle jangle jungle introductory text\n' + + 'Horgle worgle borgle introductory text\n' + + 'Wiggly jiggly piggly introductory text\n' + + 'A B C introductory text\n' + + '1 2 3 introductory text\n' + + 'Baa oink moo introductory text\n' + + 'Probably enough introductory text\n' + + 'Incy wincy mincy introductory text\n' + + '\n' + + 'Three repeated verses:\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n', + + patch + )).to.equal( + 'line1\n' + + 'line2\n' + + 'line3\n' + + 'line4\n' + + 'line5\n' + + 'Bla bla bla introductory text\n' + + 'Foo bar baz introductory text\n' + + 'Fworble worble glorble introductory text\n' + + 'Need to be at least 6 lines of introductory text\n' + + 'Jingle jangle jungle introductory text\n' + + 'Horgle worgle borgle introductory text\n' + + 'Wiggly jiggly piggly introductory text\n' + + 'A B C introductory text\n' + + '1 2 3 introductory text\n' + + 'Baa oink moo introductory text\n' + + 'Probably enough introductory text\n' + + '\n' + + 'Three repeated verses:\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Bob\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + + '\n' + + 'There was an old man named Michael Finnegan\n' + + 'He had whiskers on his chinnegan\n' + + 'The wind came along and blew them in again\n' + + 'Poor old Michael Finnegan, begin again\n' + ); + }); it('should succeed when hunk needs a negative offset', function() { expect(applyPatch( From 104555a7079400c9406a9ee31f07b45c2fdeb943 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Fri, 19 Jul 2024 15:11:03 +0100 Subject: [PATCH 24/33] Placate eslint --- src/patch/apply.js | 9 ++++++--- test/patch/apply.js | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/patch/apply.js b/src/patch/apply.js index 53d90a8b..9ad679b8 100644 --- a/src/patch/apply.js +++ b/src/patch/apply.js @@ -205,15 +205,18 @@ export function applyPatch(source, uniDiff, options = {}) { let hunkResult; let maxLine = lines.length - hunk.oldLines + fuzzFactor; let toPos; - maxErrorsLoop: for (let maxErrors = 0; maxErrors <= fuzzFactor; maxErrors++) { - toPos = hunk.oldStart + prevHunkOffset - 1 + for (let maxErrors = 0; maxErrors <= fuzzFactor; maxErrors++) { + toPos = hunk.oldStart + prevHunkOffset - 1; let iterator = distanceIterator(toPos, minLine, maxLine); for (; toPos !== undefined; toPos = iterator()) { hunkResult = applyHunk(hunk.lines, toPos, maxErrors); if (hunkResult) { - break maxErrorsLoop; + break; } } + if (hunkResult) { + break; + } } if (!hunkResult) { diff --git a/test/patch/apply.js b/test/patch/apply.js index bfcbd095..150e96e7 100755 --- a/test/patch/apply.js +++ b/test/patch/apply.js @@ -877,7 +877,7 @@ describe('patch/apply', function() { '+There was an old man named Bob\n' + ' He had whiskers on his chinnegan\n' + ' The wind came along and blew them in again\n' + - ' Poor old Michael Finnegan, begin again\n' + ' Poor old Michael Finnegan, begin again\n'; // First we try applying the text to the original text I used to generate the patch. From 830817966f212bd5d946549c772bedce8ed8a20a Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Fri, 19 Jul 2024 15:18:29 +0100 Subject: [PATCH 25/33] Add a silly additional test to placate istanbul --- src/patch/apply.js | 4 ++-- test/patch/apply.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/patch/apply.js b/src/patch/apply.js index 9ad679b8..a66377bd 100644 --- a/src/patch/apply.js +++ b/src/patch/apply.js @@ -32,8 +32,8 @@ export function applyPatch(source, uniDiff, options = {}) { fuzzFactor = options.fuzzFactor || 0, minLine = 0; - if (fuzzFactor < 0) { - throw new Error('fuzzFactor must be non-negative'); + if (fuzzFactor < 0 || !Number.isInteger(fuzzFactor)) { + throw new Error('fuzzFactor must be a non-negative integer'); } // Special case for empty patch. diff --git a/test/patch/apply.js b/test/patch/apply.js index 150e96e7..e3f7b961 100755 --- a/test/patch/apply.js +++ b/test/patch/apply.js @@ -1491,6 +1491,48 @@ describe('patch/apply', function() { expect(applyPatch(oldFile, diffFile, {autoConvertLineEndings: false})).to.equal(false); }); + + it('rejects negative or non-integer fuzz factors', () => { + expect(() => { + applyPatch( + 'line2\n' + + 'line3\n' + + 'line5\n', + + 'Index: test\n' + + '===================================================================\n' + + '--- test\theader1\n' + + '+++ test\theader2\n' + + '@@ -1,3 +1,4 @@\n' + + ' line2\n' + + ' line3\n' + + '+line4\n' + + ' line5\n', + + {fuzzFactor: -1} + ); + }).to['throw']('fuzzFactor must be a non-negative integer'); + + expect(() => { + applyPatch( + 'line2\n' + + 'line3\n' + + 'line5\n', + + 'Index: test\n' + + '===================================================================\n' + + '--- test\theader1\n' + + '+++ test\theader2\n' + + '@@ -1,3 +1,4 @@\n' + + ' line2\n' + + ' line3\n' + + '+line4\n' + + ' line5\n', + + {fuzzFactor: 1.5} + ); + }).to['throw']('fuzzFactor must be a non-negative integer'); + }); }); describe('#applyPatches', function() { From 92d48d1e1743eebede5e912b9bfce4d9941d2900 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Fri, 19 Jul 2024 15:22:39 +0100 Subject: [PATCH 26/33] Fix something silly I did, and further improve coverage metric in doing so --- src/patch/apply.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/patch/apply.js b/src/patch/apply.js index a66377bd..86b02df3 100644 --- a/src/patch/apply.js +++ b/src/patch/apply.js @@ -94,8 +94,8 @@ export function applyPatch(source, uniDiff, options = {}) { lastContextLineMatched = true, patchedLines = [], patchedLinesLength = 0, - nConsecutiveOldContextLines = 0, ) { + let nConsecutiveOldContextLines = 0; let nextContextLineMustMatch = false; for (; hunkLinesI < hunkLines.length; hunkLinesI++) { let hunkLine = hunkLines[hunkLinesI], From 826de6cfbe482ad439ab156cfc7facd6b920fb21 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Fri, 19 Jul 2024 15:28:12 +0100 Subject: [PATCH 27/33] Add more tests to pump up coverage metric further --- test/patch/apply.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/patch/apply.js b/test/patch/apply.js index e3f7b961..28a8b7eb 100755 --- a/test/patch/apply.js +++ b/test/patch/apply.js @@ -1492,6 +1492,36 @@ describe('patch/apply', function() { expect(applyPatch(oldFile, diffFile, {autoConvertLineEndings: false})).to.equal(false); }); + it('fails if asked to remove a non-existent trailing newline with fuzzFactor 0', () => { + const oldFile = 'foo\nbar\nbaz\nqux'; + const diffFile = + 'Index: bla\n' + + '===================================================================\n' + + '--- bla\tOld Header\n' + + '+++ bla\tNew Header\n' + + '@@ -4,1 +4,1 @@\n' + + '-qux\n' + + '+qux\n' + + '\\ No newline at end of file\n'; + + expect(applyPatch(oldFile, diffFile)).to.equal(false); + }); + + it('fails if asked to add an EOF newline, with fuzzFactor 0, when one already exists', () => { + const oldFile = 'foo\nbar\nbaz\nqux\n'; + const diffFile = + 'Index: bla\n' + + '===================================================================\n' + + '--- bla\tOld Header\n' + + '+++ bla\tNew Header\n' + + '@@ -4,1 +4,1 @@\n' + + '-qux\n' + + '\\ No newline at end of file\n' + + '+qux\n'; + + expect(applyPatch(oldFile, diffFile)).to.equal(false); + }); + it('rejects negative or non-integer fuzz factors', () => { expect(() => { applyPatch( From 431ca68c24cf30a35ef4c417748ac8cbd9f45f59 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Fri, 19 Jul 2024 15:36:50 +0100 Subject: [PATCH 28/33] Eliminate for...of loops to placate the coverage checker --- src/patch/apply.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/patch/apply.js b/src/patch/apply.js index 86b02df3..6980f904 100644 --- a/src/patch/apply.js +++ b/src/patch/apply.js @@ -49,7 +49,8 @@ export function applyPatch(source, uniDiff, options = {}) { let prevLine = '', removeEOFNL = false, addEOFNL = false; - for (const line of hunks[hunks.length - 1].lines) { + for (let i = 0; i < hunks[hunks.length - 1].lines.length; i++) { + const line = hunks[hunks.length - 1].lines[i]; if (line[0] == '\\') { if (prevLine[0] == '+') { removeEOFNL = true; @@ -201,7 +202,8 @@ export function applyPatch(source, uniDiff, options = {}) { // Search best fit offsets for each hunk based on the previous ones let prevHunkOffset = 0; - for (const hunk of hunks) { + for (let i = 0; i < hunks.length; i++) { + const hunk = hunks[i]; let hunkResult; let maxLine = lines.length - hunk.oldLines + fuzzFactor; let toPos; @@ -229,7 +231,8 @@ export function applyPatch(source, uniDiff, options = {}) { } // Add the lines produced by applying the hunk: - for (const line of hunkResult.patchedLines) { + for (let i = 0; i < hunkResult.patchedLines.length; i++) { + const line = hunkResult.patchedLines[i]; resultLines.push(line); } From 533692e944203f3d0389edda91c7384f39e9fea0 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Fri, 19 Jul 2024 16:00:07 +0100 Subject: [PATCH 29/33] Add yet more tests to reach 100% coverage --- test/patch/apply.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/patch/apply.js b/test/patch/apply.js index 28a8b7eb..a05b7133 100755 --- a/test/patch/apply.js +++ b/test/patch/apply.js @@ -1522,6 +1522,36 @@ describe('patch/apply', function() { expect(applyPatch(oldFile, diffFile)).to.equal(false); }); + it('ignores being asked to remove a non-existent trailing newline if fuzzFactor >0', () => { + const oldFile = 'foo\nbar\nbaz\nqux'; + const diffFile = + 'Index: bla\n' + + '===================================================================\n' + + '--- bla\tOld Header\n' + + '+++ bla\tNew Header\n' + + '@@ -4,1 +4,1 @@\n' + + '-qux\n' + + '+qux\n' + + '\\ No newline at end of file\n'; + + expect(applyPatch(oldFile, diffFile, {fuzzFactor: 1})).to.equal(oldFile); + }); + + it('ignores being asked to add an EOF newline when one already exists if fuzzFactor>0', () => { + const oldFile = 'foo\nbar\nbaz\nqux\n'; + const diffFile = + 'Index: bla\n' + + '===================================================================\n' + + '--- bla\tOld Header\n' + + '+++ bla\tNew Header\n' + + '@@ -4,1 +4,1 @@\n' + + '-qux\n' + + '\\ No newline at end of file\n' + + '+qux\n'; + + expect(applyPatch(oldFile, diffFile, {fuzzFactor: 1})).to.equal(oldFile); + }); + it('rejects negative or non-integer fuzz factors', () => { expect(() => { applyPatch( From d8b5d1108ef345896b57ac2be509f58b16c016fd Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Fri, 19 Jul 2024 18:57:33 +0100 Subject: [PATCH 30/33] Add docs --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a8aefc97..621c4ff5 100644 --- a/README.md +++ b/README.md @@ -119,13 +119,21 @@ Broadly, jsdiff's diff functions all take an old text and a new text and perform * `Diff.applyPatch(source, patch[, options])` - attempts to apply a unified diff patch. - If the patch was applied successfully, returns a string containing the patched text. If the patch could not be applied (because some hunks in the patch couldn't be fitted to the text in `source`), returns false. + Hunks are applied first to last. `applyPatch` first tries to apply the first hunk at the line number specified in the hunk header, and with all context lines matching exactly. If that fails, it tries scanning backwards and forwards, one line at a time, to find a place to apply the hunk where the context lines match exactly. If that still fails, and `fuzzFactor` is greater than zero, it increments the maximum number of mismatches (insertions, deletions, substitutions) that there can be between the hunk context and a region where we are trying to apply the patch such that the hunk will still be considered to match. (Regardless of `fuzzFactor`, lines to be deleted in the hunk *must* be present for a hunk to match, and the context lines *immediately* before and after an insertion must match exactly.) + + Once a hunk is successfully fitted, the process begins again with the next hunk. Regardless of `fuzzFactor`, later hunks must be applied later in the file than earlier hunks. + + If any hunk cannot be fitted to any region with fewer than `fuzzFactor` mismatches, `applyPatch` fails and returns `false`. + + If a hunk is successfully fitted but not at the line number specified by the hunk header, all subsequent hunks have their target line number adjusted accordingly. (e.g. if the first hunk is applied 10 lines below where the hunk header said it should fit, `applyPatch` will *start* looking for somewhere to apply the second hunk 10 lines below where its hunk header says it goes.) + + If the patch was applied successfully, returns a string containing the patched text. If the patch could not be applied (because some hunks in the patch couldn't be fitted to the text in `source`), `applyPatch` returns false. `patch` may be a string diff or the output from the `parsePatch` or `structuredPatch` methods. The optional `options` object may have the following keys: - - `fuzzFactor`: Number of lines that are allowed to differ before rejecting a patch. Defaults to 0. + - `fuzzFactor`: Maximum Levenshtein distance (in lines deleted, added, or subtituted) between the context shown in a patch hunk and the lines found in the file. Defaults to 0. - `autoConvertLineEndings`: If `true`, and if the file to be patched consistently uses different line endings to the patch (i.e. either the file always uses Unix line endings while the patch uses Windows ones, or vice versa), then `applyPatch` will behave as if the line endings in the patch were the same as those in the source file. (If `false`, the patch will usually fail to apply in such circumstances since lines deleted in the patch won't be considered to match those in the source file.) Defaults to `true`. - `compareLine(lineNumber, line, operation, patchContent)`: Callback used to compare to given lines to determine if they should be considered equal when patching. Defaults to strict equality but may be overridden to provide fuzzier comparison. Should return false if the lines should be rejected. From bd0954b1d400c773128e65335bb55e1d88d3f9a2 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Fri, 19 Jul 2024 18:58:51 +0100 Subject: [PATCH 31/33] Tweak docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 621c4ff5..bce0e72f 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ Broadly, jsdiff's diff functions all take an old text and a new text and perform * `Diff.applyPatch(source, patch[, options])` - attempts to apply a unified diff patch. - Hunks are applied first to last. `applyPatch` first tries to apply the first hunk at the line number specified in the hunk header, and with all context lines matching exactly. If that fails, it tries scanning backwards and forwards, one line at a time, to find a place to apply the hunk where the context lines match exactly. If that still fails, and `fuzzFactor` is greater than zero, it increments the maximum number of mismatches (insertions, deletions, substitutions) that there can be between the hunk context and a region where we are trying to apply the patch such that the hunk will still be considered to match. (Regardless of `fuzzFactor`, lines to be deleted in the hunk *must* be present for a hunk to match, and the context lines *immediately* before and after an insertion must match exactly.) + Hunks are applied first to last. `applyPatch` first tries to apply the first hunk at the line number specified in the hunk header, and with all context lines matching exactly. If that fails, it tries scanning backwards and forwards, one line at a time, to find a place to apply the hunk where the context lines match exactly. If that still fails, and `fuzzFactor` is greater than zero, it increments the maximum number of mismatches (missing, extra, or changed context lines) that there can be between the hunk context and a region where we are trying to apply the patch such that the hunk will still be considered to match. (Regardless of `fuzzFactor`, lines to be deleted in the hunk *must* be present for a hunk to match, and the context lines *immediately* before and after an insertion must match exactly.) Once a hunk is successfully fitted, the process begins again with the next hunk. Regardless of `fuzzFactor`, later hunks must be applied later in the file than earlier hunks. From 7bbf583d3391c2470f5a8256a6237dbb1a281218 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Fri, 19 Jul 2024 18:59:48 +0100 Subject: [PATCH 32/33] Tweak docs --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bce0e72f..0916884e 100644 --- a/README.md +++ b/README.md @@ -119,11 +119,11 @@ Broadly, jsdiff's diff functions all take an old text and a new text and perform * `Diff.applyPatch(source, patch[, options])` - attempts to apply a unified diff patch. - Hunks are applied first to last. `applyPatch` first tries to apply the first hunk at the line number specified in the hunk header, and with all context lines matching exactly. If that fails, it tries scanning backwards and forwards, one line at a time, to find a place to apply the hunk where the context lines match exactly. If that still fails, and `fuzzFactor` is greater than zero, it increments the maximum number of mismatches (missing, extra, or changed context lines) that there can be between the hunk context and a region where we are trying to apply the patch such that the hunk will still be considered to match. (Regardless of `fuzzFactor`, lines to be deleted in the hunk *must* be present for a hunk to match, and the context lines *immediately* before and after an insertion must match exactly.) + Hunks are applied first to last. `applyPatch` first tries to apply the first hunk at the line number specified in the hunk header, and with all context lines matching exactly. If that fails, it tries scanning backwards and forwards, one line at a time, to find a place to apply the hunk where the context lines match exactly. If that still fails, and `fuzzFactor` is greater than zero, it increments the maximum number of mismatches (missing, extra, or changed context lines) that there can be between the hunk context and a region where we are trying to apply the patch such that the hunk will still be considered to match. Regardless of `fuzzFactor`, lines to be deleted in the hunk *must* be present for a hunk to match, and the context lines *immediately* before and after an insertion must match exactly. Once a hunk is successfully fitted, the process begins again with the next hunk. Regardless of `fuzzFactor`, later hunks must be applied later in the file than earlier hunks. - If any hunk cannot be fitted to any region with fewer than `fuzzFactor` mismatches, `applyPatch` fails and returns `false`. + If a hunk cannot be successfully fitted *anywhere* with fewer than `fuzzFactor` mismatches, `applyPatch` fails and returns `false`. If a hunk is successfully fitted but not at the line number specified by the hunk header, all subsequent hunks have their target line number adjusted accordingly. (e.g. if the first hunk is applied 10 lines below where the hunk header said it should fit, `applyPatch` will *start* looking for somewhere to apply the second hunk 10 lines below where its hunk header says it goes.) From 4d41449224b21a38d2f79afba15c6f5096d676d9 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Fri, 19 Jul 2024 19:18:17 +0100 Subject: [PATCH 33/33] Add release notes --- release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/release-notes.md b/release-notes.md index 9fa328f1..e254a056 100644 --- a/release-notes.md +++ b/release-notes.md @@ -28,6 +28,10 @@ - [#521](https://github.com/kpdecker/jsdiff/pull/521) **the `callback` option is now supported by `structuredPatch`, `createPatch - [#529](https://github.com/kpdecker/jsdiff/pull/529) **`parsePatch` can now parse patches where lines starting with `--` or `++` are deleted/inserted**; previously, there were edge cases where the parser would choke on valid patches or give wrong results. - [#530](https://github.com/kpdecker/jsdiff/pull/530) **Added `ignoreNewlineAtEof` option` to `diffLines`** +- [#533](https://github.com/kpdecker/jsdiff/pull/533) **`applyPatch` uses an entirely new algorithm for fuzzy matching.** Differences between the old and new algorithm are as follows: + * The `fuzzFactor` now indicates the maximum [*Levenshtein* distance](https://en.wikipedia.org/wiki/Levenshtein_distance) that there can be between the context shown in a hunk and the actual file content at a location where we try to apply the hunk. (Previously, it represented a maximum [*Hamming* distance](https://en.wikipedia.org/wiki/Hamming_distance), meaning that a single insertion or deletion in the source file could stop a hunk from applying even with a high `fuzzFactor`.) + * A hunk containing a deletion can now only be applied in a context where the line to be deleted actually appears verbatim. (Previously, as long as enough context lines in the hunk matched, `applyPatch` would apply the hunk anyway and delete a completely different line.) + * The context line immediately before and immediately after an insertion must match exactly between the hunk and the file for a hunk to apply. (Previously this was not required.) ## v5.2.0