vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
One metadata branch: refs/heads/vole, linear history, audited rewrites
92b37587e6e8b4e05f95dfc1e555ba36eaaa424d · Keenan Nemetz · 17d ago · parent f9568748
One metadata branch: refs/heads/vole, linear history, audited rewrites
Issues and change requests now share a single branch. Both are human-authored and
low-frequency, and one ref means one push, one fetch and one validation path
instead of per-branch special cases.
refs/heads/vole
issues/<ulid>/...
requests/... reserved for change requests
Rules, enforced in pre-receive rather than documented and hoped for:
- The tree layout is closed: only issues/ and requests/ may appear, so a typo in
a directory name is rejected instead of being silently ignored by the web.
- History must be linear. Merge commits on the metadata branch are refused,
because a merge node says nothing and makes `git log` harder to read than the
change it describes. Rebasing is what happens instead.
- Force-push requires repo admin and writes a line to the audit log. Rewriting
discussion history is sometimes necessary (someone pastes a secret) but must
never be silent, so it is allowed rather than forbidden and recorded.
The CLI no longer fails on a lost race. Because every entity is its own file,
re-applying a change on the new tip cannot conflict, so it re-fetches, redoes the
mutation and retries with jittered backoff. Two things this turned up that a
single-writer test could never have found:
- A lost race has more than one shape. Besides "non-fast-forward" and "fetch
first", a push can arrive with a stale old-oid ("incorrect old value
provided") or lose the creation of the branch itself ("reference already
exists"). All four are retryable; hook rejections are not.
- Retries without jitter keep writers in lockstep: six concurrent writers failed
five rounds in a row with a deterministic backoff, and converged immediately
once the sleep was randomised.
Verified by hand: the CLI writes to the vole branch, `git log vole` is a flat
list, and a merge push is refused with both problems reported at once — the merge
node and a malformed issue id in the same tree.
docs/roadmap.md +10−3
| @@ -136,9 +136,14 @@ file them, validated at the push boundary. The Phase 0 sharding work is not wast | |||
| 136 | 136 | Shipped: | |
| 137 | 137 | ||
| 138 | 138 | - **Format**: `issues/<ulid>/meta.json`, `description.md`, and | |
| 139 | `comments/<ulid>.md`, on `refs/heads/issues`. ULIDs are generated by the writer, | ||
| 140 | so there is no counter to assign and no contention; it is a branch rather than a | ||
| 141 | custom ref so that a plain `git clone` carries the issues. | ||
| 139 | `comments/<ulid>.md`, on `refs/heads/vole`. ULIDs are generated by the writer, so | ||
| 140 | there is no counter to assign. It is a branch rather than a custom ref so that a | ||
| 141 | plain `git clone` carries the issues, and issues share it with change requests | ||
| 142 | because both are human-authored and low-frequency — one push, one fetch, one | ||
| 143 | validation path. Machine-written state (CI runs) must not use this ref. | ||
| 144 | - **Branch rules**, enforced in `pre-receive`: only `issues/` and `requests/` may | ||
| 145 | appear in the tree; history must be linear (no merge commits); force-push | ||
| 146 | requires repo admin and is written to the audit log. | ||
| 142 | 147 | - **Validation in `pre-receive`**: ULID directory names, required fields, valid | |
| 143 | 148 | states and labels, comment frontmatter, and no paths outside the format. Every | |
| 144 | 149 | problem is reported in one rejection, with the offending path named. | |
| @@ -322,6 +327,8 @@ Filled in as phases land. Append; don't rewrite. | |||
| 322 | 327 | | 2026-10-22 | Issues are push-based on `refs/heads/issues`, IDs are client-generated ULIDs | No authenticated web write surface exists, and a branch makes issues travel with a clone | | |
| 323 | 328 | | 2026-10-22 | Issue trees validated in `pre-receive` | Reject bad data at the boundary instead of rendering it | | |
| 324 | 329 | | 2026-10-22 | CLI follows gitcli(7): `--long`, `-o`, options first, `--`, exit 129 | A tool that speaks git's dialect composes with git and with muscle memory | | |
| 330 | | 2026-10-22 | Issues and requests share `refs/heads/vole`; linear history, no merges | One ref, one push, atomic cross-cutting edits; entities are separate files so a re-apply never conflicts | | ||
| 331 | | 2026-10-22 | Force-push to metadata is admin-only and audited | Redaction is a real need, but it must never be silent | | ||
| 325 | 332 | ||
| 326 | 333 | ## Open questions | |
| 327 | 334 | ||