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.
Compact the change request target controls
41d1fee900568b1bff5c7cff3b4c52942f47fabd · Keenan Nemetz · 13d ago · parent e050e14a
Compact the change request target controls Replace the separate base and head forms and the duplicated labels with one row: base picker, head picker, update. The pickers preselect the current targets, and a pinned head appears as the current commit. Collapse the help text to one line. Post both targets to one /target handler and write them in one metadata commit. Either change invalidates approvals; keeping the current value is allowed even if its branch has gone missing so the other side can still be repaired.
metadata/requests_test.go +54
| @@ -596,6 +596,60 @@ func TestSetRequestBaseRetargetsAStackedRequest(t *testing.T) { | |||
| 596 | 596 | } | |
| 597 | 597 | } | |
| 598 | 598 | ||
| 599 | // The request page posts base and head together, so the writer moves both in one | ||
| 600 | // metadata commit. Either field changing makes approvals stale; setting both to what | ||
| 601 | // they already are is not a commit. | ||
| 602 | func TestSetRequestTargetMovesBaseAndFollowedHead(t *testing.T) { | ||
| 603 | t.Parallel() | ||
| 604 | g := newRequestRepo(t) | ||
| 605 | now := time.Date(2026, 10, 27, 12, 0, 0, 0, time.UTC) | ||
| 606 | r, err := NewRequest("Stacked", "", "keenan", "main", headSHA, now) | ||
| 607 | if err != nil { | ||
| 608 | t.Fatal(err) | ||
| 609 | } | ||
| 610 | r.HeadRef = "feature" | ||
| 611 | if err := CreateRequest(g, r, "keenan"); err != nil { | ||
| 612 | t.Fatal(err) | ||
| 613 | } | ||
| 614 | if _, err := ApproveRequest(g, r.ID, "wilbur", "main", headSHA, now, "wilbur"); err != nil { | ||
| 615 | t.Fatal(err) | ||
| 616 | } | ||
| 617 | |||
| 618 | got, err := SetRequestTarget(g, r.ID, "next", sideSHA, "other", "keenan") | ||
| 619 | if err != nil { | ||
| 620 | t.Fatal(err) | ||
| 621 | } | ||
| 622 | if got.Base != "next" || got.Head != sideSHA || got.HeadRef != "other" { | ||
| 623 | t.Fatalf("target = %s <- %s at %s", got.Base, got.Head, got.HeadRef) | ||
| 624 | } | ||
| 625 | if got.CurrentApprovals() != 0 { | ||
| 626 | t.Fatalf("an approval of the old target still counts: %+v", got.Approvals) | ||
| 627 | } | ||
| 628 | |||
| 629 | // Repeating the same target is not an edit, so the metadata branch does not move. | ||
| 630 | before, err := g.RevParse(Ref) | ||
| 631 | if err != nil { | ||
| 632 | t.Fatal(err) | ||
| 633 | } | ||
| 634 | if _, err := SetRequestTarget(g, r.ID, "next", sideSHA, "other", "keenan"); err != nil { | ||
| 635 | t.Fatal(err) | ||
| 636 | } | ||
| 637 | after, err := g.RevParse(Ref) | ||
| 638 | if err != nil { | ||
| 639 | t.Fatal(err) | ||
| 640 | } | ||
| 641 | if after != before { | ||
| 642 | t.Fatalf("a no-op target move changed %s: %s -> %s", Ref, before, after) | ||
| 643 | } | ||
| 644 | |||
| 645 | if _, err := SetRequestState(g, r.ID, "closed", "keenan"); err != nil { | ||
| 646 | t.Fatal(err) | ||
| 647 | } | ||
| 648 | if _, err := SetRequestTarget(g, r.ID, "main", headSHA, "feature", "keenan"); err == nil { | ||
| 649 | t.Fatal("a closed request's target moved") | ||
| 650 | } | ||
| 651 | } | ||
| 652 | |||
| 599 | 653 | // A request opened from a branch follows it: a push moves the head to the new tip, | |
| 600 | 654 | // the approval of the old tip stops counting, and a pinned or closed request is left | |
| 601 | 655 | // alone. A sync that changes nothing does not grow the metadata log. | |