From d525fa0eff7ecf7fce9a3eb25b806e7ab53df1c0 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Wed, 31 Dec 2025 13:48:27 +0000 Subject: [PATCH 01/11] Add support for omitting headers --- src/index.ts | 8 +++- src/patch/create.ts | 47 +++++++++++++++---- test/patch/create.js | 109 +++++++++++++++++++++++++++++-------------- 3 files changed, 120 insertions(+), 44 deletions(-) diff --git a/src/index.ts b/src/index.ts index a21c29b5..d1743b6f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,7 +33,10 @@ import { structuredPatch, createTwoFilesPatch, createPatch, - formatPatch + formatPatch, + INCLUDE_HEADERS, + FILE_HEADERS_ONLY, + OMIT_HEADERS } from './patch/create.js'; import type { StructuredPatchOptionsAbortable, @@ -91,6 +94,9 @@ export { createTwoFilesPatch, createPatch, formatPatch, + INCLUDE_HEADERS, + FILE_HEADERS_ONLY, + OMIT_HEADERS, applyPatch, applyPatches, parsePatch, diff --git a/src/patch/create.ts b/src/patch/create.ts index 138f9544..49cf4a2e 100644 --- a/src/patch/create.ts +++ b/src/patch/create.ts @@ -4,6 +4,28 @@ import type { StructuredPatch, DiffLinesOptionsAbortable, DiffLinesOptionsNonabo type StructuredPatchCallbackAbortable = (patch: StructuredPatch | undefined) => void; type StructuredPatchCallbackNonabortable = (patch: StructuredPatch) => void; +export interface HeaderOptions { + includeIndex: boolean; + includeUnderline: boolean; + includeFileHeaders: boolean; +} + +export const INCLUDE_HEADERS = { + includeIndex: true, + includeUnderline: true, + includeFileHeaders: true +}; +export const FILE_HEADERS_ONLY = { + includeIndex: false, + includeUnderline: false, + includeFileHeaders: true +}; +export const OMIT_HEADERS = { + includeIndex: false, + includeUnderline: false, + includeFileHeaders: false +}; + interface _StructuredPatchOptionsAbortable extends Pick { /** * describes how many lines of context should be included. @@ -254,18 +276,25 @@ export function structuredPatch( * creates a unified diff patch. * @param patch either a single structured patch object (as returned by `structuredPatch`) or an array of them (as returned by `parsePatch`) */ -export function formatPatch(patch: StructuredPatch | StructuredPatch[]): string { +export function formatPatch(patch: StructuredPatch | StructuredPatch[], headerOptions?: HeaderOptions): string { + if (!headerOptions) { + headerOptions = INCLUDE_HEADERS; + } if (Array.isArray(patch)) { - return patch.map(formatPatch).join('\n'); + return patch.map(p => formatPatch(p, headerOptions)).join('\n'); } const ret = []; - if (patch.oldFileName == patch.newFileName) { + if (headerOptions.includeIndex && patch.oldFileName == patch.newFileName) { ret.push('Index: ' + patch.oldFileName); } - ret.push('==================================================================='); - ret.push('--- ' + patch.oldFileName + (typeof patch.oldHeader === 'undefined' ? '' : '\t' + patch.oldHeader)); - ret.push('+++ ' + patch.newFileName + (typeof patch.newHeader === 'undefined' ? '' : '\t' + patch.newHeader)); + if (headerOptions.includeUnderline) { + ret.push('==================================================================='); + } + if (headerOptions.includeFileHeaders) { + ret.push('--- ' + patch.oldFileName + (typeof patch.oldHeader === 'undefined' ? '' : '\t' + patch.oldHeader)); + ret.push('+++ ' + patch.newFileName + (typeof patch.newHeader === 'undefined' ? '' : '\t' + patch.newHeader)); + } for (let i = 0; i < patch.hunks.length; i++) { const hunk = patch.hunks[i]; @@ -297,11 +326,13 @@ type CreatePatchCallbackNonabortable = (patch: string) => void; interface _CreatePatchOptionsAbortable extends Pick { context?: number, callback?: CreatePatchCallbackAbortable, + headerOptions?: HeaderOptions, } export type CreatePatchOptionsAbortable = _CreatePatchOptionsAbortable & AbortableDiffOptions; export interface CreatePatchOptionsNonabortable extends Pick { context?: number, callback?: CreatePatchCallbackNonabortable, + headerOptions?: HeaderOptions, } interface CreatePatchCallbackOptionAbortable { callback: CreatePatchCallbackAbortable; @@ -382,7 +413,7 @@ export function createTwoFilesPatch( if (!patchObj) { return; } - return formatPatch(patchObj); + return formatPatch(patchObj, options?.headerOptions); } else { const {callback} = options; structuredPatch( @@ -398,7 +429,7 @@ export function createTwoFilesPatch( if (!patchObj) { (callback as CreatePatchCallbackAbortable)(undefined); } else { - callback(formatPatch(patchObj)); + callback(formatPatch(patchObj, options.headerOptions)); } } } diff --git a/test/patch/create.js b/test/patch/create.js index 862b3b7b..222e9b5f 100644 --- a/test/patch/create.js +++ b/test/patch/create.js @@ -1,5 +1,5 @@ import {diffWords} from 'diff'; -import {createPatch, createTwoFilesPatch, formatPatch, structuredPatch} from '../../libesm/patch/create.js'; +import {createPatch, createTwoFilesPatch, FILE_HEADERS_ONLY, formatPatch, INCLUDE_HEADERS, OMIT_HEADERS, structuredPatch} from '../../libesm/patch/create.js'; import {parsePatch} from '../../libesm/patch/parse.js'; import {expect} from 'chai'; @@ -628,43 +628,82 @@ describe('patch/create', function() { expect(diffResult).to.equal(expectedResult); }); - it('should output headers only for identical files', function() { - const expectedResult = - 'Index: testFileName\n' - + '===================================================================\n' - + '--- testFileName\tOld Header\n' - + '+++ testFileName\tNew Header\n'; - const diffResult = createPatch('testFileName', oldFile, oldFile, 'Old Header', 'New Header'); - expect(diffResult).to.equal(expectedResult); - }); + describe('headers handling', function() { + it('should output headers only for identical files', function() { + const expectedResult = + 'Index: testFileName\n' + + '===================================================================\n' + + '--- testFileName\tOld Header\n' + + '+++ testFileName\tNew Header\n'; + const diffResult = createPatch('testFileName', oldFile, oldFile, 'Old Header', 'New Header'); + expect(diffResult).to.equal(expectedResult); + }); - it('should omit headers if undefined', function() { - const expectedResult = - 'Index: testFileName\n' - + '===================================================================\n' - + '--- testFileName\n' - + '+++ testFileName\n'; - const diffResult = createPatch('testFileName', oldFile, oldFile); - expect(diffResult).to.equal(expectedResult); - }); + it('should omit headers if undefined', function() { + const expectedResult = + 'Index: testFileName\n' + + '===================================================================\n' + + '--- testFileName\n' + + '+++ testFileName\n'; + const diffResult = createPatch('testFileName', oldFile, oldFile); + expect(diffResult).to.equal(expectedResult); + }); - it('should safely handle empty inputs', function() { - const expectedResult = - 'Index: testFileName\n' - + '===================================================================\n' - + '--- testFileName\n' - + '+++ testFileName\n'; - const diffResult = createPatch('testFileName', '', ''); - expect(diffResult).to.equal(expectedResult); - }); + it('should safely handle empty inputs', function() { + const expectedResult = + 'Index: testFileName\n' + + '===================================================================\n' + + '--- testFileName\n' + + '+++ testFileName\n'; + const diffResult = createPatch('testFileName', '', ''); + expect(diffResult).to.equal(expectedResult); + }); - it('should omit index with multiple file names', function() { - const expectedResult = - '===================================================================\n' - + '--- foo\n' - + '+++ bar\n'; - const diffResult = createTwoFilesPatch('foo', 'bar', '', ''); - expect(diffResult).to.equal(expectedResult); + it('should omit index with multiple file names', function() { + const expectedResult = + '===================================================================\n' + + '--- foo\n' + + '+++ bar\n'; + const diffResult = createTwoFilesPatch('foo', 'bar', '', ''); + expect(diffResult).to.equal(expectedResult); + }); + + it('should handle INCLUDE_HEADERS the same as not specifying options regarding headers', function() { + const expectedResult = + 'Index: testFileName\n' + + '===================================================================\n' + + '--- testFileName\n' + + '+++ testFileName\n' + + '@@ -1,1 +1,1 @@\n' + + '-foo\n' + + '+bar\n'; + const diffResult1 = createTwoFilesPatch('testFileName', 'testFileName', 'foo\n', 'bar\n'); + const diffResult2 = createTwoFilesPatch('testFileName', 'testFileName', 'foo\n', 'bar\n', undefined, undefined, {}); + const diffResult3 = createTwoFilesPatch('testFileName', 'testFileName', 'foo\n', 'bar\n', undefined, undefined, { headerOptions: INCLUDE_HEADERS }); + expect(diffResult1).to.equal(expectedResult); + expect(diffResult2).to.equal(expectedResult); + expect(diffResult3).to.equal(expectedResult); + }); + + it('should respect FILE_HEADERS_ONLY', function() { + const expectedResult = + '--- testFileName\n' + + '+++ testFileName\n' + + '@@ -1,1 +1,1 @@\n' + + '-foo\n' + + '+bar\n'; + const diffResult = createTwoFilesPatch('testFileName', 'testFileName', 'foo\n', 'bar\n', undefined, undefined, {headerOptions: FILE_HEADERS_ONLY}); + expect(diffResult).to.equal(expectedResult); + }); + + it('should respect OMIT_HEADERS', function() { + const expectedResult = + '@@ -1,1 +1,1 @@\n' + + '-foo\n' + + '+bar\n'; + const diffResult = createTwoFilesPatch('testFileName', 'testFileName', 'foo\n', 'bar\n', undefined, undefined, {headerOptions: OMIT_HEADERS}); + expect(diffResult).to.equal(expectedResult); + }); }); it('should respect maxEditLength', function() { From 5b81ebf7474dad6ed9c8277eb7aee926a2833a47 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Wed, 31 Dec 2025 14:12:54 +0000 Subject: [PATCH 02/11] Fix some wonky pre-existing formatting --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 8d650fc3..4a09989a 100644 --- a/README.md +++ b/README.md @@ -108,14 +108,14 @@ jsdiff's diff functions all take an old text and a new text and perform three st * `createTwoFilesPatch(oldFileName, newFileName, oldStr, newStr[, oldHeader[, newHeader[, options]]])` - creates a unified diff patch by first computing a diff with `diffLines` and then serializing it to unified diff format. Parameters: - * `oldFileName` : String to be output in the filename section of the patch for the removals - * `newFileName` : String to be output in the filename section of the patch for the additions - * `oldStr` : Original string value - * `newStr` : New string value - * `oldHeader` : Optional additional information to include in the old file header. Default: `undefined`. - * `newHeader` : Optional additional information to include in the new file header. Default: `undefined`. - * `options` : An object with options. - - `context` describes how many lines of context should be included. You can set this to `Number.MAX_SAFE_INTEGER` or `Infinity` to include the entire file content in one hunk. + * `oldFileName`: String to be output in the filename section of the patch for the removals + * `newFileName`: String to be output in the filename section of the patch for the additions + * `oldStr`: Original string value + * `newStr`: New string value + * `oldHeader`: Optional additional information to include in the old file header. Default: `undefined`. + * `newHeader`: Optional additional information to include in the new file header. Default: `undefined`. + * `options`: An object with options. + - `context`: describes how many lines of context should be included. You can set this to `Number.MAX_SAFE_INTEGER` or `Infinity` to include the entire file content in one hunk. - `ignoreWhitespace`: Same as in `diffLines`. Defaults to `false`. - `stripTrailingCr`: Same as in `diffLines`. Defaults to `false`. From 5d468e7bf2765725ff46dd56d5d65f31c6a0c11e Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Wed, 31 Dec 2025 14:13:01 +0000 Subject: [PATCH 03/11] Document new options --- README.md | 8 ++++++++ release-notes.md | 1 + 2 files changed, 9 insertions(+) diff --git a/README.md b/README.md index 4a09989a..3f022dda 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,14 @@ jsdiff's diff functions all take an old text and a new text and perform three st - `context`: describes how many lines of context should be included. You can set this to `Number.MAX_SAFE_INTEGER` or `Infinity` to include the entire file content in one hunk. - `ignoreWhitespace`: Same as in `diffLines`. Defaults to `false`. - `stripTrailingCr`: Same as in `diffLines`. Defaults to `false`. + - `headerOptions`: Configures the format of patch headers in the returned patch. (Note these are distinct from *hunk* headers, which are a mandatory part of the unified diff format and not configurable.) Has three subfields (all default to `true`): + - `includeIndex`: whether to include a line like `Index: filename.txt` at the start of the patch header. (Even if this is `true`, this line will be omitted if `oldFileName` and `newFileName` are not identical.) + - `includeUnderline`: whether to include `===================================================================`. + - `includeFileHeaders`: whether to include two lines indicating the old and new filename, formatted like `--- old.txt` & `+++ new.txt`. + + Note further that jsdiff exports three top-level constants that can be used as `headerOptions` values, named `INCLUDE_HEADERS` (the default), `FILE_HEADERS_ONLY`, and `OMIT_HEADERS`. + + The Unix `patch` util will accept patches regardless of these header options (and refers to them as "leading garbage", which in typical usage it makes no attempt to parse or use in any way). However, other tools for working with unified diff format patches may be less liberal (and are not unambiguously wrong to be so, since the format has no rigorous standard). Tinkering with the `includeIndex` setting thus provides a way to help make patches produced by jsdiff compatible with other tools. * `createPatch(fileName, oldStr, newStr[, oldHeader[, newHeader[, options]]])` - creates a unified diff patch. diff --git a/release-notes.md b/release-notes.md index 2bb2108f..67e37513 100644 --- a/release-notes.md +++ b/release-notes.md @@ -4,6 +4,7 @@ - [#631](https://github.com/kpdecker/jsdiff/pull/631) - **fix support for using an `Intl.Segmenter` with `diffWords`**. This has been almost completely broken since the feature was added in v6.0.0, since it would outright crash on any text that featured two consecutive newlines between a pair of words (a very common case). - [#635](https://github.com/kpdecker/jsdiff/pull/635) - **small tweaks to tokenization behaviour of `diffWords`** when used *without* an `Intl.Segmenter`. Specifically, the soft hyphen (U+00AD) is no longer considered to be a word break, and the multiplication and division signs (`×` and `÷`) are now treated as punctuation instead of as letters / word characters. +- [#641](https://github.com/kpdecker/jsdiff/pull/641) - **the format of file headers in `createPatch` etc. patches can now be customised somewhat**. It now takes a `headerOptions` option that can be used to disable the file headers entirely, or omit the `Index:` line and/or the underline. ## 8.0.2 From 955a95ed47c3abfbcdc2894b96620e0d53332d95 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Wed, 31 Dec 2025 14:16:54 +0000 Subject: [PATCH 04/11] Proofread & tweak --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3f022dda..9afaa13a 100644 --- a/README.md +++ b/README.md @@ -121,11 +121,13 @@ jsdiff's diff functions all take an old text and a new text and perform three st - `headerOptions`: Configures the format of patch headers in the returned patch. (Note these are distinct from *hunk* headers, which are a mandatory part of the unified diff format and not configurable.) Has three subfields (all default to `true`): - `includeIndex`: whether to include a line like `Index: filename.txt` at the start of the patch header. (Even if this is `true`, this line will be omitted if `oldFileName` and `newFileName` are not identical.) - `includeUnderline`: whether to include `===================================================================`. - - `includeFileHeaders`: whether to include two lines indicating the old and new filename, formatted like `--- old.txt` & `+++ new.txt`. + - `includeFileHeaders`: whether to include two lines indicating the old and new filename, formatted like `--- old.txt` and `+++ new.txt`. Note further that jsdiff exports three top-level constants that can be used as `headerOptions` values, named `INCLUDE_HEADERS` (the default), `FILE_HEADERS_ONLY`, and `OMIT_HEADERS`. - The Unix `patch` util will accept patches regardless of these header options (and refers to them as "leading garbage", which in typical usage it makes no attempt to parse or use in any way). However, other tools for working with unified diff format patches may be less liberal (and are not unambiguously wrong to be so, since the format has no rigorous standard). Tinkering with the `includeIndex` setting thus provides a way to help make patches produced by jsdiff compatible with other tools. + (Note that in the case where `includeIndex` and `includeFileHeaders` are both false, the `oldFileName` and `newFileName` parameters are ignored entirely.) + + The Unix `patch` util will accept patches produced with any configuration of these header options (and refers to patch headers as "leading garbage", which in typical usage it makes no attempt to parse or use in any way). However, other tools for working with unified diff format patches may be less liberal (and are not unambiguously wrong to be so, since the format has no rigorous standard). Tinkering with the `includeIndex` setting thus provides a way to help make patches produced by jsdiff compatible with other tools. * `createPatch(fileName, oldStr, newStr[, oldHeader[, newHeader[, options]]])` - creates a unified diff patch. From f28365c8b8a39415d1a49881855578296349fdb6 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Wed, 31 Dec 2025 14:17:25 +0000 Subject: [PATCH 05/11] Further proofread --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9afaa13a..fac5ff9e 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ jsdiff's diff functions all take an old text and a new text and perform three st (Note that in the case where `includeIndex` and `includeFileHeaders` are both false, the `oldFileName` and `newFileName` parameters are ignored entirely.) - The Unix `patch` util will accept patches produced with any configuration of these header options (and refers to patch headers as "leading garbage", which in typical usage it makes no attempt to parse or use in any way). However, other tools for working with unified diff format patches may be less liberal (and are not unambiguously wrong to be so, since the format has no rigorous standard). Tinkering with the `includeIndex` setting thus provides a way to help make patches produced by jsdiff compatible with other tools. + The Unix `patch` util will accept patches produced with any configuration of these header options (and refers to patch headers as "leading garbage", which in typical usage it makes no attempt to parse or use in any way). However, other tools for working with unified diff format patches may be less liberal (and are not unambiguously wrong to be so, since the format has no rigorous standard). Tinkering with the `headerOptions` setting thus provides a way to help make patches produced by jsdiff compatible with other tools. * `createPatch(fileName, oldStr, newStr[, oldHeader[, newHeader[, options]]])` - creates a unified diff patch. From 5cf92428e45022e615a6c4f3a29dba70b6eadb94 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Wed, 31 Dec 2025 14:18:26 +0000 Subject: [PATCH 06/11] Another tweak - I guess not every Unix machine uses GNU patch, and I am quoting from the docs for the GNU version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fac5ff9e..c7fc52cd 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ jsdiff's diff functions all take an old text and a new text and perform three st (Note that in the case where `includeIndex` and `includeFileHeaders` are both false, the `oldFileName` and `newFileName` parameters are ignored entirely.) - The Unix `patch` util will accept patches produced with any configuration of these header options (and refers to patch headers as "leading garbage", which in typical usage it makes no attempt to parse or use in any way). However, other tools for working with unified diff format patches may be less liberal (and are not unambiguously wrong to be so, since the format has no rigorous standard). Tinkering with the `headerOptions` setting thus provides a way to help make patches produced by jsdiff compatible with other tools. + The GNU `patch` util will accept patches produced with any configuration of these header options (and refers to patch headers as "leading garbage", which in typical usage it makes no attempt to parse or use in any way). However, other tools for working with unified diff format patches may be less liberal (and are not unambiguously wrong to be so, since the format has no rigorous standard). Tinkering with the `headerOptions` setting thus provides a way to help make patches produced by jsdiff compatible with other tools. * `createPatch(fileName, oldStr, newStr[, oldHeader[, newHeader[, options]]])` - creates a unified diff patch. From a804a0a701e6d28bc388bf4fb69c63f8104aeb27 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Wed, 31 Dec 2025 14:19:32 +0000 Subject: [PATCH 07/11] Language tweak --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c7fc52cd..44933552 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ jsdiff's diff functions all take an old text and a new text and perform three st (Note that in the case where `includeIndex` and `includeFileHeaders` are both false, the `oldFileName` and `newFileName` parameters are ignored entirely.) - The GNU `patch` util will accept patches produced with any configuration of these header options (and refers to patch headers as "leading garbage", which in typical usage it makes no attempt to parse or use in any way). However, other tools for working with unified diff format patches may be less liberal (and are not unambiguously wrong to be so, since the format has no rigorous standard). Tinkering with the `headerOptions` setting thus provides a way to help make patches produced by jsdiff compatible with other tools. + The GNU `patch` util will accept patches produced with any configuration of these header options (and refers to patch headers as "leading garbage", which in typical usage it makes no attempt to parse or use in any way). However, other tools for working with unified diff format patches may be less liberal (and are not unambiguously wrong to be so, since the format has no real standard). Tinkering with the `headerOptions` setting thus provides a way to help make patches produced by jsdiff compatible with other tools. * `createPatch(fileName, oldStr, newStr[, oldHeader[, newHeader[, options]]])` - creates a unified diff patch. From e214131f330275a1c28e986b4bbc96970357edf7 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Wed, 31 Dec 2025 14:23:54 +0000 Subject: [PATCH 08/11] More detail --- release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-notes.md b/release-notes.md index 67e37513..815fbc9f 100644 --- a/release-notes.md +++ b/release-notes.md @@ -4,7 +4,7 @@ - [#631](https://github.com/kpdecker/jsdiff/pull/631) - **fix support for using an `Intl.Segmenter` with `diffWords`**. This has been almost completely broken since the feature was added in v6.0.0, since it would outright crash on any text that featured two consecutive newlines between a pair of words (a very common case). - [#635](https://github.com/kpdecker/jsdiff/pull/635) - **small tweaks to tokenization behaviour of `diffWords`** when used *without* an `Intl.Segmenter`. Specifically, the soft hyphen (U+00AD) is no longer considered to be a word break, and the multiplication and division signs (`×` and `÷`) are now treated as punctuation instead of as letters / word characters. -- [#641](https://github.com/kpdecker/jsdiff/pull/641) - **the format of file headers in `createPatch` etc. patches can now be customised somewhat**. It now takes a `headerOptions` option that can be used to disable the file headers entirely, or omit the `Index:` line and/or the underline. +- [#641](https://github.com/kpdecker/jsdiff/pull/641) - **the format of file headers in `createPatch` etc. patches can now be customised somewhat**. It now takes a `headerOptions` option that can be used to disable the file headers entirely, or omit the `Index:` line and/or the underline. In particular, this was motivated by a request to make jsdiff patches compatible with react-diff-view, which they now are if produced with `headerOptions: FILE_HEADERS_ONLY`. ## 8.0.2 From ce1998d3da0c02796fa269c66e847376cf3ae44f Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Wed, 31 Dec 2025 14:41:49 +0000 Subject: [PATCH 09/11] Export HeaderOptions interface --- src/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index d1743b6f..526f3a13 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,7 +42,8 @@ import type { StructuredPatchOptionsAbortable, StructuredPatchOptionsNonabortable, CreatePatchOptionsAbortable, - CreatePatchOptionsNonabortable + CreatePatchOptionsNonabortable, + HeaderOptions } from './patch/create.js'; import {convertChangesToDMP} from './convert/dmp.js'; @@ -133,5 +134,6 @@ export type { StructuredPatchOptionsAbortable, StructuredPatchOptionsNonabortable, CreatePatchOptionsAbortable, - CreatePatchOptionsNonabortable + CreatePatchOptionsNonabortable, + HeaderOptions }; From 2943b63c7dc2461d6027461c4f718fc5271498c6 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Wed, 31 Dec 2025 14:49:26 +0000 Subject: [PATCH 10/11] Fix docs gap --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 44933552..8d7db61b 100644 --- a/README.md +++ b/README.md @@ -133,9 +133,9 @@ jsdiff's diff functions all take an old text and a new text and perform three st Just like createTwoFilesPatch, but with oldFileName being equal to newFileName. -* `formatPatch(patch)` - creates a unified diff patch. +* `formatPatch(patch[, headerOptions])` - creates a unified diff patch. - `patch` may be either a single structured patch object (as returned by `structuredPatch`) or an array of them (as returned by `parsePatch`). + `patch` may be either a single structured patch object (as returned by `structuredPatch`) or an array of them (as returned by `parsePatch`). The optional `headerOptions` argument behaves the same as the `headerOptions` option of `createTwoFilesPatch`. * `structuredPatch(oldFileName, newFileName, oldStr, newStr[, oldHeader[, newHeader[, options]]])` - returns an object with an array of hunk objects. From 537210e328775e9b7f01c4559981198126d8e250 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 31 Dec 2025 15:09:48 +0000 Subject: [PATCH 11/11] Add direct test coverage for formatPatch with headerOptions parameter (#642) * Initial plan * Add direct test coverage for formatPatch with headerOptions Co-authored-by: ExplodingCabbage <2358339+ExplodingCabbage@users.noreply.github.com> * Refactor: Extract common patch array to reduce duplication Co-authored-by: ExplodingCabbage <2358339+ExplodingCabbage@users.noreply.github.com> * Remove redundant test (covered entirely below) * Make test for multiple patches with OMIT_HEADERS demand sensible behaviour (which we don't yet provide) * Fix behaviour --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ExplodingCabbage <2358339+ExplodingCabbage@users.noreply.github.com> Co-authored-by: Mark Amery --- src/patch/create.ts | 7 +++ test/patch/create.js | 129 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) diff --git a/src/patch/create.ts b/src/patch/create.ts index 49cf4a2e..13c2a2b1 100644 --- a/src/patch/create.ts +++ b/src/patch/create.ts @@ -281,6 +281,13 @@ export function formatPatch(patch: StructuredPatch | StructuredPatch[], headerOp headerOptions = INCLUDE_HEADERS; } if (Array.isArray(patch)) { + if (patch.length > 1 && !headerOptions.includeFileHeaders) { + throw new Error( + 'Cannot omit file headers on a multi-file patch. ' + + '(The result would be unparseable; how would a tool trying to apply ' + + 'the patch know which changes are to which file?)' + ); + } return patch.map(p => formatPatch(p, headerOptions)).join('\n'); } diff --git a/test/patch/create.js b/test/patch/create.js index 222e9b5f..204818d8 100644 --- a/test/patch/create.js +++ b/test/patch/create.js @@ -1047,5 +1047,134 @@ describe('patch/create', function() { expect(roundTrippedPatch).to.deep.equal([patchObj]); }); + + describe('with headerOptions parameter', function() { + const patch = { + oldFileName: 'oldfile', + oldHeader: 'old-timestamp', + newFileName: 'newfile', + newHeader: 'new-timestamp', + hunks: [ + { + oldStart: 1, + oldLines: 1, + newStart: 1, + newLines: 1, + lines: [ + '-old line', + '+new line' + ] + } + ] + }; + + const patchArray = [ + { + oldFileName: 'file1', + oldHeader: 'timestamp1', + newFileName: 'file1', + newHeader: 'timestamp2', + hunks: [ + { + oldStart: 1, + oldLines: 1, + newStart: 1, + newLines: 1, + lines: ['-a', '+b'] + } + ] + }, + { + oldFileName: 'file2', + oldHeader: 'timestamp3', + newFileName: 'file2', + newHeader: 'timestamp4', + hunks: [ + { + oldStart: 1, + oldLines: 1, + newStart: 1, + newLines: 1, + lines: ['-x', '+y'] + } + ] + } + ]; + + it('should include all headers with INCLUDE_HEADERS', function() { + const result = formatPatch(patch, INCLUDE_HEADERS); + const expected = + '===================================================================\n' + + '--- oldfile\told-timestamp\n' + + '+++ newfile\tnew-timestamp\n' + + '@@ -1,1 +1,1 @@\n' + + '-old line\n' + + '+new line\n'; + expect(result).to.equal(expected); + }); + + it('should include only file headers with FILE_HEADERS_ONLY', function() { + const result = formatPatch(patch, FILE_HEADERS_ONLY); + const expected = + '--- oldfile\told-timestamp\n' + + '+++ newfile\tnew-timestamp\n' + + '@@ -1,1 +1,1 @@\n' + + '-old line\n' + + '+new line\n'; + expect(result).to.equal(expected); + }); + + it('should omit all headers with OMIT_HEADERS', function() { + const result = formatPatch(patch, OMIT_HEADERS); + const expected = + '@@ -1,1 +1,1 @@\n' + + '-old line\n' + + '+new line\n'; + expect(result).to.equal(expected); + }); + + it('should work with array of patches and INCLUDE_HEADERS', function() { + const result = formatPatch(patchArray, INCLUDE_HEADERS); + const expected = + 'Index: file1\n' + + '===================================================================\n' + + '--- file1\ttimestamp1\n' + + '+++ file1\ttimestamp2\n' + + '@@ -1,1 +1,1 @@\n' + + '-a\n' + + '+b\n' + + '\n' + + 'Index: file2\n' + + '===================================================================\n' + + '--- file2\ttimestamp3\n' + + '+++ file2\ttimestamp4\n' + + '@@ -1,1 +1,1 @@\n' + + '-x\n' + + '+y\n'; + expect(result).to.equal(expected); + }); + + it('should work with array of patches and FILE_HEADERS_ONLY', function() { + const result = formatPatch(patchArray, FILE_HEADERS_ONLY); + const expected = + '--- file1\ttimestamp1\n' + + '+++ file1\ttimestamp2\n' + + '@@ -1,1 +1,1 @@\n' + + '-a\n' + + '+b\n' + + '\n' + + '--- file2\ttimestamp3\n' + + '+++ file2\ttimestamp4\n' + + '@@ -1,1 +1,1 @@\n' + + '-x\n' + + '+y\n'; + expect(result).to.equal(expected); + }); + + it('should throw an error when given an array of patches and OMIT_HEADERS', function() { + // eslint-disable-next-line dot-notation + expect(() => formatPatch(patchArray, OMIT_HEADERS)).to.throw(); + }); + }); }); });