zz_

app/worker/src/cloudflareGitBackend.ts

zzmq.dev@gmail.com/zz / app / worker / src / cloudflareGitBackend.ts @ de14145f Download
Ladder Summary
  • file The module implements a Cloudflare Artifacts‑backed Git helper that lets the worker read and write commits without cloning the repository: it builds nested trees in a temporary in‑memory filesystem, pushes the resulting commit with isomorphic‑git, and queries the current HEAD directly through the native Artifacts bindings. **Exported members** * **createInitialCommit(remote, token, files, author): Promise<string>** – creates an empty (or pre‑populated) repository, writes the first tree/commit in memory, pushes it to the given remote, and returns the new commit OID. * **headOid(repo): Promise<string>** – returns the SHA‑1 of the repository’s current `main` HEAD by calling the native `repo.log` binding. (Additionally, the file re‑exports the Cloudflare types `ArtifactsCommit`, `ArtifactsRepoHandle`, and `ArtifactsNamespace` for use by other modules.)
 // Git content (commitChangeset + reads) against a real Artifacts repo
 // -- the other half of Backend the DO needs, alongside
 // doLaneBackend.ts's Lane/Ask/Answer storage. Reads go straight
 // through the native ARTIFACTS binding (readCommit/readTree/readBlob
 // -- no isomorphic-git, no clone, verified directly in step 2).
 // Writing a commit uses the "minimal object" push pattern proven live
 // in step 2's scratch test: never clone the repo's history locally,
 // just reconstruct the handful of objects a push actually needs --
 // the parent tree's unchanged entries (re-fetched by content via the
 // native binding, which reproduces the same oid) plus whatever's
 // actually new -- into a throwaway in-memory fs, then push.
 import * as git from "isomorphic-git";
 import http from "isomorphic-git/http/web";
 import { createMemFs } from "./memFs.js";
 import type { ChangesetFile, CommitAuthor } from "../../engine/changeset.js";
 
 // Aliases, not reimplementations: @cloudflare/workers-types ambiently
 // declares the real Artifacts/ArtifactsRepo/ArtifactsCommitMetadata
 // shapes (confirmed directly against node_modules -- info() does
 // exist and does carry `remote`; commit metadata does carry
 // authoredAt/committedAt/committer). A hand-rolled copy of these
 // interfaces lived here before and drifted from the real ones
 // (missing info(), missing authoredAt) -- caught by tsc once
 // cloudflareBackend.ts actually exercised those fields. Aliasing
 // keeps these names stable for every file that imports them.
 export type ArtifactsCommit = ArtifactsCommitMetadata;
 export type ArtifactsRepoHandle = ArtifactsRepo;
 export type ArtifactsNamespace = Artifacts;
 
 function tokenString(token: { plaintext: string } | string): string {
  return typeof token === "string" ? token.split("?")[0] : token.plaintext;
 }
 
 /**
  * Pushes a brand-new repo's first commit. Needed because
  * commitChangesetOnArtifacts always diffs against a parent via
  * headOid() -- a repo with zero commits has nothing to diff against,
  * so every real repo needs exactly one of these before
  * commit_changeset (or any Ladder/zz-meta write, which all anchor to
  * a commit) can touch it. Safe to call with an empty `files` array
  * (an empty-tree root commit).
  */
 export async function createInitialCommit(remote: string, token: { plaintext: string } | string, files: { path: string; content: string }[], author: CommitAuthor): Promise<string> {
  const fs = createMemFs();
  const dir = "/init";
  await git.init({ fs, dir, defaultBranch: "main" });
  const pending = new Map<string, PendingEntry>(files.map((f) => [f.path, { kind: "new" as const, content: f.content }]));
  const treeOid = await writeNestedTree(null, fs, dir, pending);
  const person = { name: author.name, email: author.email, timestamp: Math.floor(Date.now() / 1000), timezoneOffset: 0 };
  const commitOid = await git.writeCommit({ fs, dir, commit: { message: "initial commit", tree: treeOid, parent: [], author: person, committer: person } });
  await git.writeRef({ fs, dir, ref: "refs/heads/main", value: commitOid, force: true });
  const onAuth = () => ({ username: "x-token-auth", password: tokenString(token) });
  const pushResult = await git.push({ fs, http, dir, url: remote, onAuth, ref: "main", remoteRef: "main" });
  if (!pushResult.ok) throw new Error(`initial commit push rejected: ${JSON.stringify(pushResult.refs)}`);
  return commitOid;
 }
 
 /** The repo's current HEAD commit, via the native binding -- no clone. */
 export async function headOid(repo: ArtifactsRepoHandle): Promise<string> {
  const [latest] = await repo.log({ ref: "main", limit: 1 });
  if (!latest) throw new Error("repo has no commits yet");
  return latest.hash;
 }
 
 interface FlatEntry {
  oid: string;
  mode: string;
 }
 
 /** Walks a tree's "tree"-typed entries recursively, naming every real (blob/symlink/exec/gitlink) leaf by its full path relative to the root -- the native readTree binding only ever returns one level at a time, so this is the one place anything needs to actually recurse into subdirectories. */
 async function flattenTree(repo: ArtifactsRepoHandle, treeHash: string, prefix: string, out: Map<string, FlatEntry>): Promise<void> {
  const entries = await repo.readTree(treeHash);
  if (!entries) return;
  for (const entry of entries) {
  const path = prefix ? `${prefix}/${entry.name}` : entry.name;
  if (entry.type === "tree") {
  await flattenTree(repo, entry.hash, path, out);
  } else {
  out.set(path, { oid: entry.hash, mode: entry.mode });
  }
  }
 }
 
 type PendingEntry = { kind: "existing"; oid: string; mode: string } | { kind: "new"; content: string };
 
 /**
  * Writes a real, nested git tree from a flat `path -> entry` map,
  * grouping by each path's first segment and recursing for anything
  * with a "/" left in it -- the inverse of flattenTree, and the actual
  * fix for subdirectory support: a single flat tree object can't
  * represent "tests/test_shoes.py" at all, it needs a "tests" tree
  * entry pointing at its own child tree. Existing (unchanged) entries
  * are re-fetched and re-written by content so their oid reproduces
  * exactly (same reasoning commitChangesetOnArtifacts always used for
  * its flat unchanged entries); new ones are written straight from the
  * proposed content. Returns the oid of the tree written at this level.
  */
 async function writeNestedTree(repo: ArtifactsRepoHandle | null, fs: ReturnType<typeof createMemFs>, dir: string, flat: Map<string, PendingEntry>): Promise<string> {
  const topLevel = new Map<string, { entry?: PendingEntry; children?: Map<string, PendingEntry> }>();
  for (const [path, entry] of flat) {
  const slash = path.indexOf("/");
  if (slash === -1) {
  topLevel.set(path, { entry });
  continue;
  }
  const name = path.slice(0, slash);
  const rest = path.slice(slash + 1);
  let node = topLevel.get(name);
  if (!node?.children) {
  node = { children: new Map() };
  topLevel.set(name, node);
  }
  node.children!.set(rest, entry);
  }
 
  const treeEntries: { mode: string; path: string; oid: string; type: "blob" | "tree" }[] = [];
  for (const [name, node] of topLevel) {
  if (node.children) {
  const subOid = await writeNestedTree(repo, fs, dir, node.children);
  treeEntries.push({ mode: "40000", path: name, oid: subOid, type: "tree" });
  continue;
  }
  const entry = node.entry!;
  if (entry.kind === "existing") {
  if (!repo) throw new Error(`internal error: an "existing" tree entry ("${name}") with no repo to read it from -- should never happen outside commitChangesetOnArtifacts`);
  const blob = await repo.readBlob(entry.oid);
  if (!blob) throw new Error(`could not read unchanged blob for "${name}" (${entry.oid})`);
  const bytes = new Uint8Array(await blob.arrayBuffer());
  const oid = await git.writeBlob({ fs, dir, blob: bytes });
  treeEntries.push({ mode: entry.mode, path: name, oid, type: "blob" });
  } else {
  const oid = await git.writeBlob({ fs, dir, blob: new TextEncoder().encode(entry.content) });
  treeEntries.push({ mode: "100644", path: name, oid, type: "blob" });
  }
  }
  return git.writeTree({ fs, dir, tree: treeEntries });
 }
 
 /**
  * Commits a changeset against a real Artifacts repo without ever
  * cloning its history: reads the parent tree natively (recursively,
  * via flattenTree, so entries in subdirectories are found too),
  * reconstructs every entry locally (re-fetching unchanged files' real
  * bytes so their oid matches -- confirmed this reproduces the
  * identical oid, since git blob hashing is purely content-addressed),
  * overlays the proposed files, writes the real nested tree structure
  * (writeNestedTree), and pushes.
  */
 export async function commitChangesetOnArtifacts(repoName: string, remote: string, token: { plaintext: string } | string, repo: ArtifactsRepoHandle, input: { files: ChangesetFile[]; message: string; author: CommitAuthor }): Promise<string> {
  const parentOid = await headOid(repo);
  const parentCommit = await repo.readCommit(parentOid);
  if (!parentCommit) throw new Error(`could not read parent commit ${parentOid}`);
 
  const flat = new Map<string, FlatEntry>();
  await flattenTree(repo, parentCommit.treeHash, "", flat);
 
  const pending = new Map<string, PendingEntry>();
  for (const [path, entry] of flat) pending.set(path, { kind: "existing", ...entry });
  for (const f of input.files) pending.set(f.path, { kind: "new", content: f.content });
 
  const fs = createMemFs();
  const dir = "/repo";
  await git.init({ fs, dir, defaultBranch: "main" });
 
  const treeOid = await writeNestedTree(repo, fs, dir, pending);
  const person = { name: input.author.name, email: input.author.email, timestamp: Math.floor(Date.now() / 1000), timezoneOffset: 0 };
  const commitOid = await git.writeCommit({ fs, dir, commit: { message: input.message, tree: treeOid, parent: [parentOid], author: person, committer: person } });
  await git.writeRef({ fs, dir, ref: "refs/heads/main", value: commitOid, force: true });
 
  const onAuth = () => ({ username: "x-token-auth", password: tokenString(token) });
  const pushResult = await git.push({ fs, http, dir, url: remote, onAuth, ref: "main", remoteRef: "main" });
  if (!pushResult.ok) throw new Error(`push rejected: ${JSON.stringify(pushResult.refs)}`);
 
  return commitOid;
 }
 
 export async function readFileAtCommit(repo: ArtifactsRepoHandle, commitHash: string, path: string): Promise<string> {
  const commit = await repo.readCommit(commitHash);
  if (!commit) return "";
  const tree = await repo.readTree(commit.treeHash);
  const entry = tree?.find((e) => e.name === path);
  if (!entry) return "";
  const blob = await repo.readBlob(entry.hash);
  return blob ? await blob.text() : "";
 }