app/engine/changeset.ts
zzmq.dev@gmail.com/zz / app / engine / changeset.ts
@ 157c371a
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;
}