From b257c312567da10726cf92ec3510b206ca831a07 Mon Sep 17 00:00:00 2001 From: Quest Date: Wed, 22 Jul 2015 18:19:12 +0200 Subject: [PATCH 1/6] Allow retreiving the patch data as a list of hunk objects --- diff.js | 101 ++++++++++++++++++++++++---------------- test/createPatch.js | 76 +++++++++++++++++++++++++++++- test/structuredPatch.js | 26 +++++++++++ 3 files changed, 162 insertions(+), 41 deletions(-) create mode 100644 test/structuredPatch.js diff --git a/diff.js b/diff.js index 421854a1..1c9e5c5d 100644 --- a/diff.js +++ b/diff.js @@ -398,39 +398,22 @@ callback ); }, - - createTwoFilesPatch: function(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader) { - var ret = []; - - if (oldFileName == newFileName) { - ret.push('Index: ' + oldFileName); + + structuredPatch: function(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options) { + if(!options) { + options = { context: 4 }; } - ret.push('==================================================================='); - ret.push('--- ' + oldFileName + (typeof oldHeader === 'undefined' ? '' : '\t' + oldHeader)); - ret.push('+++ ' + newFileName + (typeof newHeader === 'undefined' ? '' : '\t' + newHeader)); - + var diff = PatchDiff.diff(oldStr, newStr); diff.push({value: '', lines: []}); // Append an empty value to make cleanup easier - - // Formats a given set of lines for printing as context lines in a patch + function contextLines(lines) { return map(lines, function(entry) { return ' ' + entry; }); } - // Outputs the no newline at end of file warning if needed - function eofNL(curRange, i, current) { - var last = diff[diff.length - 2], - isLast = i === diff.length - 2, - isLastOfType = i === diff.length - 3 && current.added !== last.added; - - // Figure out if this is the last line for the given file and missing NL - if (!(/\n$/.test(current.value)) && (isLast || isLastOfType)) { - curRange.push('\\ No newline at end of file'); - } - } - + var hunks = []; var oldRangeStart = 0, newRangeStart = 0, curRange = [], - oldLine = 1, newLine = 1; + oldLine = 1, newLine = 1; for (var i = 0; i < diff.length; i++) { var current = diff[i], lines = current.lines || current.value.replace(/\n$/, '').split('\n'); @@ -444,7 +427,7 @@ newRangeStart = newLine; if (prev) { - curRange = contextLines(prev.lines.slice(-4)); + curRange = options.context > 0 ? contextLines(prev.lines.slice(-options.context)) : []; oldRangeStart -= curRange.length; newRangeStart -= curRange.length; } @@ -454,7 +437,6 @@ curRange.push.apply(curRange, map(lines, function(entry) { return (current.added ? '+' : '-') + entry; })); - eofNL(curRange, i, current); // Track the updated file position if (current.added) { @@ -466,21 +448,34 @@ // Identical context lines. Track line changes if (oldRangeStart) { // Close out any changes that have been output (or join overlapping) - if (lines.length <= 8 && i < diff.length - 2) { + if (lines.length <= options.context * 2 && i < diff.length - 2) { // Overlapping curRange.push.apply(curRange, contextLines(lines)); } else { // end the range and output - var contextSize = Math.min(lines.length, 4); - ret.push( - '@@ -' + oldRangeStart + ',' + (oldLine - oldRangeStart + contextSize) - + ' +' + newRangeStart + ',' + (newLine - newRangeStart + contextSize) - + ' @@'); - ret.push.apply(ret, curRange); - ret.push.apply(ret, contextLines(lines.slice(0, contextSize))); - if (lines.length <= 4) { - eofNL(ret, i, current); + var contextSize = Math.min(lines.length, options.context); + var hunklines = []; + hunklines.push.apply(hunklines, curRange); + hunklines.push.apply(hunklines, contextLines(lines.slice(0, contextSize))); + + var hunk = { + oldStart: oldRangeStart, + oldLines: (oldLine - oldRangeStart + contextSize), + newStart: newRangeStart, + newLines: (newLine - newRangeStart + contextSize), + lines: hunklines + } + if(i >= diff.length - 2 && lines.length <= options.context) { + // EOF is inside this hunk + var oldEOFNewline = /\n$/.test(oldStr); + var newEOFNewline = /\n$/.test(newStr); + if(lines.length == 0 && !oldEOFNewline) { + hunklines.splice(hunk.oldLines, 0, '\\ No newline at end of file') + } else if (!oldEOFNewline || !newEOFNewline) { + hunklines.push('\\ No newline at end of file') + } } + hunks.push(hunk); oldRangeStart = 0; newRangeStart = 0; @@ -491,12 +486,40 @@ newLine += lines.length; } } + + return { + oldFileName: oldFileName, newFileName: newFileName, + oldHeader: oldHeader, newHeader: newHeader, + hunks: hunks + }; + }, + createTwoFilesPatch: function(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options) { + var diff = JsDiff.structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options); + + var ret = []; + if (oldFileName == newFileName) { + ret.push('Index: ' + oldFileName); + } + ret.push('==================================================================='); + ret.push('--- ' + diff.oldFileName + (typeof diff.oldHeader === 'undefined' ? '' : '\t' + diff.oldHeader)); + ret.push('+++ ' + diff.newFileName + (typeof diff.newHeader === 'undefined' ? '' : '\t' + diff.newHeader)); + + for(var i = 0; i < diff.hunks.length; i++) { + var hunk = diff.hunks[i]; + ret.push( + '@@ -' + hunk.oldStart + ',' + hunk.oldLines + + ' +' + hunk.newStart + ',' + hunk.newLines + + ' @@' + ); + ret.push.apply(ret, hunk.lines); + } + return ret.join('\n') + '\n'; }, - createPatch: function(fileName, oldStr, newStr, oldHeader, newHeader) { - return JsDiff.createTwoFilesPatch(fileName, fileName, oldStr, newStr, oldHeader, newHeader); + createPatch: function(fileName, oldStr, newStr, oldHeader, newHeader, options) { + return JsDiff.createTwoFilesPatch(fileName, fileName, oldStr, newStr, oldHeader, newHeader, options); }, applyPatch: function(oldStr, uniDiff) { diff --git a/test/createPatch.js b/test/createPatch.js index 62665acf..fea4aa83 100644 --- a/test/createPatch.js +++ b/test/createPatch.js @@ -56,7 +56,7 @@ describe('#createPatch', function() { + '+line5\n'); }); - it('should output no newline at end of file message', function() { + it('should output "no newline" at end of file message on new missing nl', function() { diff.createPatch('test', 'line1\nline2\nline3\nline4\n', 'line1\nline2\nline3\nline4', 'header1', 'header2').should.equal( 'Index: test\n' + '===================================================================\n' @@ -69,7 +69,9 @@ describe('#createPatch', function() { + '-line4\n' + '+line4\n' + '\\ No newline at end of file\n'); + }); + it('should output "no newline" at end of file message on old missing nl', function() { diff.createPatch('test', 'line1\nline2\nline3\nline4', 'line1\nline2\nline3\nline4\n', 'header1', 'header2').should.equal( 'Index: test\n' + '===================================================================\n' @@ -82,7 +84,9 @@ describe('#createPatch', function() { + '-line4\n' + '\\ No newline at end of file\n' + '+line4\n'); + }); + it('should output "no newline" at end of file message on context missing nl', function() { diff.createPatch('test', 'line11\nline2\nline3\nline4', 'line1\nline2\nline3\nline4', 'header1', 'header2').should.equal( 'Index: test\n' + '===================================================================\n' @@ -95,7 +99,9 @@ describe('#createPatch', function() { + ' line3\n' + ' line4\n' + '\\ No newline at end of file\n'); + }); + it('should not output no newline at end of file message when eof outside hunk', function() { diff.createPatch('test', 'line11\nline2\nline3\nline4\nline4\nline4\nline4', 'line1\nline2\nline3\nline4\nline4\nline4\nline4', 'header1', 'header2').should.equal( 'Index: test\n' + '===================================================================\n' @@ -422,7 +428,7 @@ describe('#createPatch', function() { + 'context\n' + 'context'; - it('should generate a patch', function() { + it('should generate a patch with default context size', function() { var expectedResult = 'Index: testFileName\n' + '===================================================================\n' @@ -475,6 +481,72 @@ describe('#createPatch', function() { var diffResult = diff.createPatch('testFileName', oldFile, newFile, 'Old Header', 'New Header'); diffResult.should.equal(expectedResult); }); + + it('should generatea a patch with context size 0', function() { + var expectedResult = + 'Index: testFileName\n' + + '===================================================================\n' + + '--- testFileName\tOld Header\n' + + '+++ testFileName\tNew Header\n' + + '@@ -1,1 +1,2 @@\n' + + '-value\n' + + '+new value\n' + + '+new value 2\n' + + '@@ -11,1 +12,0 @@\n' + + '-remove value\n' + + '@@ -21,1 +21,0 @@\n' + + '-remove value\n' + + '@@ -30,0 +29,1 @@\n' + + '+add value\n' + + '@@ -34,1 +34,2 @@\n' + + '-value\n' + + '+new value\n' + + '+new value 2\n'; + var diffResult = diff.createPatch('testFileName', oldFile, newFile, 'Old Header', 'New Header', { context: 0 }); + diffResult.should.equal(expectedResult); + }); + + it('should generate a patch with context size 2', function() { + var expectedResult = + 'Index: testFileName\n' + + '===================================================================\n' + + '--- testFileName\tOld Header\n' + + '+++ testFileName\tNew Header\n' + + '@@ -1,3 +1,4 @@\n' + + '-value\n' + + '+new value\n' + + '+new value 2\n' + + ' context\n' + + ' context\n' + + '@@ -9,5 +10,4 @@\n' + + ' context\n' + + ' context\n' + + '-remove value\n' + + ' context\n' + + ' context\n' + + '@@ -19,5 +19,4 @@\n' + + ' context\n' + + ' context\n' + + '-remove value\n' + + ' context\n' + + ' context\n' + + '@@ -28,9 +27,11 @@\n' + + ' context\n' + + ' context\n' + + '+add value\n' + + ' context\n' + + ' context\n' + + ' context\n' + + ' context\n' + + '-value\n' + + '+new value\n' + + '+new value 2\n' + + ' context\n' + + ' context\n' + + '\\ No newline at end of file\n'; + var diffResult = diff.createPatch('testFileName', oldFile, newFile, 'Old Header', 'New Header', { context: 2 }); + diffResult.should.equal(expectedResult); + }); it('should output headers only for identical files', function() { var expectedResult = diff --git a/test/structuredPatch.js b/test/structuredPatch.js new file mode 100644 index 00000000..08201b81 --- /dev/null +++ b/test/structuredPatch.js @@ -0,0 +1,26 @@ +const VERBOSE = false; + +var diff = require('../diff'); + +function log() { + VERBOSE && console.log.apply(console, arguments); +} + +describe('#structuredPatch', function() { + it('should handle files with the last line changed', function() { + var res = diff.structuredPatch( + 'oldfile', 'newfile', + 'line2\nline3\nline5\n', 'line2\nline3\nline4\nline5\n', + 'header1', 'header2' + ); + res.should.eql({ + oldFileName: 'oldfile', newFileName: 'newfile', + oldHeader: 'header1', newHeader: 'header2', + hunks: [{ + oldStart: 1, oldLines: 3, newStart: 1, newLines: 4, + lines: [' line2', ' line3', '+line4', ' line5'], + oldEOFNewline: true, newEOFNewline: true, + }] + }); + }); +}); From bc11bccc0fcebefc2655081928407fb548804d3b Mon Sep 17 00:00:00 2001 From: Quest Date: Wed, 22 Jul 2015 22:46:55 +0200 Subject: [PATCH 2/6] Test on structured patch method updated --- test/structuredPatch.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/structuredPatch.js b/test/structuredPatch.js index 08201b81..d88077c5 100644 --- a/test/structuredPatch.js +++ b/test/structuredPatch.js @@ -10,16 +10,15 @@ describe('#structuredPatch', function() { it('should handle files with the last line changed', function() { var res = diff.structuredPatch( 'oldfile', 'newfile', - 'line2\nline3\nline5\n', 'line2\nline3\nline4\nline5\n', + 'line2\nline3\nline4\n', 'line2\nline3\nline5', 'header1', 'header2' ); res.should.eql({ oldFileName: 'oldfile', newFileName: 'newfile', oldHeader: 'header1', newHeader: 'header2', hunks: [{ - oldStart: 1, oldLines: 3, newStart: 1, newLines: 4, - lines: [' line2', ' line3', '+line4', ' line5'], - oldEOFNewline: true, newEOFNewline: true, + oldStart: 1, oldLines: 3, newStart: 1, newLines: 3, + lines: [' line2', ' line3', '-line4', '+line5', '\\ No newline at end of file'], }] }); }); From 8209a6bf8becad36a010a1575b6c0e4df9f27636 Mon Sep 17 00:00:00 2001 From: Quest Date: Fri, 31 Jul 2015 01:32:20 +0200 Subject: [PATCH 3/6] As per @kpdecker comments to PR #62 --- diff.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/diff.js b/diff.js index 1c9e5c5d..66f4a02d 100644 --- a/diff.js +++ b/diff.js @@ -400,7 +400,7 @@ }, structuredPatch: function(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options) { - if(!options) { + if (!options) { options = { context: 4 }; } @@ -413,7 +413,7 @@ var hunks = []; var oldRangeStart = 0, newRangeStart = 0, curRange = [], - oldLine = 1, newLine = 1; + oldLine = 1, newLine = 1; for (var i = 0; i < diff.length; i++) { var current = diff[i], lines = current.lines || current.value.replace(/\n$/, '').split('\n'); @@ -454,25 +454,24 @@ } else { // end the range and output var contextSize = Math.min(lines.length, options.context); - var hunklines = []; - hunklines.push.apply(hunklines, curRange); - hunklines.push.apply(hunklines, contextLines(lines.slice(0, contextSize))); + curRange.push.apply(curRange, contextLines(lines.slice(0, contextSize))); var hunk = { oldStart: oldRangeStart, oldLines: (oldLine - oldRangeStart + contextSize), newStart: newRangeStart, newLines: (newLine - newRangeStart + contextSize), - lines: hunklines + lines: curRange } - if(i >= diff.length - 2 && lines.length <= options.context) { + if (i >= diff.length - 2 && lines.length <= options.context) { // EOF is inside this hunk var oldEOFNewline = /\n$/.test(oldStr); var newEOFNewline = /\n$/.test(newStr); - if(lines.length == 0 && !oldEOFNewline) { - hunklines.splice(hunk.oldLines, 0, '\\ No newline at end of file') + if (lines.length == 0 && !oldEOFNewline) { + // special case: old has no eol and no trailing context; no-nl can end up before adds + curRange.splice(hunk.oldLines, 0, '\\ No newline at end of file') } else if (!oldEOFNewline || !newEOFNewline) { - hunklines.push('\\ No newline at end of file') + curRange.push('\\ No newline at end of file') } } hunks.push(hunk); @@ -505,7 +504,7 @@ ret.push('--- ' + diff.oldFileName + (typeof diff.oldHeader === 'undefined' ? '' : '\t' + diff.oldHeader)); ret.push('+++ ' + diff.newFileName + (typeof diff.newHeader === 'undefined' ? '' : '\t' + diff.newHeader)); - for(var i = 0; i < diff.hunks.length; i++) { + for (var i = 0; i < diff.hunks.length; i++) { var hunk = diff.hunks[i]; ret.push( '@@ -' + hunk.oldStart + ',' + hunk.oldLines From cf440256a4548722319838be7b7bee44434e77aa Mon Sep 17 00:00:00 2001 From: Quest Date: Fri, 31 Jul 2015 16:25:00 +0200 Subject: [PATCH 4/6] Documentationn for structuredPatch --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 7ff7fffc..6f415f8c 100644 --- a/README.md +++ b/README.md @@ -62,11 +62,29 @@ or * `newStr` : New string value * `oldHeader` : Additional information to include in the old file header * `newHeader` : Additional information to include in thew new file header + * `options` : An object with options. Currently, only `context` is supported and describes how many lines of context should be included. * `JsDiff.createPatch(fileName, oldStr, newStr, oldHeader, newHeader)` - creates a unified diff patch. Just like JsDiff.createTwoFilesPatch, but with oldFileName being equal to newFileName. + + * `JsDiff.structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options)` - returns an object with an array of hunk objects. + + This method is similar to createTwoFilesPatch, but returns a data structure + suitable for further processing. Parameters are the same as createTwoFilesPatch. The data structure returned may look like this: + + ```js + { + oldFileName: 'oldfile', newFileName: 'newfile', + oldHeader: 'header1', newHeader: 'header2', + hunks: [{ + oldStart: 1, oldLines: 3, newStart: 1, newLines: 3, + lines: [' line2', ' line3', '-line4', '+line5', '\\ No newline at end of file'], + }] + } + ``` + * `JsDiff.applyPatch(oldStr, diffStr)` - applies a unified diff patch. Return a string containing new version of provided data. From b49299afd6f59e4b07369d07dd53b7f60a7f3c8f Mon Sep 17 00:00:00 2001 From: Quest Date: Fri, 31 Jul 2015 16:26:54 +0200 Subject: [PATCH 5/6] Documentationn for structuredPatch --- README.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 6f415f8c..8308de6a 100644 --- a/README.md +++ b/README.md @@ -69,20 +69,20 @@ or Just like JsDiff.createTwoFilesPatch, but with oldFileName being equal to newFileName. - * `JsDiff.structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options)` - returns an object with an array of hunk objects. - - This method is similar to createTwoFilesPatch, but returns a data structure - suitable for further processing. Parameters are the same as createTwoFilesPatch. The data structure returned may look like this: - - ```js - { - oldFileName: 'oldfile', newFileName: 'newfile', - oldHeader: 'header1', newHeader: 'header2', - hunks: [{ - oldStart: 1, oldLines: 3, newStart: 1, newLines: 3, - lines: [' line2', ' line3', '-line4', '+line5', '\\ No newline at end of file'], - }] - } +* `JsDiff.structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options)` - returns an object with an array of hunk objects. + + This method is similar to createTwoFilesPatch, but returns a data structure + suitable for further processing. Parameters are the same as createTwoFilesPatch. The data structure returned may look like this: + +```js +{ + oldFileName: 'oldfile', newFileName: 'newfile', + oldHeader: 'header1', newHeader: 'header2', + hunks: [{ + oldStart: 1, oldLines: 3, newStart: 1, newLines: 3, + lines: [' line2', ' line3', '-line4', '+line5', '\\ No newline at end of file'], + }] +} ``` * `JsDiff.applyPatch(oldStr, diffStr)` - applies a unified diff patch. From 30189fbe094fd0eca9e89539cc816bf9e74fa3f0 Mon Sep 17 00:00:00 2001 From: Quest Date: Fri, 31 Jul 2015 16:30:03 +0200 Subject: [PATCH 6/6] Documentationn for structuredPatch --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 8308de6a..65ea13f2 100644 --- a/README.md +++ b/README.md @@ -74,15 +74,15 @@ or This method is similar to createTwoFilesPatch, but returns a data structure suitable for further processing. Parameters are the same as createTwoFilesPatch. The data structure returned may look like this: -```js -{ - oldFileName: 'oldfile', newFileName: 'newfile', - oldHeader: 'header1', newHeader: 'header2', - hunks: [{ - oldStart: 1, oldLines: 3, newStart: 1, newLines: 3, - lines: [' line2', ' line3', '-line4', '+line5', '\\ No newline at end of file'], - }] -} + ```js + { + oldFileName: 'oldfile', newFileName: 'newfile', + oldHeader: 'header1', newHeader: 'header2', + hunks: [{ + oldStart: 1, oldLines: 3, newStart: 1, newLines: 3, + lines: [' line2', ' line3', '-line4', '+line5', '\\ No newline at end of file'], + }] + } ``` * `JsDiff.applyPatch(oldStr, diffStr)` - applies a unified diff patch.