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.

Make change requests follow branches and allow base retargeting

e050e14a64f6f19c1b1478c4c073d690ef3d3129 · Keenan Nemetz · 13d ago · parent 59464445

Make change requests follow branches and allow base retargeting

A request opened from a branch now follows it: post-receive moves the
request's head to the new tip after an accepted push, so approvals and
checks that named the old tip stop counting. Approvals stay bound to an
exact base and commit; the approval writers now require the base and head
the approver saw, so a push or retarget cannot silently approve a target
nobody reviewed. Merged requests freeze the reviewed head.

Add `vole change update --base` and a Change base control for stacked
requests. A new base changes the diff and therefore invalidates approvals,
which is what retargeting an upper request after its lower branch lands
needs. Base names may now contain slashes.

Also fix a second-boundary flake in the landing test.

forge/landing/land_test.go +14−4

@@ -104,7 +104,7 @@ func (f *fixture) reviewWith(author, head, reviewer string, closes []string) str
104 104 f.t.Fatal(err)
105 105 }
106 106 if reviewer != "" {
107 if _, err := metadata.ApproveRequest(f.g, req.ID, reviewer, now.Add(time.Minute), reviewer); err != nil {
107 if _, err := metadata.ApproveRequest(f.g, req.ID, reviewer, req.Base, req.Head, now.Add(time.Minute), reviewer); err != nil {
108 108 f.t.Fatal(err)
109 109 }
110 110 }
@@ -385,13 +385,23 @@ func TestPrepareDescribesAMergeCommit(t *testing.T) {
385 385 if tip, _ := f.g.RevParse("refs/heads/main"); tip != moved {
386 386 t.Fatalf("main = %s, want the untouched %s", tip, moved)
387 387 }
388 // The plan the write carries out is the same one: same tip, same shape.
388 // The plan the write carries out is the same one: same parents and shape. The
389 // commit id may differ because a merge commit's timestamp has second resolution,
390 // so a Prepare and a Land that straddle a second boundary write different objects
391 // for the same merge.
389 392 res, err := Land(f.gate("reviewer", access.Maintain), id, "reviewer", time.Now().UTC())
390 393 if err != nil {
391 394 t.Fatalf("land: %v", err)
392 395 }
393 if res.NewTip != plan.NewTip {
394 t.Fatalf("landed %s but the plan said %s", res.NewTip, plan.NewTip)
396 if !res.MergeCommit {
397 t.Fatalf("landed %s, want a merge commit", res.NewTip)
398 }
399 landedParents, err := f.g.CommitParents(res.NewTip)
400 if err != nil {
401 t.Fatal(err)
402 }
403 if len(landedParents) != 2 || landedParents[0] != moved || landedParents[1] != head {
404 t.Fatalf("landed parents = %v, want [%s %s]", landedParents, moved, head)
395 405 }
396 406 }
397 407