import { normalize } from "node:path"; import { type CommandRunner, runChecked } from "./process.js"; export interface GitWorktreeEntry { readonly path: string; readonly head: string | null; readonly branch: string | null; } function comparablePath(value: string): string { return normalize(value).replace(/\/+$/u, ""); } /** * Reads the worktrees Git itself knows about. Herdr reports what it did; this is the * independent check that the repository really changed the same way. * * `-z` ends every record with a NUL, so a path that contains a newline cannot pretend to be * another record. It needs Git 2.36 or newer. */ export async function readGitWorktrees( runner: CommandRunner, cwd: string, signal: AbortSignal | undefined, ): Promise { const result = await runChecked( runner, "git", ["worktree", "list", "--porcelain", "-z"], { cwd, signal }, "Unable to list the Git worktrees", ); const entries: GitWorktreeEntry[] = []; let path: string | undefined; let head: string | null = null; let branch: string | null = null; const flush = () => { if (path) { entries.push({ path: comparablePath(path), head, branch }); } path = undefined; head = null; branch = null; }; // An unrecognised record, such as `bare` or `locked`, is ignored rather than read as a path. for (const record of result.stdout.split("\0")) { if (record.startsWith("worktree ")) { flush(); path = record.slice("worktree ".length); } else if (record.startsWith("HEAD ")) { head = record.slice("HEAD ".length); } else if (record.startsWith("branch refs/heads/")) { branch = record.slice("branch refs/heads/".length); } } flush(); return entries; } export function findGitWorktree( entries: readonly GitWorktreeEntry[], path: string, ): GitWorktreeEntry | undefined { const wanted = comparablePath(path); return entries.find((entry) => entry.path === wanted); }