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.

Let an author move their change request's head from the browser

53ff967be619894b62ca0ca69cd9a9f5f52bce01 · Keenan Nemetz · 13d ago · parent 8e1005eb

Let an author move their change request's head from the browser

A request names the exact commit under review, so a fixup pushed after it
was opened is invisible until the head moves. The only mover was
`vole change update --head`, which writes the metadata branch — a write a
contributor cannot make. The person who owns the branch was the one person
who could not keep the request current.

The request page now offers the author (or a maintainer) a branch picker
that moves an open request's head to that branch's tip. It applies the
same rule the new-request form does: the head must be a branch tip, and a
contributor may only name a branch under their own namespace, which is
what the pre-receive hook would have let them push. Approvals stay in the
tree as history and stop matching, which is what a new head already means.

SetRequestHead now refuses a closed or merged request, so the CLI and the
browser share that rule instead of the web handler owning it alone.

metadata/requests_test.go +52

@@ -481,6 +481,58 @@ func TestMarkMergedClosesLinkedIssues(t *testing.T) {
481 481 }
482 482 }
483 483
484 // A request's head is what the review and the approvals name, so it may only move
485 // while the request is open: a merged request's head is the record of what landed,
486 // and a closed one is not going to land at all. The refusal lives in the writer so
487 // the CLI and the browser cannot disagree about it.
488 func TestSetRequestHeadMovesOnlyAnOpenRequest(t *testing.T) {
489 t.Parallel()
490 root := t.TempDir()
491 if err := git.InitBare(root, "main"); err != nil {
492 t.Fatal(err)
493 }
494 g := git.Open(root)
495 now := time.Date(2026, 10, 27, 12, 0, 0, 0, time.UTC)
496 seed, err := g.CommitTree(mustTree(t, g), "", "seed")
497 if err != nil {
498 t.Fatal(err)
499 }
500 if err := g.UpdateRef(Ref, seed); err != nil {
501 t.Fatal(err)
502 }
503 req, err := NewRequest("Land it", "", "keenan", "main", headSHA, now)
504 if err != nil {
505 t.Fatal(err)
506 }
507 if err := CreateRequest(g, req, "keenan"); err != nil {
508 t.Fatal(err)
509 }
510
511 got, err := SetRequestHead(g, req.ID, sideSHA, "keenan")
512 if err != nil || got.Head != sideSHA {
513 t.Fatalf("open head move = %v, %v", got.Head, err)
514 }
515
516 // Closed: the head is history, not a setting.
517 if _, err := SetRequestState(g, req.ID, "closed", "keenan"); err != nil {
518 t.Fatal(err)
519 }
520 if _, err := SetRequestHead(g, req.ID, headSHA, "keenan"); err == nil {
521 t.Fatal("a closed request's head moved")
522 }
523
524 // Merged: the head names what landed, so it is settled.
525 if _, err := SetRequestState(g, req.ID, "open", "keenan"); err != nil {
526 t.Fatal(err)
527 }
528 if _, _, err := MarkMerged(g, req.ID, "", sideSHA, "reviewer", now); err != nil {
529 t.Fatal(err)
530 }
531 if _, err := SetRequestHead(g, req.ID, headSHA, "keenan"); err == nil {
532 t.Fatal("a merged request's head moved")
533 }
534 }
535
484 536 // A link is often discovered after a request is opened, so it can be set later. The
485 537 // same rules apply: a full id, an issue that exists, and only while the request can
486 538 // still land.