# How Gadget Code Changes Are Managed Using isomorphic-git in Cloudflare OS

> Discover how Cloudflare OS manages Gadget code changes using isomorphic-git. Learn about its low-level API and custom logic for Git operations.

- Repository: [Cloudflare/cloudflare-os](https://github.com/cloudflare/cloudflare-os)
- Tags: internals
- Published: 2026-09-05

---

**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`](https://github.com/cloudflare/cloudflare-os/blob/main/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`](https://github.com/cloudflare/cloudflare-os/blob/main/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`](https://github.com/cloudflare/cloudflare-os/blob/main/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`](https://github.com/cloudflare/cloudflare-os/blob/main/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`](https://github.com/cloudflare/cloudflare-os/blob/main/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:

```typescript
// 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`](https://github.com/cloudflare/cloudflare-os/blob/main/git-store.ts)):

```typescript
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:
- `writeTree` constructs the tree object from blob OIDs
- `writeCommit` creates 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:

```typescript
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`](https://github.com/cloudflare/cloudflare-os/blob/main/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`:

```typescript
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`](https://github.com/cloudflare/cloudflare-os/blob/main/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**:

```typescript
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`](https://github.com/cloudflare/cloudflare-os/blob/main/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`](https://github.com/cloudflare/cloudflare-os/blob/main/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/heads` or `refs/tags`; `GadgetRecord.commitId` serves 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 via `git push` without 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 in `GadgetRecord.commitId`
- **Merge strategy** – Three-way merges use `diff3` library, then commit results via the same plumbing pipeline
- **External sync** – Gatekeeper-context uses `clone`/`fetch` with 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`](https://github.com/cloudflare/cloudflare-os/blob/main/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.