diff --git a/README.md b/README.md index 25b0b3fb..06eebfa4 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,9 @@ npm install diff --save Just like Diff.createTwoFilesPatch, but with oldFileName being equal to newFileName. +* `Diff.formatPatch(patch)` - creates a unified diff patch. + + The argument provided can either be an object representing a structured patch (like returned by `structuredPatch`) or an array of such objects (like returned by `parsePatch`). * `Diff.structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options)` - returns an object with an array of hunk objects. diff --git a/release-notes.md b/release-notes.md index 026b0e16..628a180d 100644 --- a/release-notes.md +++ b/release-notes.md @@ -8,6 +8,7 @@ - [#448](https://github.com/kpdecker/jsdiff/pull/411) Performance improvement. Diagonals whose furthest-reaching D-path would go off the edge of the edit graph are now skipped, rather than being pointlessly considered as called for by the original Myers diff algorithm. This dramatically speeds up computing diffs where the new text just appends or truncates content at the end of the old text. - [#351](https://github.com/kpdecker/jsdiff/issues/351) Importing from the lib folder - e.g. `require("diff/lib/diff/word.js")` - will work again now. This had been broken for users on the latest version of Node since Node 17.5.0, which changed how Node interprets the `exports` property in jsdiff's `package.json` file. - [#344](https://github.com/kpdecker/jsdiff/issues/344) `diffLines`, `createTwoFilesPatch`, and other patch-creation methods now take an optional `stripTrailingCr: true` option which causes Windows-style `\r\n` line endings to be replaced with Unix-style `\n` line endings before calculating the diff, just like GNU `diff`'s `--strip-trailing-cr` flag. +- [#451](https://github.com/kpdecker/jsdiff/pull/451) Added `diff.formatPatch`. ## v5.1.0 diff --git a/src/index.js b/src/index.js index 443db7e4..4a045c91 100644 --- a/src/index.js +++ b/src/index.js @@ -28,7 +28,7 @@ import {diffArrays} from './diff/array'; import {applyPatch, applyPatches} from './patch/apply'; import {parsePatch} from './patch/parse'; import {merge} from './patch/merge'; -import {structuredPatch, createTwoFilesPatch, createPatch} from './patch/create'; +import {structuredPatch, createTwoFilesPatch, createPatch, formatPatch} from './patch/create'; import {convertChangesToDMP} from './convert/dmp'; import {convertChangesToXML} from './convert/xml'; @@ -51,6 +51,7 @@ export { structuredPatch, createTwoFilesPatch, createPatch, + formatPatch, applyPatch, applyPatches, parsePatch, diff --git a/src/patch/create.js b/src/patch/create.js index 38486545..dfb4a237 100644 --- a/src/patch/create.js +++ b/src/patch/create.js @@ -105,6 +105,10 @@ export function structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHea } export function formatPatch(diff) { + if (Array.isArray(diff)) { + return diff.map(formatPatch).join('\n'); + } + const ret = []; if (diff.oldFileName == diff.newFileName) { ret.push('Index: ' + diff.oldFileName); diff --git a/test/patch/create.js b/test/patch/create.js index 2df4bb13..4a0396b9 100644 --- a/test/patch/create.js +++ b/test/patch/create.js @@ -1,5 +1,6 @@ import {diffWords} from '../../lib'; import {createPatch, createTwoFilesPatch, formatPatch, structuredPatch} from '../../lib/patch/create'; +import {parsePatch} from '../../lib/patch/parse'; import {expect} from 'chai'; @@ -788,5 +789,102 @@ describe('patch/create', function() { 'header1', 'header2' )); }); + it('supports serializing an array of structured patch objects into a single patch file in unified diff format', function() { + const patch = [ + { + oldFileName: 'foo', + oldHeader: '2023-12-29 15:48:17.976616966 +0000', + newFileName: 'bar', + newHeader: '2023-12-29 15:48:21.400516845 +0000', + hunks: [ + { + oldStart: 1, + oldLines: 1, + newStart: 1, + newLines: 1, + lines: [ + '-xxx', + '+yyy' + ], + linedelimiters: [ + '\n', + '\n' + ] + } + ] + }, + { + oldFileName: 'baz', + oldHeader: '2023-12-29 15:48:29.376283616 +0000', + newFileName: 'qux', + newHeader: '2023-12-29 15:48:32.908180343 +0000', + hunks: [ + { + oldStart: 1, + oldLines: 1, + newStart: 1, + newLines: 1, + lines: [ + '-aaa', + '+bbb' + ], + linedelimiters: [ + '\n', + '\n' + ] + } + ] + } + ]; + expect(formatPatch(patch)).to.equal( + '===================================================================\n' + + '--- foo\t2023-12-29 15:48:17.976616966 +0000\n' + + '+++ bar\t2023-12-29 15:48:21.400516845 +0000\n' + + '@@ -1,1 +1,1 @@\n' + + '-xxx\n' + + '+yyy\n' + + '\n' + + '===================================================================\n' + + '--- baz\t2023-12-29 15:48:29.376283616 +0000\n' + + '+++ qux\t2023-12-29 15:48:32.908180343 +0000\n' + + '@@ -1,1 +1,1 @@\n' + + '-aaa\n' + + '+bbb\n' + ); + }); + it('should roughly be the inverse of parsePatch', function() { + // There are so many differences in how a semantically-equivalent patch + // can be formatted in unified diff format, AND in JsDiff's structured + // patch format as long as https://github.com/kpdecker/jsdiff/issues/434 + // goes unresolved, that a stronger claim than "roughly the inverse" is + // sadly not possible here. + + // Check 1: starting with a patch in uniform diff format, does + // formatPatch(parsePatch(...)) round-trip? + const uniformPatch = '===================================================================\n' + + '--- baz\t2023-12-29 15:48:29.376283616 +0000\n' + + '+++ qux\t2023-12-29 15:48:32.908180343 +0000\n' + + '@@ -1,1 +1,1 @@\n' + + '-aaa\n' + + '+bbb\n'; + expect(formatPatch(parsePatch(uniformPatch))).to.equal(uniformPatch); + + // Check 2: starting with a structuredPatch, does formatting and then + // parsing again basically round-trip as long as we wrap it in an array + // to match the output of parsePatch and delete the linedelimiters that + // parsePatch puts in? + const patchObj = structuredPatch( + 'oldfile', 'newfile', + 'line2\nline3\nline4\n', 'line2\nline3\nline5', + 'header1', 'header2' + ); + + const roundTrippedPatch = parsePatch(formatPatch([patchObj])); + for (const hunk of roundTrippedPatch[0].hunks) { + delete hunk.linedelimiters; + } + + expect(roundTrippedPatch).to.deep.equal([patchObj]); + }); }); });