zz_

app/engine/gitNotes.ts

zzmq.dev@gmail.com/zz / app / engine / gitNotes.ts @ 5e5720fb 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;
    }
  }
}

/** 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<boolean> {
  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<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;
  }
}