Skip to content

Commit c8a9cc5

Browse files
Remove linedelimiters, improve handling of Windows vs Unix line endings (#518)
* Remove linedelimiters * Add a util for converting patches between Windows & Unix line endings * Add some more utils * Remove lingering linedelimiters references * Fix linting errors * Add patch line ending autoconversion and tests thereof * Document autoConvertLineEndings * Add release notes * Add a failing test I need to fix * Add stubs of more necessary tests * More tests, fix a test name * Make a test pass; fix some test names * Get tests passing * Improve coverage * Add another test to get over the coverage threshold * Remove meaningless/inappropriate 'Create patch' comment - both from original location and places I dumbly copied it to
1 parent 4ebc4bf commit c8a9cc5

11 files changed

+319
-108
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ Broadly, jsdiff's diff functions all take an old text and a new text and perform
125125
The optional `options` object may have the following keys:
126126
127127
- `fuzzFactor`: Number of lines that are allowed to differ before rejecting a patch. Defaults to 0.
128+
- `autoConvertLineEndings`: If `true`, and if the file to be patched consistently uses different line endings to the patch (i.e. either the file always uses Unix line endings while the patch uses Windows ones, or vice versa), then `applyPatch` will behave as if the line endings in the patch were the same as those in the source file. (If `false`, the patch will usually fail to apply in such circumstances since lines deleted in the patch won't be considered to match those in the source file.) Defaults to `true`.
128129
- `compareLine(lineNumber, line, operation, patchContent)`: Callback used to compare to given lines to determine if they should be considered equal when patching. Defaults to strict equality but may be overridden to provide fuzzier comparison. Should return false if the lines should be rejected.
129130

130131
* `Diff.applyPatches(patch, options)` - applies one or more patches.

release-notes.md

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
- [#486](https://github.com/kpdecker/jsdiff/pull/486) **The `ignoreWhitespace` option of `diffLines` behaves more sensibly now.** `value`s in returned change objects now include leading/trailing whitespace even when `ignoreWhitespace` is used, just like how with `ignoreCase` the `value`s still reflect the case of one of the original texts instead of being all-lowercase. `ignoreWhitespace` is also now compatible with `newlineIsToken`. Finally, **`diffTrimmedLines` is deprecated** (and removed from the docs) in favour of using `diffLines` with `ignoreWhitespace: true`; the two are, and always have been, equivalent.
2525
- [#490](https://github.com/kpdecker/jsdiff/pull/490) **When calling diffing functions in async mode by passing a `callback` option, the diff result will now be passed as the *first* argument to the callback instead of the second.** (Previously, the first argument was never used at all and would always have value `undefined`.)
2626
- [#489](github.com/kpdecker/jsdiff/pull/489) **`this.options` no longer exists on `Diff` objects.** Instead, `options` is now passed as an argument to methods that rely on options, like `equals(left, right, options)`. This fixes a race condition in async mode, where diffing behaviour could be changed mid-execution if a concurrent usage of the same `Diff` instances overwrote its `options`.
27+
- [#518](https://github.com/kpdecker/jsdiff/pull/518) **`linedelimiters` no longer exists** on patch objects; instead, when a patch with Windows-style CRLF line endings is parsed, **the lines in `lines` will end with `\r`**. There is now a **new `autoConvertLineEndings` option, on by default**, which makes it so that when a patch with Windows-style line endings is applied to a source file with Unix style line endings, the patch gets autoconverted to use Unix-style line endings, and when a patch with Unix-style line endings is applied to a source file with Windows-style line endings, it gets autoconverted to use Windows-style line endings.
2728

2829
## v5.2.0
2930

src/patch/apply.js

+13-12
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {hasOnlyWinLineEndings, hasOnlyUnixLineEndings} from '../util/string';
2+
import {isWin, isUnix, unixToWin, winToUnix} from './line-endings';
13
import {parsePatch} from './parse';
24
import distanceIterator from '../util/distance-iterator';
35

@@ -14,9 +16,16 @@ export function applyPatch(source, uniDiff, options = {}) {
1416
uniDiff = uniDiff[0];
1517
}
1618

19+
if (options.autoConvertLineEndings || options.autoConvertLineEndings == null) {
20+
if (hasOnlyWinLineEndings(source) && isUnix(uniDiff)) {
21+
uniDiff = unixToWin(uniDiff);
22+
} else if (hasOnlyUnixLineEndings(source) && isWin(uniDiff)) {
23+
uniDiff = winToUnix(uniDiff);
24+
}
25+
}
26+
1727
// Apply the diff to the input
18-
let lines = source.split(/\r?\n/),
19-
delimiters = source.match(/\r?\n/g) || [],
28+
let lines = source.split('\n'),
2029
hunks = uniDiff.hunks,
2130

2231
compareLine = options.compareLine || ((lineNumber, line, operation, patchContent) => line === patchContent),
@@ -88,18 +97,15 @@ export function applyPatch(source, uniDiff, options = {}) {
8897
for (let j = 0; j < hunk.lines.length; j++) {
8998
let line = hunk.lines[j],
9099
operation = (line.length > 0 ? line[0] : ' '),
91-
content = (line.length > 0 ? line.substr(1) : line),
92-
delimiter = hunk.linedelimiters && hunk.linedelimiters[j] || '\n';
100+
content = (line.length > 0 ? line.substr(1) : line);
93101

94102
if (operation === ' ') {
95103
toPos++;
96104
} else if (operation === '-') {
97105
lines.splice(toPos, 1);
98-
delimiters.splice(toPos, 1);
99106
/* istanbul ignore else */
100107
} else if (operation === '+') {
101108
lines.splice(toPos, 0, content);
102-
delimiters.splice(toPos, 0, delimiter);
103109
toPos++;
104110
} else if (operation === '\\') {
105111
let previousOperation = hunk.lines[j - 1] ? hunk.lines[j - 1][0] : null;
@@ -116,16 +122,11 @@ export function applyPatch(source, uniDiff, options = {}) {
116122
if (removeEOFNL) {
117123
while (!lines[lines.length - 1]) {
118124
lines.pop();
119-
delimiters.pop();
120125
}
121126
} else if (addEOFNL) {
122127
lines.push('');
123-
delimiters.push('\n');
124-
}
125-
for (let _k = 0; _k < lines.length - 1; _k++) {
126-
lines[_k] = lines[_k] + delimiters[_k];
127128
}
128-
return lines.join('');
129+
return lines.join('\n');
129130
}
130131

131132
// Wrapper that supports multiple file patches via callbacks.

src/patch/line-endings.js

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
export function unixToWin(patch) {
2+
if (Array.isArray(patch)) {
3+
return patch.map(unixToWin);
4+
}
5+
6+
return {
7+
...patch,
8+
hunks: patch.hunks.map(hunk => ({
9+
...hunk,
10+
lines: hunk.lines.map(
11+
(line, i) =>
12+
(line.startsWith('\\') || line.endsWith('\r') || hunk.lines[i + 1]?.startsWith('\\'))
13+
? line
14+
: line + '\r'
15+
)
16+
}))
17+
};
18+
}
19+
20+
export function winToUnix(patch) {
21+
if (Array.isArray(patch)) {
22+
return patch.map(winToUnix);
23+
}
24+
25+
return {
26+
...patch,
27+
hunks: patch.hunks.map(hunk => ({
28+
...hunk,
29+
lines: hunk.lines.map(line => line.endsWith('\r') ? line.substring(0, line.length - 1) : line)
30+
}))
31+
};
32+
}
33+
34+
/**
35+
* Returns true if the patch consistently uses Unix line endings (or only involves one line and has
36+
* no line endings).
37+
*/
38+
export function isUnix(patch) {
39+
if (!Array.isArray(patch)) { patch = [patch]; }
40+
return !patch.some(
41+
index => index.hunks.some(
42+
hunk => hunk.lines.some(
43+
line => !line.startsWith('\\') && line.endsWith('\r')
44+
)
45+
)
46+
);
47+
}
48+
49+
/**
50+
* Returns true if the patch uses Windows line endings and only Windows line endings.
51+
*/
52+
export function isWin(patch) {
53+
if (!Array.isArray(patch)) { patch = [patch]; }
54+
return patch.some(index => index.hunks.some(hunk => hunk.lines.some(line => line.endsWith('\r'))))
55+
&& patch.every(
56+
index => index.hunks.every(
57+
hunk => hunk.lines.every(
58+
(line, i) => line.startsWith('\\') || line.endsWith('\r') || hunk.lines[i + 1]?.startsWith('\\')
59+
)
60+
)
61+
);
62+
}

src/patch/parse.js

+3-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
export function parsePatch(uniDiff) {
2-
let diffstr = uniDiff.split(/\r?\n/),
3-
delimiters = uniDiff.match(/\r?\n/g) || [],
2+
let diffstr = uniDiff.split(/\n/),
43
list = [],
54
i = 0;
65

@@ -51,7 +50,7 @@ export function parsePatch(uniDiff) {
5150
// Parses the --- and +++ headers, if none are found, no lines
5251
// are consumed.
5352
function parseFileHeader(index) {
54-
const fileHeader = (/^(---|\+\+\+)\s+(.*)$/).exec(diffstr[i]);
53+
const fileHeader = (/^(---|\+\+\+)\s+(.*)\r?$/).exec(diffstr[i]);
5554
if (fileHeader) {
5655
let keyPrefix = fileHeader[1] === '---' ? 'old' : 'new';
5756
const data = fileHeader[2].split('\t', 2);
@@ -78,8 +77,7 @@ export function parsePatch(uniDiff) {
7877
oldLines: typeof chunkHeader[2] === 'undefined' ? 1 : +chunkHeader[2],
7978
newStart: +chunkHeader[3],
8079
newLines: typeof chunkHeader[4] === 'undefined' ? 1 : +chunkHeader[4],
81-
lines: [],
82-
linedelimiters: []
80+
lines: []
8381
};
8482

8583
// Unified Diff Format quirk: If the chunk size is 0,
@@ -107,7 +105,6 @@ export function parsePatch(uniDiff) {
107105

108106
if (operation === '+' || operation === '-' || operation === ' ' || operation === '\\') {
109107
hunk.lines.push(diffstr[i]);
110-
hunk.linedelimiters.push(delimiters[i] || '\n');
111108

112109
if (operation === '+') {
113110
addCount++;

src/patch/reverse.js

-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ export function reversePatch(structuredPatch) {
1515
oldStart: hunk.newStart,
1616
newLines: hunk.oldLines,
1717
newStart: hunk.oldStart,
18-
linedelimiters: hunk.linedelimiters,
1918
lines: hunk.lines.map(l => {
2019
if (l.startsWith('-')) { return `+${l.slice(1)}`; }
2120
if (l.startsWith('+')) { return `-${l.slice(1)}`; }

src/util/string.js

+15
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,18 @@ function overlapCount(a, b) {
8686
}
8787
return k;
8888
}
89+
90+
91+
/**
92+
* Returns true if the string consistently uses Windows line endings.
93+
*/
94+
export function hasOnlyWinLineEndings(string) {
95+
return string.includes('\r\n') && !string.match(/(?<!\r)\n/);
96+
}
97+
98+
/**
99+
* Returns true if the string consistently uses Unix line endings.
100+
*/
101+
export function hasOnlyUnixLineEndings(string) {
102+
return !string.includes('\r\n') && string.includes('\n');
103+
}

test/patch/apply.js

+70-1
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,6 @@ describe('patch/apply', function() {
201201
});
202202

203203
it('should apply patches', function() {
204-
// Create patch
205204
const oldFile =
206205
'value\n'
207206
+ 'context\n'
@@ -755,6 +754,76 @@ describe('patch/apply', function() {
755754
expect(applyPatch(fileContents, patch))
756755
.to.equal('');
757756
});
757+
758+
it('should automatically convert a patch with Unix file endings to Windows when patching a Windows file', () => {
759+
const oldFile = 'foo\r\nbar\r\nbaz\r\nqux\r\n';
760+
const diffFile =
761+
'Index: testFileName\n'
762+
+ '===================================================================\n'
763+
+ '--- testFileName\tOld Header\n'
764+
+ '+++ testFileName\tNew Header\n'
765+
+ '@@ -2,2 +2,3 @@\n'
766+
+ '-bar\n'
767+
+ '-baz\n'
768+
+ '+new\n'
769+
+ '+two\n'
770+
+ '+three\n';
771+
772+
expect(applyPatch(oldFile, diffFile)).to.equal('foo\r\nnew\r\ntwo\r\nthree\r\nqux\r\n');
773+
});
774+
775+
it('should automatically convert a patch with Windows file endings to Unix when patching a Unix file', () => {
776+
const oldFile = 'foo\nbar\nbaz\nqux\n';
777+
const diffFile =
778+
'Index: testFileName\r\n'
779+
+ '===================================================================\r\n'
780+
+ '--- testFileName\tOld Header\r\n'
781+
+ '+++ testFileName\tNew Header\r\n'
782+
+ '@@ -2,2 +2,3 @@\r\n'
783+
+ '-bar\r\n'
784+
+ '-baz\r\n'
785+
+ '+new\r\n'
786+
+ '+two\r\n'
787+
+ '+three\r\n';
788+
789+
expect(applyPatch(oldFile, diffFile)).to.equal('foo\nnew\ntwo\nthree\nqux\n');
790+
});
791+
792+
it('should leave line endings in the patch alone if the target file has mixed file endings, even if this means the patch does not apply', () => {
793+
const oldFile1 = 'foo\r\nbar\nbaz\nqux\n';
794+
const oldFile2 = 'foo\nbar\r\nbaz\r\nqux\n';
795+
const diffFile =
796+
'Index: testFileName\r\n'
797+
+ '===================================================================\r\n'
798+
+ '--- testFileName\tOld Header\r\n'
799+
+ '+++ testFileName\tNew Header\r\n'
800+
+ '@@ -2,2 +2,3 @@\r\n'
801+
+ '-bar\r\n'
802+
+ '-baz\r\n'
803+
+ '+new\r\n'
804+
+ '+two\r\n'
805+
+ '+three\r\n';
806+
807+
expect(applyPatch(oldFile1, diffFile)).to.equal(false);
808+
expect(applyPatch(oldFile2, diffFile)).to.equal('foo\nnew\r\ntwo\r\nthree\r\nqux\n');
809+
});
810+
811+
it('should leave patch file endings alone if autoConvertLineEndings=false', () => {
812+
const oldFile = 'foo\r\nbar\r\nbaz\r\nqux\r\n';
813+
const diffFile =
814+
'Index: testFileName\n'
815+
+ '===================================================================\n'
816+
+ '--- testFileName\tOld Header\n'
817+
+ '+++ testFileName\tNew Header\n'
818+
+ '@@ -2,2 +2,3 @@\n'
819+
+ '-bar\n'
820+
+ '-baz\n'
821+
+ '+new\n'
822+
+ '+two\n'
823+
+ '+three\n';
824+
825+
expect(applyPatch(oldFile, diffFile, {autoConvertLineEndings: false})).to.equal(false);
826+
});
758827
});
759828

760829
describe('#applyPatches', function() {

test/patch/create.js

+1-13
Original file line numberDiff line numberDiff line change
@@ -809,10 +809,6 @@ describe('patch/create', function() {
809809
lines: [
810810
'-xxx',
811811
'+yyy'
812-
],
813-
linedelimiters: [
814-
'\n',
815-
'\n'
816812
]
817813
}
818814
]
@@ -831,10 +827,6 @@ describe('patch/create', function() {
831827
lines: [
832828
'-aaa',
833829
'+bbb'
834-
],
835-
linedelimiters: [
836-
'\n',
837-
'\n'
838830
]
839831
}
840832
]
@@ -875,18 +867,14 @@ describe('patch/create', function() {
875867

876868
// Check 2: starting with a structuredPatch, does formatting and then
877869
// parsing again basically round-trip as long as we wrap it in an array
878-
// to match the output of parsePatch and delete the linedelimiters that
879-
// parsePatch puts in?
870+
// to match the output of parsePatch?
880871
const patchObj = structuredPatch(
881872
'oldfile', 'newfile',
882873
'line2\nline3\nline4\n', 'line2\nline3\nline5',
883874
'header1', 'header2'
884875
);
885876

886877
const roundTrippedPatch = parsePatch(formatPatch([patchObj]));
887-
for (const hunk of roundTrippedPatch[0].hunks) {
888-
delete hunk.linedelimiters;
889-
}
890878

891879
expect(roundTrippedPatch).to.deep.equal([patchObj]);
892880
});

0 commit comments

Comments
 (0)