How Gadget Code Changes Are Managed Using isomorphic-git in Cloudflare OS
Cloudflare OS stores Gadget source code as real Git objects and uses isomorphic-git only for its low-level plumbing API—readBlob, writeBlob, readTree, writeTree, readCommit, and writeCommit—while implementing all higher-level operations like commit creation and merging through custom logic.
Each Workspace in Cloudflare OS maintains a dedicated Overseer Durable Object that owns a collection of Git loose objects (SHA-1, zlib-deflated) in typed storage. This article explains how Gadget code changes are managed using isomorphic-git in Cloudflare OS, from file edits through commit creation to three-way merges.
Core Architecture: Plumbing-Only Design
The Cloudflare OS codebase deliberately avoids isomorphic-git's high-level commands (git commit, git merge). Instead, it builds a virtual filesystem shim that maps Git object paths onto Cloudflare's typed storage, then invokes only the raw object operations.
| Component | Role | isomorphic-git Usage |
|---|---|---|
GitStore (packages/workshop-backend/src/git-store.ts) |
Virtual filesystem shim mapping /git/objects/xx/yyyy… to typed storage |
readBlob, writeBlob, readTree, writeTree, readCommit, writeCommit, log |
Overseer (packages/workshop-backend/src/overseer.ts) |
Per-workspace DO orchestrating gadget lifecycle | Calls GitStore.writeFilesAsCommit to batch edits into commits |
GitCache (packages/workshop-backend/src/git-cache.ts) |
Parse cache per Overseer instance | Shared isomorphic-git object cache |
Artifact-Sync (packages/gatekeeper-context/src/artifact-sync.ts) |
External Git operations for gatekeeper context | High-level clone/fetch with custom HTTP client only |
The GitStore implementation in git-store.ts creates a virtual filesystem via makeGitObjectsFs. Every call from isomorphic-git routes through this shim, translating path lookups into typed-storage queries.
The Gadget Code Change Flow
When a user or agent modifies Gadget code, changes flow through a deterministic pipeline that produces standard Git objects.
Step 1: Blob Creation from File Edits
File-tool calls (writeFile, editFile) create or modify blobs directly:
// Simplified example from git-store.ts operations
await gitStore.writeBlob({ oid, data: new Uint8Array(content) });
Each file's content is zlib-deflated and stored as a SHA-1-addressed loose object. No deltas, no packfiles—content-addressed storage only.
Step 2: Batch Commit Creation
When a turn ends, the Overseer invokes GitStore.writeFilesAsCommit (lines ~90-120 in git-store.ts):
import { GitStore, WriteCommitOptions } from "./git-store";
async function commitGadgetChanges(
gitStore: GitStore,
files: Record<string, Uint8Array>,
parentCommit: string | null,
author: CommitIdentity,
): Promise<string> {
const options: WriteCommitOptions = {
parents: parentCommit ? [parentCommit] : [],
author,
message: "Update gadget code\n",
};
// Writes blobs, builds tree, creates commit
const newCommitOid = await gitStore.writeFilesAsCommit(files, options);
// Update the GadgetRecord head pointer
await gadgetRecords.update(gadgetId, { commitId: newCommitOid });
return newCommitOid;
}
Inside writeFilesAsCommit, isomorphic-git's plumbing executes sequentially:
writeTreeconstructs the tree object from blob OIDswriteCommitcreates the commit object with parent pointer, author, and message
Step 3: Pointer Update
The new commit OID is written to GadgetRecord.commitId. This ref-less model eliminates branches and refs/heads management—each gadget simply points to its current head commit.
Reading Gadget Code with isomorphic-git
Execution environments read code by traversing the commit→tree→blob hierarchy:
import { readBlob, readCommit, readTree } from "isomorphic-git";
async function readGadgetFile(
gitStore: GitStore,
commitOid: string,
filePath: string,
): Promise<Uint8Array> {
const commit = await readCommit({
fs: gitStore.fs,
gitdir: GitStore.GITDIR,
oid: commitOid
});
const tree = await readTree({
fs: gitStore.fs,
gitdir: GitStore.GITDIR,
oid: commit.tree
});
const entry = tree.entries.find(e => e.path === filePath);
if (!entry) throw new Error(`File not found: ${filePath}`);
return await readBlob({
fs: gitStore.fs,
gitdir: GitStore.GITDIR,
oid: entry.oid
});
}
The virtual fs object (lines ~38-45 in git-store.ts) translates these calls into typed-storage reads without actual disk I/O.
Three-Way Merging with diff3
When concurrent edits require reconciliation, Cloudflare OS implements its own merge logic using the diff3 library rather than isomorphic-git's mergeTree:
import diff3Merge from "diff3";
async function threeWayMerge(
baseOid: string,
oursOid: string,
theirsOid: string,
gitStore: GitStore,
): Promise<string> {
// Load all three trees via isomorphic-git plumbing
const baseTree = await readTree({ fs: gitStore.fs, gitdir: GitStore.GITDIR, oid: baseOid });
const oursTree = await readTree({ fs: gitStore.fs, gitdir: GitStore.GITDIR, oid: oursOid });
const theirsTree = await readTree({ fs: gitStore.fs, gitdir: GitStore.GITDIR, oid: theirsOid });
// Convert to path→content maps
const toMap = async (tree) => {
const map = new Map<string, Uint8Array>();
for (const entry of tree.entries) {
const data = await readBlob({ fs: gitStore.fs, gitdir: GitStore.GITDIR, oid: entry.oid });
map.set(entry.path, data);
}
return map;
};
const base = await toMap(baseTree);
const ours = await toMap(oursTree);
const theirs = await toMap(theirsTree);
// diff3 per-file merge
const mergedFiles: Record<string, Uint8Array> = {};
for (const path of new Set([...base.keys(), ...ours.keys(), ...theirs.keys()])) {
const merged = diff3Merge(
ours.get(path) ?? Buffer.alloc(0),
base.get(path) ?? Buffer.alloc(0),
theirs.get(path) ?? Buffer.alloc(0),
);
mergedFiles[path] = Buffer.from(merged.result);
}
// Commit merged result
return await gitStore.writeFilesAsCommit(mergedFiles, {
parents: [oursOid, theirsOid],
author: { name: "Merge Bot", email: "merge@cloudflare.com", timestamp: Date.now() / 1000 },
message: "Merge changes",
});
}
This custom merge implementation (around line ~600 in git-store.ts) handles conflict semantics specific to Cloudflare OS's collaborative editing model.
External Repository Integration
For gatekeeper-context operations, the Artifact-Sync module uses isomorphic-git's high-level clone and fetch APIs—but only with a custom HTTP client:
import { clone, fetch, listFiles } from "isomorphic-git";
import { request as httpRequest } from "isomorphic-git/http/web";
async function syncExternalRepo(url: string, ref: string): Promise<string[]> {
await fetch({
fs,
dir: "/tmp",
url,
http: httpRequest // Cloudflare Worker-compatible HTTP
});
await clone({ fs, dir: "/tmp", url, ref, http: httpRequest });
return await listFiles({ fs, dir: "/tmp", ref });
}
This isolated usage in artifact-sync.ts demonstrates that even "high-level" isomorphic-git operations ultimately depend on the same plumbing layer employed throughout Cloudflare OS.
Design Constraints and Rationale
The plans/git-storage.md document explicitly mandates:
- Plumbing API only – No
git commit,git merge, or other porcelain commands - Content-addressed storage – Objects stored as loose, SHA-1-addressed blobs; never delta-compressed
- Ref-less architecture – No
refs/headsorrefs/tags;GadgetRecord.commitIdserves as the single pointer
These constraints enable:
- Deterministic replication – Same content produces same OID across all instances
- Standard Git compatibility – Export via
git clone, import viagit pushwithout conversion - Worker-optimized performance – No packfile overhead, cache-friendly object access
Summary
- Storage model – Gadget code persists as real Git objects (blobs, trees, commits) in typed storage, accessed through
GitStore's virtual filesystem shim - isomorphic-git scope – Only plumbing functions (
readBlob,writeBlob,readTree,writeTree,readCommit,writeCommit) are used; all higher-level logic is custom - Commit workflow – File edits batch through
writeFilesAsCommit, producing a new commit OID stored inGadgetRecord.commitId - Merge strategy – Three-way merges use
diff3library, then commit results via the same plumbing pipeline - External sync – Gatekeeper-context uses
clone/fetchwith custom HTTP client for repository interoperability
Frequently Asked Questions
Why doesn't Cloudflare OS use isomorphic-git's commit and merge commands?
isomorphic-git's high-level commands assume a traditional Git working directory with refs, branches, and porcelain semantics. Cloudflare OS operates in a ref-less environment where Durable Objects manage multiple workspaces concurrently—custom commit creation and diff3-based merging provide the exact control needed for this architecture.
Can Gadget code be exported to a standard Git repository?
Yes. Because Cloudflare OS stores objects in the exact on-disk Git format (loose objects, zlib-deflated, SHA-1 addressed), a gadget's commit history can be exported via git clone or imported via git push without any format conversion. Future "mount" features will enable push/pull via the gatekeeper framework.
How does the GitCache improve performance?
GitCache (packages/workshop-backend/src/git-cache.ts) maintains a shared parse cache per Overseer instance. Since isomorphic-git re-parses objects on every read, caching deserialized tree and commit objects eliminates redundant parsing overhead during repeated file accesses within a single request or session.
What happens when two users edit the same Gadget simultaneously?
The Overseer detects concurrent modifications through commit parent conflicts. It retrieves the three relevant trees (base, ours, theirs) using readTree, invokes diff3 for per-file three-way merging, and commits the resolved result with both parents. This merge-then-commit pattern preserves full edit history without blocking either user.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →