From 095339441a96053c91025aed1847a25e6b964f73 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 31 Dec 2025 14:31:45 +0000 Subject: [PATCH 1/6] Initial plan From e85b25ba5a9e0c032ad20e6255a3a518707ae892 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 31 Dec 2025 14:39:45 +0000 Subject: [PATCH 2/6] Add direct test coverage for formatPatch with headerOptions Co-authored-by: ExplodingCabbage <2358339+ExplodingCabbage@users.noreply.github.com> --- test/patch/create.js | 231 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) diff --git a/test/patch/create.js b/test/patch/create.js index 222e9b5f..a529ccb1 100644 --- a/test/patch/create.js +++ b/test/patch/create.js @@ -1047,5 +1047,236 @@ 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' + ] + } + ] + }; + + 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 Index line when oldFileName equals newFileName with INCLUDE_HEADERS', function() { + const sameFilePatch = { + oldFileName: 'samefile', + oldHeader: 'old-timestamp', + newFileName: 'samefile', + newHeader: 'new-timestamp', + hunks: [ + { + oldStart: 1, + oldLines: 1, + newStart: 1, + newLines: 1, + lines: [ + '-old line', + '+new line' + ] + } + ] + }; + const result = formatPatch(sameFilePatch, INCLUDE_HEADERS); + const expected = + 'Index: samefile\n' + + '===================================================================\n' + + '--- samefile\told-timestamp\n' + + '+++ samefile\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 patches = [ + { + 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'] + } + ] + } + ]; + const result = formatPatch(patches, 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 patches = [ + { + 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'] + } + ] + } + ]; + const result = formatPatch(patches, 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 work with array of patches and OMIT_HEADERS', function() { + const patches = [ + { + 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'] + } + ] + } + ]; + const result = formatPatch(patches, OMIT_HEADERS); + const expected = + '@@ -1,1 +1,1 @@\n' + + '-a\n' + + '+b\n' + + '\n' + + '@@ -1,1 +1,1 @@\n' + + '-x\n' + + '+y\n'; + expect(result).to.equal(expected); + }); + }); }); }); From 310e3de6696874fa8fb24594786f4029af3812ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 31 Dec 2025 14:41:56 +0000 Subject: [PATCH 3/6] Refactor: Extract common patch array to reduce duplication Co-authored-by: ExplodingCabbage <2358339+ExplodingCabbage@users.noreply.github.com> --- test/patch/create.js | 135 ++++++++++++------------------------------- 1 file changed, 36 insertions(+), 99 deletions(-) diff --git a/test/patch/create.js b/test/patch/create.js index a529ccb1..bbfe9781 100644 --- a/test/patch/create.js +++ b/test/patch/create.js @@ -1068,6 +1068,39 @@ describe('patch/create', function() { ] }; + 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 = @@ -1132,39 +1165,7 @@ describe('patch/create', function() { }); it('should work with array of patches and INCLUDE_HEADERS', function() { - const patches = [ - { - 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'] - } - ] - } - ]; - const result = formatPatch(patches, INCLUDE_HEADERS); + const result = formatPatch(patchArray, INCLUDE_HEADERS); const expected = 'Index: file1\n' + '===================================================================\n' + @@ -1185,39 +1186,7 @@ describe('patch/create', function() { }); it('should work with array of patches and FILE_HEADERS_ONLY', function() { - const patches = [ - { - 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'] - } - ] - } - ]; - const result = formatPatch(patches, FILE_HEADERS_ONLY); + const result = formatPatch(patchArray, FILE_HEADERS_ONLY); const expected = '--- file1\ttimestamp1\n' + '+++ file1\ttimestamp2\n' + @@ -1234,39 +1203,7 @@ describe('patch/create', function() { }); it('should work with array of patches and OMIT_HEADERS', function() { - const patches = [ - { - 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'] - } - ] - } - ]; - const result = formatPatch(patches, OMIT_HEADERS); + const result = formatPatch(patchArray, OMIT_HEADERS); const expected = '@@ -1,1 +1,1 @@\n' + '-a\n' + From 8ba3a8e1796cb151cd68b52ea5aee5017c482497 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Wed, 31 Dec 2025 14:54:28 +0000 Subject: [PATCH 4/6] Remove redundant test (covered entirely below) --- test/patch/create.js | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/test/patch/create.js b/test/patch/create.js index bbfe9781..1a1b2250 100644 --- a/test/patch/create.js +++ b/test/patch/create.js @@ -1113,37 +1113,6 @@ describe('patch/create', function() { expect(result).to.equal(expected); }); - it('should include Index line when oldFileName equals newFileName with INCLUDE_HEADERS', function() { - const sameFilePatch = { - oldFileName: 'samefile', - oldHeader: 'old-timestamp', - newFileName: 'samefile', - newHeader: 'new-timestamp', - hunks: [ - { - oldStart: 1, - oldLines: 1, - newStart: 1, - newLines: 1, - lines: [ - '-old line', - '+new line' - ] - } - ] - }; - const result = formatPatch(sameFilePatch, INCLUDE_HEADERS); - const expected = - 'Index: samefile\n' + - '===================================================================\n' + - '--- samefile\told-timestamp\n' + - '+++ samefile\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 = From 56916f6f33a8a12ddbd915f0e16382fc386bdfc4 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Wed, 31 Dec 2025 14:57:47 +0000 Subject: [PATCH 5/6] Make test for multiple patches with OMIT_HEADERS demand sensible behaviour (which we don't yet provide) --- test/patch/create.js | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/test/patch/create.js b/test/patch/create.js index 1a1b2250..204818d8 100644 --- a/test/patch/create.js +++ b/test/patch/create.js @@ -1171,17 +1171,9 @@ describe('patch/create', function() { expect(result).to.equal(expected); }); - it('should work with array of patches and OMIT_HEADERS', function() { - const result = formatPatch(patchArray, OMIT_HEADERS); - const expected = - '@@ -1,1 +1,1 @@\n' + - '-a\n' + - '+b\n' + - '\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(); }); }); }); From a0cf53608f48fcdb218830b43cca802800e300ea Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Wed, 31 Dec 2025 15:09:07 +0000 Subject: [PATCH 6/6] Fix behaviour --- src/patch/create.ts | 7 +++++++ 1 file changed, 7 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'); }