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