diff --git a/README.md b/README.md index 7ff7fffc..65ea13f2 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. diff --git a/diff.js b/diff.js index 421854a1..66f4a02d 100644 --- a/diff.js +++ b/diff.js @@ -398,37 +398,20 @@ 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; for (var i = 0; i < diff.length; i++) { @@ -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,33 @@ // 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); + curRange.push.apply(curRange, contextLines(lines.slice(0, contextSize))); + + var hunk = { + oldStart: oldRangeStart, + oldLines: (oldLine - oldRangeStart + contextSize), + newStart: newRangeStart, + newLines: (newLine - newRangeStart + contextSize), + lines: curRange + } + 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) { + // 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) { + curRange.push('\\ No newline at end of file') + } } + hunks.push(hunk); oldRangeStart = 0; newRangeStart = 0; @@ -491,12 +485,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..d88077c5 --- /dev/null +++ b/test/structuredPatch.js @@ -0,0 +1,25 @@ +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\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: 3, + lines: [' line2', ' line3', '-line4', '+line5', '\\ No newline at end of file'], + }] + }); + }); +});