/** * @file Folds the Changes panel's flat list of changed files into a folder * tree, JetBrains' commit-view shape: folders first, then files, each sorted * by name, and a chain of folders that hold nothing but one sub-folder drawn * as ONE row (`src/lib`), so a deep path does not cost a row per level. * @author Nguyễn Ngọc Trí Vĩ */ export interface ChangeTreeDir { /** The row's label — one segment, or several joined by `/` when compacted. */ name: string; /** The folder's repo-relative path ("" for the root). */ path: string; dirs: ChangeTreeDir[]; files: { name: string; file: T }[]; /** Every file anywhere below. */ count: number; } export function changeTree(rows: T[]): ChangeTreeDir { const root: ChangeTreeDir = { name: "", path: "", dirs: [], files: [], count: 0 }; for (const file of rows) { const parts = file.path.split("/"); let dir = root; dir.count++; for (const seg of parts.slice(0, -1)) { const path = dir.path ? `${dir.path}/${seg}` : seg; let next = dir.dirs.find((d) => d.path === path); if (!next) dir.dirs.push((next = { name: seg, path, dirs: [], files: [], count: 0 })); next.count++; dir = next; } dir.files.push({ name: parts[parts.length - 1] ?? file.path, file }); } const finish = (d: ChangeTreeDir): ChangeTreeDir => { // Never the root: its children are the group's top level. let only: ChangeTreeDir | undefined; while (d.path && d.files.length === 0 && d.dirs.length === 1 && (only = d.dirs[0])) { d = { ...only, name: `${d.name}/${only.name}` }; } d.dirs = d.dirs.map(finish).sort((a, b) => a.name.localeCompare(b.name)); d.files.sort((a, b) => a.name.localeCompare(b.name)); return d; }; return finish(root); } /** Every file under a folder, for its stage/unstage-all button. */ export function treeFiles(d: ChangeTreeDir): T[] { return [...d.files.map((f) => f.file), ...d.dirs.flatMap(treeFiles)]; }