ts-repo-utils 6.1.0 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +11 -7
  2. package/dist/cmd/assert-repo-is-clean.mjs +1 -1
  3. package/dist/cmd/check-should-run-type-checks.mjs +1 -1
  4. package/dist/cmd/format-diff-from.mjs +29 -8
  5. package/dist/cmd/format-diff-from.mjs.map +1 -1
  6. package/dist/cmd/format-uncommitted.d.mts +3 -0
  7. package/dist/cmd/format-uncommitted.d.mts.map +1 -0
  8. package/dist/cmd/format-uncommitted.mjs +59 -0
  9. package/dist/cmd/format-uncommitted.mjs.map +1 -0
  10. package/dist/cmd/gen-index-ts.mjs +1 -1
  11. package/dist/functions/diff.d.mts +32 -2
  12. package/dist/functions/diff.d.mts.map +1 -1
  13. package/dist/functions/diff.mjs +47 -29
  14. package/dist/functions/diff.mjs.map +1 -1
  15. package/dist/functions/exec-async.d.mts +2 -2
  16. package/dist/functions/exec-async.d.mts.map +1 -1
  17. package/dist/functions/format.d.mts +20 -11
  18. package/dist/functions/format.d.mts.map +1 -1
  19. package/dist/functions/format.mjs +134 -95
  20. package/dist/functions/format.mjs.map +1 -1
  21. package/dist/functions/index.mjs +2 -2
  22. package/dist/index.mjs +2 -2
  23. package/package.json +2 -2
  24. package/src/cmd/assert-repo-is-clean.mts +1 -1
  25. package/src/cmd/check-should-run-type-checks.mts +1 -1
  26. package/src/cmd/format-diff-from.mts +35 -9
  27. package/src/cmd/format-uncommitted.mts +67 -0
  28. package/src/cmd/gen-index-ts.mts +1 -1
  29. package/src/functions/diff.mts +85 -32
  30. package/src/functions/diff.test.mts +569 -102
  31. package/src/functions/exec-async.mts +2 -2
  32. package/src/functions/exec-async.test.mts +77 -47
  33. package/src/functions/format.mts +224 -141
  34. package/src/functions/format.test.mts +625 -20
  35. package/src/functions/workspace-utils/run-cmd-in-stages.test.mts +266 -0
  36. package/dist/cmd/format-untracked.d.mts +0 -3
  37. package/dist/cmd/format-untracked.d.mts.map +0 -1
  38. package/dist/cmd/format-untracked.mjs +0 -34
  39. package/dist/cmd/format-untracked.mjs.map +0 -1
  40. package/src/cmd/format-untracked.mts +0 -31
@@ -1,16 +1,31 @@
1
1
  import dedent from 'dedent';
2
2
  import { Result } from 'ts-data-forge';
3
3
  import '../node-global.mjs';
4
- import { getDiffFrom, getUntrackedFiles } from './diff.mjs';
5
- import { formatDiffFrom, formatFiles, formatFilesList } from './format.mjs';
4
+ import {
5
+ getDiffFrom,
6
+ getModifiedFiles,
7
+ getStagedFiles,
8
+ getUntrackedFiles,
9
+ } from './diff.mjs';
10
+ import {
11
+ formatDiffFrom,
12
+ formatFiles,
13
+ formatFilesGlob,
14
+ formatUncommittedFiles,
15
+ } from './format.mjs';
6
16
 
7
17
  vi.mock('./diff.mjs', () => ({
8
18
  getDiffFrom: vi.fn(),
19
+ getModifiedFiles: vi.fn(),
20
+ getStagedFiles: vi.fn(),
9
21
  getUntrackedFiles: vi.fn(),
10
22
  }));
11
23
 
12
- describe('formatFiles', () => {
13
- const testDir = path.join(process.cwd(), 'test-format-files');
24
+ describe('formatFilesGlob', () => {
25
+ const testDir = path.join(
26
+ process.cwd(),
27
+ `test-format-files-${crypto.randomUUID()}`,
28
+ );
14
29
 
15
30
  // Helper to create a test file with unformatted content
16
31
  const createTestFile = async (
@@ -54,8 +69,8 @@ describe('formatFiles', () => {
54
69
  await createTestFile('test.md', '# Test\n\nSome spaces');
55
70
 
56
71
  // Format TypeScript files
57
- const result = await formatFiles(`${testDir}/*.ts`, { silent: true });
58
- expect(result).toBe('ok');
72
+ const result = await formatFilesGlob(`${testDir}/*.ts`, { silent: true });
73
+ expect(Result.isOk(result)).toBe(true);
59
74
 
60
75
  // Check that files were formatted
61
76
  const content1 = await readTestFile(file1);
@@ -86,10 +101,10 @@ describe('formatFiles', () => {
86
101
 
87
102
  test('should return ok when no files match pattern', async () => {
88
103
  vi.clearAllMocks();
89
- const result = await formatFiles('/non-existent-path/*.ts', {
104
+ const result = await formatFilesGlob('/non-existent-path/*.ts', {
90
105
  silent: true,
91
106
  });
92
- expect(result).toBe('ok');
107
+ expect(Result.isOk(result)).toBe(true);
93
108
  });
94
109
 
95
110
  test('should handle nested directories with glob pattern', async () => {
@@ -107,10 +122,10 @@ describe('formatFiles', () => {
107
122
  );
108
123
 
109
124
  // Format with recursive glob
110
- const result = await formatFiles(`${testDir}/**/*.ts`, {
125
+ const result = await formatFilesGlob(`${testDir}/**/*.ts`, {
111
126
  silent: true,
112
127
  });
113
- expect(result).toBe('ok');
128
+ expect(Result.isOk(result)).toBe(true);
114
129
 
115
130
  // Check that nested file was formatted
116
131
  const content = await readTestFile(nestedFile);
@@ -128,8 +143,11 @@ describe('formatFiles', () => {
128
143
  });
129
144
  });
130
145
 
131
- describe('formatFilesList', () => {
132
- const testDir = path.join(process.cwd(), 'test-format-files-list');
146
+ describe('formatFiles', () => {
147
+ const testDir = path.join(
148
+ process.cwd(),
149
+ `test-format-files-list-${crypto.randomUUID()}`,
150
+ );
133
151
 
134
152
  // Helper to create a test file with unformatted content
135
153
  const createTestFile = async (
@@ -166,10 +184,10 @@ describe('formatFilesList', () => {
166
184
  );
167
185
 
168
186
  // Format the files
169
- const result = await formatFilesList([file1, file2], {
187
+ const result = await formatFiles([file1, file2], {
170
188
  silent: true,
171
189
  });
172
- expect(result).toBe('ok');
190
+ expect(Result.isOk(result)).toBe(true);
173
191
 
174
192
  // Check formatted content
175
193
  const content1 = await readTestFile(file1);
@@ -194,15 +212,386 @@ describe('formatFilesList', () => {
194
212
 
195
213
  test('should return ok for empty file list', async () => {
196
214
  vi.clearAllMocks();
197
- const result = await formatFilesList([], {
215
+ const result = await formatFiles([], {
198
216
  silent: true,
199
217
  });
200
- expect(result).toBe('ok');
218
+ expect(Result.isOk(result)).toBe(true);
219
+ });
220
+ });
221
+
222
+ describe('formatUncommittedFiles', () => {
223
+ const testDir = path.join(
224
+ process.cwd(),
225
+ `test-format-uncommitted-${crypto.randomUUID()}`,
226
+ );
227
+
228
+ const createTestFile = async (
229
+ filename: string,
230
+ content: string,
231
+ ): Promise<string> => {
232
+ const filePath = path.join(testDir, filename);
233
+ const dir = path.dirname(filePath);
234
+ await fs.mkdir(dir, { recursive: true });
235
+ await fs.writeFile(filePath, content, 'utf8');
236
+ return filePath;
237
+ };
238
+
239
+ const setupTest = async (): Promise<void> => {
240
+ vi.clearAllMocks();
241
+ await fs.mkdir(testDir, { recursive: true });
242
+ };
243
+
244
+ const cleanupTest = async (): Promise<void> => {
245
+ await fs.rm(testDir, { recursive: true, force: true });
246
+ };
247
+
248
+ test('should format all uncommitted files by default', async () => {
249
+ await setupTest();
250
+ try {
251
+ const untrackedFiles = ['untracked1.ts', 'untracked2.ts'];
252
+ const modifiedFiles = ['modified1.ts', 'modified2.ts'];
253
+ const stagedFiles = ['staged1.ts', 'staged2.ts'];
254
+
255
+ // Create test files
256
+ const allFiles = [...untrackedFiles, ...modifiedFiles, ...stagedFiles];
257
+ const filePromises = allFiles.map((file) =>
258
+ createTestFile(file, 'const x=1'),
259
+ );
260
+ await Promise.all(filePromises);
261
+
262
+ // Mock git functions
263
+ vi.mocked(getUntrackedFiles).mockResolvedValue(
264
+ Result.ok(untrackedFiles.map((f) => path.join(testDir, f))),
265
+ );
266
+ vi.mocked(getModifiedFiles).mockResolvedValue(
267
+ Result.ok(modifiedFiles.map((f) => path.join(testDir, f))),
268
+ );
269
+ vi.mocked(getStagedFiles).mockResolvedValue(
270
+ Result.ok(stagedFiles.map((f) => path.join(testDir, f))),
271
+ );
272
+
273
+ const result = await formatUncommittedFiles({ silent: true });
274
+ expect(Result.isOk(result)).toBe(true);
275
+
276
+ // Verify all git functions were called
277
+ expect(getUntrackedFiles).toHaveBeenCalledWith({ silent: true });
278
+ expect(getModifiedFiles).toHaveBeenCalledWith({ silent: true });
279
+ expect(getStagedFiles).toHaveBeenCalledWith({ silent: true });
280
+
281
+ // Verify files were formatted
282
+ const verifyPromises = allFiles.map(async (file) => {
283
+ const content = await fs.readFile(path.join(testDir, file), 'utf8');
284
+ expect(content).toBe('const x = 1;\n');
285
+ });
286
+ await Promise.all(verifyPromises);
287
+ } finally {
288
+ await cleanupTest();
289
+ }
290
+ });
291
+
292
+ test('should format only untracked files when specified', async () => {
293
+ await setupTest();
294
+ try {
295
+ const untrackedFiles = ['untracked.ts'] as const;
296
+ await createTestFile(untrackedFiles[0], 'const x=1');
297
+
298
+ vi.mocked(getUntrackedFiles).mockResolvedValue(
299
+ Result.ok(untrackedFiles.map((f) => path.join(testDir, f))),
300
+ );
301
+
302
+ const result = await formatUncommittedFiles({
303
+ untracked: true,
304
+ modified: false,
305
+ staged: false,
306
+ silent: true,
307
+ });
308
+ expect(Result.isOk(result)).toBe(true);
309
+
310
+ expect(getUntrackedFiles).toHaveBeenCalledWith({ silent: true });
311
+ expect(getModifiedFiles).not.toHaveBeenCalled();
312
+ expect(getStagedFiles).not.toHaveBeenCalled();
313
+ } finally {
314
+ await cleanupTest();
315
+ }
316
+ });
317
+
318
+ test('should format only modified files when specified', async () => {
319
+ await setupTest();
320
+ try {
321
+ const modifiedFiles = ['modified.ts'] as const;
322
+ await createTestFile(modifiedFiles[0], 'const x=1');
323
+
324
+ vi.mocked(getModifiedFiles).mockResolvedValue(
325
+ Result.ok(modifiedFiles.map((f) => path.join(testDir, f))),
326
+ );
327
+
328
+ const result = await formatUncommittedFiles({
329
+ untracked: false,
330
+ modified: true,
331
+ staged: false,
332
+ silent: true,
333
+ });
334
+ expect(Result.isOk(result)).toBe(true);
335
+
336
+ expect(getUntrackedFiles).not.toHaveBeenCalled();
337
+ expect(getModifiedFiles).toHaveBeenCalledWith({ silent: true });
338
+ expect(getStagedFiles).not.toHaveBeenCalled();
339
+ } finally {
340
+ await cleanupTest();
341
+ }
342
+ });
343
+
344
+ test('should format only staged files when specified', async () => {
345
+ await setupTest();
346
+ try {
347
+ const stagedFiles = ['staged.ts'] as const;
348
+ await createTestFile(stagedFiles[0], 'const x=1');
349
+
350
+ vi.mocked(getStagedFiles).mockResolvedValue(
351
+ Result.ok(stagedFiles.map((f) => path.join(testDir, f))),
352
+ );
353
+
354
+ const result = await formatUncommittedFiles({
355
+ untracked: false,
356
+ modified: false,
357
+ staged: true,
358
+ silent: true,
359
+ });
360
+ expect(Result.isOk(result)).toBe(true);
361
+
362
+ expect(getUntrackedFiles).not.toHaveBeenCalled();
363
+ expect(getModifiedFiles).not.toHaveBeenCalled();
364
+ expect(getStagedFiles).toHaveBeenCalledWith({ silent: true });
365
+ } finally {
366
+ await cleanupTest();
367
+ }
368
+ });
369
+
370
+ test('should handle combinations of file types', async () => {
371
+ await setupTest();
372
+ try {
373
+ const untrackedFiles = ['untracked.ts'];
374
+ const stagedFiles = ['staged.ts'];
375
+
376
+ const allFiles = [...untrackedFiles, ...stagedFiles];
377
+ const filePromises = allFiles.map((file) =>
378
+ createTestFile(file, 'const x=1'),
379
+ );
380
+ await Promise.all(filePromises);
381
+
382
+ vi.mocked(getUntrackedFiles).mockResolvedValue(
383
+ Result.ok(untrackedFiles.map((f) => path.join(testDir, f))),
384
+ );
385
+ vi.mocked(getStagedFiles).mockResolvedValue(
386
+ Result.ok(stagedFiles.map((f) => path.join(testDir, f))),
387
+ );
388
+
389
+ const result = await formatUncommittedFiles({
390
+ untracked: true,
391
+ modified: false,
392
+ staged: true,
393
+ silent: true,
394
+ });
395
+ expect(Result.isOk(result)).toBe(true);
396
+
397
+ expect(getUntrackedFiles).toHaveBeenCalledWith({ silent: true });
398
+ expect(getModifiedFiles).not.toHaveBeenCalled();
399
+ expect(getStagedFiles).toHaveBeenCalledWith({ silent: true });
400
+ } finally {
401
+ await cleanupTest();
402
+ }
403
+ });
404
+
405
+ test('should deduplicate files that appear in multiple categories', async () => {
406
+ await setupTest();
407
+ try {
408
+ const duplicateFile = path.join(testDir, 'duplicate.ts');
409
+ await createTestFile('duplicate.ts', 'const x=1');
410
+
411
+ // Mock the same file appearing in multiple categories
412
+ vi.mocked(getUntrackedFiles).mockResolvedValue(
413
+ Result.ok([duplicateFile]),
414
+ );
415
+ vi.mocked(getModifiedFiles).mockResolvedValue(Result.ok([duplicateFile]));
416
+ vi.mocked(getStagedFiles).mockResolvedValue(Result.ok([duplicateFile]));
417
+
418
+ const result = await formatUncommittedFiles({ silent: true });
419
+ expect(Result.isOk(result)).toBe(true);
420
+
421
+ // Verify file was formatted (only once despite appearing in all categories)
422
+ const content = await fs.readFile(duplicateFile, 'utf8');
423
+ expect(content).toBe('const x = 1;\n');
424
+ } finally {
425
+ await cleanupTest();
426
+ }
427
+ });
428
+
429
+ test('should handle empty file lists', async () => {
430
+ await setupTest();
431
+ try {
432
+ vi.mocked(getUntrackedFiles).mockResolvedValue(Result.ok([]));
433
+ vi.mocked(getModifiedFiles).mockResolvedValue(Result.ok([]));
434
+ vi.mocked(getStagedFiles).mockResolvedValue(Result.ok([]));
435
+
436
+ const result = await formatUncommittedFiles({ silent: true });
437
+ expect(Result.isOk(result)).toBe(true);
438
+ } finally {
439
+ await cleanupTest();
440
+ }
441
+ });
442
+
443
+ test('should return error when getUntrackedFiles fails', async () => {
444
+ await setupTest();
445
+ try {
446
+ const error = { message: 'Git error' };
447
+ vi.mocked(getUntrackedFiles).mockResolvedValue(Result.err(error));
448
+
449
+ const result = await formatUncommittedFiles({
450
+ untracked: true,
451
+ modified: false,
452
+ staged: false,
453
+ silent: true,
454
+ });
455
+ expect(Result.isErr(result)).toBe(true);
456
+ if (Result.isErr(result)) {
457
+ expect(result.value).toStrictEqual(error);
458
+ }
459
+ } finally {
460
+ await cleanupTest();
461
+ }
462
+ });
463
+
464
+ test('should return error when getModifiedFiles fails', async () => {
465
+ await setupTest();
466
+ try {
467
+ const error = { message: 'Git error' };
468
+ vi.mocked(getModifiedFiles).mockResolvedValue(Result.err(error));
469
+
470
+ const result = await formatUncommittedFiles({
471
+ untracked: false,
472
+ modified: true,
473
+ staged: false,
474
+ silent: true,
475
+ });
476
+ expect(Result.isErr(result)).toBe(true);
477
+ if (Result.isErr(result)) {
478
+ expect(result.value).toStrictEqual(error);
479
+ }
480
+ } finally {
481
+ await cleanupTest();
482
+ }
483
+ });
484
+
485
+ test('should return error when getStagedFiles fails', async () => {
486
+ await setupTest();
487
+ try {
488
+ const error = { message: 'Git error' };
489
+ vi.mocked(getStagedFiles).mockResolvedValue(Result.err(error));
490
+
491
+ const result = await formatUncommittedFiles({
492
+ untracked: false,
493
+ modified: false,
494
+ staged: true,
495
+ silent: true,
496
+ });
497
+ expect(Result.isErr(result)).toBe(true);
498
+ if (Result.isErr(result)) {
499
+ expect(result.value).toStrictEqual(error);
500
+ }
501
+ } finally {
502
+ await cleanupTest();
503
+ }
504
+ });
505
+
506
+ test('should respect silent option', async () => {
507
+ await setupTest();
508
+ try {
509
+ // Using vi.stubGlobal to avoid direct assignment
510
+ const consoleErrorStub = vi.fn();
511
+ vi.stubGlobal('console', {
512
+ ...console,
513
+ error: consoleErrorStub,
514
+ });
515
+
516
+ vi.mocked(getUntrackedFiles).mockResolvedValue(Result.ok([]));
517
+ vi.mocked(getModifiedFiles).mockResolvedValue(Result.ok([]));
518
+ vi.mocked(getStagedFiles).mockResolvedValue(Result.ok([]));
519
+
520
+ await formatUncommittedFiles({ silent: false });
521
+ // With silent: false, console output may occur
522
+
523
+ await formatUncommittedFiles({ silent: true });
524
+ // With silent: true, console output should be suppressed
525
+
526
+ vi.unstubAllGlobals();
527
+ } finally {
528
+ await cleanupTest();
529
+ }
530
+ });
531
+
532
+ test('should format TypeScript files correctly', async () => {
533
+ await setupTest();
534
+ try {
535
+ const testFile = 'test.ts';
536
+ await createTestFile(
537
+ testFile,
538
+ dedent`
539
+ function test(){return"hello"}
540
+ const obj={a:1,b:2}
541
+ `,
542
+ );
543
+
544
+ vi.mocked(getUntrackedFiles).mockResolvedValue(
545
+ Result.ok([path.join(testDir, testFile)]),
546
+ );
547
+ vi.mocked(getModifiedFiles).mockResolvedValue(Result.ok([]));
548
+ vi.mocked(getStagedFiles).mockResolvedValue(Result.ok([]));
549
+
550
+ const result = await formatUncommittedFiles({ silent: true });
551
+ expect(Result.isOk(result)).toBe(true);
552
+
553
+ const content = await fs.readFile(path.join(testDir, testFile), 'utf8');
554
+ expect(content).toBe(
555
+ `${dedent`
556
+ function test() {
557
+ return 'hello';
558
+ }
559
+ const obj = { a: 1, b: 2 };
560
+ `}\n`,
561
+ );
562
+ } finally {
563
+ await cleanupTest();
564
+ }
565
+ });
566
+
567
+ test('should handle non-formattable files gracefully', async () => {
568
+ await setupTest();
569
+ try {
570
+ const binaryFile = 'test.bin';
571
+ const binaryPath = path.join(testDir, binaryFile);
572
+ await createTestFile(
573
+ binaryFile,
574
+ Buffer.from([0x00, 0x01, 0x02]).toString(),
575
+ );
576
+
577
+ vi.mocked(getUntrackedFiles).mockResolvedValue(Result.ok([binaryPath]));
578
+ vi.mocked(getModifiedFiles).mockResolvedValue(Result.ok([]));
579
+ vi.mocked(getStagedFiles).mockResolvedValue(Result.ok([]));
580
+
581
+ const result = await formatUncommittedFiles({ silent: true });
582
+ // Should handle error gracefully
583
+ expect(Result.isOk(result) || Result.isErr(result)).toBe(true);
584
+ } finally {
585
+ await cleanupTest();
586
+ }
201
587
  });
202
588
  });
203
589
 
204
590
  describe('formatDiffFrom', () => {
205
- const testDir = path.join(process.cwd(), 'test-format-diff');
591
+ const testDir = path.join(
592
+ process.cwd(),
593
+ `test-format-diff-${crypto.randomUUID()}`,
594
+ );
206
595
 
207
596
  const createTestFile = async (
208
597
  filename: string,
@@ -232,9 +621,11 @@ describe('formatDiffFrom', () => {
232
621
  vi.mocked(getDiffFrom).mockResolvedValue(Result.ok([file1]));
233
622
 
234
623
  vi.mocked(getUntrackedFiles).mockResolvedValue(Result.ok([]));
624
+ vi.mocked(getModifiedFiles).mockResolvedValue(Result.ok([]));
625
+ vi.mocked(getStagedFiles).mockResolvedValue(Result.ok([]));
235
626
 
236
627
  const result = await formatDiffFrom('main', { silent: true });
237
- expect(result).toBe('ok');
628
+ expect(Result.isOk(result)).toBe(true);
238
629
 
239
630
  // Check file was formatted
240
631
  const content = await readTestFile(file1);
@@ -275,12 +666,14 @@ describe('formatDiffFrom', () => {
275
666
  vi.mocked(getUntrackedFiles).mockResolvedValue(
276
667
  Result.ok([untrackedFile]),
277
668
  );
669
+ vi.mocked(getModifiedFiles).mockResolvedValue(Result.ok([]));
670
+ vi.mocked(getStagedFiles).mockResolvedValue(Result.ok([]));
278
671
 
279
672
  const result = await formatDiffFrom('main', {
280
673
  includeUntracked: true,
281
674
  silent: true,
282
675
  });
283
- expect(result).toBe('ok');
676
+ expect(Result.isOk(result)).toBe(true);
284
677
 
285
678
  // Check both files were formatted
286
679
  const diffContent = await readTestFile(diffFile);
@@ -319,12 +712,14 @@ describe('formatDiffFrom', () => {
319
712
  // Mock both functions to return the same file
320
713
  vi.mocked(getDiffFrom).mockResolvedValue(Result.ok([sharedFile]));
321
714
  vi.mocked(getUntrackedFiles).mockResolvedValue(Result.ok([sharedFile]));
715
+ vi.mocked(getModifiedFiles).mockResolvedValue(Result.ok([]));
716
+ vi.mocked(getStagedFiles).mockResolvedValue(Result.ok([]));
322
717
 
323
718
  const result = await formatDiffFrom('main', {
324
719
  includeUntracked: true,
325
720
  silent: true,
326
721
  });
327
- expect(result).toBe('ok');
722
+ expect(Result.isOk(result)).toBe(true);
328
723
 
329
724
  // Verify both functions were called
330
725
  expect(getDiffFrom).toHaveBeenCalledWith('main', { silent: true });
@@ -341,4 +736,214 @@ describe('formatDiffFrom', () => {
341
736
  await fs.rm(testDir, { recursive: true, force: true });
342
737
  }
343
738
  });
739
+
740
+ test('should include both staged and untracked files by default', async () => {
741
+ vi.clearAllMocks();
742
+ await fs.mkdir(testDir, { recursive: true });
743
+
744
+ try {
745
+ const diffFile = await createTestFile(
746
+ 'diff.ts',
747
+ dedent`
748
+ const diff=true
749
+ `,
750
+ );
751
+
752
+ const stagedFile = await createTestFile(
753
+ 'staged.ts',
754
+ dedent`
755
+ const staged=true
756
+ `,
757
+ );
758
+
759
+ const untrackedFile = await createTestFile(
760
+ 'untracked.ts',
761
+ dedent`
762
+ const untracked=true
763
+ `,
764
+ );
765
+
766
+ // Mock all functions
767
+ vi.mocked(getDiffFrom).mockResolvedValue(Result.ok([diffFile]));
768
+ vi.mocked(getUntrackedFiles).mockResolvedValue(
769
+ Result.ok([untrackedFile]),
770
+ );
771
+ vi.mocked(getModifiedFiles).mockResolvedValue(Result.ok([]));
772
+ vi.mocked(getStagedFiles).mockResolvedValue(Result.ok([stagedFile]));
773
+
774
+ // Test default behavior (no options provided)
775
+ const result = await formatDiffFrom('main', { silent: true });
776
+ expect(Result.isOk(result)).toBe(true);
777
+
778
+ // Check all files were formatted
779
+ const diffContent = await readTestFile(diffFile);
780
+ expect(diffContent).toBe(
781
+ `${dedent`
782
+ const diff = true;
783
+ `}\n`,
784
+ );
785
+
786
+ const stagedContent = await readTestFile(stagedFile);
787
+ expect(stagedContent).toBe(
788
+ `${dedent`
789
+ const staged = true;
790
+ `}\n`,
791
+ );
792
+
793
+ const untrackedContent = await readTestFile(untrackedFile);
794
+ expect(untrackedContent).toBe(
795
+ `${dedent`
796
+ const untracked = true;
797
+ `}\n`,
798
+ );
799
+
800
+ // Verify all functions were called by default
801
+ expect(getDiffFrom).toHaveBeenCalledWith('main', { silent: true });
802
+ expect(getStagedFiles).toHaveBeenCalledWith({ silent: true });
803
+ expect(getUntrackedFiles).toHaveBeenCalledWith({ silent: true });
804
+ } finally {
805
+ await fs.rm(testDir, { recursive: true, force: true });
806
+ }
807
+ });
808
+
809
+ test('should include staged files when option is set', async () => {
810
+ vi.clearAllMocks();
811
+ await fs.mkdir(testDir, { recursive: true });
812
+
813
+ try {
814
+ const diffFile = await createTestFile(
815
+ 'diff.ts',
816
+ dedent`
817
+ const diff=true
818
+ `,
819
+ );
820
+
821
+ const stagedFile = await createTestFile(
822
+ 'staged.ts',
823
+ dedent`
824
+ const staged=true
825
+ `,
826
+ );
827
+
828
+ // Mock all functions
829
+ vi.mocked(getDiffFrom).mockResolvedValue(Result.ok([diffFile]));
830
+ vi.mocked(getUntrackedFiles).mockResolvedValue(Result.ok([]));
831
+ vi.mocked(getModifiedFiles).mockResolvedValue(Result.ok([]));
832
+ vi.mocked(getStagedFiles).mockResolvedValue(Result.ok([stagedFile]));
833
+
834
+ const result = await formatDiffFrom('main', {
835
+ includeStaged: true,
836
+ includeUntracked: false,
837
+ includeModified: false,
838
+ silent: true,
839
+ });
840
+ expect(Result.isOk(result)).toBe(true);
841
+
842
+ // Check both files were formatted
843
+ const diffContent = await readTestFile(diffFile);
844
+ expect(diffContent).toBe(
845
+ `${dedent`
846
+ const diff = true;
847
+ `}\n`,
848
+ );
849
+
850
+ const stagedContent = await readTestFile(stagedFile);
851
+ expect(stagedContent).toBe(
852
+ `${dedent`
853
+ const staged = true;
854
+ `}\n`,
855
+ );
856
+
857
+ expect(getDiffFrom).toHaveBeenCalledWith('main', { silent: true });
858
+ expect(getStagedFiles).toHaveBeenCalledWith({ silent: true });
859
+ expect(getUntrackedFiles).not.toHaveBeenCalled();
860
+ } finally {
861
+ await fs.rm(testDir, { recursive: true, force: true });
862
+ }
863
+ });
864
+
865
+ test('should deduplicate files when including both staged and untracked', async () => {
866
+ vi.clearAllMocks();
867
+ await fs.mkdir(testDir, { recursive: true });
868
+
869
+ try {
870
+ const sharedFile = await createTestFile(
871
+ 'shared.ts',
872
+ dedent`
873
+ const shared={value:1}
874
+ `,
875
+ );
876
+
877
+ // Mock all functions to return the same file
878
+ vi.mocked(getDiffFrom).mockResolvedValue(Result.ok([sharedFile]));
879
+ vi.mocked(getUntrackedFiles).mockResolvedValue(Result.ok([sharedFile]));
880
+ vi.mocked(getModifiedFiles).mockResolvedValue(Result.ok([]));
881
+ vi.mocked(getStagedFiles).mockResolvedValue(Result.ok([sharedFile]));
882
+
883
+ const result = await formatDiffFrom('main', {
884
+ includeUntracked: true,
885
+ includeStaged: true,
886
+ silent: true,
887
+ });
888
+ expect(Result.isOk(result)).toBe(true);
889
+
890
+ // Verify all functions were called
891
+ expect(getDiffFrom).toHaveBeenCalledWith('main', { silent: true });
892
+ expect(getUntrackedFiles).toHaveBeenCalledWith({ silent: true });
893
+ expect(getStagedFiles).toHaveBeenCalledWith({ silent: true });
894
+
895
+ // Check that the file was formatted (content should change)
896
+ const finalContent = await readTestFile(sharedFile);
897
+ expect(finalContent).toBe(
898
+ `${dedent`
899
+ const shared = { value: 1 };
900
+ `}\n`,
901
+ );
902
+ } finally {
903
+ await fs.rm(testDir, { recursive: true, force: true });
904
+ }
905
+ });
906
+
907
+ test('should exclude staged files when option is set to false', async () => {
908
+ vi.clearAllMocks();
909
+ await fs.mkdir(testDir, { recursive: true });
910
+
911
+ try {
912
+ const diffFile = await createTestFile(
913
+ 'diff.ts',
914
+ dedent`
915
+ const diff=true
916
+ `,
917
+ );
918
+
919
+ // Mock functions - staged should not be called
920
+ vi.mocked(getDiffFrom).mockResolvedValue(Result.ok([diffFile]));
921
+ vi.mocked(getUntrackedFiles).mockResolvedValue(Result.ok([]));
922
+ vi.mocked(getModifiedFiles).mockResolvedValue(Result.ok([]));
923
+ vi.mocked(getStagedFiles).mockResolvedValue(Result.ok([]));
924
+
925
+ const result = await formatDiffFrom('main', {
926
+ includeStaged: false,
927
+ includeUntracked: false,
928
+ includeModified: false,
929
+ silent: true,
930
+ });
931
+ expect(Result.isOk(result)).toBe(true);
932
+
933
+ // Check only diff file was formatted
934
+ const diffContent = await readTestFile(diffFile);
935
+ expect(diffContent).toBe(
936
+ `${dedent`
937
+ const diff = true;
938
+ `}\n`,
939
+ );
940
+
941
+ expect(getDiffFrom).toHaveBeenCalledWith('main', { silent: true });
942
+ expect(getStagedFiles).not.toHaveBeenCalled();
943
+ expect(getUntrackedFiles).not.toHaveBeenCalled();
944
+ expect(getModifiedFiles).not.toHaveBeenCalled();
945
+ } finally {
946
+ await fs.rm(testDir, { recursive: true, force: true });
947
+ }
948
+ });
344
949
  });