/** * Pure helper that folds a flat list of changed file paths into a directory * tree, so the Git panel can show changes as a file tree (L8 visual). Kept free * of React for straightforward unit testing. */ import type { GitFileStatus } from "@/domain"; /** A node in the changed-files tree: a directory or a file leaf. */ export interface GitTreeNode { /** Last path segment (folder or file name). */ name: string; /** Full repo-relative path of this node. */ path: string; /** Whether this node is a directory (has children) or a file leaf. */ isDir: boolean; /** For file leaves: whether the change is staged. */ staged: boolean; /** Child nodes (empty for files). */ children: GitTreeNode[]; } /** * Builds the directory tree for a set of changed files. Directories come before * files at each level, both alphabetically sorted. */ export function buildFileTree(files: GitFileStatus[]): GitTreeNode[] { const root: GitTreeNode = { name: "", path: "", isDir: true, staged: false, children: [], }; for (const file of files) { const parts = file.path.split("/").filter(Boolean); let node = root; parts.forEach((part, i) => { const isLeaf = i === parts.length - 1; const path = parts.slice(0, i + 1).join("/"); let child = node.children.find( (c) => c.name === part && c.isDir === !isLeaf, ); if (!child) { child = { name: part, path, isDir: !isLeaf, staged: file.staged, children: [], }; node.children.push(child); } node = child; }); } sort(root); return root.children; } function sort(node: GitTreeNode): void { node.children.sort((a, b) => { if (a.isDir !== b.isDir) return a.isDir ? -1 : 1; return a.name.localeCompare(b.name); }); node.children.forEach(sort); }