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 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.

README.md +40−4

@@ -38,8 +38,10 @@ 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 `vole` branch, validated at
42 push time and rendered read-only on the web
41 - Issues and change requests stored as files in the repository on the `vole`
42 branch, validated at push time and rendered read-only on the web
43 - Review as a git artifact: an approval names the exact commit it reviewed, and
44 `git push` is the merge, gated by `pre-receive`
43 45 - Repo create / rename / delete from the CLI
44 46 - A derived SQLite index that can be deleted and rebuilt from the repos
45 47 - An instance ed25519 key generated at first boot (used for metadata signing in
@@ -91,8 +93,8 @@ git add -A && git commit -m "issue: docs page blank" && git push origin vole
91 93
92 94 That branch holds Vole's metadata and is deliberately narrow:
93 95
94 - Only `issues/` (and `requests/`, once change requests land) may appear in it;
95 anything else is rejected at push.
96 - Only `issues/` and `requests/` may appear in it; anything else is rejected at
97 push.
96 98 - History must be linear. Merge commits are refused, so `git log vole` reads as
97 99 a list of changes rather than a graph.
98 100 - Force-pushing it requires repo admin and is written to `data/audit.log`, because
@@ -129,6 +131,40 @@ frontmatter. Closing an issue is editing `state` to `closed`. The push is
129 131 rejected with the offending path named if the tree does not match the format, and
130 132 every issue is a normal commit you can `git log`, `git blame` and merge.
131 133
134 ## Reviewing a change
135
136 Review is a git artifact, not a web session. An approval records the exact commit
137 that was reviewed, so a branch that moves after approval is not approved any more.
138 Because `git push` is the merge, the gate lives in `pre-receive` and there is no
139 write surface to authenticate:
140
141 ```sh
142 # the author pushes a branch and opens a request
143 git push origin feature:refs/heads/feature
144 vole request new --title "Retry on timeout" --base main --head feature -m "Rationale."
145
146 # an approver reviews that commit and approves it
147 vole request approve <id> --as <your-name>
148
149 # the author lands it; the hook checks the approvals match the new tip
150 git push origin feature:refs/heads/main
151 ```
152
153 The push is refused unless the new tip *is* the approved commit, or is a merge
154 whose parents are the branch tip and the approved commit. That second shape is why
155 "merge, then push" works while "append a commit and call it approved" does not.
156
157 ```sh
158 vole acl protect demo refs/heads/main # protect a branch
159 vole acl approvals demo 2 # require two approvals (default 1)
160 vole acl approvals demo 0 # back to admin-only
161 ```
162
163 Admins can push a protected branch directly. An approval can only be written by
164 the person it names — the hook compares the file's name to the SSH identity — so
165 `approvals/keenan.json` cannot be committed by anyone else. Every merge is
166 recorded in `data/audit.log` with the request id and approvers.
167
132 168 ## Backup
133 169
134 170 Two things, and both are small: