git engine without fs nor git command. WASM+JS that speaks directly to any git http. Inspired by Cloudflare Artifacts:
The entire git protocol engine is written in pure Zig (no libc), compiled to a ~100KB WASM binary ... It implements SHA-1, zlib inflate/deflate, delta encoding/decoding, pack parsing, and the full git smart HTTP protocol — all from scratch, with zero external dependencies.
This repo is a minimal reproduction focused on read/write a git remote as a versioned blob store, not a dev workspace.
One branch == one keyspace (path -> bytes), one commit == one version.
There is no workdir, no merge, no checkout — just read / write / fetch / push.
Requires Node 22.3+, or any runtime with fetch, CompressionStream and
crypto.subtle (browsers, Cloudflare Workers, Deno, Bun).
Node 22.3 is the floor because the default wasm lookup reads the file next to the JS module via
process.getBuiltinModule, which does not exist earlier. On older Node, pass the engine yourself — bytes, aWebAssembly.Module, or anhttp(s)url — and everything else works unchanged.
npm install zig-wasm-git
import { RemoteGit } from "zig-wasm-git"; // TypeScript types included
const git = await RemoteGit.open("https://git.example.com/team/docs.git");
No build step, no wasm option: the package ships the engine next to the JS,
so open() finds it on its own. Bundlers that want the binary as an asset can
import it explicitly:
import wasmUrl from "zig-wasm-git/wasm?url"; // vite
const git = await RemoteGit.open(url, { wasm: wasmUrl });
Grab the prebuilt artefacts — no toolchain needed:
curl -LO https://github.com/est/zig-wasm-git/releases/latest/download/zig_wasm_git.wasm
curl -LO https://github.com/est/zig-wasm-git/releases/latest/download/zig_wasm_git.portable.mjs
Each release ships fixed-name files + SHA256SUMS, built by CI from the tagged
commit (pin a version via the per-tag download path):
zig_wasm_git.wasm — the protocol engine (~60KB)zig_wasm_git.portable.mjs — single-file JS for browser/CF Worker/Node (RemoteGit + memoryStore)import { RemoteGit } from "zig-wasm-git";
const git = await RemoteGit.open("https://user:[email protected]/team/docs.git", {
ref: "main",
});
await git.putMany({ "notes/hello.md": "# hello\n" }, "add hello"); // -> commit sha
await git.push(); // fast-forward only
const blobs = await git.getMany(["notes/hello.md"]);
console.log(new TextDecoder().decode(blobs.get("notes/hello.md"))); // -> "# hello\n"
wasm32-freestanding ReleaseSmall binary with no libc, importing
only env.host_* — SHA-1, zlib inflate/deflate, pack v2 (incl. ofs/ref
delta), delta apply, pkt-line, and smart HTTP (v1 + v2 ls-refs/fetch=filter
fetch, CompressionStream,
crypto.subtle and a pluggable store are the only platform dependencies, so
the same JS runs in Node, browsers and Workers with no node: imports.GitError (kind: "io" for
transport, "protocol" for server refusals); programmer mistakes throw
TypeError. A network failure is never reported as "key not found"
(Errors).RemoteGit is the whole API. Always construct it with await RemoteGit.open()
— instantiation is async, so it is a factory, not a constructor. Async methods
that touch wasm share one queue (single shared memory); version()/log()
are local and skip it.
const git = await RemoteGit.open(url, {
ref: "main", // one branch == one keyspace
// auth: "Bearer ghp_xxx", // see Authentication
// store: myStore, // default memoryStore(); must be SYNCHRONOUS
// wasm: ..., // see "Where the wasm comes from"
// identity: { author: "Bot <[email protected]>" }, // commit author for every write
});
| method | returns | notes |
|---|---|---|
getMany(paths, opts?) | Map(path -> bytes) | paths is a key or an array. Missing keys are skipped, not errors. opts.as: "text" decodes to strings. May hit the network — see Reads can touch the network |
putMany(entries, msg?, opts?) | commit sha | one version (a commit) on the current tip. A null value deletes the key (missing keys are a no-op, empty dirs are pruned), so one call mixes upserts and deletes. { parent } oid for CAS, { identity } for the commit author, { headers } for extra commit headers |
list(prefix?, opts?) | [{path, oid}] | key enumeration. "" (default) lists everything |
log(limit = 10) | [{sha, tree, parents, author, message}] | newest first, local only |
version() | string | null | local tip oid; null when the keyspace is empty. Never hits the network |
remoteVersion() | string | null | remote tip oid, store untouched. Throws on network error |
pull(opts?) | PullResult | refresh from the remote. { filter: "blob:none" } for versions-without-bytes |
push() | PushResult | fast-forward only; rejects on non-fast-forward |
// Reads: a key or an array; missing keys are skipped, never errors.
// { as: "text" } decodes UTF-8 so small text keyspaces skip TextDecoder.
await git.getMany(["some/path/README.md"]); // Map(path -> Uint8Array)
await git.getMany(["some/path/README.md"], { as: "text" }); // Map(path -> string)
await git.getMany(["config.json"], { local: true }); // cache-only, never any I/O
// Writes. Keys are relative paths: no empty segment, no `.` / `..` / `.git`; a
// malformed key throws TypeError rather than being normalized. Content is a
// string or Uint8Array/ArrayBuffer — anything else throws TypeError
// rather than being stored as "[object Object]". A `null` value deletes the
// key (missing keys are a no-op, empty dirs are pruned).
await git.putMany({ "a.txt": "hi" }, "update greeting"); // -> commit sha
await git.putMany({ "a.txt": null }, "drop it"); // -> commit sha
await git.putMany({ "old.txt": null, "new.txt": "hi" }, "rename in one version");
A transport failure throws instead of returning a partial Map, so map.size
is not a health check.
Optimistic concurrency. putMany(..., { parent }) throws CAS_MISMATCH
when the tip moved since you read it, at no extra roundtrip:
const tip = git.version(); // sync, local only; null when the keyspace is empty
if (tip) await git.putMany({ "a.txt": "v2" }, "cas write", { parent: tip });
Check tip for null — a null parent means "no check", which would turn a CAS
write into an unguarded one.
Commit identity. With nothing configured, every commit is written as
zig-wasm-git <zig-wasm-git@localhost> 0 +0000 — deterministic, so a default
sha depends only on content. Set a default per instance and/or override it per
write:
const git = await RemoteGit.open(url, {
identity: { author: "Bot <[email protected]>", timezone: "+0800" },
});
// one write as someone else; fields you omit fall back to the instance default
await git.putMany({ "a.txt": "v2" }, "edit", {
identity: { author: "Eve <[email protected]>", time: new Date("2026-01-01") },
});
author/committer are "Name <email>" (committer defaults to the author),
time is a Date or unix seconds (defaults to now once an identity is set),
and timezone is a "+0800"-style offset. A malformed identity throws
TypeError before anything is written.
Custom headers. putMany(..., { headers }) writes extra commit headers
between committer and the message — "encoding UTF-8", a gpgsig block,
mergetag, or anything else git will carry. Pass a string of lines or an array
of them; a folded value's continuation lines start with a space. The escape
hatch is raw: a verifiable gpgsig still has to be computed over the commit
payload yourself, and tree/parent/author/committer stay managed by the
builder (and are rejected if you try).
await git.putMany({ "a.txt": "v2" }, "sign", {
headers: ["encoding UTF-8", "x-note see RFC 1234"],
});
auth is a raw Authorization header value. Credentials in the URL work too:
they are sent as Basic and stripped from the request URL (this also works
around runtimes that drop URL userinfo, notably workerd).
// 1. in the URL — simplest, and the credentials never reach the URL on the wire
await RemoteGit.open("https://user:[email protected]/team/docs.git");
// 2. a raw header value — tokens, Basic you built yourself
await RemoteGit.open(url, { auth: "Bearer ghp_xxx" });
await RemoteGit.open(url, { auth: `Basic ${btoa("user:token")}` });
In the browser, the git server must send CORS headers for both
/info/refs?service=git-upload-pack and the POST /git-upload-pack. GitHub
and GitLab do not serve smart-HTTP endpoints to arbitrary browser origins, so
browser use usually means going through a same-origin proxy. Workers have no
CORS restriction. If a request fails with NETWORK and the console mentions
CORS, that is why.
Objects live behind a four-method interface, in memory by default:
const git = await RemoteGit.open(url, { store: myStore });
// { get(hex), put(hex, loose), getRef(name), putRef(name, sha) }
A custom store must be synchronous. It is called from wasm host callbacks
that cannot await, so a Promise-returning store would write commits it cannot
read back — a silent data-loss failure, not an error. open() probes the store
and throws TypeError if the contract is broken.
That means you cannot wrap an inherently async backend (IndexedDB, D1, R2)
directly. Buffer in memory and flush, or prehydrate before open().
{ wasm } accepts, in order of convenience:
| you pass | how it loads | use when |
|---|---|---|
| nothing | zig_wasm_git.wasm next to portable.mjs | npm install, or a release pair kept in one directory |
"/path/to.wasm" | fs.readFileSync (Node 22.3+ only) | a checkout — the built wasm is at zig-out/bin/zig_wasm_git.wasm |
"https://…/x.wasm" | fetch | browsers / Workers, wasm served over HTTP |
bytes / ArrayBuffer | passed straight to instantiate | you already fetched or embedded it |
WebAssembly.Module | instantiated, no codegen | workerd CompiledWasm |
Omitting wasm in a clone of this repo fails with a raw filesystem error:
Error: ENOENT: no such file or directory, open '.../src/host/zig_wasm_git.wasm'
That is not a bug in the path — the file simply is not there. The built wasm
lands in zig-out/, so pass it:
const git = await RemoteGit.open(url, { wasm: "zig-out/bin/zig_wasm_git.wasm" });
On a runtime with no filesystem and no reachable default (Node before 22.3,
workerd), the default location is unreachable and open() says so with
TypeError rather than a misleading io/NETWORK error.
getMany and list are reads, but on a cold store they do I/O:
| situation | what happens |
|---|---|
| no tip cached (cold store) | bootstrap: a blob:none pull for structure only, then continue |
| key in the tree, blob not cached | one want=[oids] roundtrip for all such keys |
| key not in the tree at all | zero requests — skipped |
pull() already called | local store only, no requests |
So "read" is not "offline". In a serverless or Worker context the first read of
each instance costs a request. Pass { local: true } when the answer must come
from cache — no bootstrap, no on-demand fetch, and a cache miss stays a cheap
miss:
await git.getMany(["config.json"], { local: true }); // never any I/O
await git.list("", { local: true }); // [] if the tip isn't cached
Each instance holds a wasm linear memory of roughly 5MB (a 4MB arena plus growth) for its lifetime — fine for a long-lived process, worth knowing in a serverless handler or a Worker that creates instances per request.
The store itself never shrinks. It is append-only: every version you write
and every object you pull stays resident, because git history is the point.
A custom store is where you add an eviction policy (TTL, LRU, size cap) if a
long-lived process needs one — memoryStore() has none.
Two kinds, one import. Branch on .kind, never on the message text:
import { GitError } from "zig-wasm-git";
try {
await git.push();
} catch (e) {
if (GitError.isIO(e)) retryLater(e.status); // NETWORK or HTTP; .status is set for HTTP
else if (GitError.isProtocol(e, "NON_FAST_FORWARD")) { /* pull, then rewrite */ }
else if (GitError.isProtocol(e, "CAS_MISMATCH")) { /* someone else wrote; re-read */ }
else throw e; // TypeError / Error: fix the code, don't retry
}
| you write | it means |
|---|---|
GitError.isIO(e) | transport failed (NETWORK / HTTP) — retry later |
GitError.isProtocol(e) | server refused (CAS_MISMATCH, NON_FAST_FORWARD, PUSH_REJECTED, UNPACK_FAILED, NO_V2, NO_REMOTE_REF, NO_SUCH_OBJECT, PROTOCOL_ERROR) — fix the request |
Programmer mistakes are not GitError on purpose — catching them as
"retryable" would loop forever on a bug:
| thrown as | when |
|---|---|
TypeError | bad key, bad arg, oversize value (over the 4MB wasm arena), bad store (missing methods / async), unresolvable local ref, wasm path unreadable on this runtime |
Error | internal invariant (wasm failure, corrupt local store) — report a bug |
A missing key stays a skip and a transport failure stays a throw — never mistake one for the other.
push is fast-forward only. On NON_FAST_FORWARD, pull and rewrite — last writer
wins, and nobody merges for you.TypeError. One wasm call holds several copies of
the data at once, so the measured ceiling is ~768KB per value
(round-trip; reads materialize one extra copy) and ~1MB per putMany
batch. Split bigger values across multiple keys, bigger batches across
calls. Reads loop key-by-key, so one getMany call has no total cap.
Packs arrive whole (arrayBuffer), so keys approaching megabytes also
cost Worker memory per fetch.have lines (protocol v2 fetch is want-only), so incremental bandwidth
relies on server-side delta plus a cached-tip short-circuit. Push sends full objects — no
delta encode; the server re-deltifies on gc.shallow / deepen / notes / LFS / submodules, and no AbortSignal or timeout — a
hung request cannot be cancelled.pull and
remoteVersion refuse with NO_V2.RemoteGit only; use RemoteGit.The protocol-level detail behind each of these — which want/have/delta mechanism is used, what
each filter does, and the module map — is in docs/CAPABILITIES.md. Building,
testing and the release flow are in CONTRIBUTING.md.
Apache-2.0.
git engine without fs nor git command. WASM+JS that speaks directly to any git http. Inspired by Cloudflare Artifacts:
The entire git protocol engine is written in pure Zig (no libc), compiled to a ~100KB WASM binary ... It implements SHA-1, zlib inflate/deflate, delta encoding/decoding, pack parsing, and the full git smart HTTP protocol — all from scratch, with zero external dependencies.
This repo is a minimal reproduction focused on read/write a git remote as a versioned blob store, not a dev workspace.
One branch == one keyspace (path -> bytes), one commit == one version.
There is no workdir, no merge, no checkout — just read / write / fetch / push.
Requires Node 22.3+, or any runtime with fetch, CompressionStream and
crypto.subtle (browsers, Cloudflare Workers, Deno, Bun).
Node 22.3 is the floor because the default wasm lookup reads the file next to the JS module via
process.getBuiltinModule, which does not exist earlier. On older Node, pass the engine yourself — bytes, aWebAssembly.Module, or anhttp(s)url — and everything else works unchanged.
npm install zig-wasm-git
import { RemoteGit } from "zig-wasm-git"; // TypeScript types included
const git = await RemoteGit.open("https://git.example.com/team/docs.git");
No build step, no wasm option: the package ships the engine next to the JS,
so open() finds it on its own. Bundlers that want the binary as an asset can
import it explicitly:
import wasmUrl from "zig-wasm-git/wasm?url"; // vite
const git = await RemoteGit.open(url, { wasm: wasmUrl });
Grab the prebuilt artefacts — no toolchain needed:
curl -LO https://github.com/est/zig-wasm-git/releases/latest/download/zig_wasm_git.wasm
curl -LO https://github.com/est/zig-wasm-git/releases/latest/download/zig_wasm_git.portable.mjs
Each release ships fixed-name files + SHA256SUMS, built by CI from the tagged
commit (pin a version via the per-tag download path):
zig_wasm_git.wasm — the protocol engine (~60KB)zig_wasm_git.portable.mjs — single-file JS for browser/CF Worker/Node (RemoteGit + memoryStore)import { RemoteGit } from "zig-wasm-git";
const git = await RemoteGit.open("https://user:[email protected]/team/docs.git", {
ref: "main",
});
await git.putMany({ "notes/hello.md": "# hello\n" }, "add hello"); // -> commit sha
await git.push(); // fast-forward only
const blobs = await git.getMany(["notes/hello.md"]);
console.log(new TextDecoder().decode(blobs.get("notes/hello.md"))); // -> "# hello\n"
wasm32-freestanding ReleaseSmall binary with no libc, importing
only env.host_* — SHA-1, zlib inflate/deflate, pack v2 (incl. ofs/ref
delta), delta apply, pkt-line, and smart HTTP (v1 + v2 ls-refs/fetch=filter
fetch, CompressionStream,
crypto.subtle and a pluggable store are the only platform dependencies, so
the same JS runs in Node, browsers and Workers with no node: imports.GitError (kind: "io" for
transport, "protocol" for server refusals); programmer mistakes throw
TypeError. A network failure is never reported as "key not found"
(Errors).RemoteGit is the whole API. Always construct it with await RemoteGit.open()
— instantiation is async, so it is a factory, not a constructor. Async methods
that touch wasm share one queue (single shared memory); version()/log()
are local and skip it.
const git = await RemoteGit.open(url, {
ref: "main", // one branch == one keyspace
// auth: "Bearer ghp_xxx", // see Authentication
// store: myStore, // default memoryStore(); must be SYNCHRONOUS
// wasm: ..., // see "Where the wasm comes from"
// identity: { author: "Bot <[email protected]>" }, // commit author for every write
});
| method | returns | notes |
|---|---|---|
getMany(paths, opts?) | Map(path -> bytes) | paths is a key or an array. Missing keys are skipped, not errors. opts.as: "text" decodes to strings. May hit the network — see Reads can touch the network |
putMany(entries, msg?, opts?) | commit sha | one version (a commit) on the current tip. A null value deletes the key (missing keys are a no-op, empty dirs are pruned), so one call mixes upserts and deletes. { parent } oid for CAS, { identity } for the commit author, { headers } for extra commit headers |
list(prefix?, opts?) | [{path, oid}] | key enumeration. "" (default) lists everything |
log(limit = 10) | [{sha, tree, parents, author, message}] | newest first, local only |
version() | string | null | local tip oid; null when the keyspace is empty. Never hits the network |
remoteVersion() | string | null | remote tip oid, store untouched. Throws on network error |
pull(opts?) | PullResult | refresh from the remote. { filter: "blob:none" } for versions-without-bytes |
push() | PushResult | fast-forward only; rejects on non-fast-forward |
// Reads: a key or an array; missing keys are skipped, never errors.
// { as: "text" } decodes UTF-8 so small text keyspaces skip TextDecoder.
await git.getMany(["some/path/README.md"]); // Map(path -> Uint8Array)
await git.getMany(["some/path/README.md"], { as: "text" }); // Map(path -> string)
await git.getMany(["config.json"], { local: true }); // cache-only, never any I/O
// Writes. Keys are relative paths: no empty segment, no `.` / `..` / `.git`; a
// malformed key throws TypeError rather than being normalized. Content is a
// string or Uint8Array/ArrayBuffer — anything else throws TypeError
// rather than being stored as "[object Object]". A `null` value deletes the
// key (missing keys are a no-op, empty dirs are pruned).
await git.putMany({ "a.txt": "hi" }, "update greeting"); // -> commit sha
await git.putMany({ "a.txt": null }, "drop it"); // -> commit sha
await git.putMany({ "old.txt": null, "new.txt": "hi" }, "rename in one version");
A transport failure throws instead of returning a partial Map, so map.size
is not a health check.
Optimistic concurrency. putMany(..., { parent }) throws CAS_MISMATCH
when the tip moved since you read it, at no extra roundtrip:
const tip = git.version(); // sync, local only; null when the keyspace is empty
if (tip) await git.putMany({ "a.txt": "v2" }, "cas write", { parent: tip });
Check tip for null — a null parent means "no check", which would turn a CAS
write into an unguarded one.
Commit identity. With nothing configured, every commit is written as
zig-wasm-git <zig-wasm-git@localhost> 0 +0000 — deterministic, so a default
sha depends only on content. Set a default per instance and/or override it per
write:
const git = await RemoteGit.open(url, {
identity: { author: "Bot <[email protected]>", timezone: "+0800" },
});
// one write as someone else; fields you omit fall back to the instance default
await git.putMany({ "a.txt": "v2" }, "edit", {
identity: { author: "Eve <[email protected]>", time: new Date("2026-01-01") },
});
author/committer are "Name <email>" (committer defaults to the author),
time is a Date or unix seconds (defaults to now once an identity is set),
and timezone is a "+0800"-style offset. A malformed identity throws
TypeError before anything is written.
Custom headers. putMany(..., { headers }) writes extra commit headers
between committer and the message — "encoding UTF-8", a gpgsig block,
mergetag, or anything else git will carry. Pass a string of lines or an array
of them; a folded value's continuation lines start with a space. The escape
hatch is raw: a verifiable gpgsig still has to be computed over the commit
payload yourself, and tree/parent/author/committer stay managed by the
builder (and are rejected if you try).
await git.putMany({ "a.txt": "v2" }, "sign", {
headers: ["encoding UTF-8", "x-note see RFC 1234"],
});
auth is a raw Authorization header value. Credentials in the URL work too:
they are sent as Basic and stripped from the request URL (this also works
around runtimes that drop URL userinfo, notably workerd).
// 1. in the URL — simplest, and the credentials never reach the URL on the wire
await RemoteGit.open("https://user:[email protected]/team/docs.git");
// 2. a raw header value — tokens, Basic you built yourself
await RemoteGit.open(url, { auth: "Bearer ghp_xxx" });
await RemoteGit.open(url, { auth: `Basic ${btoa("user:token")}` });
In the browser, the git server must send CORS headers for both
/info/refs?service=git-upload-pack and the POST /git-upload-pack. GitHub
and GitLab do not serve smart-HTTP endpoints to arbitrary browser origins, so
browser use usually means going through a same-origin proxy. Workers have no
CORS restriction. If a request fails with NETWORK and the console mentions
CORS, that is why.
Objects live behind a four-method interface, in memory by default:
const git = await RemoteGit.open(url, { store: myStore });
// { get(hex), put(hex, loose), getRef(name), putRef(name, sha) }
A custom store must be synchronous. It is called from wasm host callbacks
that cannot await, so a Promise-returning store would write commits it cannot
read back — a silent data-loss failure, not an error. open() probes the store
and throws TypeError if the contract is broken.
That means you cannot wrap an inherently async backend (IndexedDB, D1, R2)
directly. Buffer in memory and flush, or prehydrate before open().
{ wasm } accepts, in order of convenience:
| you pass | how it loads | use when |
|---|---|---|
| nothing | zig_wasm_git.wasm next to portable.mjs | npm install, or a release pair kept in one directory |
"/path/to.wasm" | fs.readFileSync (Node 22.3+ only) | a checkout — the built wasm is at zig-out/bin/zig_wasm_git.wasm |
"https://…/x.wasm" | fetch | browsers / Workers, wasm served over HTTP |
bytes / ArrayBuffer | passed straight to instantiate | you already fetched or embedded it |
WebAssembly.Module | instantiated, no codegen | workerd CompiledWasm |
Omitting wasm in a clone of this repo fails with a raw filesystem error:
Error: ENOENT: no such file or directory, open '.../src/host/zig_wasm_git.wasm'
That is not a bug in the path — the file simply is not there. The built wasm
lands in zig-out/, so pass it:
const git = await RemoteGit.open(url, { wasm: "zig-out/bin/zig_wasm_git.wasm" });
On a runtime with no filesystem and no reachable default (Node before 22.3,
workerd), the default location is unreachable and open() says so with
TypeError rather than a misleading io/NETWORK error.
getMany and list are reads, but on a cold store they do I/O:
| situation | what happens |
|---|---|
| no tip cached (cold store) | bootstrap: a blob:none pull for structure only, then continue |
| key in the tree, blob not cached | one want=[oids] roundtrip for all such keys |
| key not in the tree at all | zero requests — skipped |
pull() already called | local store only, no requests |
So "read" is not "offline". In a serverless or Worker context the first read of
each instance costs a request. Pass { local: true } when the answer must come
from cache — no bootstrap, no on-demand fetch, and a cache miss stays a cheap
miss:
await git.getMany(["config.json"], { local: true }); // never any I/O
await git.list("", { local: true }); // [] if the tip isn't cached
Each instance holds a wasm linear memory of roughly 5MB (a 4MB arena plus growth) for its lifetime — fine for a long-lived process, worth knowing in a serverless handler or a Worker that creates instances per request.
The store itself never shrinks. It is append-only: every version you write
and every object you pull stays resident, because git history is the point.
A custom store is where you add an eviction policy (TTL, LRU, size cap) if a
long-lived process needs one — memoryStore() has none.
Two kinds, one import. Branch on .kind, never on the message text:
import { GitError } from "zig-wasm-git";
try {
await git.push();
} catch (e) {
if (GitError.isIO(e)) retryLater(e.status); // NETWORK or HTTP; .status is set for HTTP
else if (GitError.isProtocol(e, "NON_FAST_FORWARD")) { /* pull, then rewrite */ }
else if (GitError.isProtocol(e, "CAS_MISMATCH")) { /* someone else wrote; re-read */ }
else throw e; // TypeError / Error: fix the code, don't retry
}
| you write | it means |
|---|---|
GitError.isIO(e) | transport failed (NETWORK / HTTP) — retry later |
GitError.isProtocol(e) | server refused (CAS_MISMATCH, NON_FAST_FORWARD, PUSH_REJECTED, UNPACK_FAILED, NO_V2, NO_REMOTE_REF, NO_SUCH_OBJECT, PROTOCOL_ERROR) — fix the request |
Programmer mistakes are not GitError on purpose — catching them as
"retryable" would loop forever on a bug:
| thrown as | when |
|---|---|
TypeError | bad key, bad arg, oversize value (over the 4MB wasm arena), bad store (missing methods / async), unresolvable local ref, wasm path unreadable on this runtime |
Error | internal invariant (wasm failure, corrupt local store) — report a bug |
A missing key stays a skip and a transport failure stays a throw — never mistake one for the other.
push is fast-forward only. On NON_FAST_FORWARD, pull and rewrite — last writer
wins, and nobody merges for you.TypeError. One wasm call holds several copies of
the data at once, so the measured ceiling is ~768KB per value
(round-trip; reads materialize one extra copy) and ~1MB per putMany
batch. Split bigger values across multiple keys, bigger batches across
calls. Reads loop key-by-key, so one getMany call has no total cap.
Packs arrive whole (arrayBuffer), so keys approaching megabytes also
cost Worker memory per fetch.have lines (protocol v2 fetch is want-only), so incremental bandwidth
relies on server-side delta plus a cached-tip short-circuit. Push sends full objects — no
delta encode; the server re-deltifies on gc.shallow / deepen / notes / LFS / submodules, and no AbortSignal or timeout — a
hung request cannot be cancelled.pull and
remoteVersion refuse with NO_V2.RemoteGit only; use RemoteGit.The protocol-level detail behind each of these — which want/have/delta mechanism is used, what
each filter does, and the module map — is in docs/CAPABILITIES.md. Building,
testing and the release flow are in CONTRIBUTING.md.
Apache-2.0.