the binary is named git and shadows stock git
August 30, 2026 · View on GitHub
███████╗██╗ ██╗ ██████╗███████╗
╚══███╔╝██║ ██║██╔════╝██╔════╝
███╔╝ ██║ ██║██║ ███████╗
███╔╝ ╚██╗ ██╔╝██║ ╚════██║
███████╗ ╚████╔╝ ╚██████╗███████║
╚══════╝ ╚═══╝ ╚═════╝╚══════╝
[GIT, SHADOWED AND SUPERSET — ONE RUST BINARY NAMED git]
"gitoxide already ported git to Rust. zvcs does the thing git structurally cannot: fair mutual exclusion, self-attaching submodules, forward-only pointer bumps — a VCS built for a meta-repo under many concurrent agents."
zvcs is a git-shadowing superset VCS. It ships a single Rust binary named
git that shadows stock git on PATH and serves subcommands natively via
vendored gitoxide crates — there is
no fork/exec of stock git. On top of git compatibility it adds the zvcs
superset: coordination verbs stock git cannot have, aimed at the exact
failure modes of driving a large meta-repo of submodules under many concurrent
automated agents.
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 attacher, and forward-only gitlink bumps, served from the
same binary that answers rev-parse.
Read the Docs · Engineering Report · Parity Report · gitoxide
Table of Contents
- [0x00] Overview
- [0x01] The Problem It Solves
- [0x02] Build
- [0x03] Subcommands
- [0x04] The Superset Verbs
- [0x05] The zdaemon Coordinator
- [0x06] Layout
- [0x07] Status & Roadmap
- [0x08] Benchmarks
- [0x09] Documentation
- [0xFF] License
[0x00] OVERVIEW
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 — this is a from-scratch engine, not a shim.
The reason zvcs exists is the superset. The meta-repo it 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.
[0x01] THE PROBLEM IT SOLVES
| Stock-git failure mode | zvcs answer |
|---|---|
index.lock contention. Git guards index writes with an O_EXCL lockfile; a contended writer does not wait, it fails (Unable to create '.git/index.lock'). Under N agents that is a thundering herd of retries with no fairness. | zdaemon — one machine-wide daemon with a per-repo FIFO lane replaces the flock. A contended writer blocks in arrival order and is answered GRANTED only at the head of its repo's lane; first-come-first-served, no starvation, unrelated repos fully parallel. A foreign holder of git's own lockfile (stock git, an IDE, a hook) is invisible to that lane, so it is waited out (bounded) and then the command is queued as a job — never surfaced as a lock error. |
Detached HEAD by default. git submodule update leaves every submodule on a detached HEAD, so committed work is orphaned unless the caller re-attaches by hand. | zsync + the daemon's attach-scan reconcile each submodule to its tracked mainline (origin/main, else origin/master) and keep HEAD attached — even a dirty detached HEAD is rescued in place (no-clobber ref op). Fast-forward only. |
Constant modified: <sub> (new commits) markers + stale pointers. Every submodule commit dirties the parent's gitlink; a blanket git add can also move it backwards. | zbump + autobump — forward-only gitlink bumps, committed (clears the marker), coalesced, done by the daemon on a file-watch so agents never touch the root. Never regresses a pointer. |
| Agents colliding on one shared tree. N agents editing one meta tree collide on files, index, and HEAD. | zworktree — one command gives each agent a private, object-sharing worktree of the whole submodule tree; complete isolation, no re-clone. |
[0x02] BUILD
Homebrew installs the binary as zvcs, so it never clobbers the git formula —
shadowing is opt-in, and zvcs zshadow is the whole opt-in:
brew install menketechnologies/menketech/zvcs
zvcs zshadow # install ~/.zvcs/{bin,man,completions}, print the shell lines
eval "$(zvcs zshadow)" # …apply them here, or paste them into ~/.zshrc
git zdoctor # in a new shell, `git` is zvcs
Re-run zvcs zshadow after a brew upgrade so the ~/.zvcs/bin/git symlink
follows the new build. From source:
git clone https://github.com/MenkeTechnologies/zvcs
cd zvcs
cargo build
# the binary is named `git` and shadows stock git — put it first on PATH
export PATH="$PWD/target/debug:$PATH"
git rev-parse HEAD
git zshadow then installs the permanent shadow — a git symlink and the
git-<verb> dashed links in ~/.zvcs/bin, every man page in ~/.zvcs/man, the
HTML documentation set in ~/.zvcs/share/doc/git-doc, and the forked zsh _git
in ~/.zvcs/completions — and prints the three shell lines that activate them
(stdout is shell code only; the summary goes to stderr):
git zshadow # install, then print the lines
eval "$(git zshadow)" # …and apply them to this shell
git zshadow --print --all # print without installing (to paste into ~/.zshrc)
The workspace has two members-by-convention: src/ported (the vendored gitoxide
crates, a self-contained workspace excluded from the root) is consumed by
src/extensions (the zvcs crate) as a path dependency. gix is built with the
blocking-http-transport-reqwest-rust-tls feature so zsync's reconcile fetch
runs over HTTPS on a pure-Rust TLS stack — no curl/openssl C toolchain.
[0x03] SUBCOMMANDS
Two namespaces share one dispatch table (src/extensions/src/dispatch.rs):
- superset verbs (
z*) — the novel coordination layer. - git-compat porcelain — stock git subcommands served via gitoxide, ported incrementally.
| Verb group | Verbs | What it does |
|---|---|---|
| Coordination | zdaemon zsync zbump zup | singleton daemon; reconcile submodules; forward-only bumps; zup brings the whole tree (parent + nested submodules) to latest origin/main |
| Stash | zstash zunstash zstashes | park/restore uncommitted work across the whole submodule tree as one unit |
| Repo index | zrepos zreindex | machine-wide index of every git repo (retires a shell repo-list) |
| Async queue | zcommit zpush zsubmit zjobs zjob | fire-and-forget commit/push/arbitrary-command jobs on the daemon's worker pool + ledger (zjob stop/restart); zsubmit [--] <cmd> ships any command and returns a job id |
| Multi-agent | zclaim zunclaim zwho | advisory per-repo leases so agents don't collide |
| Observability | zstatus [--all] zlog zundo | instant machine-wide status; cross-repo timeline; one-step rewind |
| Snapshots | zsnapshot zrestore zsnapshots | tree-wide restore points across all submodules |
| Worktrees | zworktree add/list/remove | per-agent isolated, object-sharing worktree of the whole tree |
| Fan-out | zforeach [selectors] -- <cmd> | run a command across all/subset of indexed repos, in parallel (selectors: --repo/--dirty/--ahead/--behind/--claimed/--session) |
| Selectors | zselectors | the shared [selectors] grammar the fleet verbs accept — a bare path pattern, --dirty/--ahead/--behind, --claimed/--session, ANDed (git help zselectors) |
| Parallel query | zheads zdirty zbranches ztags zremotes zsize zage zcommits zpristine | native, fork-free reads fanned across every indexed repo — HEAD/branch, dirty set, branches, tag counts, remotes, .git sizes, HEAD age, commit depth, the clean-and-in-sync set; all honor the [selectors] grammar |
| Parallel pull | zpull [selectors] | fetch + fast-forward every indexed repo in parallel (ff-only, same native reconcile as zsync; dirty/diverged skipped) |
| Review | zreview [selectors] | aggregate the pending uncommitted change across the fleet — every dirty repo's status --short + diffstat, grouped on one screen (the read-side companion to zcommitall) |
| Search & analytics | zgrep [-i] <pattern> zahead zbehind zunpushed zunpulled zauthors zhot [<days>] zconflicts | cross-repo, fanned in parallel — regex content search; upstream ahead/behind counts and the detailed per-repo unpushed/unpulled commit lists; commit counts by author; repos by recent activity; repos mid-merge/rebase/conflicted |
| Security | zscan [selectors] | parallel secret scan of tracked content across the fleet — AWS/GitHub/Slack/Google/JWT/PEM keys + high-entropy key = "…" assignments, path:line:pattern per hit; exits non-zero when any found, so it drops straight into a pre-push / CI gate |
| Signatures | zsigs [selectors] [-n <count>] | fleet commit-signature check — flags unsigned/bad/unverifiable HEAD commits (git's %G? codes) across the tree; signature + payload reconstructed natively, verified via gpg (the tool git itself uses), exits non-zero if any found. Also adds the %G? / %GK pretty-format placeholders to git log |
| Parallel mutations | zfetch zgc zfsck zprune zreset zabort zcheckout <branch> ztagall <tag> zcommitall -m <msg> zpushall zclean -f | run a git operation across every indexed repo in parallel (via this binary's own porcelain + fair lane); zreset parallel-resets, zabort aborts an in-progress merge/rebase/cherry-pick/revert in mid-op repos; ops that don't apply are skipped, not forced |
| Re-attach | zattach [selectors] | put every detached-HEAD indexed repo back on its mainline branch — the state git submodule update leaves a tree in, and the state in which a commit is written unreachable. The same local, no-clobber attach zsync applies to submodules (ensure_attached): never contacts a remote, never moves the checked-out commit, never touches the worktree or index, so it is safe on a dirty repo. A repo whose local mainline is ahead of or diverged from HEAD is refused, one with no main/master is skipped, and the tally reports each class |
| Remotes | zremote set <old> <new> [selectors] [-n] | fleet-wide remote-URL rewrite — replace substring <old> with <new> in every matching remote across the tree (org move / host change / ssh↔https); -n / --dry-run previews without applying |
| Rollback | zrollback [selectors] [--steps <n>] [--apply] [--force] | fleet-wide undo of the last mutating op — resolve HEAD@{n} from the reflog and reset --hard to it (the multi-repo zundo); dry-run unless --apply, and skips dirty / mid-op / would-diverge-from-remote repos unless --force |
| Live monitor | ztop [selectors] | full-screen htop-style fleet monitor — every indexed repo, sorted by churn, read from the daemon status cache each frame (scales to thousands). 31 htoprs colorschemes with a live picker (c) + palette editor (~), F1 help, F6 sort-by-column, / search, p full-path toggle |
| Command feed | zcommands | live feed of every git command run across the fleet — time, pid←ppid (which agent), cwd, and argv, appended to its own $ZVCS_HOME/commands.log; a single stat gates the hot path when off |
| Audit | zaudit [--agent <ppid>] [--repo <s>] [--cmd <s>] [--mutating] [--summary] | queryable audit trail over that same command log — filter by agent (which ppid ran it), repo, or command; --mutating keeps only state-changing commands; --summary tallies per-agent and per-command; --json for tooling |
| Event feed | zevents ztail | one live semantic feed of commits, reconciles, and status changes across the whole tree (from the append-only events table) |
| AOP | zintercept before|after|around <pattern> -- <cmd> | aspect-oriented hooks on git commands (ported from zshrs) — run advice before/after/around any matching command, with INTERCEPT_NAME/ARGS/CMD (and STATUS/MS/US for after) in the environment; an around advice runs eval "$INTERCEPT_CMD" to proceed |
| Plugins | znative add|load|remove|list|info|update|gc <SOURCE|NAME> | the plugin package manager (ported from zshrs) — install third-party subcommands from one content-addressed store under $ZVCS_HOME/pkg, in two kinds: a native plugin is a Rust cdylib compiled against the stable znative C ABI and dlopened, a script plugin is a repo of git-<verb> executables. Sources are owner/repo, git+URL or path:DIR, @ref-pinnable and SHA-256 integrity-pinned; a plugin verb resolves after built-ins and before the git-<verb> PATH lookup, and a native plugin may override an existing verb and delegate back to the original |
| Policy | zguard/zpolicy deny|warn <pattern> [--when <pred>] | declarative fleet-wide command policy — refuse or warn on a matching git command before it runs (the veto evolution of zintercept). Glob on the command line (deny 'push*--force*', warn 'rm*-rf*') plus repo-state predicates (--when detached|dirty|protected|unsigned, e.g. deny 'commit*' --when unsigned requires signed commits). list/rm/clear/test; a single stat on the hot path when no rule is set |
| Autonomy config | zconfig [<name> on|off|<n>] | toggle the daemon's feature switches from the CLI (autoreconcile, autobump, autostatus, …, statusinterval, watchmru); all on|off flips them together, reloading a running daemon |
| Automation | zpin zunpin zbroadcast zhandoff zon zsince zcontend zwaitfor zgraph zrewind | freeze repos from autonomy; inter-agent messaging and claim hand-off; run a command on a semantic feed event; a time-window feed; live agent-vs-agent contention; block until a tree-wide state holds; fleet topology (dup groups) |
| Scheduler | zsched add <duration> -- <cmd> zsched list/rm/clear/run | daemon-hosted cron for the tree — fire any command on an interval (add 5m -- git zpull --dirty); the CLI owns the schedule file, the daemon reads it each tick and fires due jobs (add/rm take effect within a tick, no reload) |
| Coordination | zwait [<path>] zqueue zbarrier | the join side of the async queue — wait for one repo's jobs to drain, list what's queued/running, or block until the whole queue is idle |
| Profiling | zstale [<days>] zlast zbig [<n>] zfiles zdivergent zorphans | native, fanned in parallel — abandoned repos, most-recently-committed, largest tracked files, file counts, repos diverged from upstream, repos with no remote |
| Multi-agent view | zsessions zidle zdashboard zppid zprocs | sessions ranked by repos held; repos free to pick up; an instant one-screen health summary aggregated from the status cache + ledger (dirty/ahead/behind/diverged/detached/no-upstream + claims/sessions/queue/procs) — no live walk, so it scales to thousands of repos like zstatus --all; a per-process commit tally (zppid) — the durable process responsible for each commit (found by walking up past the throwaway per-command shells to the real agent/program/login-shell), with its pid, command, cwd, live/dead state, and commits landed; and a per-process command breakdown (zprocs) — how many of each mutating verb (commit/push/add/merge/rebase/…) every process has run |
| Precompute | zprecache [-n <commits>] [-q] | fill the log caches — abbreviations and the per-file line tallies --stat/--numstat/--shortstat/--name-status need — for the newest commits, so those formats read a memory-mapped image instead of the object store. All of it is a pure function of immutable objects, so an entry is correct forever. The daemon does this on its own whenever a watched repo's refs move (zvcs.precache, on by default); this verb is the same pass on demand, e.g. after a clone or a fetch that landed while the daemon was down. git cannot do this at all — no part of git runs between two commands, so its first log --stat after a fetch always pays in full |
| Hooks | zhook set/unset/show/list/test | manage & test the current repo's ref-change hook (zvcs.hook); zvcs.autohook fires each repo's own local hook |
| Triggers | ztrigger DIR <cmd> [--throttle <dur>] ztrigger list/rm/test/tail/top | watch any directory (git repo or not) and run a command on any file change under it — command runs with the dir as cwd and $ZVCS_DIR set; a leading-edge throttle (default 500ms) collapses the event burst of one file action into a single fire; tail streams fires live, top is an in-place fire-rate HUD |
| Watch | zwatch DIR zwatch list/rm | watch any directory and log each change to the daemon log (a trigger with a built-in logging command) |
| Console | zrepl zbanner [--color|--no-color] | interactive line console over every command — each line runs as git <line>, so the z* verbs and all git porcelain work alike (startup stats banner + Tab completion of every verb); zbanner reprints that banner on demand, with every stat re-read at call time |
| Shell | zcd zpwd zls zenv zunset zecho zmkdir ztouch zrm zcp zmv zcat zln | shell builtins so zrepl drives like a shell — zcd/zenv/zunset mutate the console's cwd/environment and persist across lines; zls is a git-aware listing (per-file status like eza --git); zmkdir/ztouch/zrm/zcp/zmv/zcat/zln are native filesystem commands (zrm/zmv are on-disk, distinct from git rm/git mv) |
| Discovery | zverbs | list every extension verb and its one-line usage (sourced from each verb's own -h) |
| Setup | zshadow [<dir>] [-n|--print] [--all] | install the whole ~/.zvcs shadow — git shim + git-<verb> dashed links in ~/.zvcs/bin, man pages in ~/.zvcs/man, the HTML doc set in ~/.zvcs/share/doc/git-doc, the forked zsh _git in ~/.zvcs/completions — then print the PATH, MANPATH, and fpath lines on stdout (shell code only, so eval "$(git zshadow)" works; a line the environment already satisfies is printed commented out) |
| Health | zdoctor | environment health check — git shadow on PATH, daemon, ledger, man pages, MANPATH, dashed forms, installed completion (OK/WARN/FAIL, exits non-zero on FAIL) |
| git-compat | every stock subcommand | dispatched natively; depth varies — see the parity report |
Scripting output. Every read verb — the query, analytics, discovery, and
coordination views (zheads, zdirty, zrepos, ztags, zstatus --all,
zjobs, zwho, zcontend, zgraph, zsince, zdashboard, …) — takes
--json for NDJSON: one JSON object per line, so git zdirty --json | jq -r .repo
streams and jq -s slurps to an array. The live feeds (zevents/ztail,
zcommands) stream --json too. An integration test parses every verb's
--json output so it can't drift.
Every subcommand stock git ships has a dispatch arm, so nothing reaches the
not yet ported path; there is no fallthrough to stock git. Dispatching is not
the same as agreeing with git, and the two are measured separately — an
unimplemented flag errors terse rather than guessing, and the parity harness
scores that as a failure.
External and dashed forms (full shadow). An unknown verb follows git's exact
precedence — builtin → git-<verb> on PATH (git.c's execv_dashed_external) →
help_unknown_cmd — so third-party subcommands (git fuzzy, git lfs,
git flow, …) work under the shim; without this, git-fuzzy breaks (it recurses
through git fuzzy helper on every keystroke). The binary also honors dashed
invocation: run as git-<verb> it strips the prefix from argv[0] and dispatches
<verb>. git zdashed [<dir>] installs a git-<verb> symlink for every verb
into <dir> (default ~/.zvcs/bin), so the dashed forms exist once stock git is
removed (git zshadow does the same as one step of the full install). Verbs come
from the dispatch tables, so the set never drifts.
Run the harness to see current depth per subcommand:
cargo run -p zvcs-parity # curated corpus
cargo run -p zvcs-parity -- --fuzz 12 # plus generated flag combinations and workflows
cargo run -p zvcs-parity -- --fuzz 12 --fuzz-sequences 0 # flag combinations only
cargo run -p zvcs-parity -- --fuzz 12 --list-cases # print what that run would execute
cargo run -p zvcs-parity -- --alt-git /usr/bin/git # name the second oracle
cargo run -p zvcs-parity -- --alt-git-every-case # ask it about passing cases too
cargo run -p zvcs-parity -- --concurrency # concurrent writers and held locks
It builds fixture repositories with stock git, runs each invocation against both binaries, and compares stdout, exit code, and the resulting repository state.
A fourth comparison covers what the first three structurally cannot. Every state
probe asks stock git what a repository means and recomputes the answer from
scratch, so it is blind by construction to everything a repository holds beyond
its logical content — the index cache-tree, the untracked cache, the split index,
pack indexes, bitmaps, the multi-pack-index — because all of those are
accelerators the logical view can rebuild without. That blindness shipped a
defect: zvcs add destroyed the index cache-tree, so every stock write-tree,
commit or status afterwards had to rebuild it, and the case scored a match on
stdout, exit code, refs, objects, index entries and config alike. So for every
case that writes under the git directory, both finished repositories are handed
back to stock git — fsck --strict for validity, write-tree for whether stock
can use the index as written or has to repair it first — and the same
write-tree question is put to the binary under test, since a port that cannot
read what git writes is the same class of bug as one that writes what git cannot
read. A disagreement there is reported as its own verdict and its own column,
because "the port wrote a structure git would not have" is a different finding
from "the port printed the wrong thing". The probe never mutates what it
measures: GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY and
GIT_ALTERNATE_OBJECT_DIRECTORIES redirect every write it would make into a
scratch directory, and each run prints how many of its cases the gate opened for.
Cases come in two shapes. Most are a single invocation against a pristine copy of
a fixture. The rest are sequences — an ordered list of invocations against one
repository, compared after every step — because git's stateful operations are
multi-step by construction and the state that makes --continue, --skip and
--abort work (.git/sequencer/todo, .git/rebase-merge/, MERGE_HEAD,
BISECT_*, ORIG_HEAD, the reflog trail) is written by one invocation and read
by the next. A sequence stops at the first divergence and reports it against that
step's own argv, so a conflicted cherry-pick whose --continue writes the wrong
reflog message is named as step 6 of 7 rather than as a difference that appeared
somewhere in a workflow. Every step after the first therefore runs on a premise
already proven identical on both sides.
Both shapes are also generated. --fuzz <n> draws n invocations per
subcommand from grammars extracted from git's own documentation; the same flag
draws n workflows per entry point, and --fuzz-sequences <n> sets that count
on its own because a sequence costs several invocations where a case costs one.
A generated workflow is not a random chain of commands — that would be two
independent cases sharing a directory. Each one encodes a dependency: park the
repository in an interrupted operation and walk the resumption verbs including
the illegal transitions (rebase --continue with a cherry-pick in progress); run
a mutating invocation and then the readers whose answer it should have changed;
run an operation and then its inverse, where the end state must equal the start.
Every draw is a pure function of the seed, so a reported step replays exactly.
Configuration is sampled as two dimensions, not one. Git's behaviour is a
function of its configuration at least as much as of its argv, and it reads that
configuration from a sequence of sources — /etc-style system, global,
.git/config, .git/config.worktree, .gitmodules, GIT_CONFIG_KEY_<n>, and
-c — each a different parser with a defined precedence between them. A draw
picks a set of keys that interact (core.autocrlf with core.eol and
core.safecrlf; diff.noprefix with diff.srcPrefix and diff.mnemonicPrefix;
feature.manyFiles with index.skipHash; push.default with
branch.<name>.merge) and then a scope for each one, so one key set twice in
one file is the last-value-wins rule and one key set in two scopes is precedence
— neither of which a -c-only harness can express. The file scopes also carry
malformed content, which is where git's line-numbered
bad config line 12 in file .git/config lives; -c has no line to number. Each
case id names the scope every setting came from, so a failure is reconstructed by
hand from the id alone.
Every dimension above runs one writer against a repository nobody else is
touching, which is the one condition under which lock handling cannot be wrong —
and not the condition this port ships into, where sixteen agents and stock git
share a worktree. --concurrency adds the two dimensions that ask about the
rest, and neither can be a byte comparison. Stock git guards the index with an
O_EXCL lock and does not wait, so its losers die; zvcs routes contended writers
through a daemon FIFO so they queue and land. Under six-way contention stock
stages one file and zvcs stages six, and diffing that reports the fair queue as a
six-way regression.
So each asserts an invariant instead. Concurrent writers — N processes
released against one repository at the same instant — assert that a writer that
exits 0 has done its work, which stock satisfies by failing its losers honestly
and which the port may satisfy by queueing or serializing. Every case also runs
against stock, and an invariant stock breaks too is not scored, so the bar stays
git's. Deferral is not loss: a writer that announced a queued job is given a
settle window before its effect is required. Foreign locks plant a lock a
killed process or a concurrent pack-refs would have left, and assert only the
indefensible direction — whatever stock completes with the lock held, the port
must complete too. Doing more than git under contention is the feature; doing
less is an availability failure. When both refuse, their exit codes must match,
which is the one axis the queue cannot excuse. A lock that is released part way
through the wait is its own case: it is the only shape that measures the queue
succeeding rather than merely expiring, and it is where the port's differentiator
is either real or it is not.
Writers within one case are heterogeneous where the interesting races are: a
case names one or more roles, writer i takes role i mod roles, and each
role carries its own effect, because an add and a commit have finished
different work. A case may also pin the port's daemon-less fallback by
pointing ZVCS_SOCK at a path that cannot exist, so the run measures that path
deliberately rather than whichever one the machine happened to offer. Every
writer is bounded: its side has a deadline, its output goes to a file rather
than a pipe nobody drains, and expiry kills the whole process group, so the
git a writer forked dies with it. A killed writer is scored as a failure, not
skipped — which is how a commit that never returns under a held branch lock
became a finding instead of a wedged run.
Both are opt-in, and the only dimensions that are: a case costs seconds rather
than milliseconds, and its result is a distribution rather than a fact, since a
race reproduces on some runs and not others. A defect either finds still fails
the run — reporting success for work that never happened is not a softer failure
than a stdout diff — but a clean run is evidence, not proof. A case where no
writer succeeded scores Vacuous rather than passing, because an invariant that
held for want of any writer that exited 0 has measured nothing.
A case that fails is then re-run on both sides, because a byte comparison is
only meaningful between values each binary can produce twice. A case stock git
cannot reproduce itself (a temp name, a wall-clock stamp) is excluded from the
parity denominator; a case zvcs cannot reproduce itself is reported in its own
zvcs-flaky bucket and counted as a failure, never as a match. Both are named
individually under --verbose, so an exclusion can never read as coverage.
The stock git it measures against is resolved explicitly, never through PATH —
PATH finds this binary on any machine where the shadow is installed, and
comparing zvcs with zvcs measures nothing. Each of /usr/bin/git,
/opt/homebrew/bin/git and /usr/local/bin/git is probed, any that answers the
superset verb zverbs is rejected as this binary wearing git's name, and the
newest of the rest wins. One older than the version this port targets is refused
outright rather than measured against: the two disagree about real behaviour, so
its numbers would read like the others while describing a git nobody runs. Name a
specific binary with ZVCS_STOCK_GIT to override the search.
Measuring against one git leaves a question it cannot answer. When zvcs differs
from the newest installed git, either the port is wrong or git changed between
releases and the port reproduces the older behaviour — and both read as the same
stdout-diff with the same diff attached, so one of them costs an afternoon
spent matching a behaviour upstream moved on purpose. So when a second real
git is installed, a failing case is run against it too and the three answers are
classified together: the two gits agreeing is a port defect corroborated by two
independent releases; the two gits disagreeing while zvcs reproduces the second
one is a version difference, reported as version-skew in its own column; the
two gits disagreeing while zvcs matches neither keeps the verdict it earned,
because no choice of target version makes that case right. A second oracle killed
by the case timeout concludes nothing in either direction. Every case where the
two gits answered differently is listed by name, which is a useful artifact on its
own: it is the set of behaviours where "parity" has no single answer, and
therefore the set of curated expectations that may be pinned to the wrong side of
a git release.
A version difference is counted as a failure and stays inside the parity
denominator, and the report prints the forgiving number on a line of its own
rather than folding it into the headline. Excluding those cases would be an
exclusion the binary under test can trigger — the condition is "the gits disagree
and zvcs reproduces the older one", and the second half is zvcs's own output, so
a port that reproduced the older git on its hardest cases would shrink its own
denominator. The parity percentage is therefore bit-identical with and without a
second oracle: the dimension can move a case from one failure bucket to another
and can never move one into or out of the numerator. Cost is one extra invocation
on a case that already failed and nothing at all on one that matched, and each run
prints how many cases it paid for. The second git is discovered automatically
(the newest installed git of a different version than the primary oracle);
--alt-git <path> or ZVCS_STOCK_GIT_ALT names one, --no-alt-git or
ZVCS_STOCK_GIT_ALT=none switches it off, and a machine with one git behaves
exactly as it did before the dimension existed — no third invocation, no new
report line, no new column.
[0x04] THE SUPERSET VERBS
Over a hundred superset (z*) verbs share the one binary, grouped into a few
families below. git zverbs prints the complete, authoritative list with each
verb's one-line usage (each verb also answers -h), and git help <zverb> opens
its man page — so the live set is always a command away and never drifts from the
prose here.
Coordination. git zsync [<path>...] reconciles submodules to their tracked
mainline (origin/main, else origin/master), fast-forward only, leaving HEAD
attached; a dirty worktree is skipped. git zbump [<path>...] advances the
parent's gitlink to each submodule's HEAD only on a fast-forward, then
commits the coalesced bumps (clearing the (new commits) marker). git zdaemon <start|stop|status> controls the singleton coordinator (below). git zdaemon <start|stop|restart|reload|status|info|ping|log> is the full daemon
control surface — restart/reload respawn it (re-reading config), ping is a
scriptable liveness check, info prints pid/socket/paths/config, and log [-n N] [-f] shows/tails ~/.zvcs/zvcs.log — every line there opens with a local
RFC-3339 stamp (2026-08-20T14:03:11-07:00) ahead of its [zvcs ...] tag, so a
day's activity is a plain grep and a stall is visible in the tail. git zup brings the whole tree — the top-level repo and every nested submodule —
to latest origin/main (fetch + fast-forward, attached; dirty/diverged skipped).
Stash. git zstash [<name>] parks uncommitted work across every dirty repo
in the tree as one named unit, git zunstash [<name>] restores it (LIFO), and
git zstashes lists them. Restore applies onto the same commits it was stashed
on (3-way apply onto a moved HEAD is the not-yet-ported porcelain territory; a
repo whose HEAD moved is reported and its stash kept intact).
Repo index. git zreindex [--sync|--async] [<path>...] crawls for git
repositories and records them in the ledger, pruning ones deleted from disk;
git zrepos lists them (pipe-clean, one path per line) — a drop-in for a shell
git-repo index. The walk is parallel and skips the mounts that would hang or loop
a whole-device scan (zreindex /): kernel pseudo-filesystems, automounted/network
volumes, and the macOS data-volume firmlink reflection. At a terminal it runs
async by default — the crawl detaches to the background (results → zvcs.log,
follow with git zdaemon log -f) and the prompt returns immediately; piped or
scripted it runs inline so indexed N, pruned M stays on stdout. --sync
and --async override the default. Pruning is not something you have to remember
to run: every crawl ends with it, and the daemon sweeps the whole index at startup
and hourly, so repos deleted (or thrown away with a temp directory) leave the index
on their own rather than inflating the count zrepos and zdashboard report.
Async queue. git zcommit <paths> -m <msg> [--push] and git zpush submit
fire-and-forget jobs to the daemon (with a network-free / live ls-refs push
pre-flight that refuses a non-fast-forward before enqueue) and return a job
number; git zjobs / git zjob <id> show the ledger, and git zjob stop|restart <id> control a running/queued job. Falls back to synchronous execution when no
daemon is running.
Multi-agent. git zclaim [<path>] takes an advisory per-repo lease for the
session (ZVCS_SESSION), refusing if another agent holds it; git zunclaim
releases and git zwho lists who holds what.
Observability. git zstatus reports the current repo's status live; git zstatus --all reads every indexed repo's status from the daemon-maintained
cache (zero-walk). git zlog merges every repo's reflog into one machine-wide
timeline; git zundo [<path>] rewinds a repo one step (reset --hard to the
previous HEAD, refuses on dirty).
Live monitoring. git ztop is a full-screen, htop-style monitor of the whole
indexed tree: every repo with a churn bar, HEAD, state, and last-change age,
sorted so the repos changing right now rise to the top. It reads the
daemon-maintained status cache each frame — no live walk — so it stays responsive
across thousands of repos. It ships the 31 htoprs colorschemes with a live picker
(c) and a palette editor (~), an F1 help overlay, sort-by-column (F6, </>,
I), incremental / search, and a p full-path toggle; the theme choice
persists. git zcommands is a live feed of every git command run across the
fleet — because the one binary is the sole dispatcher, each invocation logs its
time, pid←ppid (so you can tell which agent ran it), cwd, and argv to its own
$ZVCS_HOME/commands.log; a single stat gates the hot path when logging is off.
git zevents (alias ztail) is one live semantic feed of commits, reconciles,
and status changes across the tree.
Interception (AOP). git zintercept before|after|around <pattern> -- <cmd>
registers aspect-oriented advice on git commands, ported from zshrs. A before
hook runs a shell command before every matching git command, an after hook runs
after it (with the command's exit status and timing in INTERCEPT_STATUS/_MS),
and an around hook replaces it — running eval "$INTERCEPT_CMD" to proceed with the
original. The advice sees the intercepted command through INTERCEPT_NAME /
INTERCEPT_ARGS / INTERCEPT_CMD; the pattern matches the git subcommand
(commit, commit *, */all). Registrations persist to
$ZVCS_HOME/intercepts.tsv and load at dispatch, gated by a single stat when
none are set. list / remove <id> / clear manage them.
Plugins. git znative add <source> installs a plugin that adds subcommands to
this binary, from one content-addressed store under $ZVCS_HOME/pkg — ported
from the zshrs package manager of the same name. Two kinds share that store: a
native plugin is a Rust cdylib compiled against the stable znative C ABI
and loaded with dlopen, and a script plugin is a repository of
git-<verb> executables. A source is owner/repo, github:owner/repo,
git+URL, or a local path:DIR, optionally @ref-pinned so update re-fetches
that exact tag or commit; each install is SHA-256 pinned. Installing needs no
second VCS on the machine — the clone runs through this binary's own native
clone.
A plugin verb resolves after built-in verbs and aliases and before the
git-<verb> PATH lookup, so it wins over a same-named script on PATH and can
never shadow a git command by accident. A native plugin may instead override
an existing verb, running in place of the built-in implementation and calling
the host back to run the original when it wants it — so it can wrap git blame
without reimplementing it. git znative itself can never be overridden.
Nothing is loaded until a verb proves to belong to a plugin: the verbs a native
plugin registers are discovered by loading it once at install time and recorded
in the index's two derived tables, which are deleted rather than written empty
when there is nothing in them. A machine with no plugins installed therefore
pays two failed stats per command. See ZNATIVE.md for
the ABI, and three runnable plugins: plugin-hello (the
minimum, plus a verb override), plugin-wip (git wip
composing add/commit through the host, no fork), and
plugin-todo (the script kind — a git-todo shell
script run from the store).
Autonomy config. git zconfig toggles the daemon's feature switches from the
CLI instead of editing ~/.gitconfig: with no argument it lists every setting
and whether it is on; git zconfig <name> on|off (or a count) sets one, git zconfig all on|off flips them together, and a running daemon is reloaded so the
change takes effect at once.
Snapshots. git zsnapshot <name> records the HEAD of the repo + every nested
submodule as one restore point; git zrestore <name> resets the whole tree back
to it; git zsnapshots lists them. A restore discards tracked changes and keeps
untracked files, and because a snapshot holds commit ids rather than branch
names it moves whichever branch is checked out when it runs.
Worktrees. git zworktree add <name> provisions a complete, object-sharing,
isolated worktree of the repo + all nested submodules (each on a zwt/<name>
branch) at ~/.zvcs/worktrees/<name>/, so each agent gets a private tree that
can't collide with any other — no re-clone. list / remove <name> manage them.
Console. git zrepl opens an interactive line console. Each line is run
exactly as git <line> would be, so it drives every dispatchable command —
the z* superset verbs and every git-compat porcelain command alike (the latter
operating on the current repo) — doubling as a live daemon/ledger console. On a
tty it opens with a stats banner and edits with Tab-completion of every verb plus
persistent history; piped stdin falls back to a raw reader so scripts stay
usable. git zbanner [--color|--no-color] prints that banner again on demand —
the logo plus os/arch/pid, cores, indexed repos, and the superset/git-compat/total
verb counts, all read at call time, so a long-lived console can refresh the
numbers instead of showing what was true when it opened.
Shell builtins. Because the console is one long-lived process, a handful of
shell verbs make it navigable like a shell: git zcd [<dir>|-] changes the
working directory (persisting across lines, ~/- supported), git zpwd prints
it, and git zls [-alrt] [<path>] is a git-aware listing — each entry carries a
two-column git status field (staged, then unstaged) like eza --git, a directory
folding the status of the paths under it, colored from the same palette eza reads
(LS_COLORS for file kinds/extensions, EXA_COLORS/EZA_COLORS for permissions,
size, date, and git columns). git zenv [<NAME=VALUE>...] prints,
sets, or queries environment variables —
anything set persists so every later git line sees it — git zunset <NAME>...
clears them, and git zecho [-n] <arg>... prints its arguments. The mutating
verbs (zcd/zenv/zunset) only affect this process, so they are aimed at the
console. Rounding out the set are native filesystem commands — git zmkdir [-p],
git ztouch, git zrm [-r] [-f], git zcp [-r], git zmv, git zcat, and
git zln [-s] — so files can be created, copied, moved, and removed without
leaving the console. These act on disk (no fork); zrm/zmv are distinct from
git rm/git mv, which stage changes in the index.
Discovery & help. git zverbs lists every extension verb with its one-line
usage (each verb also answers -h with the same line). git help <zverb> opens
a full man page — the pages are generated from a table in
src/extensions/src/superset/manpage.rs (one source of truth, covering every
verb in SUPERSET_VERBS), written on demand under ~/.zvcs/man and opened with
man -M, so it works with no setup. git zshadow (or git zdashed) writes them
all up front, so man git-<verb> resolves once ~/.zvcs/man is on MANPATH —
the line zshadow prints:
export MANPATH="$HOME/.zvcs/man:$MANPATH"
man git-zsync
git help -w <cmd> opens the HTML manual instead, and resolves it the way the
man-page path does: git's own installed documentation first. git help status
hands the page name to the viewer chain — every man.viewer in configuration
order, then $GIT_MAN_VIEWER, then plain man, with man.<tool>.path naming
the program for the three viewers git drives itself (man, woman,
konqueror) and man.<tool>.cmd supplying the whole command line for any
other. git help -w status opens the
git-status.html that same installation laid down — so the two viewers show one
manual, and git's asciidoc prose is never re-written here. The installed
directory is found from the man page itself (make install puts
share/man/man<n> and share/doc/git-doc under one prefix), so no path is
compiled in and a host with no git man pages is exactly a host with no git HTML
pages.
For everything no git installation holds — the superset verbs, and every page at
all on a host without git's documentation — zvcs ships its own set under
~/.zvcs/share/doc/git-doc. It is generated from the same two tables the rest of
git help reads — the command-list blocks git help -a/-g print, and the
superset manual in manpage.rs — so a verb cannot exist without a page. A z*
verb's page is its complete manual; a stock command's page carries git's own
one-line description, the category it is filed under, whether this build
dispatches it, and the cross-reference to the man page that holds the prose.
Pages are written on demand by git help -w, and all at once by git zshadow /
git zdashed; nothing is generated at startup.
git --html-path reports whichever of the two the viewer resolves stock pages
against — the installed directory when there is one, the generated set otherwise
(--man-path and --info-path report ~/.zvcs, which is where this build's man
pages and info tree live).
git --html-path # e.g. /usr/share/doc/git-doc, else ~/.zvcs/share/doc/git-doc
git help -w status # open git's own git-status.html
git help -w zsync # write git-zsync.html into the generated set, open it
git -c help.htmlpath=/elsewhere help -w status # or point it at your own tree
[0x05] THE zdaemon COORDINATOR
zdaemon is one machine-wide daemon (state under ~/.zvcs/, socket
~/.zvcs/zvcs.sock) — the fair replacement for index.lock plus the host for
autonomy, the SQLite ledger, and the async job queue. It is reactive: no
timers, no polling; a git pull/commit updates local refs, a notify
file-watch fires, and the daemon reacts. It never contacts a remote itself.
The lock is per-repo: unrelated repos run fully in parallel; only writers to
the same repo serialize, first-come-first-served. Clients reach it through
RepoLock::acquire (src/extensions/src/lock.rs), an RAII guard; release is
automatic on drop and on socket EOF, so a crashed holder can't wedge a repo. Index
writes also go through index.lock via gix-lock for interop with stock git.
With no daemon the lock falls back to a lane file — <git_dir>/zvcs-lane.lock,
held with flock(LOCK_EX) for the whole command. It cannot be a no-op: an index
write is a read-modify-write, and the port's only index lock is the one the writer
takes at write time, so two unserialized writers each read the same base index
and write back their own copy — the loser's change disappears and both exit 0.
(Git avoids this by holding index.lock from before it reads: builtin/add.c
calls repo_hold_locked_index(..., LOCK_DIE_ON_ERROR) ahead of
repo_read_index_preload().) The lane file is deliberately not index.lock,
which gix-lock acquires with Fail::Immediately and which zvcs holding would
make zvcs's own writer fail. Exclusion is the kernel's flock, not the file's
existence, so a killed holder releases instantly and wedges nothing; a lane a live
peer will not release within the wait budget makes the command exit non-zero with
a message rather than run unserialized.
A foreign holder of that lockfile — stock git, an IDE, a hook shelling out —
is invisible to the lane, and the index writer takes the file with one attempt
and no wait. So an index-writing command first waits out a foreign index.lock
(2 s, ZVCS_INDEX_LOCK_WAIT_MS overrides, 0 disables); if it is still held,
the command is submitted to the queue as a job rather than failing, and runs on
the repo's fair lane once the lock clears.
The second contention shape takes no lockfile at all: a ref race, where two
writers each committed cleanly and the loser's compare-and-swap on
refs/heads/<branch> is rejected because the winner moved it first. That is
routed to the queue too (ref moved under another writer — queueing), since a
re-run once the winner has landed is exactly what resolves it. A job's own re-run
never re-queues itself, so a conflict that keeps losing reports the failure
instead of spawning jobs forever.
Wire protocol — 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; next waiter granted. |
SUBMIT <json> | client → daemon | Queue an async job; answered JOB <id>. |
JOBSTOP <id> / JOBRESTART <id> | client → daemon | Cancel / re-enqueue a job. |
STATUS / STOP | client → daemon | Snapshot / shut down. |
Autonomous mode + configuration
All autonomy is gated by [zvcs] gitconfig and defaults off, so it runs in
the dev environment and nowhere else. Enable it in ~/.gitconfig or a repo's
.git/config; stock git ignores the keys:
[zvcs]
autoreconcile = true ; reconcile clean submodules to origin/main (reactive)
autobump = true ; forward-only local pointer bumps + commit (kills the marker)
interval = 2 ; debounce window (seconds) for coalescing bursts
autocrawl = true ; background repo-index crawl on daemon start
crawlroots = /abs/src /abs/wk ; crawler roots (absolute; default $HOME)
autostatus = true ; reactively update a repo's status on its ref-change
statusinterval = 10 ; continuous status maintainer: any non-zero enables the always-on worker pool; 0 disables
watchmru = 512 ; file-watch the N most-recently-used repos so their status updates instantly on change; 0 disables
hook = /abs/on-change ; run on ref-change in any indexed repo (typed event env)
autohook = true ; fire each repo's own local zvcs.hook (no global hook needed)
worktreebase = /abs/worktrees ; base for zworktree (default ~/.zvcs/worktrees)
precache = false ; stop precomputing log caches on ref-change (default on)
replvimode = true ; vi keybindings in the `git zrepl` console (default emacs)
git ztop writes its own topscheme / toppalette keys when you pick a colour
scheme in its UI; nothing else reads them.
When anything is enabled, a git invocation auto-spawns the daemon (detached,
output to ~/.zvcs/zvcs.log); it watches indexed repos and reacts by attaching
detached HEADs, fetch-free reconciling, forward-only autobumping, maintaining
status, and firing hooks. A dirty worktree or a diverged/ahead branch is always
skipped — autonomy never regresses or clobbers in-flight work. Headless failures
are recorded in the ledger and surfaced on your next git command (stderr).
Hooks get a typed environment: ZVCS_EVENT (commit/checkout/merge/pull/rebase/
reset), ZVCS_REPO, ZVCS_GIT_DIR, ZVCS_OLD_SHA, ZVCS_NEW_SHA, ZVCS_REF —
enough for "on commit in X, do Y in repo Z" cross-repo rules.
ztrigger watches any directory — a git repo or not — and runs a command on
any file change under it. Triggers live in the triggers index (keyed by path),
so no git config is involved:
$ git ztrigger ~/Desktop 'say 45' # any dir works — not just repos
$ git ztrigger ~/src/api 'make test' # a repo works too (watches worktree + .git)
$ git ztrigger ~/logs 'reload' --throttle 2s # coalesce bursts to one fire per 2s
$ git ztrigger list # path <tab> command <tab> throttle
$ git ztrigger test ~/Desktop # run its command once now
$ git ztrigger rm ~/Desktop # remove it
$ git ztrigger tail # live stream of fires as they happen
$ git ztrigger top # in-place HUD: fires, events, /sec, last
$ git zwatch ~/Downloads # watch a dir and log each change
The command runs via sh -c with the watched directory as cwd and $ZVCS_DIR
set. One file action emits several filesystem events, so each trigger has a
leading-edge throttle (default 500ms, --throttle <dur>, 0 disables): the
first event fires immediately, the rest of the burst is coalesced into that one
fire — so a save fires once, not five times. The daemon records every fire to
~/.zvcs/fires.log; ztrigger tail streams them and ztrigger top shows a live
per-trigger rate HUD (spot a runaway trigger at a glance). It watches only the
directories you triggered, so startup stays instant no matter how many repos are
indexed. Caveats: it fires on every change under the dir — including a repo's
.git churn — and a command that writes back into the watched dir re-fires on its
own writes. For a repo's git hook (ref-change semantics in .git/config), use
git zhook instead.
[0x06] LAYOUT
| Path | Contents |
|---|---|
src/ported | Vendored gitoxide crates (gix + the gix-* library crates), in-tree. A self-contained workspace, excluded from the root and consumed as a path dependency. The gix/ein CLI binaries and their gitoxide-core backend are removed; git is the only binary. |
src/extensions | The zvcs crate (library + the git binary): main.rs/lib.rs (entry, session_key, notify-on-next-command), dispatch.rs (routing), porcelain/ (git-compat), lock.rs (daemon client), config.rs ([zvcs] settings plus the shared stock-git config primitives, the ordered config walk, and the config-file refusals — bad config line <n> in file <path> and the repository-format check, neither of which -c can reach), repo_settings.rs/default_config.rs/diff_config.rs/status_config.rs/log_config.rs/cmd_config.rs (git's config callbacks, which refuse an unreadable value with git's own diagnostic before the command runs), autostart.rs (daemon auto-spawn), db.rs (SQLite ledger/index), rcache.rs (zero-copy rkyv caches for tree diffs/blames/abbreviations), crawler.rs (repo crawl), jobpool.rs/jobrun.rs/index_commit.rs (async jobs), worktree.rs (checkout helper), and superset/ (zdaemon, zsync, zbump, reconcile, attach, watch, hooks, trigger, ledger, status, oplog, snapshot, claim, queue, repl, zworktree). |
src/plugin | The znative plugin SDK — the stable, versioned C ABI (#[repr(C)] structs + extern "C" function pointers) that the plugin host and every native plugin compile against, so the two agree on the exact layout. Deliberately dependency-free. |
examples | Standalone plugin crates, built on install by git znative add path:examples/… rather than by this workspace. |
[0x07] STATUS & ROADMAP
Early and in active development.
The coordination and superset layers are implemented and tested: the singleton
daemon with per-repo FIFO lanes; reactive file-watcher autonomy (attach,
autobump-with-commit, fetch-free reconcile) toggled from the CLI (zconfig); the
SQLite ledger + repo index with a daemon-maintained status cache; async
zcommit/zpush/zsubmit with job control; multi-agent claims and messaging;
the parallel fleet layer over the indexed set (fork-free queries, analytics,
mutations, all sharing one [selectors] grammar and a bounded worker pool); live
monitoring (ztop htop-style, zcommands command feed, zevents semantic feed);
AOP command interception (zintercept); machine-wide zstatus; the cross-repo op
ledger (zlog/zundo); tree-wide snapshots; typed cross-repo hooks; and per-agent
isolated worktrees (zworktree). Each is covered by an integration test, and
zvcs↔stock-git interoperability (round-trip read, git fsck, submodule pointer
bumps, worktrees) is verified by a regression suite. See DESIGN.md
for the architecture and the honest list of partials.
Git compatibility is tracked as two independent numbers, because a subcommand that dispatches is not thereby correct:
- Coverage — every subcommand stock git ships is dispatched natively.
- Parity — the share of harness cases whose stdout, exit code, and resulting repository state match stock git exactly, and — for the cases that write to the repository — where stock git still reads both finished repositories the same way.
Parity is the number that matters and it is the work that remains. Depth varies
widely per subcommand; some are byte-faithful across their documented flag set,
others implement the common flags and bail terse on the rest. A few subcommands
are honest skeletons that name the missing substrate instead of pretending:
the foreign-SCM bridges (p4, cvsimport, cvsserver, cvsexportcommit,
archimport) have no gitoxide backing to port onto. Their argument surface is
still git's — the option parsers, usage blocks and exit codes are ported from the
stock Perl and Python, so an invocation that never reaches a Perforce or CVS
server answers exactly as stock does; only the parts needing a foreign server
refuse, by name.
Compressed output is byte-identical to stock. gix-zlib no longer compresses
through zlib-rs, whose zlib-ng-lineage match finders produce a valid but
different deflate stream at levels 0 through 8. gix_zlib::deflate is a
transcription of zlib's own deflate.c and trees.c, so a packfile, a bundle, a
loose object and a diff --binary payload are the same bytes git would have
written — which also means two clones of one history agree on disk, not merely on
object ids. Decompression stays on zlib-rs, where the output is fixed by the
format and it is the faster decoder. The cost is confined to compression and is
paid where zlib is slower than zlib-ng: pack-objects over this repository (15098
objects, level 6) goes from 4.04s to 4.93s, while loose-object writes at git's
default level 1 are unchanged at 0.44s and get 8% smaller, because zlib-ng's
level-1 coder trades ratio for speed.
Two limits are structural rather than unfinished work.
A handful of commands print a path into their own installation —
git p4's usage embeds sys.argv[0], and git help --all --no-verbose heads its
listing with available git commands in '<exec-path>' — so no independent
implementation can reproduce those bytes. Matching them would mean reporting some
other installation's libexec/git-core as this binary's own exec-path, which
would be false. Everything below that heading does match: the command names stock
lists are exactly the ones this binary dispatches, laid out through the same
column engine at the same width, followed by the same trailer and the same
fall-through to the common help. Stock's libexec/git-core shell libraries
(git-sh-setup, git-sh-i18n, git-mergetool--lib) are not in that listing on
either side — they are not executable, so list_commands_in_dir() skips them.
git version --build-options describes the build that prints it. Stock
reports the C toolchain it was compiled and linked against; this binary reports
what is true of a Rust binary on gitoxide and omits the rest, rather than copying
stock's values into a report about itself. Ten of stock's fifteen lines match.
cpu, the no-build-commit line, sizeof-long, sizeof-size_t,
default-ref-format and default-hash agree because they are the same facts.
SHA-1: SHA1_DC and SHA-256: SHA256_BLK agree because both tokens name a
backend category in hash.h and this build is in both categories: SHA1_DC is
the collision-detecting one — its three alternatives all read (No collision detection) — and this build's sha1-checked is sha1collisiondetection in
git's own bail-out configuration, while SHA256_BLK is the #else against
SHA256_NETTLE/SHA256_GCRYPT/SHA256_OPENSSL and this build's sha2 links no
crypto library either. The SHA-256 line is backed by a working object format,
not just a compiled-in enum: git init --object-format=sha256 writes stock's
extensions.objectformat + core.repositoryformatversion = 1 pair, and the
objects, packs, index, refs, bundles, pushes and fsck walks over that
repository all produce stock's bytes and stock's ids.
rust: enabled disagrees because the answers really are different here, and no
honest report can close that line. zlib-rs takes the slot stock fills with
zlib, naming the flate library this build does link and the version its
lockfile resolved, qualified (inflate only) because deflate here is an in-tree
transcription of zlib's deflate.c/trees.c rather than that crate. feature: fsmonitor--daemon, gettext, libcurl and OpenSSL are absent because no such
component is present — only the client half of fsmonitor--daemon is ported, and
none of the other three is linked — the same reason git's own #ifdefs drop a
line. The same block is what git diagnose and git bugreport embed, through
the same function git shares between them.
subtree, filter-branch and instaweb are ported directly from their stock
shell scripts. subtree add, merge, pull, split, and push all produce
the same commits and object ids as stock; only -S/--gpg-sign is refused.
filter-branch
reproduces stock's commit ids everywhere the corpus looks: every differential
case it carries — --msg-filter, --tree-filter, --index-filter,
--env-filter, --subdirectory-filter, --prune-empty, --tag-name-filter,
--commit-filter, --original and the error paths, over the linear, branched
and merged fixtures — matches stock's stdout, exit code and post-command state,
where state is a set of stock probes read back from both repositories —
for-each-ref, rev-parse HEAD, ls-files --stage, status --porcelain,
cat-file --batch-all-objects, config --list --local among them. That is the
whole of the evidence for the claim: it is a statement about those filters on
those shapes, not about a repository the port has never
been pointed at, and the rev-list option surface it accepts is deliberately
narrow (--since, --author, --max-count, <a>...<b> and magic pathspecs
are refused rather than approximated). Until v0.16.0 the claim was also plainly
false in a case the corpus did not reach: the script's update-index --refresh
was modelled for its read semantics but not its write, so the on-disk index kept
stale mtimes, the final read-tree -u -m HEAD refused, and the run left a
half-rewritten history behind.
One property of this port belongs with that claim, because it decides which
git the ids come from: unlike stock, it does not prepend its own directory to
PATH. The filters are shell snippets run under /bin/sh, and the script's own
machinery is written the same way — the default commit filter is literally
git commit-tree "$@", the empty-tree constant comes from git hash-object -t tree /dev/null, and the ident probe from git var GIT_AUTHOR_IDENT — so every
one of those children, and every git a user's own filter runs, resolves
through the caller's PATH. With a shim named git first on PATH, stock's
filter-branch calls it zero times and this port's calls it for each of those
three. Put the build you mean at the front of PATH before reading a
filter-branch result as that build's.
instaweb generates
the same daemon configuration as stock for whichever of lighttpd, apache2,
mongoose, plackup, webrick or python is installed, and serves gitweb through
it; like stock, it ships no web server of its own.
Generated cases cover mutating subcommands as well as read-only ones — the
hardened environment neutralizes every interactive hook (GIT_EDITOR=true and
its siblings), which is what made a command that opens an editor fuzzable at all.
That coverage earns its keep: a sweep found git init --bare nested/dir failing
outright, fast-import leaving a different object store than stock after a
rejected command line, and cherry-pick refusing strategies git accepts — none
of which any curated case reached.
What no score covers is the situation a case cannot describe. Until recently every case ran at the worktree root, so repository discovery went unmeasured and three bugs shipped behind that gap, including a process abort in any bare repository's subdirectory. A case can now name its own working directory and environment; read a green subcommand as "agrees on what was asked", never as "agrees".
[0x08] BENCHMARKS
Same repository, same commands, same machine, both binaries measured in one interleaved run so load moves them together. Regenerate with:
cargo build --release && scripts/bench.sh /path/to/some/repo
Repository: zshrs (6,376 commits) · stock git 2.50.1 · 18 cores · 12 runs after 3 warmups · release build · the machine was busy (load ~17-20), which compresses zvcs's lead rather than git's.
| Command | zvcs | git | |
|---|---|---|---|
git log --stat -n 30 | 8.3 ms | 133.2 ms | 16.05x |
git status | 20.0 ms | 207.4 ms | 10.37x |
git blame README.md | 8.4 ms | 38.2 ms | 4.55x |
git log -S return --format=%H | 1.858 s | 6.679 s | 3.59x |
git log | 22.3 ms | 73.9 ms | 3.31x |
git log --oneline | 20.5 ms | 65.8 ms | 3.21x |
git log --format=%s | 19.0 ms | 55.1 ms | 2.90x |
git log --format=%h | 11.5 ms | 26.3 ms | 2.29x |
git log -p -n 20 | 35.7 ms | 66.4 ms | 1.86x |
git cat-file -p HEAD | 8.0 ms | 12.5 ms | 1.56x |
git shortlog -s | 7.2 ms | 10.8 ms | 1.50x |
git describe | 10.3 ms | 14.4 ms | 1.40x |
git for-each-ref | 9.4 ms | 12.7 ms | 1.35x |
git ls-files | 8.3 ms | 11.2 ms | 1.35x |
git rev-list --count HEAD | 9.9 ms | 13.3 ms | 1.34x |
git show HEAD | 10.2 ms | 13.3 ms | 1.30x |
git tag -l | 10.5 ms | 13.0 ms | 1.24x |
git diff --name-only HEAD~5 | 15.2 ms | 18.2 ms | 1.20x |
git diff --stat HEAD~5 | 28.4 ms | 33.9 ms | 1.19x |
Three things produce the difference, and only the first is ordinary optimization. The performance architecture page diagrams these and the two supporting levers — doing less work, and keeping cache writes off the caller's critical path:
- Every core, not one. git's diff and log machinery is single-threaded. A
blob pair is diffed in isolation and the object store cannot change while a
read-only verb runs, so patches, per-file analysis, pickaxe scans and record
rendering are fanned across the pool. Workers pull from a shared cursor rather
than a fixed slice, because one commit that rewrites a large file outweighs a
hundred that touch a line each.
ZVCS_THREADS=1forces the sequential path and produces byte-identical output. - A cache that remembers. An abbreviation is fixed once the object and the
width
core.abbrevresolves to are both known — which is why the width is part of the key, not an assumption — and a tree pair's change list and per-file line tallies are a pure function of two immutable trees. None of it can go stale, so it is computed once and read back forever after — which is whatlog --statandblameare reading instead of the object store. The answers live in memory-mapped rkyv images under~/.zvcs/cache/, so a hit is a binary search and a slice into the mapping: nothing is decoded, allocated or copied, and a short command pays for the entries it touches rather than for every one on the machine. - A daemon that computes it early.
zvcs.precachewarms those caches when a watched repo's refs move, so the work is done before anyone asks. git cannot do this at all: no part of git runs between two commands.
Cold versus warm
The table above is steady state — a repository the daemon has seen. The cold column below is the opposite extreme: a repository with no cache at all, where the cache-backed commands compute every answer from the object store and write it down for next time.
| Command | zvcs cold | zvcs warm | git |
|---|---|---|---|
git log --stat -n 30 | 121.6 ms | 8.2 ms | 142.0 ms |
git log --stat -n 150 | 432.2 ms | 12.0 ms | 544.0 ms |
git blame README.md | 72.1 ms | 10.4 ms | 43.7 ms |
Cold --stat stays ahead of git because filling a cache is not something the
caller waits for: the entries are queued to a writer thread and the command
returns, with one wait at the very end for whatever the writer has not already
absorbed. They still land — a detached thread would be killed at exit, and a
cache that never persists is just a slower uncached path.
Cold blame is the exception and is reported as measured: gix's blame walk is
slower than git's, so the first blame of a file loses to git by ~1.7x. The cache
is what turns that around — the second one is 4.2x faster than git, and the entry
is valid in every clone holding those commits.
Cold here means the cache is deleted before every single run, which is the worst
case and not one a running daemon leaves you in: it warms the newest commits on
every ref change, and git zprecache does the same pass on demand (150 commits
in 0.52 s).
[0x09] DOCUMENTATION
- Docs hub — https://menketechnologies.github.io/zvcs/
- Design document — DESIGN.md — daemon architecture, concurrency model, autonomous behaviors, ledger/queue
- Command listing for completions —
git --list-cmds=<group>[,<group>...]answers the same groups stock git does (builtins,main,others,nohelpers,alias,config,deprecated,list-<category>), so a completion script written against git works unchanged. The binary groups are derived from the dispatch table, so thez*verbs are listed alongside the git verbs; thelist-<category>groups come from the same tablesgit help -a/-gprint and match stock byte for byte.--list-cmds=parseoptanswers empty: it names the commands that implement--git-completion-helper, and none do here yet. - zsh completion — completions/_git — the stock zsh
_gitforked with thez*verbs; put the dir first onfpathto shadow the system_git. It is compiled into the binary, sogit zshadowinstalls it as~/.zvcs/completions/_gitand prints thefpathline for it (put that line beforecompinit) - Plugin system — ZNATIVE.md —
git znative, the store layout, the two plugin kinds, and the C ABI a native plugin is written against; examples/ holds three runnable plugins (plugin-hello,plugin-wip,plugin-todo) - Performance architecture — https://menketechnologies.github.io/zvcs/#performance — diagrams of how the speedup is achieved: the worker pool over work git does single-threaded, the pickaxe rewrite, the ledger of never-stale values, daemon precompute, and the off-critical-path write queue
- Verb reference — https://menketechnologies.github.io/zvcs/reference.html — every superset (
z*) verb with its synopsis and manual text; the same contentgit help <verb>opens in a terminal. Generated byperl scripts/gen_reference.plfromsrc/extensions/src/superset/manpage.rs, so the page can never drift from the man pages;--checkfails if it is stale. - Engineering report — https://menketechnologies.github.io/zvcs/report.html
- gitoxide — https://github.com/GitoxideLabs/gitoxide (the ported library)
- Source — https://github.com/MenkeTechnologies/zvcs
[0xFF] LICENSE
MIT — free and open source. See LICENSE.