zz_

app/engine/changeset.ts

zzmq.dev@gmail.com/zz / app / engine / changeset.ts @ b9c01cc1 Download
Ladder Summary
  • file The module provides an in‑memory way to create a Git commit from a list of file paths and contents without using a working‑tree checkout. It validates that only flat (single‑segment) paths are used, builds a new tree by writing blobs for each file, creates a commit with the supplied message and author, and updates the current branch reference. Exported symbols are the `ChangesetFile` and `CommitAuthor` interfaces and the `commitChangeset` async function.
 // Composing and committing a real code change directly from protocol
 // data -- no working-tree file, no `write_file` tool. The content of
 // every changed file lives entirely in the Ask/Answer payload that
 // proposed it (the same way a Rung carries its own full content); this
 // is the one place that content actually becomes a real git commit,
 // using isomorphic-git's plumbing (writeBlob/writeTree/writeCommit/
 // writeRef) rather than a working-tree checkout. That also makes this
 // directly portable to the Cloudflare move: a Worker has no persistent
 // working tree either, so building a commit from in-memory content is
 // the only shape that was ever going to survive that move.
 //
 // Flat paths only (single path segment, no subdirectories) -- writeTree
 // writes one tree level per call, so a nested path would need manual
 // subtree construction. Not needed yet; add it if a real changeset
 // actually requires nested directories.
 import * as git from "isomorphic-git";
 import fs from "node:fs";
 
 export interface ChangesetFile {
  path: string;
  content: string;
 }
 
 export interface CommitAuthor {
  name: string;
  email: string;
 }
 
 export async function commitChangeset(repoDir: string, input: { files: ChangesetFile[]; message: string; author: CommitAuthor }): Promise<string> {
  for (const f of input.files) {
  if (f.path.includes("/")) throw new Error(`commitChangeset: "${f.path}" has a subdirectory; only flat, single-segment paths are supported right now.`);
  }
 
  let parentOid: string | undefined;
  try {
  parentOid = await git.resolveRef({ fs, dir: repoDir, ref: "HEAD" });
  } catch {
  parentOid = undefined; // the repo's first commit
  }
 
  const entries: git.TreeEntry[] = parentOid ? (await git.readTree({ fs, dir: repoDir, oid: parentOid })).tree : [];
 
  for (const f of input.files) {
  const oid = await git.writeBlob({ fs, dir: repoDir, blob: new TextEncoder().encode(f.content) });
  const entry: git.TreeEntry = { mode: "100644", path: f.path, oid, type: "blob" };
  const i = entries.findIndex((e) => e.path === f.path);
  if (i >= 0) entries[i] = entry;
  else entries.push(entry);
  }
 
  const treeOid = await git.writeTree({ fs, dir: repoDir, tree: entries });
  const timestamp = Math.floor(Date.now() / 1000);
  const person = { ...input.author, timestamp, timezoneOffset: 0 };
  const commitOid = await git.writeCommit({
  fs,
  dir: repoDir,
  commit: { message: input.message, tree: treeOid, parent: parentOid ? [parentOid] : [], author: person, committer: person },
  });
 
  const branch = (await git.currentBranch({ fs, dir: repoDir })) ?? "main";
  await git.writeRef({ fs, dir: repoDir, ref: `refs/heads/${branch}`, value: commitOid, force: true });
 
  return commitOid;
 }