Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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.

Phase 2, first slice: issues as files on a branch

37c01e5aa9b98bbea8567e741cd3af508ae1ba7d · Keenan Nemetz · 17d ago · parent 197ecaa2

Phase 2, first slice: issues as files on a branch

Push-based issues, per the decision to keep the web read-only. The format is the
contract; nothing here requires a Vole binary on a contributor's machine.

  issues/<ulid>/meta.json
  issues/<ulid>/description.md
  issues/<ulid>/comments/<ulid>.md      on refs/heads/issues

Design points worth recording:

- IDs are ULIDs generated by the writer, so there is no server-assigned counter,
  no contention, and no ID allocation in the write path at all.
- Issues live on a branch rather than a custom ref, so a plain `git clone` carries
  them. That is the same lesson that moved the user roster off refs/vole/users:
  `git clone` only mirrors refs/heads/*.
- File-per-comment means two people replying to the same issue never touch the
  same file, so the only merge conflict is two edits to one comment.
- Validation happens in pre-receive, so a malformed tree never reaches the web.
  Every problem is reported at once with the offending path, and a rejected push
  leaves the branch unmoved.

The index gained issue rows, refreshed when the issues ref moves rather than by a
hook, plus coverage in the full rebuild. The list page reads the index; the
detail page reads git, so bodies and comments are never stale.

Markdown is rendered with goldmark and sanitised with bluemonday. Issue bodies
arrive by push, which makes them untrusted input on a public page; a test asserts
that <script> and javascript: URLs do not survive.

Verified by hand with a user who has an editor and a key: filed an issue, added a
comment, saw both rendered, then pushed an issue with no title and got back
"issues/01M5.../meta.json: title is required" with the branch unmoved.

README.md +31−1

@@ -38,12 +38,14 @@ 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
42 push time and rendered read-only on the web
41 43 - Repo create / rename / delete from the CLI
42 44 - A derived SQLite index that can be deleted and rebuilt from the repos
43 45 - An instance ed25519 key generated at first boot (used for metadata signing in
44 46 Phase 7)
45 47
46 Not yet: issues, change requests, CI, LFS, static sites, signature verification.
48 Not yet: change requests, CI, LFS, static sites, signature verification.
47 49
48 50 ## Try it
49 51
@@ -68,6 +70,34 @@ git clone http://127.0.0.1:8080/git/demo.git # read only, no credentials
68 70 Repositories are private by default. A private repo is a `404` on the web and
69 71 over HTTP — not a `403` — so the read-only surface never confirms that it exists.
70 72
73 ## Filing an issue
74
75 An issue is a directory on the `issues` branch. No client tooling is required:
76
77 ```sh
78 git switch --orphan issues
79 ID=01M5JR2JK29KKMAA1T9MDNTPBP # any 26-char ULID; unique per issue
80 mkdir -p issues/$ID
81 cat > issues/$ID/meta.json <<'JSON'
82 {"title": "Nothing renders on the docs page",
83 "state": "open",
84 "author": "you",
85 "created": "2026-10-22T21:30:00Z",
86 "labels": ["bug"]}
87 JSON
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
90 ```
91
92 The id must be a ULID (26 characters, Crockford base32 — uppercase, no `I`, `L`,
93 `O` or `U`). A UUID is not one and will be rejected. Any ULID generator works, or
94 `vole issue new` once that lands.
95
96 A comment is `issues/<id>/comments/<ulid>.md` with `author` and `created`
97 frontmatter. Closing an issue is editing `state` to `closed`. The push is
98 rejected with the offending path named if the tree does not match the format, and
99 every issue is a normal commit you can `git log`, `git blame` and merge.
100
71 101 ## Backup
72 102
73 103 Two things, and both are small: