// ZVCS — ENGINEERING REPORT

zvcs v0.22.5 · one Rust binary named git · git-compat via vendored gitoxide · superset: fair FIFO index-lock daemon, reconcile-to-mainline attaching, forward-only gitlink bumps · MIT · in active development

Docs GitHub
// Color scheme

>_ENGINEERING REPORT

zvcs is a git-shadowing superset VCS in Rust: a single binary named git shadows stock git on PATH, serves git-compat porcelain via vendored gitoxide, and adds a superset coordination layer for meta-repos under many concurrent agents. This report describes the architecture, the superset design, the concurrency model, the current state of the tree, and the dependency posture. The statements below are facts about the design and the source, not aspirational metrics.

git
the one binary, shadowed
119
superset verbs
181
git-compat verbs dispatched
v0.22.5
version
MIT
license · free / OSS

Summary

gitoxide already ports git to Rust. zvcs does the thing git structurally cannot: it wraps that library in a single binary named git that shadows stock git on PATH, and layers on coordination verbs designed for the exact failure modes of driving a large meta-repo of submodules under many concurrent automated agents. There is no fork/exec of stock git — every subcommand is served in-process against the vendored gix crates, so tools already on PATH (RustRover, gh, cargo) read the same on-disk .git and see identical behavior.

The world's-first leg is the superset, not the port. Its founding three verbs close three reproducible stock-git failure modes: zdaemon replaces the index.lock flock with a fair FIFO backed by a machine-wide daemon; zsync reconciles submodules to their tracked mainline and keeps HEAD attached; zbump makes gitlink bumps forward-only and commits them. The superset has since grown to 116 verbs — coordination, an async job queue, a machine-wide repo index and status cache, cross-repo timeline and undo, tree-wide snapshots, per-agent worktrees, policy, and live monitoring.

The current tree dispatches 181 git-compat verbs and 119 superset verbs natively — no fork/exec of stock git at any point — across ~381k lines in src/extensions, on top of 67 vendored gix crates, with 2,557 integration tests across 404 files. Coverage is not parity: a subcommand that dispatches is not thereby byte-faithful, and depth varies per subcommand. See the performance section for how the read path compares to stock git, measured rather than asserted.


Architecture

One dispatch table routes two namespaces; the execution path never leaves the process:

git <subcommand>  →  dispatch::run  ┬─  superset verbs (z*, 119)  →  daemon · queue · ledger · fleet
                                    └─  git-compat porcelain (181) →  gitoxide (gix) library

No fork/exec

Every subcommand runs in-process against the vendored gix crates. Unported subcommands error terse (zsh-style zvcs: <cmd>: <reason> on stderr) rather than falling through to stock git — this is a from-scratch engine, not a shim (src/dispatch.rs).

Vendored, owned 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 backend are removed; git is the only binary.

Same on-disk .git

The binary discovers and reads the same .git directory stock git does, so RustRover, gh, and cargo observe identical repository state and a consistently staged index.

Pure-Rust TLS fetch

gix is built with blocking-http-transport-reqwest-rust-tls, so zsync's reconcile fetch runs over HTTPS on a pure-Rust TLS stack with no curl/openssl C toolchain.


The concurrency model — zdaemon

Stock git serializes index writes with an index.lock file created via O_EXCL: a contended writer does not wait, it fails (fatal: Unable to create '.git/index.lock': File exists). Under many concurrent agents that turns a queue into a thundering herd of retries with no fairness guarantee. zdaemon replaces the flock with a FIFO userspace barrier: a single worker thread owns the abstract critical section and drains an mpsc channel of requests in arrival order. That arrival order is the fairness guarantee — first-come-first-served, no starvation, no lost wakeups.

Clients reach the daemon through RepoLock::acquire (src/lock.rs), an RAII guard that blocks in the FIFO and returns only when the caller holds the lock. Release is automatic: dropping the guard sends RELEASE and closes the socket, and the daemon also auto-releases on socket EOF, so a crashed holder can never wedge the repo. If no daemon is reachable the lock degrades to a no-op guard — the operation still runs (stock-git behavior minus the fair queue); ensuring a daemon is up is the autonomous layer's job, not the writer's.

LineDirectionMeaning
ACQUIRE <id>client → daemonEnqueue a lock request; answered GRANTED on the same stream at the FIFO head. The client keeps the connection open while it holds the lock.
RELEASE <id>client → daemonCurrent holder releases; the daemon grants the next queued waiter.
STATUSclient → daemonReply one line holder=<id|none> queue=<depth>, then close.
STOPclient → daemonReply STOPPING, remove the socket at <git-dir>/zvcs.sock, exit.
GRANTEDdaemon → clientThe lock is now yours (in response to ACQUIRE).
ERR <reason>daemon → clientMalformed request.

The submodule model — zsync & zbump

zsync reconciles each submodule (or a named subset) to its tracked mainline. Mainline detection prefers refs/remotes/origin/main and falls back to origin/master; a repo with neither is skipped, not errored. The operation fetches origin, then fast-forwards only: a dirty worktree is skipped untouched, and an unpushed local commit is never regressed or clobbered. On a genuine fast-forward the local mainline branch is advanced, HEAD is re-attached to it, and the clean worktree plus index are moved to the new tree by writing only the files that actually changed (reconcile_repo in src/superset/zsync.rs).

zbump advances the parent's recorded gitlink to each submodule worktree's current HEAD, but only when that HEAD is a descendant of the pointer already recorded — a fast-forward. It never regresses or diverges a pointer. The parent index is opened once, mutated for every qualifying submodule, and staged once at the end, so tools on PATH observe a single consistent index write (src/superset/zbump.rs).

Always attached

zsync leaves HEAD pointing at the mainline branch, never detached — the orphaning failure mode of git submodule update cannot occur.

Never destructive

Fast-forward only, dirty worktrees skipped. An unpushed local commit is preserved, not clobbered; there is no reset-to-remote.

Forward-only pointers

zbump's descendant check makes a backwards gitlink move structurally impossible, closing the stale-worktree regression hole of a blanket git add.

Minimal writes

Reconcile moves only the files that differ between the old and new tree, keeping worktree churn and index writes proportional to the actual change.


Component status

ComponentStateNotes
git shadow binary + dispatchImplementedSingle binary named git; two-namespace dispatch, terse zsh-style errors (src/main.rs, src/dispatch.rs).
vendored gitoxide (src/ported)ImplementedIn-tree gix + gix-* crates, own excluded workspace; gix/ein/gitoxide-core removed.
zdaemon — fair FIFO index-lock coordinatorImplementedstart/stop/status; mpsc arrival-order barrier, socket-EOF auto-release (src/superset/zdaemon.rs).
RepoLock daemon clientImplementedRAII acquire/release, no-op-guard fallback when no daemon is reachable (src/lock.rs).
zsync — reconcile-to-mainline attacherImplementedFast-forward only; skips dirty; re-attaches HEAD; writes only changed files (src/superset/zsync.rs).
zbump — forward-only gitlink bumpsImplementedDescendant check before staging; parent index staged once (src/superset/zbump.rs).
rev-parsePartialResolves HEAD and --abbrev-ref HEAD only (src/porcelain.rs).
git-compat porcelain surfacePlannedstatus, add, commit, log, and the rest ported incrementally on gitoxide.

gitconfig coverage

zvcs honors 152 of the 1003 configuration keys git 2.55 documents via git help --config — about 15%. This is a measured figure, not an estimate: git's key list intersected with the keys read in src/extensions/src and the keys the vendored gix engine declares in config/tree. A setting is only counted where the behavior that consumes it exists; the loader itself (gix) parses every key regardless.

SourceKeysNotes
git 2.55 documented total1003Everything git help --config lists.
Honored by zvcs porcelain92Read and applied directly in the ported subcommands (e.g. core.pager, alias.*, help.autocorrect, merge.ff, tag.sort, format.*).
Honored by the gix engine75Applied when gix performs the operation — core.autocrlf on checkout, diff.renames on diff, and the rest of its typed config tree.
Distinct union152 (15%)Union of the two, de-duplicated against git's key list (15 keys overlap).

The remaining ~851 keys are dominated by configuration for commands that are not yet ported or are server-side, where reading the key would be meaningless without the command behind it: receive.* (94), fsck.* (77), sendemail.* (42), and the gui / gitweb / imap / gitcvs / uploadpack / instaweb / trace2 families. Coverage of the config surface reachable from ported commands is far higher than the headline 15%.


Dependency posture

Dependencies are kept foundational and durable. The src/extensions crate has a deliberately small direct-dependency surface; everything git-related comes from the vendored, owned gitoxide tree rather than external crates.

DependencyRole
gix (path: src/ported/gix)The vendored gitoxide library — repository discovery, refs, index, submodules, transport. Built with blocking-http-transport-reqwest-rust-tls for pure-Rust HTTPS fetch.
anyhowError propagation for the dispatch layer and terse stderr reporting.
std::os::unix::netThe zdaemon unix-socket coordinator and RepoLock client (standard library, no dependency).

Compatibility & longevity

Drop-in on PATH

The binary is named git and reads the same on-disk .git, so it slots under existing tooling without config changes for ported subcommands.

Cross-architecture

macOS aarch64 and Linux x86_64 / aarch64 — the same targets the meta-repo agents run on. No C toolchain required for the TLS fetch path.

Crash-safe locking

Socket-EOF auto-release means a killed lock holder cannot wedge the repo — a strict improvement over a stale index.lock that must be removed by hand.

Owned vendor tree

gitoxide is vendored in-tree, not pulled from a registry, so the build is reproducible and the port surface is under direct control.


Links