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 a change request gain its issue links after it is opened

bdf6a0de1ad493c9c4650a27c926767b06621a70 · Keenan Nemetz · 14d ago · parent 183d48e5

Let a change request gain its issue links after it is opened

A link is usually discovered after the request exists: the issue turns up
while reviewing, or while writing the landing note. Without an edit path
the only options were to open a second request or leave the issue behind,
which is the drift the link was meant to stop.

`vole change update <id> --closes <issue>...` replaces the list and
`--no-closes` clears it, alongside the existing `--head`, in one command
and one metadata commit. An unchanged list is not an edit, so the branch
does not move, and the ids are checked against the issues tree at that
point as well as at creation — a link added later is exactly where a typo
would otherwise hide until the issue quietly never closed.

Only an open request may be relinked. A merged request's list records what
the merge did, and a closed one is not going to run, so both refuse.

metadata/requests_test.go +67

@@ -364,3 +364,70 @@ func TestMarkMergedClosesLinkedIssues(t *testing.T) {
364 364 t.Fatalf("second mark = %v, %v", again, err)
365 365 }
366 366 }
367
368 // A link is often discovered after a request is opened, so it can be set later. The
369 // same rules apply: a full id, an issue that exists, and only while the request can
370 // still land.
371 func TestSetRequestClosesEditsAnOpenRequest(t *testing.T) {
372 t.Parallel()
373 root := t.TempDir()
374 if err := git.InitBare(root, "main"); err != nil {
375 t.Fatal(err)
376 }
377 g := git.Open(root)
378 now := time.Date(2026, 10, 27, 12, 0, 0, 0, time.UTC)
379 seed, err := g.CommitTree(mustTree(t, g), "", "seed")
380 if err != nil {
381 t.Fatal(err)
382 }
383 if err := g.UpdateRef(Ref, seed); err != nil {
384 t.Fatal(err)
385 }
386 iss, err := NewIssue("Later", "", "keenan", nil, nil, now)
387 if err != nil {
388 t.Fatal(err)
389 }
390 if err := CreateIssue(g, iss, "keenan"); err != nil {
391 t.Fatal(err)
392 }
393 req, err := NewRequest("Land it", "", "keenan", "main", headSHA, now)
394 if err != nil {
395 t.Fatal(err)
396 }
397 if err := CreateRequest(g, req, "keenan"); err != nil {
398 t.Fatal(err)
399 }
400
401 got, err := SetRequestCloses(g, req.ID, []string{iss.ID}, "keenan")
402 if err != nil {
403 t.Fatalf("link: %v", err)
404 }
405 if len(got.Closes) != 1 || got.Closes[0] != iss.ID {
406 t.Fatalf("closes = %v, want [%s]", got.Closes, iss.ID)
407 }
408 // A second call with the same set is not an edit, so the branch does not move.
409 if _, err := SetRequestCloses(g, req.ID, []string{iss.ID}, "keenan"); err != nil {
410 t.Fatalf("re-link: %v", err)
411 }
412 for _, bad := range [][]string{{"01M5JR2JK29KKMAA1T9MDNTRBZ"}, {"short"}} {
413 if _, err := SetRequestCloses(g, req.ID, bad, "keenan"); err == nil {
414 t.Fatalf("closes %v was accepted", bad)
415 }
416 }
417 // Once merged, the list is a record of what happened, not a setting.
418 if _, _, err := MarkMerged(g, req.ID, headSHA, "reviewer", now); err != nil {
419 t.Fatal(err)
420 }
421 if _, err := SetRequestCloses(g, req.ID, nil, "keenan"); err == nil {
422 t.Fatal("a merged request's links were changed")
423 }
424 }
425
426 func mustTree(t *testing.T, g *git.Repo) string {
427 t.Helper()
428 tree, err := g.MkTree(nil)
429 if err != nil {
430 t.Fatal(err)
431 }
432 return tree
433 }