critique 0.1.127 → 0.1.129

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.
@@ -1 +1 @@
1
- {"version":3,"file":"diff-utils.d.ts","sourceRoot":"","sources":["../src/diff-utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"diff-utils.d.ts","sourceRoot":"","sources":["../src/diff-utils.ts"],"names":[],"mappings":"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"}
@@ -2,6 +2,7 @@
2
2
  // Builds git commands, parses diff files, detects filetypes for syntax highlighting,
3
3
  // and provides helpers for unified/split view mode selection.
4
4
  import { execSync } from "child_process";
5
+ import { buildDirectoryTree } from "./directory-tree.js";
5
6
  /**
6
7
  * Strip submodule status lines from git diff output.
7
8
  * git diff --submodule=diff adds various status lines that the diff parser doesn't understand:
@@ -414,11 +415,41 @@ export function processFiles(files, formatPatch) {
414
415
  const totalLines = file.hunks.reduce((sum, hunk) => sum + hunk.lines.length, 0);
415
416
  return totalLines <= 6000;
416
417
  });
417
- const sortedFiles = filteredFiles.sort((a, b) => {
418
- const aSize = a.hunks.reduce((sum, hunk) => sum + hunk.lines.length, 0);
419
- const bSize = b.hunks.reduce((sum, hunk) => sum + hunk.lines.length, 0);
420
- return aSize - bSize;
418
+ const treeFiles = filteredFiles.map((file, index) => {
419
+ const { additions, deletions } = countChanges(file.hunks);
420
+ return {
421
+ path: getFileName(file),
422
+ status: getFileStatus(file),
423
+ additions,
424
+ deletions,
425
+ fileIndex: index,
426
+ };
421
427
  });
428
+ const treeFileOrder = buildDirectoryTree(treeFiles)
429
+ .filter((node) => node.isFile && node.fileIndex !== undefined)
430
+ .map((node) => node.fileIndex);
431
+ const seenIndexes = new Set();
432
+ const sortedFiles = [];
433
+ for (const index of treeFileOrder) {
434
+ if (seenIndexes.has(index))
435
+ continue;
436
+ const file = filteredFiles[index];
437
+ if (!file)
438
+ continue;
439
+ seenIndexes.add(index);
440
+ sortedFiles.push(file);
441
+ }
442
+ // Defensive fallback: keep any unmatched files in original order.
443
+ // This should be rare, but avoids dropping files if tree metadata and
444
+ // parsed file list ever diverge.
445
+ for (let index = 0; index < filteredFiles.length; index++) {
446
+ if (seenIndexes.has(index))
447
+ continue;
448
+ const file = filteredFiles[index];
449
+ if (!file)
450
+ continue;
451
+ sortedFiles.push(file);
452
+ }
422
453
  // Add rawDiff for each file
423
454
  return sortedFiles.map((file) => ({
424
455
  ...file,
@@ -4,7 +4,70 @@
4
4
  // extracts rename metadata for all rename/copy sections.
5
5
  import { describe, expect, it } from "bun:test";
6
6
  import { parsePatch, formatPatch } from "diff";
7
- import { preprocessDiff, parseGitDiffFiles, getFileStatus, getFileName, getOldFileName, buildGitCommand, buildSubmoduleDiffCommand, filterParsedFilesByPatterns, getFilterPatterns, matchesFileFilters, detectFiletype, } from "./diff-utils.js";
7
+ import { preprocessDiff, parseGitDiffFiles, processFiles, getFileStatus, getFileName, getOldFileName, buildGitCommand, buildSubmoduleDiffCommand, filterParsedFilesByPatterns, getFilterPatterns, matchesFileFilters, detectFiletype, } from "./diff-utils.js";
8
+ // ============================================================================
9
+ // processFiles ordering
10
+ // ============================================================================
11
+ describe("processFiles ordering", () => {
12
+ it("should order output files to match directory tree traversal", () => {
13
+ const files = [
14
+ {
15
+ oldFileName: "src/components/button.tsx",
16
+ newFileName: "src/components/button.tsx",
17
+ hunks: [{ lines: Array.from({ length: 90 }, () => "+line") }],
18
+ },
19
+ {
20
+ oldFileName: "src/index.ts",
21
+ newFileName: "src/index.ts",
22
+ hunks: [{ lines: ["+line"] }],
23
+ },
24
+ {
25
+ oldFileName: "README.md",
26
+ newFileName: "README.md",
27
+ hunks: [{ lines: ["+line", "+line", "+line"] }],
28
+ },
29
+ ];
30
+ const processed = processFiles(files, (file) => `diff --git ${file.oldFileName} ${file.newFileName}`);
31
+ expect(processed.map((file) => getFileName(file))).toEqual([
32
+ "README.md",
33
+ "src/components/button.tsx",
34
+ "src/index.ts",
35
+ ]);
36
+ });
37
+ it("should keep tree order with renamed, added, and deleted files", () => {
38
+ const files = [
39
+ {
40
+ oldFileName: "src/alpha.ts",
41
+ newFileName: "src/alpha.ts",
42
+ hunks: [{ lines: Array.from({ length: 30 }, () => "+line") }],
43
+ },
44
+ {
45
+ oldFileName: "docs/old-name.md",
46
+ newFileName: "docs/new-name.md",
47
+ renameFrom: "docs/old-name.md",
48
+ renameTo: "docs/new-name.md",
49
+ hunks: [{ lines: ["+line"] }],
50
+ },
51
+ {
52
+ oldFileName: "/dev/null",
53
+ newFileName: "docs/guide.md",
54
+ hunks: [{ lines: ["+line", "+line"] }],
55
+ },
56
+ {
57
+ oldFileName: "src/remove.ts",
58
+ newFileName: "/dev/null",
59
+ hunks: [{ lines: ["-line"] }],
60
+ },
61
+ ];
62
+ const processed = processFiles(files, (file) => `diff --git ${file.oldFileName} ${file.newFileName}`);
63
+ expect(processed.map((file) => getFileName(file))).toEqual([
64
+ "docs/guide.md",
65
+ "docs/new-name.md",
66
+ "src/alpha.ts",
67
+ "src/remove.ts",
68
+ ]);
69
+ });
70
+ });
8
71
  // ============================================================================
9
72
  // preprocessDiff
10
73
  // ============================================================================
@@ -1 +1 @@
1
- {"version":3,"file":"directory-tree.d.ts","sourceRoot":"","sources":["../src/directory-tree.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAA;AAErE;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,UAAU,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,8DAA8D;IAC9D,WAAW,EAAE,MAAM,CAAA;IACnB,yDAAyD;IACzD,MAAM,EAAE,OAAO,CAAA;IACf,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAA;IACjB,iDAAiD;IACjD,MAAM,EAAE,MAAM,CAAA;CACf;AAYD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,QAAQ,EAAE,CAOpE"}
1
+ {"version":3,"file":"directory-tree.d.ts","sourceRoot":"","sources":["../src/directory-tree.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAA;AAErE;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,UAAU,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,8DAA8D;IAC9D,WAAW,EAAE,MAAM,CAAA;IACnB,yDAAyD;IACzD,MAAM,EAAE,OAAO,CAAA;IACf,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAA;IACjB,iDAAiD;IACjD,MAAM,EAAE,MAAM,CAAA;CACf;AAYD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,QAAQ,EAAE,CAQpE"}
@@ -11,6 +11,7 @@ export function buildDirectoryTree(files) {
11
11
  return [];
12
12
  }
13
13
  const tree = buildInternalTree(files);
14
+ sortInternalTree(tree);
14
15
  return flattenTree(tree);
15
16
  }
16
17
  /**
@@ -55,6 +56,18 @@ function getName(node) {
55
56
  const parts = node.path.split("/");
56
57
  return parts[parts.length - 1] || node.path;
57
58
  }
59
+ /**
60
+ * Sort tree nodes by name at every level so file ordering is deterministic
61
+ * and independent from incoming git diff section order.
62
+ */
63
+ function sortInternalTree(nodes) {
64
+ nodes.sort((a, b) => getName(a).toLowerCase().localeCompare(getName(b).toLowerCase()));
65
+ for (const node of nodes) {
66
+ if (node.children.length > 0) {
67
+ sortInternalTree(node.children);
68
+ }
69
+ }
70
+ }
58
71
  /**
59
72
  * Collapse directories that only contain a single subdirectory (no files)
60
73
  */
@@ -44,6 +44,28 @@ describe("buildDirectoryTree", () => {
44
44
  expect(result[1].displayPath).toBe("Button.tsx");
45
45
  expect(result[1].isFile).toBe(true);
46
46
  });
47
+ it("should sort nodes alphabetically regardless of input order", () => {
48
+ const files = [
49
+ { path: "website/src/index.ts", status: "modified", additions: 1, deletions: 0 },
50
+ { path: "db/schema.prisma", status: "modified", additions: 1, deletions: 0 },
51
+ { path: "discord/src/utils.ts", status: "modified", additions: 1, deletions: 0 },
52
+ { path: "discord/src/cli.ts", status: "modified", additions: 1, deletions: 0 },
53
+ { path: "gateway-proxy/src/main.rs", status: "modified", additions: 1, deletions: 0 },
54
+ ];
55
+ const result = buildDirectoryTree(files);
56
+ const rendered = result.map((node) => `${node.prefix}${node.connector}${node.displayPath}`);
57
+ expect(rendered).toEqual([
58
+ "├── db",
59
+ "│ └── schema.prisma",
60
+ "├── discord/src",
61
+ "│ ├── cli.ts",
62
+ "│ └── utils.ts",
63
+ "├── gateway-proxy/src",
64
+ "│ └── main.rs",
65
+ "└── website/src",
66
+ " └── index.ts",
67
+ ]);
68
+ });
47
69
  });
48
70
  describe("TreeRenderer visual tests", () => {
49
71
  let testSetup;
@@ -166,10 +188,10 @@ describe("TreeRenderer visual tests", () => {
166
188
  expect(frame).toMatchInlineSnapshot(`
167
189
  "├── package.json (+2,-1)
168
190
  ├── src
169
- │ ├── index.ts (+10,-5)
170
191
  │ ├── components
171
192
  │ │ ├── Button.tsx (+50,-0)
172
193
  │ │ └── Input.tsx (+15,-8)
194
+ │ ├── index.ts (+10,-5)
173
195
  │ └── utils
174
196
  │ └── helpers.ts (+0,-30)
175
197
  └── tests
@@ -197,8 +219,8 @@ describe("TreeRenderer visual tests", () => {
197
219
  const frame = testSetup.captureCharFrame();
198
220
  expect(frame).toMatchInlineSnapshot(`
199
221
  "└── packages/core/src/lib/utils
200
- ├── helpers.ts (+5,-3)
201
- └── format.ts (+20,-0)
222
+ ├── format.ts (+20,-0)
223
+ └── helpers.ts (+5,-3)
202
224
 
203
225
 
204
226
 
@@ -222,14 +244,14 @@ describe("TreeRenderer visual tests", () => {
222
244
  await testSetup.renderOnce();
223
245
  const frame = testSetup.captureCharFrame();
224
246
  expect(frame).toMatchInlineSnapshot(`
225
- "├── src
226
- ├── api
227
- │ │ ├── routes.ts (+10,-5)
228
- │ │ └── handlers.ts (+30,-0)
229
- └── db
230
- └── models.ts (+8,-2)
231
- └── lib
232
- └── utils.ts (+15,-0)
247
+ "├── lib
248
+ └── utils.ts (+15,-0)
249
+ └── src
250
+ ├── api
251
+ ├── handlers.ts (+30,-0)
252
+ └── routes.ts (+10,-5)
253
+ └── db
254
+ └── models.ts (+8,-2)
233
255
 
234
256
 
235
257
 
@@ -256,10 +278,10 @@ describe("DirectoryTreeView component", () => {
256
278
  await testSetup.renderOnce();
257
279
  const frame = testSetup.captureCharFrame();
258
280
  expect(frame).toMatchInlineSnapshot(`
259
- " ├── src
260
- │ ├── index.ts (+5,-2)
261
- │ └── utils.ts (+30)
262
- └── README.md (-15)
281
+ " ├── README.md (-15)
282
+ └── src
283
+ ├── index.ts (+5,-2)
284
+ └── utils.ts (+30)
263
285
 
264
286
 
265
287
 
@@ -176,6 +176,10 @@ describe("--stdin pager mode (lazygit issue #25)", () => {
176
176
  const trimmed = await session.text({ trimEnd: true });
177
177
  expect(trimmed).toMatchInlineSnapshot(`
178
178
  "
179
+ └── b/src
180
+ └── hello.ts (+1,-1)
181
+
182
+
179
183
  a/src/hello.ts → b/src/hello.ts +1-1
180
184
 
181
185
  1 const greeting = 'hello'
@@ -209,6 +213,10 @@ describe("--stdin pager mode (lazygit issue #25)", () => {
209
213
  const trimmed = await session.text({ trimEnd: true, immediate: true });
210
214
  expect(trimmed).toMatchInlineSnapshot(`
211
215
  "
216
+ └── b
217
+ └── readme.md ()
218
+
219
+
212
220
  a/readme.md → b/readme.md +0-0
213
221
 
214
222
  1 # My Project
@@ -224,6 +232,11 @@ describe("--stdin pager mode (lazygit issue #25)", () => {
224
232
  const trimmed = await session.text({ trimEnd: true });
225
233
  expect(trimmed).toMatchInlineSnapshot(`
226
234
  "
235
+ └── b/src
236
+ ├── logger.ts (+5)
237
+ └── index.ts (+2)
238
+
239
+
227
240
  b/src/logger.ts +5-0
228
241
 
229
242
  1 + export class Logger {
@@ -258,6 +271,10 @@ describe("--stdin pager mode (lazygit issue #25)", () => {
258
271
  const trimmed = await session.text({ trimEnd: true });
259
272
  expect(trimmed).toMatchInlineSnapshot(`
260
273
  "
274
+ └── a/src
275
+ └── deprecated.ts (-4)
276
+
277
+
261
278
  a/src/deprecated.ts +0-4
262
279
 
263
280
  1 - // This module is no longer needed
@@ -276,6 +293,10 @@ describe("--stdin pager mode (lazygit issue #25)", () => {
276
293
  const trimmed = await session.text({ trimEnd: true });
277
294
  expect(trimmed).toMatchInlineSnapshot(`
278
295
  "
296
+ └── b/src
297
+ └── utils.ts (+7)
298
+
299
+
279
300
  b/src/utils.ts +7-0
280
301
 
281
302
  1 + export function clamp(value: number, min: number, max: number): number {
@@ -298,6 +319,10 @@ describe("--stdin pager mode (lazygit issue #25)", () => {
298
319
  const trimmed = await session.text({ trimEnd: true });
299
320
  expect(trimmed).toMatchInlineSnapshot(`
300
321
  "
322
+ └── b
323
+ └── config.json (+6,-4)
324
+
325
+
301
326
  a/config.json → b/config.json +6-4
302
327
 
303
328
  1 {
@@ -328,6 +353,10 @@ describe("--stdin pager mode (lazygit issue #25)", () => {
328
353
  const trimmed = await session.text({ trimEnd: true });
329
354
  expect(trimmed).toMatchInlineSnapshot(`
330
355
  "
356
+ └── src
357
+ └── new-name.ts (+1,-1)
358
+
359
+
331
360
  src/old-name.ts → src/new-name.ts +1-1
332
361
 
333
362
  1 - export const name = 'old'
@@ -349,6 +378,9 @@ describe("--stdin pager mode (lazygit issue #25)", () => {
349
378
  const trimmed = await session.text({ trimEnd: true, immediate: true });
350
379
  expect(trimmed).toMatchInlineSnapshot(`
351
380
  "
381
+ └── unknown ()
382
+
383
+
352
384
  unknown +0-0"
353
385
  `);
354
386
  expect(trimmed).not.toContain("URL is private");
@@ -365,6 +397,10 @@ describe("--stdin pager mode (lazygit issue #25)", () => {
365
397
  expect(trimmed).not.toContain("unknown");
366
398
  expect(trimmed).toMatchInlineSnapshot(`
367
399
  "
400
+ └── b/src
401
+ └── hello.ts (+1,-1)
402
+
403
+
368
404
  a/src/hello.ts → b/src/hello.ts +1-1
369
405
 
370
406
  1 const greeting = 'hello'
@@ -381,6 +417,10 @@ describe("--stdin pager mode (lazygit issue #25)", () => {
381
417
  const trimmed = await session.text({ trimEnd: true });
382
418
  expect(trimmed).toMatchInlineSnapshot(`
383
419
  "
420
+ └── b/src
421
+ └── hello.ts (+1,-1)
422
+
423
+
384
424
  a/src/hello.ts → b/src/hello.ts +1-1
385
425
 
386
426
  1 const greeting = 'hello'
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "critique",
3
3
  "module": "src/diff.tsx",
4
4
  "type": "module",
5
- "version": "0.1.127",
5
+ "version": "0.1.129",
6
6
  "license": "MIT",
7
7
  "private": false,
8
8
  "bin": "./dist/cli.js",
@@ -8,6 +8,7 @@ import { parsePatch, formatPatch } from "diff"
8
8
  import {
9
9
  preprocessDiff,
10
10
  parseGitDiffFiles,
11
+ processFiles,
11
12
  getFileStatus,
12
13
  getFileName,
13
14
  getOldFileName,
@@ -19,6 +20,82 @@ import {
19
20
  detectFiletype,
20
21
  } from "./diff-utils.js"
21
22
 
23
+ // ============================================================================
24
+ // processFiles ordering
25
+ // ============================================================================
26
+
27
+ describe("processFiles ordering", () => {
28
+ it("should order output files to match directory tree traversal", () => {
29
+ const files = [
30
+ {
31
+ oldFileName: "src/components/button.tsx",
32
+ newFileName: "src/components/button.tsx",
33
+ hunks: [{ lines: Array.from({ length: 90 }, () => "+line") }],
34
+ },
35
+ {
36
+ oldFileName: "src/index.ts",
37
+ newFileName: "src/index.ts",
38
+ hunks: [{ lines: ["+line"] }],
39
+ },
40
+ {
41
+ oldFileName: "README.md",
42
+ newFileName: "README.md",
43
+ hunks: [{ lines: ["+line", "+line", "+line"] }],
44
+ },
45
+ ]
46
+
47
+ const processed = processFiles(
48
+ files,
49
+ (file) => `diff --git ${file.oldFileName} ${file.newFileName}`,
50
+ )
51
+
52
+ expect(processed.map((file) => getFileName(file))).toEqual([
53
+ "README.md",
54
+ "src/components/button.tsx",
55
+ "src/index.ts",
56
+ ])
57
+ })
58
+
59
+ it("should keep tree order with renamed, added, and deleted files", () => {
60
+ const files = [
61
+ {
62
+ oldFileName: "src/alpha.ts",
63
+ newFileName: "src/alpha.ts",
64
+ hunks: [{ lines: Array.from({ length: 30 }, () => "+line") }],
65
+ },
66
+ {
67
+ oldFileName: "docs/old-name.md",
68
+ newFileName: "docs/new-name.md",
69
+ renameFrom: "docs/old-name.md",
70
+ renameTo: "docs/new-name.md",
71
+ hunks: [{ lines: ["+line"] }],
72
+ },
73
+ {
74
+ oldFileName: "/dev/null",
75
+ newFileName: "docs/guide.md",
76
+ hunks: [{ lines: ["+line", "+line"] }],
77
+ },
78
+ {
79
+ oldFileName: "src/remove.ts",
80
+ newFileName: "/dev/null",
81
+ hunks: [{ lines: ["-line"] }],
82
+ },
83
+ ]
84
+
85
+ const processed = processFiles(
86
+ files,
87
+ (file) => `diff --git ${file.oldFileName} ${file.newFileName}`,
88
+ )
89
+
90
+ expect(processed.map((file) => getFileName(file))).toEqual([
91
+ "docs/guide.md",
92
+ "docs/new-name.md",
93
+ "src/alpha.ts",
94
+ "src/remove.ts",
95
+ ])
96
+ })
97
+ })
98
+
22
99
  // ============================================================================
23
100
  // preprocessDiff
24
101
  // ============================================================================
package/src/diff-utils.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  // and provides helpers for unified/split view mode selection.
4
4
 
5
5
  import { execSync } from "child_process"
6
+ import { buildDirectoryTree } from "./directory-tree.js"
6
7
 
7
8
  /**
8
9
  * Strip submodule status lines from git diff output.
@@ -515,11 +516,41 @@ export function processFiles<T extends ParsedFile>(
515
516
  return totalLines <= 6000;
516
517
  });
517
518
 
518
- const sortedFiles = filteredFiles.sort((a, b) => {
519
- const aSize = a.hunks.reduce((sum, hunk) => sum + hunk.lines.length, 0);
520
- const bSize = b.hunks.reduce((sum, hunk) => sum + hunk.lines.length, 0);
521
- return aSize - bSize;
522
- });
519
+ const treeFiles = filteredFiles.map((file, index) => {
520
+ const { additions, deletions } = countChanges(file.hunks)
521
+ return {
522
+ path: getFileName(file),
523
+ status: getFileStatus(file),
524
+ additions,
525
+ deletions,
526
+ fileIndex: index,
527
+ }
528
+ })
529
+
530
+ const treeFileOrder = buildDirectoryTree(treeFiles)
531
+ .filter((node) => node.isFile && node.fileIndex !== undefined)
532
+ .map((node) => node.fileIndex!)
533
+
534
+ const seenIndexes = new Set<number>()
535
+ const sortedFiles: T[] = []
536
+
537
+ for (const index of treeFileOrder) {
538
+ if (seenIndexes.has(index)) continue
539
+ const file = filteredFiles[index]
540
+ if (!file) continue
541
+ seenIndexes.add(index)
542
+ sortedFiles.push(file)
543
+ }
544
+
545
+ // Defensive fallback: keep any unmatched files in original order.
546
+ // This should be rare, but avoids dropping files if tree metadata and
547
+ // parsed file list ever diverge.
548
+ for (let index = 0; index < filteredFiles.length; index++) {
549
+ if (seenIndexes.has(index)) continue
550
+ const file = filteredFiles[index]
551
+ if (!file) continue
552
+ sortedFiles.push(file)
553
+ }
523
554
 
524
555
  // Add rawDiff for each file
525
556
  return sortedFiles.map((file) => ({
@@ -60,6 +60,31 @@ describe("buildDirectoryTree", () => {
60
60
  expect(result[1]!.displayPath).toBe("Button.tsx")
61
61
  expect(result[1]!.isFile).toBe(true)
62
62
  })
63
+
64
+ it("should sort nodes alphabetically regardless of input order", () => {
65
+ const files: TreeFileInfo[] = [
66
+ { path: "website/src/index.ts", status: "modified", additions: 1, deletions: 0 },
67
+ { path: "db/schema.prisma", status: "modified", additions: 1, deletions: 0 },
68
+ { path: "discord/src/utils.ts", status: "modified", additions: 1, deletions: 0 },
69
+ { path: "discord/src/cli.ts", status: "modified", additions: 1, deletions: 0 },
70
+ { path: "gateway-proxy/src/main.rs", status: "modified", additions: 1, deletions: 0 },
71
+ ]
72
+
73
+ const result = buildDirectoryTree(files)
74
+
75
+ const rendered = result.map((node) => `${node.prefix}${node.connector}${node.displayPath}`)
76
+ expect(rendered).toEqual([
77
+ "├── db",
78
+ "│ └── schema.prisma",
79
+ "├── discord/src",
80
+ "│ ├── cli.ts",
81
+ "│ └── utils.ts",
82
+ "├── gateway-proxy/src",
83
+ "│ └── main.rs",
84
+ "└── website/src",
85
+ " └── index.ts",
86
+ ])
87
+ })
63
88
  })
64
89
 
65
90
  describe("TreeRenderer visual tests", () => {
@@ -199,10 +224,10 @@ describe("TreeRenderer visual tests", () => {
199
224
  expect(frame).toMatchInlineSnapshot(`
200
225
  "├── package.json (+2,-1)
201
226
  ├── src
202
- │ ├── index.ts (+10,-5)
203
227
  │ ├── components
204
228
  │ │ ├── Button.tsx (+50,-0)
205
229
  │ │ └── Input.tsx (+15,-8)
230
+ │ ├── index.ts (+10,-5)
206
231
  │ └── utils
207
232
  │ └── helpers.ts (+0,-30)
208
233
  └── tests
@@ -233,8 +258,8 @@ describe("TreeRenderer visual tests", () => {
233
258
  const frame = testSetup.captureCharFrame()
234
259
  expect(frame).toMatchInlineSnapshot(`
235
260
  "└── packages/core/src/lib/utils
236
- ├── helpers.ts (+5,-3)
237
- └── format.ts (+20,-0)
261
+ ├── format.ts (+20,-0)
262
+ └── helpers.ts (+5,-3)
238
263
 
239
264
 
240
265
 
@@ -261,14 +286,14 @@ describe("TreeRenderer visual tests", () => {
261
286
  await testSetup.renderOnce()
262
287
  const frame = testSetup.captureCharFrame()
263
288
  expect(frame).toMatchInlineSnapshot(`
264
- "├── src
265
- ├── api
266
- │ │ ├── routes.ts (+10,-5)
267
- │ │ └── handlers.ts (+30,-0)
268
- └── db
269
- └── models.ts (+8,-2)
270
- └── lib
271
- └── utils.ts (+15,-0)
289
+ "├── lib
290
+ └── utils.ts (+15,-0)
291
+ └── src
292
+ ├── api
293
+ ├── handlers.ts (+30,-0)
294
+ └── routes.ts (+10,-5)
295
+ └── db
296
+ └── models.ts (+8,-2)
272
297
 
273
298
 
274
299
 
@@ -303,10 +328,10 @@ describe("DirectoryTreeView component", () => {
303
328
  await testSetup.renderOnce()
304
329
  const frame = testSetup.captureCharFrame()
305
330
  expect(frame).toMatchInlineSnapshot(`
306
- " ├── src
307
- │ ├── index.ts (+5,-2)
308
- │ └── utils.ts (+30)
309
- └── README.md (-15)
331
+ " ├── README.md (-15)
332
+ └── src
333
+ ├── index.ts (+5,-2)
334
+ └── utils.ts (+30)
310
335
 
311
336
 
312
337
 
@@ -62,6 +62,7 @@ export function buildDirectoryTree(files: TreeFileInfo[]): TreeNode[] {
62
62
  }
63
63
 
64
64
  const tree = buildInternalTree(files)
65
+ sortInternalTree(tree)
65
66
  return flattenTree(tree)
66
67
  }
67
68
 
@@ -115,6 +116,19 @@ function getName(node: InternalTreeNode): string {
115
116
  return parts[parts.length - 1] || node.path
116
117
  }
117
118
 
119
+ /**
120
+ * Sort tree nodes by name at every level so file ordering is deterministic
121
+ * and independent from incoming git diff section order.
122
+ */
123
+ function sortInternalTree(nodes: InternalTreeNode[]): void {
124
+ nodes.sort((a, b) => getName(a).toLowerCase().localeCompare(getName(b).toLowerCase()))
125
+ for (const node of nodes) {
126
+ if (node.children.length > 0) {
127
+ sortInternalTree(node.children)
128
+ }
129
+ }
130
+ }
131
+
118
132
  /**
119
133
  * Collapse directories that only contain a single subdirectory (no files)
120
134
  */
@@ -194,6 +194,10 @@ describe("--stdin pager mode (lazygit issue #25)", () => {
194
194
 
195
195
  expect(trimmed).toMatchInlineSnapshot(`
196
196
  "
197
+ └── b/src
198
+ └── hello.ts (+1,-1)
199
+
200
+
197
201
  a/src/hello.ts → b/src/hello.ts +1-1
198
202
 
199
203
  1 const greeting = 'hello'
@@ -235,6 +239,10 @@ describe("--stdin pager mode (lazygit issue #25)", () => {
235
239
 
236
240
  expect(trimmed).toMatchInlineSnapshot(`
237
241
  "
242
+ └── b
243
+ └── readme.md ()
244
+
245
+
238
246
  a/readme.md → b/readme.md +0-0
239
247
 
240
248
  1 # My Project
@@ -253,6 +261,11 @@ describe("--stdin pager mode (lazygit issue #25)", () => {
253
261
 
254
262
  expect(trimmed).toMatchInlineSnapshot(`
255
263
  "
264
+ └── b/src
265
+ ├── logger.ts (+5)
266
+ └── index.ts (+2)
267
+
268
+
256
269
  b/src/logger.ts +5-0
257
270
 
258
271
  1 + export class Logger {
@@ -292,6 +305,10 @@ describe("--stdin pager mode (lazygit issue #25)", () => {
292
305
 
293
306
  expect(trimmed).toMatchInlineSnapshot(`
294
307
  "
308
+ └── a/src
309
+ └── deprecated.ts (-4)
310
+
311
+
295
312
  a/src/deprecated.ts +0-4
296
313
 
297
314
  1 - // This module is no longer needed
@@ -313,6 +330,10 @@ describe("--stdin pager mode (lazygit issue #25)", () => {
313
330
 
314
331
  expect(trimmed).toMatchInlineSnapshot(`
315
332
  "
333
+ └── b/src
334
+ └── utils.ts (+7)
335
+
336
+
316
337
  b/src/utils.ts +7-0
317
338
 
318
339
  1 + export function clamp(value: number, min: number, max: number): number {
@@ -338,6 +359,10 @@ describe("--stdin pager mode (lazygit issue #25)", () => {
338
359
 
339
360
  expect(trimmed).toMatchInlineSnapshot(`
340
361
  "
362
+ └── b
363
+ └── config.json (+6,-4)
364
+
365
+
341
366
  a/config.json → b/config.json +6-4
342
367
 
343
368
  1 {
@@ -371,6 +396,10 @@ describe("--stdin pager mode (lazygit issue #25)", () => {
371
396
 
372
397
  expect(trimmed).toMatchInlineSnapshot(`
373
398
  "
399
+ └── src
400
+ └── new-name.ts (+1,-1)
401
+
402
+
374
403
  src/old-name.ts → src/new-name.ts +1-1
375
404
 
376
405
  1 - export const name = 'old'
@@ -396,6 +425,9 @@ describe("--stdin pager mode (lazygit issue #25)", () => {
396
425
 
397
426
  expect(trimmed).toMatchInlineSnapshot(`
398
427
  "
428
+ └── unknown ()
429
+
430
+
399
431
  unknown +0-0"
400
432
  `)
401
433
 
@@ -416,6 +448,10 @@ describe("--stdin pager mode (lazygit issue #25)", () => {
416
448
 
417
449
  expect(trimmed).toMatchInlineSnapshot(`
418
450
  "
451
+ └── b/src
452
+ └── hello.ts (+1,-1)
453
+
454
+
419
455
  a/src/hello.ts → b/src/hello.ts +1-1
420
456
 
421
457
  1 const greeting = 'hello'
@@ -435,6 +471,10 @@ describe("--stdin pager mode (lazygit issue #25)", () => {
435
471
 
436
472
  expect(trimmed).toMatchInlineSnapshot(`
437
473
  "
474
+ └── b/src
475
+ └── hello.ts (+1,-1)
476
+
477
+
438
478
  a/src/hello.ts → b/src/hello.ts +1-1
439
479
 
440
480
  1 const greeting = 'hello'