import { stat } from "node:fs/promises";
import { dirname, join, resolve, sep } from "node:path";
import { LRUMap } from "./LRUMap.js";
const NOT_FOUND = Symbol("findGitRoot.NOT_FOUND");
* LRU-cached filesystem walk that finds the directory containing `.git`
* (regular repo: `.git` is a directory; worktree/submodule: `.git` is a file).
*
* Behaviour mirrors the legacy upstream git-root walk:
* - walk parent → stat `<dir>/.git`; both file and directory count as a hit
* - return path NFC-normalized (`.normalize('NFC')`) so macOS UTF-8 forms fold
* - LRU cap 50 entries (cwd → resolved root) to keep hot paths in memory
*
* Differences from legacy:
* - async (uses `node:fs/promises`); legacy is sync `statSync` for hot path
* speed. PilotDeck callers are already async, so async fits naturally.
* - logging is intentionally omitted (no diagnostics infra in PilotDeck).
*/
const cache = new LRUMap<string, string | typeof NOT_FOUND>(50);
export async function findGitRoot(startPath: string): Promise<string | null> {
const cwd = resolve(startPath);
const cached = cache.get(cwd);
if (cached === NOT_FOUND) {
return null;
}
if (typeof cached === "string") {
return cached;
}
let current = cwd;
const root = current.substring(0, current.indexOf(sep) + 1) || sep;
while (current !== root) {
if (await hasGit(current)) {
const resolved = current.normalize("NFC");
cache.set(cwd, resolved);
return resolved;
}
const parent = dirname(current);
if (parent === current) {
break;
}
current = parent;
}
if (await hasGit(root)) {
const resolved = root.normalize("NFC");
cache.set(cwd, resolved);
return resolved;
}
cache.set(cwd, NOT_FOUND);
return null;
}
async function hasGit(dir: string): Promise<boolean> {
try {
const stats = await stat(join(dir, ".git"));
return stats.isDirectory() || stats.isFile();
} catch {
return false;
}
}
* Test-only: clears the LRU cache. Call from worktree fixtures after each test
* so cwd reuse across tests does not leak resolved paths.
*/
export function __clearFindGitRootCacheForTesting(): void {
cache.clear();
}