/** * @file nested-repos.js * @description The git repositories INSIDE a workspace folder — a parent * folder holding several repos side by side (`~/work/shop/{api,web,admin}`), * each with its own `.git` — so the Changes panel can show one section per * repo, like VS Code's Source Control. Found by reading the disk only (no git * call): a directory holding a `.git` entry (a directory, or the file a * submodule or linked worktree has) is a repo, and the walk does not go * inside one. At most `MAX_DEPTH` levels below the workspace folder, never * through a symlink, never into a dot directory or `node_modules`/`vendor`, * at most `MAX_REPOS` repos and `MAX_ENTRIES` directory entries read, so a * folder with a huge tree (or a member flooding theirs) costs a bounded read. * Cached per folder for `TTL_MS`; `refresh` reads again. * * `repoFolder` is the git routes' `?repo=`: the request names a path * RELATIVE to the workspace folder, and it is accepted only when it is one of * the repos this walk found — never a path the walk did not produce, so the * request can no more point the git routes at an arbitrary directory than * `?source=` can. * @author Nguyễn Ngọc Trí Vĩ */ const fs = require("node:fs"); const path = require("node:path"); const MAX_DEPTH = 2; const MAX_REPOS = 50; const MAX_ENTRIES = 5000; const TTL_MS = 30_000; const REREAD_MS = 2_000; const SKIP = new Set(["node_modules", "vendor", "dist", "build", "target", "__pycache__"]); const cache = new Map(); function fail(code, message) { return Object.assign(new Error(message), { code }); } async function hasGit(dir) { try { await fs.promises.lstat(path.join(dir, ".git")); return true; } catch { return false; } } /** Whether `dir` or a folder above it holds a `.git` — the folder is itself in a repo. */ async function insideRepo(dir) { for (let d = dir; ;) { if (await hasGit(d)) return true; const up = path.dirname(d); if (up === d) return false; d = up; } } async function walk(root) { const repos = []; let budget = MAX_ENTRIES; let truncated = false; const visit = async (dir, depth) => { let handle; try { handle = await fs.promises.opendir(dir); } catch { return; // unreadable: not ours to report } const children = []; try { for await (const ent of handle) { if (--budget < 0) { truncated = true; break; } // `isDirectory()` is false for a symlink: a link is never followed. if (!ent.isDirectory() || ent.name.startsWith(".") || SKIP.has(ent.name)) continue; children.push(ent.name); } } catch { /* a directory that vanished mid-read */ } children.sort((a, b) => a.localeCompare(b)); for (const name of children) { if (repos.length >= MAX_REPOS) { truncated = true; return; } const abs = path.join(dir, name); if (await hasGit(abs)) { const rel = path.relative(root, abs).split(path.sep).join("/"); repos.push({ path: rel, name: rel }); } else if (depth < MAX_DEPTH) { await visit(abs, depth + 1); } } }; await visit(root, 1); return { items: repos, truncated }; } /** * The repos below `cwd`, plus whether `cwd` itself is in a repo (`root`). * @param {string} cwd The workspace folder (absolute). * @param {{refresh?: boolean}} [opts] * @returns {Promise<{root: boolean, items: {path: string, name: string}[], truncated: boolean}>} */ async function nestedRepos(cwd, { refresh = false } = {}) { const hit = cache.get(cwd); if (!refresh && hit && Date.now() - hit.at < TTL_MS) return hit.value; const pending = (async () => { const [root, found] = await Promise.all([insideRepo(cwd), walk(cwd)]); return { root, ...found }; })(); cache.set(cwd, { at: Date.now(), value: pending }); try { return await pending; } catch (err) { cache.delete(cwd); throw err; } } /** * The directory `?repo=` names: only a repo `nestedRepos` found under * the workspace folder (re-read if the cached list does not have it — a repo * cloned in the last 30 s — but at most once per `REREAD_MS`, so a stream of * bad names cannot keep the disk walking). * @throws ENOREPO otherwise. */ async function repoFolder(cwd, rel) { const want = String(rel); const find = (list) => list.items.find((r) => r.path === want); let hit = find(await nestedRepos(cwd)); if (!hit && Date.now() - (cache.get(cwd)?.at ?? 0) >= REREAD_MS) { hit = find(await nestedRepos(cwd, { refresh: true })); } if (!hit) throw fail("ENOREPO", "that folder is not a repository inside this workspace"); return path.join(cwd, ...hit.path.split("/")); } /** Tests only. */ function clearNestedRepoCache() { cache.clear(); } module.exports = { nestedRepos, repoFolder, clearNestedRepoCache, MAX_DEPTH, MAX_REPOS };