diff --git a/README.md b/README.md index 2ff57623..672556f6 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ jsdiff's diff functions all take an old text and a new text and perform three st - `isDelete` - set to `true` when parsing a Git diff that includes a `deleted file mode` extended header, indicating the file was deleted. - `oldMode` - the file mode (e.g. `'100644'`, `'100755'`) of the old file, parsed from Git extended headers (`old mode` or `deleted file mode`). - `newMode` - the file mode (e.g. `'100644'`, `'100755'`) of the new file, parsed from Git extended headers (`new mode` or `new file mode`). - - `isBinary` - set to `true` when parsing a Git diff that includes a `Binary files ... differ` line, indicating a binary file change. Binary patches have no hunks, so the patch content alone is not sufficient to apply the change; consumers should handle this case specially (e.g. by warning the user or fetching the binary content separately). + - `isBinary` - set to `true` when parsing a Git diff that includes a `Binary files ... differ` line or a `GIT binary patch` line (as output by `git diff --binary` and `git format-patch`), indicating a binary file change. Binary patches have no hunks, so the patch content alone is not sufficient to apply the change; consumers should handle this case specially (e.g. by warning the user or fetching the binary content separately). * `reversePatch(patch)` - Returns a new structured patch which when applied will undo the original `patch`. diff --git a/release-notes.md b/release-notes.md index e6d15432..a00fe2a7 100644 --- a/release-notes.md +++ b/release-notes.md @@ -11,6 +11,7 @@ - [#707](https://github.com/kpdecker/jsdiff/pull/707) **objects with `toJSON` methods that return arrays are supported properly in `diffJson` now**. (Previously, these would be serialized as objects with numeric keys instead of arrays.) - [#712](https://github.com/kpdecker/jsdiff/pull/712) **Big performance improvement.** The core diffing loop indexed its array of furthest-reaching paths by diagonal number, and half of the diagonal numbers are negative. JavaScript engines store negative array indices as ordinary named properties rather than as array elements, so every lookup in the innermost loop was a slow dictionary access. The indices are now shifted to be non-negative. Results are unchanged; most non-trivial diffs now seem to be about 4x faster. - [#710](https://github.com/kpdecker/jsdiff/pull/710) **async mode is now nearly as fast as sync mode**. Async mode works by breaking the diff computation into small steps, with each step enqueuing the next; this enqueuing was originally done using `setTimeout`. However, nested `setTimeout` timeouts are "clamped" to a minimum delay of 4ms (see e.g. the "Timers" section of the HTML spec, as well as https://stackoverflow.com/a/9647221/1709587, https://stackoverflow.com/q/18826570/1709587, and https://www.belshe.com/2010/06/04/chrome-cranking-up-the-clock/ for some relevant discussion). This meant async mode on a long diff was spending more time waiting for the next step to begin running than it was spending actually computing the diff. This is now fixed and async mode is almost as fast as sync mode. +- [#718](https://github.com/kpdecker/jsdiff/pull/718) *`parsePatch` now sets `isBinary` on Git patches for binary files that include the binary content* - i.e. patches containing a `GIT binary patch` line, as output by `git diff --binary` and by `git format-patch`. (Previously `isBinary` was only set when the patch contained a `Binary files ... differ` line, so a binary change in `git format-patch` output looked like a change with no content - or, for a newly-added binary file, like the creation of an empty file.) ## 9.0.0 diff --git a/src/patch/parse.ts b/src/patch/parse.ts index 16c1c625..5afa4305 100755 --- a/src/patch/parse.ts +++ b/src/patch/parse.ts @@ -106,7 +106,7 @@ export function parsePatch(uniDiff: string): StructuredPatch[] { // Consume Git extended headers (`old mode`, `new mode`, `rename from`, // `rename to`, `similarity index`, `index`, `Binary files ... differ`, - // etc.) + // `GIT binary patch` and the binary data that follows it, etc.) i++; while (i < diffstr.length) { const extLine = diffstr[i]; @@ -166,7 +166,10 @@ export function parsePatch(uniDiff: string): StructuredPatch[] { index.newMode = newModeMatch[1]; } - if ((/^Binary files /).test(extLine)) { + // Git indicates a binary file change either with a `Binary files ... differ` line + // or, when invoked with `--binary` (which `git format-patch` is by default), with a + // `GIT binary patch` line followed by the binary content in Git's own encoding. + if ((/^(?:Binary files |GIT binary patch)/).test(extLine)) { index.isBinary = true; } diff --git a/src/types.ts b/src/types.ts index b5a843d1..1750c1f2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -271,8 +271,9 @@ export interface StructuredPatch { newMode?: string, /** * Set to true when parsing a Git diff that includes a - * `Binary files ... differ` line, indicating a binary file change. - * Binary patches have no hunks, so the patch content alone is not + * `Binary files ... differ` line or a `GIT binary patch` line (as output by + * `git diff --binary` and `git format-patch`), indicating a binary file + * change. Binary patches have no hunks, so the patch content alone is not * sufficient to apply the change; consumers should handle this case * specially (e.g. by warning the user or fetching the binary content * separately). diff --git a/test/patch/parse.js b/test/patch/parse.js index 45b9b38e..27d87c06 100644 --- a/test/patch/parse.js +++ b/test/patch/parse.js @@ -1133,6 +1133,66 @@ Binary files a/image.png and b/image.png differ`)) }]); }); + it('should parse a `diff --git` binary file change in `GIT binary patch` form', function() { + // As output by `git diff --binary`, and by `git format-patch` by default + expect(parsePatch( +`diff --git a/image.png b/image.png +index f584f4041fdb85307f985f76fce8c128a0d12921..23753768bb600005216588c5e1daa8c86bd1143a 100644 +GIT binary patch +literal 7 +OcmeAS@N;KiVg>*RPXTlQ + +literal 6 +NcmeAS@N;Ki1ONuw0dN2S + +diff --git a/new.bin b/new.bin +new file mode 100644 +index 0000000000000000000000000000000000000000..ba01f6b05bdbb386b35f4d086e268c5d422cafb9 +GIT binary patch +literal 2 +JcmZSh4*&rH0RR91 + +literal 0 +HcmV?d00001 + +diff --git a/file.txt b/file.txt +index 3367afd..3e75765 100644 +--- a/file.txt ++++ b/file.txt +@@ -1 +1 @@ +-old ++new +`)) + .to.eql([{ + oldFileName: 'a/image.png', + newFileName: 'b/image.png', + isGit: true, + isBinary: true, + hunks: [] + }, { + oldFileName: 'a/new.bin', + newFileName: 'b/new.bin', + isGit: true, + isCreate: true, + newMode: '100644', + isBinary: true, + hunks: [] + }, { + oldFileName: 'a/file.txt', + oldHeader: '', + newFileName: 'b/file.txt', + newHeader: '', + isGit: true, + hunks: [ + { + oldStart: 1, oldLines: 1, + newStart: 1, newLines: 1, + lines: ['-old', '+new'] + } + ] + }]); + }); + it('should not lose files when a hunkless `diff --git` file is followed by one with hunks', function() { expect(parsePatch( `diff --git a/file1.txt b/file1.txt