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/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Show which branch a change request came from

8e1005eb2c104934adef950c5afc090d4025ce06 · Keenan Nemetz · 13d ago · parent b7ac1a00

Show which branch a change request came from

A request records the exact commit under review, and the page showed only
that: `main ← 43f98bcd`. The branch the work is on is how people talk about
a change, fetch it, and find it after it lands, and it was being thrown
away when the request was opened — the web form resolved the chosen branch
to a commit and kept only the commit.

`request.md` now records `head_ref:` beside `head:`. It is a display hint
and nothing else: approvals, checks, the diff and the landing gate still
name the commit, and a test pins that a branch name cannot authorise a
different tip. The request page, the request list and `vole change show`
show it, and the page marks the branch as moved or deleted when it no
longer points at the reviewed commit, so the name reads as provenance
rather than as a claim about what is being reviewed.

It is optional on read: records written before it existed still load, and
a request opened from a bare commit id records no branch.

metadata/requests_test.go +92

@@ -256,6 +256,98 @@ func TestMergedStateNamesTheCommitThatLanded(t *testing.T) {
256 256 }
257 257 }
258 258
259 // A request may record the branch its head came from. It is provenance, not
260 // authorisation: the commit is what the gate matches, so a branch that has moved on
261 // must not carry the request's approval to the new tip. The field is optional so
262 // records written before it existed still load, and it must be a branch name with a
263 // commit behind it.
264 func TestHeadRefIsAHeadingNotAnApproval(t *testing.T) {
265 t.Parallel()
266 body := func(extra string) map[string]string {
267 return map[string]string{
268 RequestsDir + "/" + reqIDA + "/request.md": "---\n" +
269 `title: "Land the retry"` + "\n" +
270 "state: open\n" +
271 "author: keenan\n" +
272 "base: main\n" +
273 "head: " + headSHA + "\n" +
274 extra +
275 "created: 2026-10-22T22:00:00Z\n" +
276 "---\n",
277 }
278 }
279 list, err := ParseRequests(body("head_ref: contrib/keenan/fix\n"))
280 if err != nil {
281 t.Fatalf("a request naming its branch was refused: %v", err)
282 }
283 if list[0].HeadRef != "contrib/keenan/fix" {
284 t.Fatalf("head_ref = %q, want contrib/keenan/fix", list[0].HeadRef)
285 }
286 for name, extra := range map[string]string{
287 "a full ref": "head_ref: refs/heads/main\n",
288 "a space": "head_ref: two words\n",
289 } {
290 if _, err := ParseRequests(body(extra)); err == nil {
291 t.Errorf("head_ref with %s was accepted", name)
292 }
293 }
294 noHead := map[string]string{
295 RequestsDir + "/" + reqIDA + "/request.md": "---\n" +
296 `title: "Land the retry"` + "\n" +
297 "state: open\n" +
298 "author: keenan\n" +
299 "base: main\n" +
300 "head_ref: main\n" +
301 "created: 2026-10-22T22:00:00Z\n" +
302 "---\n",
303 }
304 if _, err := ParseRequests(noHead); err == nil {
305 t.Error("a branch with no head commit was accepted")
306 }
307
308 // The writer round-trips the field, and refuses what the reader would refuse.
309 now := time.Date(2026, 10, 22, 22, 0, 0, 0, time.UTC)
310 req, err := NewRequest("Land it", "", "keenan", "main", headSHA, now)
311 if err != nil {
312 t.Fatal(err)
313 }
314 req.HeadRef = "contrib/keenan/fix"
315 raw, err := renderRequest(req)
316 if err != nil {
317 t.Fatal(err)
318 }
319 again, err := ParseRequests(map[string]string{RequestsDir + "/" + req.ID + "/request.md": string(raw)})
320 if err != nil {
321 t.Fatalf("the writer produced a request the reader refuses: %v", err)
322 }
323 if again[0].HeadRef != req.HeadRef {
324 t.Fatalf("round trip head_ref = %q, want %q", again[0].HeadRef, req.HeadRef)
325 }
326 bad, err := NewRequest("Land it", "", "keenan", "main", headSHA, now)
327 if err != nil {
328 t.Fatal(err)
329 }
330 bad.HeadRef = "refs/heads/main"
331 if err := validateRequestForWrite(bad); err == nil {
332 t.Error("the writer accepted a head_ref that is not a branch name")
333 }
334
335 // The gate matches the commit. A branch name on the request authorises nothing.
336 reqs := []*Request{{
337 ID: reqIDA, Title: "Land it", Author: "keenan", Base: "main",
338 Head: headSHA, HeadRef: "contrib/keenan/fix", State: "open",
339 Approvals: []Approval{{Principal: "wilbur", Base: "main", Head: headSHA}},
340 }}
341 m, err := FindApprovedMatch(nil, reqs, ApprovalRequirement{Branch: "main", NewTip: headSHA, Required: 1})
342 if err != nil || m == nil {
343 t.Fatalf("the approved head did not match: %v, %v", m, err)
344 }
345 m, err = FindApprovedMatch(nil, reqs, ApprovalRequirement{Branch: "main", NewTip: sideSHA, Required: 1})
346 if err != nil || m != nil {
347 t.Fatalf("a branch name authorised %s: %v, %v", sideSHA, m, err)
348 }
349 }
350
259 351 // "merged" is not settable through the generic state writer: it has to travel with
260 352 // the commit, so MarkMerged is the only way in.
261 353 func TestMergedIsNotAGenericState(t *testing.T) {