app/engine/gitNotes.ts
zzmq.dev@gmail.com/zz / app / engine / gitNotes.ts
@ 3a45f02b
Download
Ladder Summary
- file This module provides low‑level helpers for reading and writing Git notes using **isomorphic‑git** (so it can run inside Cloudflare Workers). It includes utilities to resolve paths and directories at the current HEAD, locate the repository’s root commit, expand abbreviated SHAs, and read/write JSON‑encoded notes under a given ref. **Exported functions** - `resolvePathSha(repoDir, path)` - `isDirectoryAtHead(repoDir, path)` - `resolveRootCommit(repoDir)` - `expandOid(repoDir, shortOid)` - `writeNote(repoDir, ref, sha, content)` - `readNote<T>(repoDir, ref, sha)` (no exported classes).
// 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<string> {
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<string | null> {
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;
}
}
}
/**
* 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<string> {
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<string> {
return git.expandOid({ fs, dir: repoDir, oid: shortOid });
}
export async function writeNote(repoDir: string, ref: string, sha: string, content: unknown): Promise<void> {
await git.addNote({ fs, dir: repoDir, ref, oid: sha, note: JSON.stringify(content), force: true, author: AUTHOR, committer: AUTHOR });
}
export async function readNote<T>(repoDir: string, ref: string, sha: string): Promise<T | null> {
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;
}
}