isomorphic-git
Use isomorphic-git ↗ in a Cloudflare Worker when you need Git operations without a Git binary.
This works with Artifacts because Artifacts exposes standard Git smart HTTP remotes. In Workers, pair isomorphic-git/http/web with a small in-memory filesystem because the runtime does not expose a local disk.
Install isomorphic-git in your Worker project:
npm i isomorphic-git yarn add isomorphic-git pnpm add isomorphic-git bun add isomorphic-git This demo creates a new repo on each request, writes two files, commits them, and pushes main to the new remote.
Use this as a reference for the end-to-end flow. In a production Worker, look up or reuse an existing repo instead of creating a new one for every request.
import git from "isomorphic-git";import http from "isomorphic-git/http/web";import { MemoryFS } from "./memory-fs";
export default { async fetch(_request, env) { const repoName = `worker-demo-${crypto.randomUUID().slice(0, 8)}`; const created = await env.ARTIFACTS.create(repoName);
// Artifacts returns art_v1_<secret>?expires=<unix_seconds>. // For Git Basic auth, pass only the secret as the password. const tokenSecret = created.token.split("?expires=")[0]; const dir = "/workspace"; const fs = new MemoryFS();
await git.init({ fs, dir, defaultBranch: "main" });
await fs.promises.writeFile( `${dir}/README.md`, "# Artifacts repo created from a Worker\n", ); await fs.promises.writeFile( `${dir}/src/index.ts`, 'export const message = "hello from Artifacts";\n', );
await git.add({ fs, dir, filepath: "README.md" }); await git.add({ fs, dir, filepath: "src/index.ts" });
const commit = await git.commit({ fs, dir, message: "Create starter files", author: { name: "Artifacts example", email: "artifacts@example.com", }, });
const push = await git.push({ fs, http, dir, url: created.remote, ref: "main", onAuth: () => ({ username: "x", password: tokenSecret, }), });
return Response.json({ repo: created.name, remote: created.remote, commit, refs: push.refs, }); },};import git from "isomorphic-git";import http from "isomorphic-git/http/web";import { MemoryFS } from "./memory-fs";
export interface Env { ARTIFACTS: Artifacts;}
export default { async fetch(_request: Request, env: Env) { const repoName = `worker-demo-${crypto.randomUUID().slice(0, 8)}`; const created = await env.ARTIFACTS.create(repoName);
// Artifacts returns art_v1_<secret>?expires=<unix_seconds>. // For Git Basic auth, pass only the secret as the password. const tokenSecret = created.token.split("?expires=")[0]; const dir = "/workspace"; const fs = new MemoryFS();
await git.init({ fs, dir, defaultBranch: "main" });
await fs.promises.writeFile( `${dir}/README.md`, "# Artifacts repo created from a Worker\n", ); await fs.promises.writeFile( `${dir}/src/index.ts`, 'export const message = "hello from Artifacts";\n', );
await git.add({ fs, dir, filepath: "README.md" }); await git.add({ fs, dir, filepath: "src/index.ts" });
const commit = await git.commit({ fs, dir, message: "Create starter files", author: { name: "Artifacts example", email: "artifacts@example.com", }, });
const push = await git.push({ fs, http, dir, url: created.remote, ref: "main", onAuth: () => ({ username: "x", password: tokenSecret, }), });
return Response.json({ repo: created.name, remote: created.remote, commit, refs: push.refs, }); },} satisfies ExportedHandler<Env>;In-memory filesystem helper
Use this helper with isomorphic-git in Workers when you need a short-lived working tree in memory.
class MemoryStats { entry;
constructor(entry) { this.entry = entry; }
get size() { return this.entry.kind === "file" ? this.entry.data.byteLength : 0; }
get mtimeMs() { return this.entry.mtimeMs; }
get ctimeMs() { return this.entry.mtimeMs; }
get mode() { return this.entry.kind === "file" ? 0o100644 : 0o040000; }
isFile() { return this.entry.kind === "file"; }
isDirectory() { return this.entry.kind === "dir"; }
isSymbolicLink() { return false; }}
export class MemoryFS { encoder = new TextEncoder(); decoder = new TextDecoder(); entries = new Map([ ["/", { kind: "dir", children: new Set(), mtimeMs: Date.now() }], ]);
promises = { readFile: this.readFile.bind(this), writeFile: this.writeFile.bind(this), unlink: this.unlink.bind(this), readdir: this.readdir.bind(this), mkdir: this.mkdir.bind(this), rmdir: this.rmdir.bind(this), stat: this.stat.bind(this), lstat: this.lstat.bind(this), };
normalize(input) { const segments = [];
for (const part of input.split("/")) { if (!part || part === ".") { continue; }
if (part === "..") { segments.pop(); continue; }
segments.push(part); }
return `/${segments.join("/")}` || "/"; }
parent(path) { const normalized = this.normalize(path); if (normalized === "/") { return "/"; }
const parts = normalized.split("/").filter(Boolean); parts.pop(); return parts.length ? `/${parts.join("/")}` : "/"; }
basename(path) { return this.normalize(path).split("/").filter(Boolean).pop() ?? ""; }
getEntry(path) { return this.entries.get(this.normalize(path)); }
requireEntry(path) { const entry = this.getEntry(path); if (!entry) { throw new Error(`ENOENT: ${path}`); }
return entry; }
requireDir(path) { const entry = this.requireEntry(path); if (entry.kind !== "dir") { throw new Error(`ENOTDIR: ${path}`); }
return entry; }
async mkdir(path, options) { const target = this.normalize(path); if (target === "/") { return; }
const recursive = typeof options === "object" && options !== null && options.recursive; const parent = this.parent(target);
if (!this.entries.has(parent)) { if (!recursive) { throw new Error(`ENOENT: ${parent}`); }
await this.mkdir(parent, { recursive: true }); }
if (this.entries.has(target)) { return; }
this.entries.set(target, { kind: "dir", children: new Set(), mtimeMs: Date.now(), });
this.requireDir(parent).children.add(this.basename(target)); }
async writeFile(path, data) { const target = this.normalize(path); await this.mkdir(this.parent(target), { recursive: true });
const bytes = typeof data === "string" ? this.encoder.encode(data) : data instanceof Uint8Array ? data : new Uint8Array(data);
this.entries.set(target, { kind: "file", data: bytes, mtimeMs: Date.now(), });
this.requireDir(this.parent(target)).children.add(this.basename(target)); }
async readFile(path, options) { const entry = this.requireEntry(path); if (entry.kind !== "file") { throw new Error(`EISDIR: ${path}`); }
const encoding = typeof options === "string" ? options : options?.encoding; return encoding ? this.decoder.decode(entry.data) : entry.data; }
async readdir(path) { return [...this.requireDir(path).children].sort(); }
async unlink(path) { const target = this.normalize(path); const entry = this.requireEntry(target); if (entry.kind !== "file") { throw new Error(`EISDIR: ${path}`); }
this.entries.delete(target); this.requireDir(this.parent(target)).children.delete(this.basename(target)); }
async rmdir(path) { const target = this.normalize(path); const entry = this.requireDir(target); if (entry.children.size > 0) { throw new Error(`ENOTEMPTY: ${path}`); }
this.entries.delete(target); this.requireDir(this.parent(target)).children.delete(this.basename(target)); }
async stat(path) { return new MemoryStats(this.requireEntry(path)); }
async lstat(path) { return this.stat(path); }}type Entry = | { kind: "dir"; children: Set<string>; mtimeMs: number; } | { kind: "file"; data: Uint8Array; mtimeMs: number; };
class MemoryStats { entry: Entry;
constructor(entry: Entry) { this.entry = entry; }
get size() { return this.entry.kind === "file" ? this.entry.data.byteLength : 0; }
get mtimeMs() { return this.entry.mtimeMs; }
get ctimeMs() { return this.entry.mtimeMs; }
get mode() { return this.entry.kind === "file" ? 0o100644 : 0o040000; }
isFile() { return this.entry.kind === "file"; }
isDirectory() { return this.entry.kind === "dir"; }
isSymbolicLink() { return false; }}
export class MemoryFS { encoder = new TextEncoder(); decoder = new TextDecoder(); entries = new Map<string, Entry>([ ["/", { kind: "dir", children: new Set(), mtimeMs: Date.now() }], ]);
promises = { readFile: this.readFile.bind(this), writeFile: this.writeFile.bind(this), unlink: this.unlink.bind(this), readdir: this.readdir.bind(this), mkdir: this.mkdir.bind(this), rmdir: this.rmdir.bind(this), stat: this.stat.bind(this), lstat: this.lstat.bind(this), };
normalize(input: string) { const segments: string[] = [];
for (const part of input.split("/")) { if (!part || part === ".") { continue; }
if (part === "..") { segments.pop(); continue; }
segments.push(part); }
return `/${segments.join("/")}` || "/"; }
parent(path: string) { const normalized = this.normalize(path); if (normalized === "/") { return "/"; }
const parts = normalized.split("/").filter(Boolean); parts.pop(); return parts.length ? `/${parts.join("/")}` : "/"; }
basename(path: string) { return this.normalize(path).split("/").filter(Boolean).pop() ?? ""; }
getEntry(path: string) { return this.entries.get(this.normalize(path)); }
requireEntry(path: string) { const entry = this.getEntry(path); if (!entry) { throw new Error(`ENOENT: ${path}`); }
return entry; }
requireDir(path: string) { const entry = this.requireEntry(path); if (entry.kind !== "dir") { throw new Error(`ENOTDIR: ${path}`); }
return entry; }
async mkdir(path: string, options?: { recursive?: boolean } | number) { const target = this.normalize(path); if (target === "/") { return; }
const recursive = typeof options === "object" && options !== null && options.recursive; const parent = this.parent(target);
if (!this.entries.has(parent)) { if (!recursive) { throw new Error(`ENOENT: ${parent}`); }
await this.mkdir(parent, { recursive: true }); }
if (this.entries.has(target)) { return; }
this.entries.set(target, { kind: "dir", children: new Set(), mtimeMs: Date.now(), });
this.requireDir(parent).children.add(this.basename(target)); }
async writeFile(path: string, data: string | Uint8Array | ArrayBuffer) { const target = this.normalize(path); await this.mkdir(this.parent(target), { recursive: true });
const bytes = typeof data === "string" ? this.encoder.encode(data) : data instanceof Uint8Array ? data : new Uint8Array(data);
this.entries.set(target, { kind: "file", data: bytes, mtimeMs: Date.now(), });
this.requireDir(this.parent(target)).children.add(this.basename(target)); }
async readFile(path: string, options?: string | { encoding?: string }) { const entry = this.requireEntry(path); if (entry.kind !== "file") { throw new Error(`EISDIR: ${path}`); }
const encoding = typeof options === "string" ? options : options?.encoding; return encoding ? this.decoder.decode(entry.data) : entry.data; }
async readdir(path: string) { return [...this.requireDir(path).children].sort(); }
async unlink(path: string) { const target = this.normalize(path); const entry = this.requireEntry(target); if (entry.kind !== "file") { throw new Error(`EISDIR: ${path}`); }
this.entries.delete(target); this.requireDir(this.parent(target)).children.delete(this.basename(target)); }
async rmdir(path: string) { const target = this.normalize(path); const entry = this.requireDir(target); if (entry.children.size > 0) { throw new Error(`ENOTEMPTY: ${path}`); }
this.entries.delete(target); this.requireDir(this.parent(target)).children.delete(this.basename(target)); }
async stat(path: string) { return new MemoryStats(this.requireEntry(path)); }
async lstat(path: string) { return this.stat(path); }}