From 24cc3065d59045a13461780f7f1f2eb06faaaa1f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Aug 2026 14:01:48 +0000 Subject: [PATCH 1/4] Initial plan From 291addd64dd8fa9606667453f43fcdb6d8b8c89a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Aug 2026 14:06:31 +0000 Subject: [PATCH 2/4] fix: exempt lines before no-newline marker from CR check in isUnix Co-authored-by: ExplodingCabbage <2358339+ExplodingCabbage@users.noreply.github.com> --- src/patch/line-endings.ts | 2 +- test/patch/line-endings.js | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/patch/line-endings.ts b/src/patch/line-endings.ts index d36c7a59..d6c15b72 100644 --- a/src/patch/line-endings.ts +++ b/src/patch/line-endings.ts @@ -55,7 +55,7 @@ export function isUnix(patch: StructuredPatch | StructuredPatch[]): boolean { return !patch.some( index => index.hunks.some( hunk => hunk.lines.some( - line => !line.startsWith('\\') && line.endsWith('\r') + (line, i) => !line.startsWith('\\') && line.endsWith('\r') && !hunk.lines[i + 1]?.startsWith('\\') ) ) ); diff --git a/test/patch/line-endings.js b/test/patch/line-endings.js index 19d318b8..3e7728a0 100644 --- a/test/patch/line-endings.js +++ b/test/patch/line-endings.js @@ -148,4 +148,19 @@ describe('isUnix', () => { ); expect(isUnix(patch)).to.equal(true); }); + + it('should still return true if only the last line in a file is missing a LF and there is a no newline at EOF indicator', () => { + const patch = parsePatch( + 'Index: test\n' + + '===================================================================\n' + + '--- test\theader1\n' + + '+++ test\theader2\n' + + '@@ -1,2 +1,3 @@\n' + + ' line2\n' + + ' line3\n' + + '+line4\r\n' + + '\\ No newline at end of file\n' + ); + expect(isUnix(patch)).to.equal(true); + }); }); From 0eaa834c1d4cd4ba7352032da79ebec45612f483 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Mon, 17 Aug 2026 21:45:34 +0100 Subject: [PATCH 3/4] Fix nonsensical test description --- test/patch/line-endings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/patch/line-endings.js b/test/patch/line-endings.js index 3e7728a0..7ac8650a 100644 --- a/test/patch/line-endings.js +++ b/test/patch/line-endings.js @@ -149,7 +149,7 @@ describe('isUnix', () => { expect(isUnix(patch)).to.equal(true); }); - it('should still return true if only the last line in a file is missing a LF and there is a no newline at EOF indicator', () => { + it('should still return true if only the last line in a file has a CR and there is a no newline at EOF indicator', () => { const patch = parsePatch( 'Index: test\n' + '===================================================================\n' From 396e86fdb9f81e95742ffc62c45fca45635df2a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Aug 2026 20:50:19 +0000 Subject: [PATCH 4/4] test: add applyPatch regression test for Unix patch with literal CR on CRLF source; update release notes Co-authored-by: ExplodingCabbage <2358339+ExplodingCabbage@users.noreply.github.com> --- release-notes.md | 1 + test/patch/apply.js | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/release-notes.md b/release-notes.md index c3d1df85..33ea391f 100644 --- a/release-notes.md +++ b/release-notes.md @@ -4,6 +4,7 @@ - [#697](https://github.com/kpdecker/jsdiff/pull/697) *`diffJson` now correctly handles JSON objects containing a key named `__proto__`*. (Previously, the returned diff would be as if the `__proto__` key did not exist on either of the objects being diffed.) - [#700](https://github.com/kpdecker/jsdiff/pull/700) *`diffJson` now correctly handles JSON objects containing a non-callable property named `toJSON`* - i.e. it gives such a property no special behaviour whatsoever, just as `JSON.stringify` doesn't. Previously, such properties caused an error to be thrown. (*Callable* `toJSON` properties continue to get the same special behaviour that `JSON.stringify` gives them.) +- [#701](https://github.com/kpdecker/jsdiff/pull/701) *`applyPatch` with `autoConvertLineEndings` on will no longer consider a stray `\r` character occurring at the end of a file without a terminating `\n` character to be a Windows line ending*, and so will no longer strip it when converting from Windows to Unix line endings or fail to apply a Unix-style patch to a Windows file when the patch introduces such a stray `\r`. ## 9.0.0 diff --git a/test/patch/apply.js b/test/patch/apply.js index c789488e..ec09c1d3 100755 --- a/test/patch/apply.js +++ b/test/patch/apply.js @@ -1422,6 +1422,19 @@ describe('patch/apply', function() { .to.equal(''); }); + it('should correctly apply a Unix patch whose final added line ends with a literal \\r (no newline at EOF) to a Windows file', () => { + // The patch is Unix-style (no \\r\\n line endings), but the added line's content ends with a + // literal '\\r' because the new file has no trailing newline. autoConvertLineEndings must + // recognise the patch as Unix (not Windows), convert it to match the CRLF source, and apply + // it correctly — without dropping the literal '\\r'. Previously, isUnix() returned false for + // such a patch, so no conversion was attempted and applyPatch returned false. + const oldFileUnix = 'line1\nline2\n'; + const newFileUnix = 'line1\nline3\r'; // final line has literal CR and no trailing newline + const patch = structuredPatch('test', 'test', oldFileUnix, newFileUnix, undefined, undefined, {context: 0}); + const oldFileWin = 'line1\r\nline2\r\n'; + expect(applyPatch(oldFileWin, patch)).to.equal('line1\r\nline3\r'); + }); + it('should automatically convert a patch with Unix file endings to Windows when patching a Windows file', () => { const oldFile = 'foo\r\nbar\r\nbaz\r\nqux\r\n'; const diffFile =