14.0 KB562 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import {filterFilesFromPatch, parsePatch} from '../patch/parse';
9import {stringifyPatch} from '../patch/stringify';
10
11describe('patch/stringify', () => {
12 describe('round-trip conversion', () => {
13 it('should round-trip basic modified patch', () => {
14 const patch = `diff --git sapling/eden/scm/a sapling/eden/scm/a
15--- sapling/eden/scm/a
16+++ sapling/eden/scm/a
17@@ -1,1 +1,2 @@
18 1
19+2
20`;
21 const parsed = parsePatch(patch);
22 const stringified = stringifyPatch(parsed);
23 expect(stringified).toEqual(patch);
24 });
25
26 it('should round-trip rename', () => {
27 const patch = `diff --git sapling/eden/scm/a sapling/eden/scm/b
28rename from sapling/eden/scm/a
29rename to sapling/eden/scm/b
30`;
31 const parsed = parsePatch(patch);
32 const stringified = stringifyPatch(parsed);
33 expect(stringified).toEqual(patch);
34 });
35
36 it('should round-trip rename and modify', () => {
37 const patch = `diff --git sapling/eden/addons/LICENSE sapling/eden/addons/LICENSE.bak
38rename from sapling/eden/addons/LICENSE
39rename to sapling/eden/addons/LICENSE.bak
40--- sapling/eden/addons/LICENSE
41+++ sapling/eden/addons/LICENSE.bak
42@@ -2,6 +2,7 @@
43
44 Copyright (c) Meta Platforms, Inc. and its affiliates.
45
46+
47`;
48 const parsed = parsePatch(patch);
49 const stringified = stringifyPatch(parsed);
50 expect(stringified).toEqual(patch);
51 });
52
53 it('should round-trip new file', () => {
54 const patch = `diff --git sapling/eden/scm/c sapling/eden/scm/c
55new file mode 100644
56--- /dev/null
57+++ sapling/eden/scm/c
58@@ -0,0 +1,1 @@
59+1
60`;
61 const parsed = parsePatch(patch);
62 const stringified = stringifyPatch(parsed);
63 expect(stringified).toEqual(patch);
64 });
65
66 it('should round-trip new empty file', () => {
67 const patch = `diff --git sapling/eden/addons/d sapling/eden/addons/d
68new file mode 100644
69`;
70 const parsed = parsePatch(patch);
71 const stringified = stringifyPatch(parsed);
72 expect(stringified).toEqual(patch);
73 });
74
75 it('should round-trip deleted file', () => {
76 const patch = `diff --git sapling/eden/scm/a sapling/eden/scm/a
77deleted file mode 100644
78--- sapling/eden/scm/a
79+++ /dev/null
80@@ -1,1 +0,0 @@
81-1
82`;
83 const parsed = parsePatch(patch);
84 const stringified = stringifyPatch(parsed);
85 expect(stringified).toEqual(patch);
86 });
87
88 it('should round-trip copied file', () => {
89 const patch = `diff --git sapling/eden/scm/a sapling/eden/scm/b
90copy from sapling/eden/scm/a
91copy to sapling/eden/scm/b
92`;
93 const parsed = parsePatch(patch);
94 const stringified = stringifyPatch(parsed);
95 expect(stringified).toEqual(patch);
96 });
97
98 it('should round-trip multiple files', () => {
99 const patch = `diff --git sapling/eden/scm/a sapling/eden/scm/a
100--- sapling/eden/scm/a
101+++ sapling/eden/scm/a
102@@ -1,1 +1,2 @@
103 1
104+2
105diff --git sapling/eden/scm/a sapling/eden/scm/b
106copy from sapling/eden/scm/a
107copy to sapling/eden/scm/b
108diff --git sapling/eden/scm/c sapling/eden/scm/d
109copy from sapling/eden/scm/c
110copy to sapling/eden/scm/d
111`;
112 const parsed = parsePatch(patch);
113 const stringified = stringifyPatch(parsed);
114 expect(stringified).toEqual(patch);
115 });
116
117 it('should round-trip file mode change', () => {
118 const patch = `diff --git sapling/eden/scm/a sapling/eden/scm/a
119old mode 100644
120new mode 100755
121`;
122 const parsed = parsePatch(patch);
123 const stringified = stringifyPatch(parsed);
124 expect(stringified).toEqual(patch);
125 });
126
127 it('should round-trip submodule modification', () => {
128 const patch = `diff --git a/external/brotli b/external/brotli
129--- a/external/brotli
130+++ b/external/brotli
131@@ -1,1 +1,1 @@
132-Subproject commit 892110204ccf44fcd493ae415c9a69c470c2a9cf
133+Subproject commit 57de5cc4288565a9c3a7af978ef15f0abf0ada1b
134`;
135 const parsed = parsePatch(patch);
136 const stringified = stringifyPatch(parsed);
137 expect(stringified).toEqual(patch);
138 });
139
140 it('should round-trip added submodule', () => {
141 const patch = `diff --git a/path/to/submodule b/path/to/submodule
142new file mode 160000
143--- /dev/null
144+++ b/path/to/submodule
145@@ -0,0 +1,1 @@
146+Subproject commit 7ef4220022059b9b1e1d8ec4eea6f7abd011894f
147`;
148 const parsed = parsePatch(patch);
149 const stringified = stringifyPatch(parsed);
150 expect(stringified).toEqual(patch);
151 });
152 });
153
154 describe('hunk range formatting', () => {
155 it('should format single line hunk with count', () => {
156 const patch = `diff --git a/file.txt b/file.txt
157--- a/file.txt
158+++ b/file.txt
159@@ -5,1 +5,2 @@
160 line 5
161+new line
162`;
163 const parsed = parsePatch(patch);
164 const stringified = stringifyPatch(parsed);
165 expect(stringified).toEqual(patch);
166 });
167
168 it('should format empty old range', () => {
169 const patch = `diff --git sapling/eden/scm/c sapling/eden/scm/c
170new file mode 100644
171--- /dev/null
172+++ sapling/eden/scm/c
173@@ -0,0 +1,3 @@
174+line 1
175+line 2
176+line 3
177`;
178 const parsed = parsePatch(patch);
179 const stringified = stringifyPatch(parsed);
180 expect(stringified).toEqual(patch);
181 });
182
183 it('should format empty new range', () => {
184 const patch = `diff --git a/file.txt b/file.txt
185--- a/file.txt
186+++ b/file.txt
187@@ -1,3 +0,0 @@
188-line 1
189-line 2
190-line 3
191`;
192 const parsed = parsePatch(patch);
193 const stringified = stringifyPatch(parsed);
194 expect(stringified).toEqual(patch);
195 });
196 });
197
198 describe('line delimiter handling', () => {
199 it('should preserve hunk line delimiters', () => {
200 const patch = `diff --git a/file.txt b/file.txt
201--- a/file.txt
202+++ b/file.txt
203@@ -1,1 +1,2 @@
204 line 1\r
205+line 2\r
206`;
207 const parsed = parsePatch(patch);
208 const stringified = stringifyPatch(parsed);
209 // Header lines use standard \n, but hunk content preserves \r
210 expect(stringified).toEqual(patch);
211 });
212 });
213
214 describe('multiple hunks', () => {
215 it('should handle multiple hunks in a single file', () => {
216 const patch = `diff --git a/file.txt b/file.txt
217--- a/file.txt
218+++ b/file.txt
219@@ -1,3 +1,4 @@
220 line 1
221+new line 1.5
222 line 2
223 line 3
224@@ -10,2 +11,3 @@
225 line 10
226+new line 10.5
227 line 11
228`;
229 const parsed = parsePatch(patch);
230 const stringified = stringifyPatch(parsed);
231 expect(stringified).toEqual(patch);
232 });
233 });
234
235 describe('no newline at end of file', () => {
236 it('should handle backslash-no-newline marker', () => {
237 const patch = `diff --git a/file.txt b/file.txt
238--- a/file.txt
239+++ b/file.txt
240@@ -1,1 +1,1 @@
241-old content
242\\ No newline at end of file
243+new content
244\\ No newline at end of file
245`;
246 const parsed = parsePatch(patch);
247 const stringified = stringifyPatch(parsed);
248 expect(stringified).toEqual(patch);
249 });
250 });
251});
252
253describe('patch/filterFilesFromPatch', () => {
254 describe('basic filtering', () => {
255 it('should filter out a single modified file', () => {
256 const patch = `diff --git a/keep.ts b/keep.ts
257--- a/keep.ts
258+++ b/keep.ts
259@@ -1,1 +1,2 @@
260 line 1
261+line 2
262diff --git a/remove.ts b/remove.ts
263--- a/remove.ts
264+++ b/remove.ts
265@@ -1,1 +1,2 @@
266 original
267+changed
268`;
269 const filtered = filterFilesFromPatch(patch, ['a/remove.ts']);
270 const expected = `diff --git a/keep.ts b/keep.ts
271--- a/keep.ts
272+++ b/keep.ts
273@@ -1,1 +1,2 @@
274 line 1
275+line 2
276`;
277 expect(filtered).toEqual(expected);
278 });
279
280 it('should filter out multiple files', () => {
281 const patch = `diff --git a/keep.ts b/keep.ts
282--- a/keep.ts
283+++ b/keep.ts
284@@ -1,1 +1,2 @@
285 line 1
286+line 2
287diff --git a/remove1.ts b/remove1.ts
288--- a/remove1.ts
289+++ b/remove1.ts
290@@ -1,1 +1,1 @@
291-old
292+new
293diff --git a/remove2.ts b/remove2.ts
294--- a/remove2.ts
295+++ b/remove2.ts
296@@ -1,1 +1,1 @@
297-old
298+new
299`;
300 const filtered = filterFilesFromPatch(patch, ['a/remove1.ts', 'b/remove2.ts']);
301 const expected = `diff --git a/keep.ts b/keep.ts
302--- a/keep.ts
303+++ b/keep.ts
304@@ -1,1 +1,2 @@
305 line 1
306+line 2
307`;
308 expect(filtered).toEqual(expected);
309 });
310
311 it('should return empty string when all files are filtered', () => {
312 const patch = `diff --git a/remove.ts b/remove.ts
313--- a/remove.ts
314+++ b/remove.ts
315@@ -1,1 +1,2 @@
316 original
317+changed
318`;
319 const filtered = filterFilesFromPatch(patch, ['a/remove.ts']);
320 expect(filtered).toEqual('');
321 });
322
323 it('should return original patch when no files match filter', () => {
324 const patch = `diff --git a/keep.ts b/keep.ts
325--- a/keep.ts
326+++ b/keep.ts
327@@ -1,1 +1,2 @@
328 line 1
329+line 2
330`;
331 const filtered = filterFilesFromPatch(patch, ['a/nonexistent.ts']);
332 expect(filtered).toEqual(patch);
333 });
334 });
335
336 describe('path prefix handling', () => {
337 it('should filter files with a/ prefix', () => {
338 const patch = `diff --git a/file.ts b/file.ts
339--- a/file.ts
340+++ b/file.ts
341@@ -1,1 +1,2 @@
342 line 1
343+line 2
344`;
345 const filtered = filterFilesFromPatch(patch, ['a/file.ts']);
346 expect(filtered).toEqual('');
347 });
348
349 it('should filter files with b/ prefix', () => {
350 const patch = `diff --git a/file.ts b/file.ts
351--- a/file.ts
352+++ b/file.ts
353@@ -1,1 +1,2 @@
354 line 1
355+line 2
356`;
357 const filtered = filterFilesFromPatch(patch, ['b/file.ts']);
358 expect(filtered).toEqual('');
359 });
360
361 it('should filter files without prefix', () => {
362 const patch = `diff --git a/file.ts b/file.ts
363--- a/file.ts
364+++ b/file.ts
365@@ -1,1 +1,2 @@
366 line 1
367+line 2
368`;
369 const filtered = filterFilesFromPatch(patch, ['file.ts']);
370 expect(filtered).toEqual('');
371 });
372
373 it('should handle paths with slashes', () => {
374 const patch = `diff --git a/src/components/App.tsx b/src/components/App.tsx
375--- a/src/components/App.tsx
376+++ b/src/components/App.tsx
377@@ -1,1 +1,2 @@
378 import React from 'react';
379+import {useState} from 'react';
380`;
381 const filtered = filterFilesFromPatch(patch, ['src/components/App.tsx']);
382 expect(filtered).toEqual('');
383 });
384 });
385
386 describe('special file operations', () => {
387 it('should filter renamed files by old name', () => {
388 const patch = `diff --git a/old.ts b/new.ts
389rename from a/old.ts
390rename to b/new.ts
391--- a/old.ts
392+++ b/new.ts
393@@ -1,1 +1,2 @@
394 line 1
395+line 2
396`;
397 const filtered = filterFilesFromPatch(patch, ['a/old.ts']);
398 expect(filtered).toEqual('');
399 });
400
401 it('should filter renamed files by new name', () => {
402 const patch = `diff --git a/old.ts b/new.ts
403rename from a/old.ts
404rename to b/new.ts
405--- a/old.ts
406+++ b/new.ts
407@@ -1,1 +1,2 @@
408 line 1
409+line 2
410`;
411 const filtered = filterFilesFromPatch(patch, ['b/new.ts']);
412 expect(filtered).toEqual('');
413 });
414
415 it('should filter new files', () => {
416 const patch = `diff --git a/existing.ts b/existing.ts
417--- a/existing.ts
418+++ b/existing.ts
419@@ -1,1 +1,2 @@
420 line 1
421+line 2
422diff --git a/new.ts b/new.ts
423new file mode 100644
424--- /dev/null
425+++ b/new.ts
426@@ -0,0 +1,1 @@
427+new content
428`;
429 const filtered = filterFilesFromPatch(patch, ['new.ts']);
430 const expected = `diff --git a/existing.ts b/existing.ts
431--- a/existing.ts
432+++ b/existing.ts
433@@ -1,1 +1,2 @@
434 line 1
435+line 2
436`;
437 expect(filtered).toEqual(expected);
438 });
439
440 it('should filter deleted files', () => {
441 const patch = `diff --git a/keep.ts b/keep.ts
442--- a/keep.ts
443+++ b/keep.ts
444@@ -1,1 +1,2 @@
445 line 1
446+line 2
447diff --git a/deleted.ts b/deleted.ts
448deleted file mode 100644
449--- a/deleted.ts
450+++ /dev/null
451@@ -1,1 +0,0 @@
452-old content
453`;
454 const filtered = filterFilesFromPatch(patch, ['deleted.ts']);
455 const expected = `diff --git a/keep.ts b/keep.ts
456--- a/keep.ts
457+++ b/keep.ts
458@@ -1,1 +1,2 @@
459 line 1
460+line 2
461`;
462 expect(filtered).toEqual(expected);
463 });
464
465 it('should filter copied files', () => {
466 const patch = `diff --git a/original.ts b/copy.ts
467copy from a/original.ts
468copy to b/copy.ts
469`;
470 const filtered = filterFilesFromPatch(patch, ['copy.ts']);
471 expect(filtered).toEqual('');
472 });
473
474 it('should filter mode changes', () => {
475 const patch = `diff --git a/script.sh b/script.sh
476old mode 100644
477new mode 100755
478`;
479 const filtered = filterFilesFromPatch(patch, ['script.sh']);
480 expect(filtered).toEqual('');
481 });
482 });
483
484 describe('real-world use case: filtering generated files', () => {
485 it('should remove generated code files from patch', () => {
486 const patch = `diff --git a/src/manual.ts b/src/manual.ts
487--- a/src/manual.ts
488+++ b/src/manual.ts
489@@ -1,3 +1,4 @@
490 // Manually written code
491 export function doSomething() {
492+ console.log('new feature');
493 }
494diff --git a/generated/types.ts b/generated/types.ts
495--- a/generated/types.ts
496+++ b/generated/types.ts
497@@ -1,100 +1,200 @@
498-// Auto-generated - do not edit
499+// Auto-generated - do not edit
500+// Lots of noisy changes
501 export type GeneratedType = string;
502diff --git a/generated/schema.ts b/generated/schema.ts
503--- a/generated/schema.ts
504+++ b/generated/schema.ts
505@@ -1,50 +1,100 @@
506-// Auto-generated
507+// Auto-generated
508+// More noise
509 export const schema = {};
510`;
511 const filtered = filterFilesFromPatch(patch, ['generated/types.ts', 'generated/schema.ts']);
512 const expected = `diff --git a/src/manual.ts b/src/manual.ts
513--- a/src/manual.ts
514+++ b/src/manual.ts
515@@ -1,3 +1,4 @@
516 // Manually written code
517 export function doSomething() {
518+ console.log('new feature');
519 }
520`;
521 expect(filtered).toEqual(expected);
522 });
523 });
524
525 describe('edge cases', () => {
526 it('should handle empty patch', () => {
527 const filtered = filterFilesFromPatch('', ['file.ts']);
528 expect(filtered).toEqual('');
529 });
530
531 it('should handle empty file list', () => {
532 const patch = `diff --git a/file.ts b/file.ts
533--- a/file.ts
534+++ b/file.ts
535@@ -1,1 +1,2 @@
536 line 1
537+line 2
538`;
539 const filtered = filterFilesFromPatch(patch, []);
540 expect(filtered).toEqual(patch);
541 });
542
543 it('should handle files with multiple hunks', () => {
544 const patch = `diff --git a/file.ts b/file.ts
545--- a/file.ts
546+++ b/file.ts
547@@ -1,3 +1,4 @@
548 line 1
549+new line
550 line 2
551 line 3
552@@ -10,2 +11,3 @@
553 line 10
554+another new line
555 line 11
556`;
557 const filtered = filterFilesFromPatch(patch, ['file.ts']);
558 expect(filtered).toEqual('');
559 });
560 });
561});
562