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.
Issues and requests are frontmatter, not JSON
307cbc2aa624ca9b76b79849e8ce8926414be435 · Keenan Nemetz · 17d ago · parent 427c4761
Issues and requests are frontmatter, not JSON
issues/<ulid>/issue.md and requests/<ulid>/request.md replace meta.json plus
description.md, and approvals/<account>.md replaces approvals/<account>.json.
Every file on refs/heads/vole is now frontmatter markdown read by one package, so
there is one reader and one writer for issues, requests, comments and approvals
instead of a JSON path and a hand-rolled key/value path.
The diff is the interface. A JSON array rewrites its neighbour to add a comma, so
one label cost two changed lines and removing one cost three; a block sequence
costs one. docs/spikes/issue-frontmatter measured the same six edits at 25 changed
lines as meta.json and 10 as frontmatter, and one file per entity means one path
to diff, blame and log.
Three rules make YAML safe to hand-edit rather than something to police:
- the writer always double-quotes free text, so no indicator, reserved word or
" # " can change what a title means
- a label is quoted only when the parser would not read it back bare ("null"),
which leaves ordinary labels as they were
- a reader refuses an unquoted title that the parser read differently: "title:
fix #12 crash" would otherwise be stored as "fix" and nobody would find out
Control characters are refused on both sides, so a tree the server accepts is a
tree it can always rewrite.
`updated` is gone from issues, requests and the index's issues table. It cost a
changed line on every edit — 11 of the 21 frontmatter lines in the spike's
sequence — and the commit that changed the file already carries the time.
Verified: TestRenderParseRoundTrip puts 13 titles that break naive YAML (hash,
colon, leading dash, null, quotes, backslash, unicode, 190 bytes) through the
writer and the reader; the server tests push a rendered issue over SSH and render
it, and the approval-ownership tests withdraw approvals/<account>.md. Then
scripts/ci.sh green (gofmt, vet, build, markdown links, go test -race ./...).
docs/roadmap.md +6−5
| @@ -144,7 +144,8 @@ file them, validated at the push boundary. The Phase 0 sharding work is not wast | |||
| 144 | 144 | ||
| 145 | 145 | Shipped: | |
| 146 | 146 | ||
| 147 | - **Format**: `issues/<ulid>/meta.json`, `description.md`, and | ||
| 147 | - **Format**: `issues/<ulid>/issue.md` — a frontmatter head (title, state, author, | ||
| 148 | created, labels) followed by the body as markdown — and | ||
| 148 | 149 | `comments/<ulid>.md`, on `refs/heads/vole`. ULIDs are generated by the writer, so | |
| 149 | 150 | there is no counter to assign. It is a branch rather than a custom ref so that a | |
| 150 | 151 | plain `git clone` carries the issues, and issues share it with change requests | |
| @@ -177,7 +178,7 @@ Three follow-ups, none of them blocking: | |||
| 177 | 178 | per-repo Atom feed of issue and commit activity, which needs no auth — worth | |
| 178 | 179 | doing when someone asks, instead of building a notification system nobody can | |
| 179 | 180 | log into. | |
| 180 | - Assignees are in `meta.json` but not rendered or filterable | ||
| 181 | - Assignees are in the frontmatter but not rendered or filterable | ||
| 181 | 182 | - Comment editing and deletion (today: append another comment, or rewrite history | |
| 182 | 183 | and force-push) | |
| 183 | 184 | ||
| @@ -190,9 +191,8 @@ named and the branch unmoved; deleting the index loses no issues. | |||
| 190 | 191 | ✅ **Done.** A change request is a directory on `refs/heads/vole`: | |
| 191 | 192 | ||
| 192 | 193 | ``` | |
| 193 | requests/<ulid>/meta.json title, author, base branch, head commit, state | ||
| 194 | requests/<ulid>/description.md | ||
| 195 | requests/<ulid>/approvals/<principal>.json base + head + created | ||
| 194 | requests/<ulid>/request.md title, author, base branch, head commit, state, then the body | ||
| 195 | requests/<ulid>/approvals/<principal>.md base + head + created, no body | ||
| 196 | 196 | ``` | |
| 197 | 197 | ||
| 198 | 198 | - **Approvals are bound to one commit and one branch.** `git push` is the merge, so the | |
| @@ -742,6 +742,7 @@ Filled in as phases land. Append; don't rewrite. | |||
| 742 | 742 | | 2026-10-23 | `vole pages deploy` publishes a snapshot, not a merge | A site is a build directory: merging leaves deleted pages served forever. Snapshot is a normal commit on the tip, so protection and review still apply | | |
| 743 | 743 | | 2026-10-23 | Pages commands ask the server (`git ls-remote`), not the clone | A local branch outlives an unpublish, so the clone describes a site that 404s; the deploy said "already published" until it looked at the server | | |
| 744 | 744 | | 2026-10-23 | The roster is `data/users.db`; clients read it with the `vole-roster` SSH verb | Accounts are rows, not commits: SQLite enforces "a name is issued once" and "a key belongs to one account" as table keys, and a backup is one file. The client still needs an id/name map, so it asks over the key it already pushes with instead of cloning a repository that held nothing else. Supersedes both roster rows above | | |
| 745 | | 2026-10-23 | Metadata on `refs/heads/vole` is frontmatter markdown, not JSON | [Spike](spikes/issue-frontmatter/README.md): the same six edits cost 25 changed lines as `meta.json` and 10 as frontmatter. A JSON array rewrites its neighbour to add a comma; one label per line is one added line, and one file per entity means one path to diff, blame and log. Always quoting free text turns the YAML edge cases into a writer concern instead of a title-restriction list, and the validator only has to refuse an unquoted title the parser read differently. The `updated` field was dropped: it was 11 of the 21 frontmatter lines, and the commit already carries the time | | ||
| 745 | 746 | | 2026-10-22 | A published release is immutable, admin-overridable and audited | A link whose bytes can change is worse than no link; but refusing outright leaves a mistyped tag unrepairable, so the override follows the force-push precedent: admin only, always audited | | |
| 746 | 747 | ||
| 747 | 748 | ## Open questions | |