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: a request says whether it would actually land
3c5d11faadd2f1ca4788e0f7f41a77a11840517e · Keenan Nemetz · 17d ago · parent a63475ce
Phase 3: a request says whether it would actually land A diff answers "what changed" and nothing else. A branch can read perfectly line by line and still refuse to merge, and until now nobody found that out until they ran `git merge` locally — after an approval had been given. The request page now asks `git merge-tree --write-tree --name-only <base-tip> <head>`, which merges in memory without a worktree or an index, and says either "this branch no longer merges into main cleanly: code.txt — rebase or merge main before landing it" or "merges into main cleanly". `vole request show` prints the same line from a clone. Three rules, each because the obvious version is wrong: - Surfaced, not gated. A gate would only ever be right at the instant it ran: the base can move after an approval, and the roadmap asked for detection before the push rather than a second way to refuse one. - Silence when the question cannot be asked. merge-tree exits 1 both for a conflict and for a revision it cannot resolve, so the exit status alone is not an answer; the answer is believed only when the output starts with a tree id, which is what a real merge prints first. A base branch that is gone, or a head that was never pushed, therefore produces no claim at all — a page that says "clean" about a commit it could not load is worse than one that says nothing. - The clone says what it does not know. `vole request show` reports "cannot check: <sha> is not in this clone (git fetch origin)" rather than reading a head it does not have as mergeable. The same view now states the requirement, "0 of 1 required approval(s)", and counts the approvals that currently apply — the number used to live in the reviewer's head. Verified: TestMergeConflictsNamesThePaths and TestMergeConflictsIsEmptyWhenItWould Land build two branches that touch the same line and one that touches another file; TestMergeConflictsReportsAnUnaskableQuestion pins that an unknown revision is an error rather than "no conflicts" (that is the bug the tree-id check exists for); TestParseMergeTree covers clean, empty, one path, two paths and the message section; TestRequestPageSaysWhenItWouldConflict drives a real conflict through the binary — main moves the same line after the feature branch was opened — checks the page, the "0 of 1" line, a clean branch, the CLI's conflict line, the CLI's "cannot check" for a head that is not in a fresh clone, and a request whose head was never pushed, which must say nothing. scripts/ci.sh green (gofmt, vet, build, markdown links, go test -race ./...).
docs/roadmap.md +25−9
| @@ -243,15 +243,30 @@ requests/<ulid>/approvals/<principal>.md base + head + created, no body | |||
| 243 | 243 | having refused forgeries. Publishing signed attestations (Phase 7) is what would make | |
| 244 | 244 | it verifiable offline. | |
| 245 | 245 | ||
| 246 | Deferred to later phases: | ||
| 247 | |||
| 248 | - Line-anchored review comments anchored on `(path, blob-sha, line)` so they survive a | ||
| 249 | rewrite | ||
| 250 | - A rendered, per-file collapsible diff (the detail page lists changed paths only) | ||
| 251 | - Required status checks (Phase 4 wires these into the same gate) | ||
| 252 | - Conflict detection surfaced before the push is attempted | ||
| 253 | |||
| 254 | **Exit criteria:** met, except review comments and rendered diffs. Verified end to end: a | ||
| 246 | Deferred: | ||
| 247 | |||
| 248 | - Review comments, line-anchored on `(path, blob-sha, line)` so they survive a | ||
| 249 | rewrite. There is no discussion on a request at all yet — the only ways to say | ||
| 250 | something about one are editing its body or approving it — so this is two | ||
| 251 | features: a thread, and then anchors on it. | ||
| 252 | - The *collapsible* part of the per-file diff. The diff itself landed in Phase 7 | ||
| 253 | slice 1 (three-dot range, per-file hunks, line numbers), and required status | ||
| 254 | checks landed in Phase 4 slice 3, so both are struck off the original list | ||
| 255 | rather than still waiting here. | ||
| 256 | |||
| 257 | ✅ **Conflict detection, since this phase closed.** `git merge-tree --write-tree | ||
| 258 | --name-only` merges in memory and names the paths that conflict, so the request | ||
| 259 | page says "this branch no longer merges into main cleanly: `code.txt`" before | ||
| 260 | anybody approves, and `vole request show` prints the same line from a clone. It is | ||
| 261 | **surfaced, not gated**: the roadmap asked for detection before the push, and a gate | ||
| 262 | would only ever be right at the instant it ran, since the base can move after an | ||
| 263 | approval. It is also only claimed when the question could be asked — a missing base | ||
| 264 | branch or a head that was never pushed produces silence, because a page that says | ||
| 265 | "clean" for a commit it could not load is worse than one that says nothing. The | ||
| 266 | same view now states the requirement ("0 of 1 required approval(s)"), which used | ||
| 267 | to be a number the reviewer had to remember. | ||
| 268 | |||
| 269 | **Exit criteria:** met, except review comments. Verified end to end: a | ||
| 255 | 270 | writer cannot land on a protected branch without approvals, an amended head invalidates a | |
| 256 | 271 | prior approval, an approval for `main` cannot be repointed at another branch, a | |
| 257 | 272 | self-approval or impersonated approval is refused, an admin bypass works, and each merge | |
| @@ -790,6 +805,7 @@ Filled in as phases land. Append; don't rewrite. | |||
| 790 | 805 | | 2026-10-23 | The author of a metadata write is the account the key resolves to, asked with `vole-whoami`; `--author` is gone | `git config user.name` is a name somebody typed for commits: "Keenan Nemetz" sanitises to `keenan-nemetz`, which is not an account, so every issue filed from a normal machine was attributed to nobody and nothing complained. The instance is the only place that maps a key to an account, and it is one SSH verb away on the connection already open. A fallback exists for `--no-push` with the instance unreachable, and it warns loudly rather than guessing silently | | |
| 791 | 806 | | 2026-10-23 | A remote given as an SSH URL is fetched like a named one | Skipping the fetch for a URL is what made `--remote ssh://…` write on a branch that had never been updated: the first write in a fresh repository succeeded (nothing to fast-forward from) and every later one failed as a "lost race" that never happened, re-applying on the same stale tip and reporting "already exists". `git init` plus a URL is now the real no-clone filing path | | |
| 792 | 807 | | 2026-10-23 | The instance tells the client where it is served (`public_url`), so a command can print the page it wrote | A command that answers with an id leaves the reader to assemble a URL, and the instance is the only party that knows its own address behind a proxy. Optional: unset means ids only, which is what it printed before | | |
| 808 | | 2026-10-23 | "Would this merge?" is answered in memory and surfaced, never gated | `merge-tree --write-tree` merges without a worktree or an index, so the cost is one git process on a page view; gating on it would be wrong because the base can move after an approval, and the roadmap asked for detection before the push rather than a second refusal. The exit status alone is ambiguous — 1 is both "conflicts" and "bad revision" — so the answer is believed only when the output starts with a tree id, and silence otherwise | | ||
| 793 | 809 | ||
| 794 | 810 | ## Open questions | |
| 795 | 811 | ||