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.
README.md +15−4
| @@ -38,7 +38,7 @@ Working today: | |||
| 38 | 38 | every key added or revoked. A running instance re-reads the directory every few | |
| 39 | 39 | seconds, so `vole user add-key` — or an edit by hand — applies without a restart, | |
| 40 | 40 | and revocation takes effect the same way | |
| 41 | - Issues stored as files in the repository on the `issues` branch, validated at | ||
| 41 | - Issues stored as files in the repository on the `vole` branch, validated at | ||
| 42 | 42 | push time and rendered read-only on the web | |
| 43 | 43 | - Repo create / rename / delete from the CLI | |
| 44 | 44 | - A derived SQLite index that can be deleted and rebuilt from the repos | |
| @@ -72,10 +72,10 @@ over HTTP — not a `403` — so the read-only surface never confirms that it ex | |||
| 72 | 72 | ||
| 73 | 73 | ## Filing an issue | |
| 74 | 74 | ||
| 75 | An issue is a directory on the `issues` branch. No client tooling is required: | ||
| 75 | An issue is a directory on the `vole` branch. No client tooling is required: | ||
| 76 | 76 | ||
| 77 | 77 | ```sh | |
| 78 | git switch --orphan issues | ||
| 78 | git switch --orphan vole | ||
| 79 | 79 | ID=01M5JR2JK29KKMAA1T9MDNTPBP # any 26-char ULID; unique per issue | |
| 80 | 80 | mkdir -p issues/$ID | |
| 81 | 81 | cat > issues/$ID/meta.json <<'JSON' | |
| @@ -86,9 +86,20 @@ cat > issues/$ID/meta.json <<'JSON' | |||
| 86 | 86 | "labels": ["bug"]} | |
| 87 | 87 | JSON | |
| 88 | 88 | printf 'Steps to reproduce...\n' > issues/$ID/description.md | |
| 89 | git add -A && git commit -m "issue: docs page blank" && git push origin issues | ||
| 89 | git add -A && git commit -m "issue: docs page blank" && git push origin vole | ||
| 90 | 90 | ``` | |
| 91 | 91 | ||
| 92 | That branch holds Vole's metadata and is deliberately narrow: | ||
| 93 | |||
| 94 | - Only `issues/` (and `requests/`, once change requests land) may appear in it; | ||
| 95 | anything else is rejected at push. | ||
| 96 | - History must be linear. Merge commits are refused, so `git log vole` reads as | ||
| 97 | a list of changes rather than a graph. | ||
| 98 | - Force-pushing it requires repo admin and is written to `data/audit.log`, because | ||
| 99 | rewriting history is a redaction tool, not a normal operation. | ||
| 100 | - If someone else pushes first, `vole issue new` re-applies your change on the new | ||
| 101 | tip and retries; you do not have to resolve anything by hand. | ||
| 102 | |||
| 92 | 103 | The id must be a ULID (26 characters, Crockford base32 — uppercase, no `I`, `L`, | |
| 93 | 104 | `O` or `U`). A UUID is not one and will be rejected. | |
| 94 | 105 | ||