1import type { TurnDiffFileChange } from "../types";
2
3export interface TurnDiffStat {
4 additions: number;
5 deletions: number;
6}
7
8export interface TurnDiffTreeDirectoryNode {
9 kind: "directory";
10 name: string;
11 path: string;
12 stat: TurnDiffStat;
13 children: TurnDiffTreeNode[];
14}
15
16export interface TurnDiffTreeFileNode {
17 kind: "file";
18 name: string;
19 path: string;
20 stat: TurnDiffStat | null;
21}
22
23export type TurnDiffTreeNode = TurnDiffTreeDirectoryNode | TurnDiffTreeFileNode;
24
25interface MutableDirectoryNode {
26 name: string;
27 path: string;
28 stat: TurnDiffStat;
29 directories: Map<string, MutableDirectoryNode>;
30 files: TurnDiffTreeFileNode[];
31}
32
33let sortCollator: Intl.Collator | undefined;
34
35function normalizePathSegments(pathValue: string): string[] {
36 return pathValue.split("/").filter((segment) => segment.length > 0);
37}
38
39function compareByName(a: { name: string }, b: { name: string }): number {
40 // Defer ICU initialization until the first name comparison.
41 sortCollator ??= new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });
42 return sortCollator.compare(a.name, b.name);
43}
44
45function readStat(file: TurnDiffFileChange): TurnDiffStat | null {
46 if (typeof file.additions !== "number" || typeof file.deletions !== "number") {
47 return null;
48 }
49 return {
50 additions: file.additions,
51 deletions: file.deletions,
52 };
53}
54
55function compactDirectoryNode(node: TurnDiffTreeDirectoryNode): TurnDiffTreeDirectoryNode {
56 let compactedNode = node;
57
58 while (compactedNode.children.length === 1 && compactedNode.children[0]?.kind === "directory") {
59 const onlyChild = compactedNode.children[0];
60 compactedNode = {
61 kind: "directory",
62 name: `${compactedNode.name}/${onlyChild.name}`,
63 path: onlyChild.path,
64 stat: onlyChild.stat,
65 children: onlyChild.children,
66 };
67 }
68
69 return compactedNode;
70}
71
72function toTreeNodes(directory: MutableDirectoryNode): TurnDiffTreeNode[] {
73 const subdirectories: TurnDiffTreeDirectoryNode[] = Array.from(directory.directories.values())
74 .toSorted(compareByName)
75 .map<TurnDiffTreeDirectoryNode>((subdirectory) => ({
76 kind: "directory",
77 name: subdirectory.name,
78 path: subdirectory.path,
79 stat: {
80 additions: subdirectory.stat.additions,
81 deletions: subdirectory.stat.deletions,
82 },
83 children: toTreeNodes(subdirectory),
84 }))
85 .map((subdirectory) => compactDirectoryNode(subdirectory));
86
87 const files = directory.files.toSorted(compareByName);
88 return [...subdirectories, ...files];
89}
90
91export function buildTurnDiffTree(files: ReadonlyArray<TurnDiffFileChange>): TurnDiffTreeNode[] {
92 const root: MutableDirectoryNode = {
93 name: "",
94 path: "",
95 stat: { additions: 0, deletions: 0 },
96 directories: new Map(),
97 files: [],
98 };
99
100 for (const file of files) {
101 const segments = normalizePathSegments(file.path);
102 if (segments.length === 0) {
103 continue;
104 }
105
106 const filePath = segments.join("/");
107 const fileName = segments.at(-1);
108 if (!fileName) {
109 continue;
110 }
111 const stat = readStat(file);
112 const ancestors: MutableDirectoryNode[] = [root];
113 let currentDirectory = root;
114
115 for (const segment of segments.slice(0, -1)) {
116 const nextPath = currentDirectory.path ? `${currentDirectory.path}/${segment}` : segment;
117 const existing = currentDirectory.directories.get(segment);
118 if (existing) {
119 currentDirectory = existing;
120 } else {
121 const created: MutableDirectoryNode = {
122 name: segment,
123 path: nextPath,
124 stat: { additions: 0, deletions: 0 },
125 directories: new Map(),
126 files: [],
127 };
128 currentDirectory.directories.set(segment, created);
129 currentDirectory = created;
130 }
131 ancestors.push(currentDirectory);
132 }
133
134 currentDirectory.files.push({
135 kind: "file",
136 name: fileName,
137 path: filePath,
138 stat,
139 });
140
141 if (stat) {
142 for (const ancestor of ancestors) {
143 ancestor.stat.additions += stat.additions;
144 ancestor.stat.deletions += stat.deletions;
145 }
146 }
147 }
148
149 return toTreeNodes(root);
150}