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 3: change requests, with approvals bound to the commit they reviewed
1eeca4e67a65d71217d51b1c41632330225b351a · Keenan Nemetz · 17d ago · parent 92b37587
Phase 3: change requests, with approvals bound to the commit they reviewed Review is now a git artifact. A request names a base branch and one head commit; an approval names that same commit, so a branch that moves after approval is not approved any more. Because `git push` is the merge, the gate lives in pre-receive and there is no authenticated write surface to build. requests/<ulid>/meta.json title, author, base, head, state requests/<ulid>/description.md requests/<ulid>/approvals/<user>.json base + head + created Two push shapes are accepted for a protected branch: the new tip is the approved head, or the new tip is a merge whose parents are the branch tip and the approved head. "The approved head is an ancestor of the new tip" is refused on purpose — accepting it would let anyone append unapproved commits and land them together with an approved one. Admins keep their bypass; `vole acl approvals <repo> <n>` sets the count, and 0 restores admin-only. Three integrity holes found while building it, each now covered by a test: - An approval file names a principal, but nothing tied that name to the pusher, so any writer could commit approvals/<someone-else>.json. The hook now requires a newly added approval to be named after the SSH identity. That check initially skipped the push that *creates* the branch, where there is no old commit to diff against — which let whoever created the branch seed any approval they liked. Fixed to inspect the whole tree on a first push. - Approvals were bound to the head but not the branch, so rewriting a request's base repointed an approval at somewhere the approver never looked. - Self-approval was refused at the CLI but not in validation, so a hand-written tree could carry one. Also relaxes the CLI to accept options after arguments, as git does. Refusing `vole request approve <id> --as keenan` is a local dialect, not git's behaviour, and `git commit -m msg path` is the shape people already type. Exit 129, no bundling and `--`/`--end-of-options` are unchanged. Verified end to end on a real instance with two keys: a writer's direct push to a protected main is refused with a message naming the exact commit; the same push still fails with a request open but unapproved; self-approval fails; an admin approval lets it through; main advances; the web shows the approval as current and the request as already merged; the audit log records the merge with request id and approver. Known rough edge: a merged request stays open until someone closes it. Marking it merged automatically needs the server to write metadata during a push, which the design deliberately does not do. Phase 4 CI is the natural place to close it.
internal/server/server.go +2
| @@ -105,6 +105,8 @@ func (s *Server) routes() *http.ServeMux { | |||
| 105 | 105 | mux.HandleFunc("GET /r/{name}/commits/{ref}", w.Commits) | |
| 106 | 106 | mux.HandleFunc("GET /r/{name}/issues", w.Issues) | |
| 107 | 107 | mux.HandleFunc("GET /r/{name}/issues/{id}", w.IssueView) | |
| 108 | mux.HandleFunc("GET /r/{name}/requests", w.Requests) | ||
| 109 | mux.HandleFunc("GET /r/{name}/requests/{id}", w.RequestView) | ||
| 108 | 110 | ||
| 109 | 111 | mux.HandleFunc("GET /healthz", func(rw http.ResponseWriter, r *http.Request) { | |
| 110 | 112 | rw.Header().Set("Content-Type", "text/plain") | |