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. | |