>_ZVCS REFERENCE
A git-shadowing superset VCS in Rust. A single binary named git shadows stock git on PATH and serves subcommands natively via vendored gitoxide — there is no fork/exec of stock git. On top of git compatibility it adds the zvcs superset: coordination verbs stock git structurally cannot have, aimed at driving a large meta-repo of submodules under many concurrent automated agents. Early, in active development.
What it is
zvcs is a from-source VCS, not a wrapper. The git binary discovers and reads the same on-disk .git directory stock git does, so tools already on PATH (RustRover, gh, cargo) see identical behavior. Git-compat porcelain is ported incrementally on top of the vendored gitoxide (gix) library; when a subcommand is not yet ported the binary errors terse rather than falling through to stock git.
The world's-first leg is not "git in Rust" — gitoxide is already that. It is the superset coordination layer: a fair FIFO index-lock daemon, a reconcile-to-mainline submodule attacher, and forward-only gitlink bumps, served from the same binary that answers rev-parse.
The problem it solves
The meta-repo zvcs targets is a shell of git submodules driven by up to 16 concurrent automated agents. Stock git handles that topology poorly in three specific, reproducible ways, and each superset verb closes one of them:
index.lock contention
Git's O_EXCL lockfile fails a contended writer instead of queuing it. One machine-wide zdaemon with a per-repo FIFO lane serializes writers first-come-first-served; unrelated repos run fully in parallel.
Detached HEAD by default
git submodule update leaves submodules detached, orphaning work. zsync + the daemon's attach-scan keep every submodule attached to its mainline — even a dirty detached HEAD is rescued in place (no-clobber).
Constant pointer markers
Every submodule commit dirties the parent's gitlink. zbump + autobump do forward-only pointer bumps and commit them (clearing the (new commits) marker), coalesced on a file-watch, so agents never touch the root.
Agents colliding on one tree
N agents editing one meta tree collide on files, index, and HEAD. zworktree gives each agent a private, object-sharing worktree of the whole submodule tree — complete isolation, no re-clone.
Architecture
Two namespaces share one dispatch table (src/extensions/src/dispatch.rs):
git <subcommand> → dispatch ┬─ superset verbs (z*, 119) → coordination · queue · ledger
│ (zdaemon zsync zbump zcommit zpush
│ zjobs zjob zrepos zreindex zstatus
│ zlog zundo zclaim zwho zsnapshot
│ zrestore zworktree zrepl zguard
│ zprecache zppid zprocs …)
└─ git-compat porcelain (181) → gitoxide (gix) library
Vendored gitoxide
src/ported holds the gix + gix-* crates in-tree as a self-contained workspace, excluded from the root and consumed as a path dependency. The gix/ein CLIs and gitoxide-core are removed; git is the only binary.
The shadow binary
src/extensions is the zvcs crate whose one binary is named git. It shadows stock git on PATH and routes every subcommand through dispatch::run.
RAII lock client
RepoLock::acquire (src/extensions/src/lock.rs) routes index-mutating ops through the daemon's FIFO and releases on drop. No reachable daemon degrades to a no-op guard.
Pure-Rust TLS fetch
gix is built with blocking-http-transport-reqwest-rust-tls so zsync's reconcile fetch runs over HTTPS with no curl/openssl C toolchain.
The zdaemon coordinator
One machine-wide zdaemon (state under ~/.zvcs/, socket ~/.zvcs/zvcs.sock) is the fair replacement for index.lock plus the host for reactive autonomy, the SQLite ledger, and the async job queue. The lock is per-repo — unrelated repos run in parallel; only same-repo writers serialize, first-come-first-served. It is reactive (no timers, no polling): a git pull/commit updates local refs, a notify file-watch fires, and the daemon reacts (attach, autobump, reconcile, status, hooks). Release is automatic (RAII on the client, socket-EOF on the daemon). Index writes also go through index.lock via gix-lock for stock-git interop. The wire protocol is line-based over the unix socket:
| Line | Direction | Meaning |
|---|---|---|
ACQUIRE <id> <git-dir> | client → daemon | Enqueue on that repo's lane; answered GRANTED at its head. |
RELEASE <id> | client → daemon | Current holder releases; the next waiter is granted. |
SUBMIT <json> | client → daemon | Queue an async job (zcommit/zpush); answered JOB <id>. |
JOBSTOP / JOBRESTART <id> | client → daemon | Cancel / re-enqueue a job. |
STATUS / STOP | client → daemon | Snapshot / shut the daemon down. |
Autonomy is gated by [zvcs] gitconfig. Off unless set: autoreconcile, autobump, autocrawl, autostatus, autohook, autodups, hook. On by default: precache (precompute the log caches when a watched repo's refs move — see Performance). Tunables: interval (debounce), statusinterval, watchmru, crawlroots, worktreebase. UI, not daemon behaviour: replvimode (vi keys in git zrepl), topscheme and toppalette (git ztop colours, written by its own scheme picker). Headless failures are recorded in the ledger and surfaced on your next git command.
Usage
# build, then put the shadow binary first on PATH — from here on, # `git` IS zvcs (that is the whole point: it shadows stock git) cargo build export PATH="$PWD/target/debug:$PATH" # git-compat: resolve HEAD against the on-disk .git via gitoxide git rev-parse HEAD git rev-parse --abbrev-ref HEAD # superset: the singleton fair-lock coordinator (full control surface) git zdaemon start git zdaemon status # holder / lane snapshot git zdaemon info # pid, socket, paths, config git zdaemon ping # exit 0 if live (scriptable) git zdaemon restart # respawn, re-reading config git zdaemon log -f # tail ~/.zvcs/zvcs.log git zdaemon stop # coordination: reconcile submodules to mainline (attached, ff-only) + bump pointers git zsync # reconcile submodules git zbump # forward-only gitlink bumps (+ commit) git zup # bring the whole tree to latest origin/main # repo index + machine-wide status git zreindex # crawl for git repos git zrepos # list them (pipe-clean) git zstatus --all # instant status across every indexed repo # async queue + ledger git zcommit file.rs -m "msg" --push git zjobs # recent jobs git zjob 42 # one job (zjob stop|restart 42) # multi-agent, timeline, snapshots, isolated worktrees git zclaim # lease this repo for $ZVCS_SESSION git zwho # who holds what git zlog # cross-repo reflog timeline git zundo # rewind this repo one step git zsnapshot before # tree-wide restore point (committed HEADs) git zrestore before # restore the whole tree git zstash # park uncommitted work across the tree git zunstash # restore it (LIFO) git zworktree add agent3 # private isolated tree for an agent
Performance — how the read path beats stock git
Every read command measured against stock git is ahead, from 1.19x to 16.05x. The
diagrams below are the whole explanation: what the time is actually spent on, the five
levers that move it, and the three things this does not fix. Numbers come from
scripts/bench.sh on zshrs (6,376 commits) against Apple git 2.50.1, 18 cores,
release build.
The shape of the problem
A read-only git command spends almost none of its time on git. It spends it on per-item work: decode this commit, diff that tree pair, count lines in this blob, abbreviate that id. The items are independent, the objects are immutable while the command runs, and most of the answers are the same every time they are asked for.
Stock git does that work on one core, from scratch, on every invocation, with nothing of itself alive between two commands. Those three properties are what the five levers below attack — and only the first of them is ordinary optimization.
A · every core
Patches, per-file analysis, pickaxe scans and record rendering are fanned across a worker pool. git's diff and log machinery is single-threaded.
B · less work
-S counts a needle in blobs instead of rendering patches; name-only formats stop reading blobs they never print.
C · a cache
Values that are pure functions of immutable objects are stored in memory-mapped rkyv images and never recomputed — machine-wide, shared across clones. A hit is a binary search and a slice into the mapping: nothing decoded, nothing allocated.
D · a daemon
Those values are computed when a repo's refs move, before anyone asks. Structurally impossible for git.
E · off the critical path
Filling a cache is bookkeeping. The rows are queued to a writer thread; the command returns without waiting on a transaction.
Lever A — fan the work across the machine
The object store cannot change while a read-only verb runs, so nothing forces the per-item
work to be sequential. Four paths use the pool: log -p patches,
diff per-file analysis, log -S/-G scans, and
shortlog record extraction.
Workers pull from a shared cursor rather than taking a fixed slice. One commit that rewrites a large file outweighs a hundred that touch a line each, so a static split leaves every worker but one idle:
Sync,
and a handle clone shares the underlying object store rather than re-opening it.
ZVCS_THREADS pins the count; ZVCS_THREADS=1 forces the sequential
path and produces byte-identical output, which is asserted by test.
Output stays a stream. log -p renders a window of commits ahead of the writer
and hands them out in walk order, so memory is bounded by the window rather than by the
length of the history:
--oneline, %s, the default
format): reading 6,000 commit objects is the entire cost of those formats, and none of
those reads depends on another.
Lever B — stop doing work nobody asked for
Two cases mattered more than the threading. The pickaxe was building a full patch for every
candidate commit and then counting occurrences in the +/- lines.
git never does that: has_changes counts the needle in each side's whole blob
and keeps the file when the two counts differ — the needle's position is
irrelevant.
log -S return over 6,376
commits — 2.673 s against git's 7.209 s, byte-identical hit sets across
six needles including one with 5,320 hits.
The second case: --name-only, --name-status, --raw
and --summary were analyzing blobs to produce line counts that those formats
never print. Skipping the analysis took diff --name-only HEAD~5 from
26.7 ms to 9.5 ms in isolation — the change list was already in hand.
Lever C — a zero-copy cache for values that cannot go stale
Git objects are content addresses. That makes certain answers permanent, not merely cacheable-with-invalidation: there is no event that can make them wrong.
--stat,
--numstat, --shortstat, --name-only,
--name-status and the path-limited traversal predicate.
Writers append to a journal and fold it into the image once it grows;
readers take no lock, and every race degrades to a miss, never to a
wrong answer.
Lever D — compute it before it is asked for
This is the one advantage that is structural rather than algorithmic. The daemon is already
awake and already learns that a watched repository's refs moved; the values above can be
computed at that moment instead of when someone runs log --stat.
log --stat -n 150 takes 432 ms cold,
12.0 ms warm, and git zprecache -n 150 — the work the daemon does on its
own — takes 0.52 s once.
Lever E — never make the caller wait on bookkeeping
A cache row is written only because the answer was already computed. Making the user wait for it is backwards — and the first version did worse than wait: it opened a connection and re-ran the schema batch per row, in the middle of the walk. The cold run, the one the cache cannot help yet, was carrying the entire cost of building it.
log --stat -n 150: 831 ms → 402 ms, against git's 532 ms.
Results
zshrs (6,376 commits) · stock git 2.50.1 · 18 cores · 12 runs after 3 warmups · release
build · both binaries measured in one interleaved hyperfine run so machine
load moves them together. Regenerate with scripts/bench.sh <repo>.
for-each-ref
1.35x, ls-files 1.35x, rev-list --count 1.34x,
diff --name-only HEAD~5 1.20x.
Which lever moved which command
| Command | Levers | What actually changed |
|---|---|---|
| log --stat | C, D, E | tree-pair tallies memoized; daemon warms them; writes no longer stall the walk |
| status | — | already ahead: gitoxide's index + worktree scan, no per-file fork |
| blame | C, D, E | run-length attribution memoized per (commit, path, algo) |
| log -S / -G | A, B | blob counting instead of patch rendering; rename gate; merges dropped; scan fanned out |
| log --oneline, %s, default | A | commit records rendered 256 at a time across the pool |
| log -p | A | 64-patch look-ahead window, emitted in walk order |
| diff --stat (worktree) | A | per-file analysis fanned out — no cacheable key exists for a worktree side |
| diff --name-only, --raw | B | stopped reading blobs for counts those formats never print |
| tag -l | B | names-only path: a plain listing no longer decodes and peels every tag object |
| shortlog | A | per-commit record extraction fanned out |
Cold versus warm
| Command | zvcs cold | zvcs warm | git |
|---|---|---|---|
| log --stat -n 30 | 121.6 ms | 8.2 ms | 142.0 ms |
| log --stat -n 150 | 432.2 ms | 12.0 ms | 544.0 ms |
| blame README.md | 72.1 ms | 10.4 ms | 43.7 ms |
Cold means the cache is deleted before every single run — the worst case, and not
one a running daemon leaves behind. Cold blame is the one row that loses to
git, and it is reported as measured: gix's blame walk is slower than git's, so the first
blame of a file costs ~1.7x git. The cache is what turns it around on the second.
What this does not fix
Hunk boundary placement still differs from git
On 31 of 400 sampled commits, a patch places an ambiguous hunk boundary differently than
git does — same content, same file list, a valid diff, but not byte-identical, and it
shifts --stat counts by a line or two on those commits. The vendored
imara-diff slides a group to a different position than git's
xdl_change_compact. It is not the indent heuristic: disabling that in git
does not reproduce our placement either. Porting the compaction is the fix.
Nothing here speeds up writes
Every measurement on this page is a read-only command. Mutating verbs are excluded from the benchmark set on purpose — a benchmark has to be repeatable, and a mutating verb changes the repository under its own measurement.
Parallelism is not free under load
zvcs deliberately uses every core, so a busy machine compresses its lead rather than
git's. The figures above were taken at load ~20 on an 18-core box; on an idle machine the
ratios are larger, not smaller. ZVCS_THREADS pins the worker count when that
trade is not wanted.
Status & roadmap
Early and in active development. The table reflects the current state of the tree.
| Component | State | Notes |
|---|---|---|
Shadow git binary + vendored gitoxide | Implemented | Single binary named git; two-namespace dispatch; in-tree gix/gix-* workspace. |
| Singleton daemon + per-repo FIFO lanes | Implemented | ~/.zvcs/; parallel across repos; socket-EOF auto-release; reactive file-watch, no polling. |
Coordination — zsync / zbump / attach | Implemented | ff-only reconcile; forward-only bump + commit; detached-HEAD attach-scan (dirty-safe). |
| SQLite ledger + repo index + crawler | Implemented | zrepos/zreindex (pipe-clean, prunes deleted); WAL ledger; notify-on-next-command. |
Zero-copy derived-answer cache (~/.zvcs/cache/*.rkyv) | Implemented | mmap'd rkyv images for tree diffs, blames and abbreviations; lock-free reads, journal + flock'd compaction for writes. |
Async queue — zcommit/zpush/zjob(s) | Implemented | Bounded job pool; ls-refs push pre-flight; zjob stop/restart; sync fallback. |
| Multi-agent, status, timeline, snapshots | Implemented | zclaim/zwho; zstatus --all; zlog/zundo; zsnapshot/zrestore; typed hooks. |
Per-agent isolated worktrees (zworktree) | Implemented | Object-sharing linked worktrees of the whole submodule tree; stock-git interop verified. |
| Git-compat parity | Ongoing | Every subcommand dispatches natively; per-flag parity with stock git is measured by the harness and is the work that remains — see the report. |
Building from source
zvcs builds as a standalone Rust workspace:
# clone git clone https://github.com/MenkeTechnologies/zvcs cd zvcs # build and shadow stock git on PATH cargo build export PATH="$PWD/target/debug:$PATH" # smoke-test git-compat against this repo's own .git git rev-parse HEAD
src/ported is a self-contained workspace excluded from the root and consumed by src/extensions as a path dependency. gix is built with the blocking-http-transport-reqwest-rust-tls feature so zsync can fetch over HTTPS with a pure-Rust TLS stack.
License
zvcs is MIT licensed — free and open source. See LICENSE.
Repository & links
- Engineering report — report.html (architecture, the superset layer, value posture, roadmap)
- Parity report — port_report.html (per-subcommand parity against stock git, measured by the
zvcs-parityharness at generation time — regenerate withcargo run -p zvcs-parity -- --bin $PWD/target/debug/git --html docs/port_report.html) - Plugin system — ZNATIVE.md (
git znative, the plugin store, and the C ABI a native plugin is written against) - Source — github.com/MenkeTechnologies/zvcs
- Issues — github.com/MenkeTechnologies/zvcs/issues
- gitoxide — github.com/GitoxideLabs/gitoxide (the ported git library)