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.

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.

server/web_test.go +25−8

@@ -256,8 +256,8 @@ func TestRequestRecordsTheBranchItCameFrom(t *testing.T) {
256 256 t.Fatalf("change show does not name the branch:\n%s", out)
257 257 }
258 258
259 // A branch that moves past the reviewed commit is still the branch the request
260 // came from, but the page must not present it as what is being reviewed.
259 // A push to the followed branch moves the request with it: the page shows the
260 // new tip as what would land, not the old one with a "branch moved" note.
261 261 if err := os.WriteFile(filepath.Join(feature, "readme.md"), []byte("v1\nfeature line\nand more\n"), 0o644); err != nil {
262 262 t.Fatal(err)
263 263 }
@@ -267,19 +267,36 @@ func TestRequestRecordsTheBranchItCameFrom(t *testing.T) {
267 267 if _, errs, err := f.git(feature, "push", f.sshURL(f.repo), "HEAD:refs/heads/feature"); err != nil {
268 268 t.Fatalf("fixup push: %s", errs)
269 269 }
270 second, _, err := runGit(t, feature, "rev-parse", "HEAD")
271 if err != nil {
272 t.Fatal(err)
273 }
274 second = strings.TrimSpace(second)
275 if second == head {
276 t.Fatalf("the fixup did not create a new tip")
277 }
278 record, _, err = runGit(t, filepath.Join(f.cfg.ReposDir(), f.repo+".git"),
279 "show", "refs/heads/vole:requests/"+id+"/request.md")
280 if err != nil {
281 t.Fatalf("read the followed request: %v", err)
282 }
283 if !strings.Contains(record, "head: "+second) {
284 t.Fatalf("the request did not follow the branch to %s:\n%s", second, record)
285 }
270 286 _, body = f.get("/r/" + f.repo + "/requests/" + id)
271 if !strings.Contains(body, "branch moved") || !strings.Contains(body, "/commit/"+head) {
272 t.Fatalf("a moved branch is not labelled, or the reviewed commit is gone:\n%s", body)
287 if !strings.Contains(body, "/commit/"+second) {
288 t.Fatalf("the page does not show the followed tip %s:\n%s", second, body)
273 289 }
274 290
275 // A branch that is gone is still the branch the request came from, and the page
276 // says so instead of dropping the name or pretending it can check it.
291 // A branch that is gone is still the branch the request follows, and the page
292 // says so instead of dropping the name; the last tip it saw stays the head.
277 293 if _, errs, err := f.git(feature, "push", f.sshURL(f.repo), ":refs/heads/feature"); err != nil {
278 294 t.Fatalf("delete branch: %s", errs)
279 295 }
280 296 _, body = f.get("/r/" + f.repo + "/requests/" + id)
281 if !strings.Contains(body, "branch deleted") || !strings.Contains(body, ">feature</code>") {
282 t.Fatalf("a deleted branch is not labelled, or its name is gone:\n%s", body)
297 if !strings.Contains(body, "branch deleted") || !strings.Contains(body, ">feature</code>") ||
298 !strings.Contains(body, "/commit/"+second) {
299 t.Fatalf("a deleted branch is not labelled, or its last head is gone:\n%s", body)
283 300 }
284 301
285 302 // A raw commit is a head with no branch behind it.