About this project
9vcs is a version control system built on 9P, the Plan 9 protocol, via the author's Go 9p library. It is aimed at a small trusted team that wants real version control without a hosting platform. History is kept as a set of content-addressed patches rather than snapshots; the README points to PLAN.md for the design rationale and keeps itself to practical usage. The vocabulary is deliberately not GitHub-shaped: there is no clone, push, pull, fork or pull request, and a cheat sheet maps the usual terms onto 9vcs equivalents.
Requirements and installation
The only requirement is Go 1.26.5 or later. Both 9vcs and the 9p library it builds on are pure Go using only the standard library, with no daemon, database or external services. Prebuilt binaries are published on the Releases page as tar.gz archives for linux/amd64, linux/arm64, darwin/arm64 and darwin/amd64, each with a .sha256 checksum for verification; the archive contains the binary, LICENSE and the README. Building from source is a single go build command against cmd/9vcs. Alternatively, some installers are available.
Solo workflow
After running init in a directory, status prints one line per changed path using A for new, M for modified, D for deleted and U for unresolved conflict; it reports only what is dirty, not the diff. Moves are not tracked as renames — a moved file appears as a delete plus an add, matching how the underlying patch stores it. diff shows line-level changes in the working tree, can diff against another point, or can compare two points directly without involving the working tree. log lists recorded patches newest first with author, fingerprint or signature status, and the paths touched. record creates a patch. There is no staging area or index step: the working tree itself is the staging area, and record diffs it against the current head.
Branches and merging
branch lists, creates from HEAD or from another branch or hash, and checkout switches or creates and switches with a -b flag. Merging produces real patch-graph conflicts — line-level forks, binary conflicts with a comparison sidecar file, and modify/delete races — rather than a shallow three-way text diff. A clean merge is completed with record. A conflicted merge leaves inline conflict markers in the affected text files, while binary conflicts get a sidecar file named after the path and a short hash for comparison; the user edits and then records. A merge can be aborted, restoring the working tree to exactly its pre-merge state and clearing merge state, whether the merge came from the merge command or from apply.
Ignore patterns
A repo-root .9vcsignore file works like .gitignore and is meant to be recorded and shared. One pattern per line; blank lines and hash comments are skipped. Patterns without a slash match at any depth, patterns containing a slash are anchored to the repo root, and a trailing slash restricts a match to a real directory. Negation with an exclamation mark and double-star patterns are explicitly not supported. Ignore patterns only keep new files out of record and diff, and adding a pattern later never makes an already-recorded file appear deleted.
Team usage
Each install generates a long-lived Ed25519 keypair on first use, and identity show prints the fingerprint that teammates exchange out of band to authorize each other. Whoever hosts the shared history runs serve on a port; the server reads .9vcs/authorized-peers in the current repo, one fingerprint-and-permission line per teammate, where permissions are read (pull history only), propose (read plus posting a signed bundle to the offers area), and write (everything, including moving a branch ref). There is no add-collaborator command — it is just editing that file — and the file is read once at startup, so changes require a restart, which matters when revoking a possibly compromised key.
serve runs in the foreground with no background daemon. A network push refuses to move the branch currently checked out on the serving machine, so the host should keep a different branch checked out or use a directory reserved for serving. A new teammate starts with init and import, passing a peer fingerprint and a host and port plus branch name; import is fast-forward-only and pulls the branch and every patch and blob it transitively depends on. reconcile is the ongoing sync command: it pulls if the peer is ahead, pushes if the local side is, and on genuine divergence fetches what is missing and asks the user to check out and merge locally rather than resolving over the wire. Both commands can pin a peer fingerprint explicitly, otherwise they use a local trust-on-first-use store that prompts once for a new address, checks known addresses silently, and loudly refuses a fingerprint that suddenly changes.
Offline exchange and proposals
Bundles move changes without any running server: export writes a signed bundle to a file for email, chat or removable media, and the recipient inspects it with bundle show (signer, message, patches), imports it to verify the signature and store it locally without touching any ref, reviews with diff, and then applies it and records. Import never moves a ref by itself. Peers with only propose permission can submit directly using offer, which posts a signed bundle to the maintainer's server; the maintainer lists pending offers, applies one to fetch, verify and store it locally, integrates it with apply, and removes it from the queue. Because offers exist only inside the live server namespace, the maintainer connects to their own serve instance and therefore needs their own fingerprint in the authorized-peers file as well.
Windows into 9sh and namespace handling
Normally the repo is resolved by walking up from the current directory, like git. Inside 9sh, a Plan 9 style shell, the -C flag placed before the command name offers a second route: it tries the path against the 9sh namespace first — a relative path under local, or an absolute path that may resolve to anything 9sh has bound — before falling back to a literal OS path. Outside a 9sh session the flag behaves like git -C.
Recovery
Documented recovery paths cover aborting a merge or apply, recording or manually discarding uncommitted changes that block a checkout, merge or apply, restoring individual paths to their recorded state at head with restore (a path with no recorded state is removed, so reverting a rename means naming both paths), and re-pinning a peer fingerprint that legitimately changed.
Project status
The README distinguishes using 9vcs with a team from developing the tool itself, which follows trunk-based development with short-lived branches, a protected master where nobody pushes directly, CI covering build, vet, gofmt and race-enabled tests before merging, squash-only merges and automatic branch deletion. Versioning follows semver for releases, currently in the v0.x range. Below v1.0.0 there is no compatibility promise for the on-disk patch and bundle format, which may change in place without a migration path; from v1.0.0 onward any patch or bundle recorded under a released format is stated to remain decodable, with future incompatible changes handled by real version dispatch and shipped migration tooling. A changelog records what each release ships.
Comments
0 Rating appears after 10 ratings
Sign in to join the discussion.