// Low-level git notes I/O, via isomorphic-git rather than shelling out to // the real git binary. Notes live inside the repo they annotate (the real // clone under demo-repos/), under their own ref namespace, so they never // touch the tracked working tree. Regenerable from the Ladder build // script, not a source of truth in their own right. // // Chose isomorphic-git over shelling out to `git`, and over a native // binding like es-git, for one reason that isn't speed: it's pure JS, so // it's the only one of the three that can run in Cloudflare Workers, the // actual target deployment. Workers has no child_process and no native // addon loading, so both `execFileSync("git", ...)` and an N-API module // like es-git simply don't run there, no matter how fast they are // locally. es-git (libgit2 via napi-rs) is a better fit for local-only // tooling later (the TUI), where it isn't Workers-constrained. import * as git from "isomorphic-git"; import fs from "node:fs"; const AUTHOR = { name: "zz-onboarding", email: "zz-onboarding@local" }; async function headCommitOid(dir: string): Promise { return git.resolveRef({ fs, dir, ref: "HEAD" }); } /** Resolves a path at HEAD to its blob (file) or tree (directory) SHA. Null if the path doesn't exist. */ export async function resolvePathSha(repoDir: string, path: string): Promise { const oid = await headCommitOid(repoDir); try { const { oid: blobOid } = await git.readBlob({ fs, dir: repoDir, oid, filepath: path }); return blobOid; } catch { try { const { oid: treeOid } = await git.readTree({ fs, dir: repoDir, oid, filepath: path }); return treeOid; } catch { return null; } } } /** Whether `path` currently names a directory at HEAD -- readAnchor's own read-side needs this to pick the right anchoring strategy for a path it has no Rung (and so no `kind`) for: a directory anchors to the repo's root commit (ladderNotes.ts's own anchorSha, same reasoning as "repo"), a file to its own blob sha, which only ever matters because the two differ at all. */ export async function isDirectoryAtHead(repoDir: string, path: string): Promise { const oid = await headCommitOid(repoDir); try { await git.readTree({ fs, dir: repoDir, oid, filepath: path }); return true; } catch { return false; } } /** * The repo's first commit (no parents) -- a fixed point that never changes * for the life of the repo, unlike HEAD. The anchor for repo-wide things * that don't correspond to any one file or any one commit, like most * issues: plenty are never closed by a specific fix, so there's nothing * else stable to attach them to. One full history walk; cache the result * rather than calling this per issue. */ export async function resolveRootCommit(repoDir: string): Promise { const commits = await git.log({ fs, dir: repoDir, ref: "HEAD" }); const root = commits[commits.length - 1]; if (!root) throw new Error("repo has no commits"); return root.oid; } /** Expands a short (abbreviated) SHA to the full 40-character one. Git notes key by full SHA internally, so a note written under a short SHA is invisible to the real `git notes` CLI even though isomorphic-git itself round-trips it. */ export async function expandOid(repoDir: string, shortOid: string): Promise { return git.expandOid({ fs, dir: repoDir, oid: shortOid }); } export async function writeNote(repoDir: string, ref: string, sha: string, content: unknown): Promise { await git.addNote({ fs, dir: repoDir, ref, oid: sha, note: JSON.stringify(content), force: true, author: AUTHOR, committer: AUTHOR }); } export async function readNote(repoDir: string, ref: string, sha: string): Promise { try { const raw = await git.readNote({ fs, dir: repoDir, ref, oid: sha }); return JSON.parse(Buffer.from(raw).toString("utf8")) as T; } catch { return null; } }