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.

cmd/vole/change.go +56−8

@@ -3,6 +3,7 @@ package main
3 3 import (
4 4 "fmt"
5 5 "os"
6 "sort"
6 7 "strconv"
7 8 "strings"
8 9 "time"
@@ -21,7 +22,7 @@ func changeUsage() string {
21 22 comment <id> [-m <text> | -F <file>] [--path <file> --line <n> [--side new|old]] [--reply-to <comment-id>]
22 23 edit-comment <id> <comment-id> [-m <text> | -F <file>]
23 24 rm-comment <id> <comment-id>
24 update <id> [--head <rev>]
25 update <id> [--head <rev>] [--closes <issue>]... [--no-closes]
25 26 approve <id> [--as <account>]
26 27 merge <id>
27 28 close|reopen <id>
@@ -58,6 +59,7 @@ func cmdChange(args []string) error {
58 59 {long: "remote"},
59 60 {long: "format"},
60 61 {long: "no-push", boolean: true},
62 {long: "no-closes", boolean: true},
61 63 }
62 64 f, err := parseFlags(args[1:], specs...)
63 65 if err != nil {
@@ -74,7 +76,7 @@ func cmdChange(args []string) error {
74 76 "rm-comment": {"remote", "no-push"},
75 77 "list": {"state", "format", "remote"},
76 78 "show": {"remote"},
77 "update": {"head", "remote", "no-push"},
79 "update": {"head", "closes", "no-closes", "remote", "no-push"},
78 80 "approve": {"as", "remote", "no-push"},
79 81 "merge": {"remote"},
80 82 "close": {"remote", "no-push"},
@@ -335,33 +337,61 @@ func cmdChange(args []string) error {
335 337
336 338 case "update":
337 339 if len(f.rest) != 1 {
338 return usagef("usage: vole change update <id> [--head <rev>]")
340 return usagef("usage: vole change update <id> [--head <rev>] [--closes <issue>]... [--no-closes]")
339 341 }
340 342 r, err := metadata.FindRequest(c.Repo, f.rest[0])
341 343 if err != nil {
342 344 return err
343 345 }
344 346 head := r.Head
347 headMoved := false
345 348 if v := f.one("head"); v != "" {
346 349 head, err = client.ResolveHead(c, v)
347 350 if err != nil {
348 351 return err
349 352 }
350 }
351 if head == r.Head {
352 return usagef("nothing to update: pass --head <rev>")
353 headMoved = head != r.Head
354 }
355 // A link can be added or corrected long after a request was opened, which is
356 // the case that made prose references useless: the issue is discovered later.
357 linksChanged := false
358 closes := r.Closes
359 switch {
360 case f.has("no-closes") && f.given("closes"):
361 return usagef("--closes and --no-closes say different things")
362 case f.has("no-closes"):
363 closes, linksChanged = nil, len(r.Closes) > 0
364 case f.given("closes"):
365 closes = closesFlag(f.all("closes"))
366 linksChanged = !equalIDs(closes, r.Closes)
367 }
368 if !headMoved && !linksChanged {
369 return usagef("nothing to update: pass --head <rev> or --closes <issue>")
353 370 }
354 371 var out *metadata.Request
355 372 reapply := func() error {
356 373 var uerr error
357 out, uerr = metadata.SetRequestHead(c.Repo, r.ID, head, author)
374 out = r
375 if linksChanged {
376 if out, uerr = metadata.SetRequestCloses(c.Repo, r.ID, closes, author); uerr != nil {
377 return uerr
378 }
379 }
380 if headMoved {
381 out, uerr = metadata.SetRequestHead(c.Repo, r.ID, head, author)
382 }
358 383 return uerr
359 384 }
360 385 if err := reapply(); err != nil {
361 386 return err
362 387 }
363 388 fmt.Printf("%s\t%s\t%s\n", out.ID, format.ID(out.Head), out.Title)
364 fmt.Fprintf(os.Stderr, "note: existing approvals named the old head and no longer apply\n")
389 for _, id := range out.Closes {
390 fmt.Fprintf(os.Stderr, "closes %s when merged\n", id)
391 }
392 if headMoved {
393 fmt.Fprintf(os.Stderr, "note: existing approvals named the old head and no longer apply\n")
394 }
365 395 return c.CommitAndPush(noPush, reapply)
366 396
367 397 case "merge":
@@ -449,3 +479,21 @@ func closesFlag(values []string) []string {
449 479 }
450 480 return out
451 481 }
482
483 // equalIDs compares two id lists as sets after normalisation, which is what deciding
484 // "did the links change" needs: order and duplicates are not edits.
485 func equalIDs(a, b []string) bool {
486 norm := func(in []string) string {
487 seen := map[string]bool{}
488 out := make([]string, 0, len(in))
489 for _, v := range in {
490 if v = strings.TrimSpace(v); v != "" && !seen[v] {
491 seen[v] = true
492 out = append(out, v)
493 }
494 }
495 sort.Strings(out)
496 return strings.Join(out, ",")
497 }
498 return norm(a) == norm(b)
499 }