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/request_write.go +69
| @@ -478,3 +478,72 @@ func loadFrom(files map[string][]byte, id string) (*Request, error) { | |||
| 478 | 478 | } | |
| 479 | 479 | return nil, fmt.Errorf("requests: no request %s", id) | |
| 480 | 480 | } | |
| 481 | |||
| 482 | // SetRequestCloses replaces the issues a request promises to close. An open request is | ||
| 483 | // the only thing that can land, so it is the only thing whose links are worth | ||
| 484 | // changing: a merged request's list is a record of what the merge did, and a closed | ||
| 485 | // one is not going to run. | ||
| 486 | // | ||
| 487 | // The ids are checked against the issues tree here as well as at creation, because a | ||
| 488 | // request may be linked long after it was opened, and a typo at that point would | ||
| 489 | // otherwise surface as an issue that quietly never closes. | ||
| 490 | func SetRequestCloses(g *git.Repo, id string, closes []string, actor string) (*Request, error) { | ||
| 491 | closes, problems := parseCloses(closes, "request") | ||
| 492 | if len(problems) > 0 { | ||
| 493 | return nil, fmt.Errorf("%s", strings.Join(problems, "; ")) | ||
| 494 | } | ||
| 495 | if len(closes) > 0 { | ||
| 496 | issues, err := LoadIssues(g, Ref) | ||
| 497 | if err != nil { | ||
| 498 | return nil, err | ||
| 499 | } | ||
| 500 | known := make(map[string]bool, len(issues)) | ||
| 501 | for _, iss := range issues { | ||
| 502 | known[iss.ID] = true | ||
| 503 | } | ||
| 504 | for _, want := range closes { | ||
| 505 | if !known[want] { | ||
| 506 | return nil, fmt.Errorf("requests: closes %s, and there is no such issue", want) | ||
| 507 | } | ||
| 508 | } | ||
| 509 | } | ||
| 510 | var out *Request | ||
| 511 | base := fmt.Sprintf("%s/%s/", RequestsDir, id) | ||
| 512 | _, err := git.CommitFiles(g, Ref, func(files map[string][]byte) error { | ||
| 513 | r, err := loadFrom(files, id) | ||
| 514 | if err != nil { | ||
| 515 | return err | ||
| 516 | } | ||
| 517 | if r.State != "open" { | ||
| 518 | return fmt.Errorf("requests: %s is %s, so its links cannot change", id, r.State) | ||
| 519 | } | ||
| 520 | if equalStrings(r.Closes, closes) { | ||
| 521 | out = r | ||
| 522 | return ErrNoChange | ||
| 523 | } | ||
| 524 | r.Closes = closes | ||
| 525 | raw, err := renderRequest(r) | ||
| 526 | if err != nil { | ||
| 527 | return err | ||
| 528 | } | ||
| 529 | files[base+requestName] = raw | ||
| 530 | out = r | ||
| 531 | return nil | ||
| 532 | }, fmt.Sprintf("request: %s closes %d issue(s)", id, len(closes)), actor) | ||
| 533 | if errors.Is(err, ErrNoChange) { | ||
| 534 | return out, nil | ||
| 535 | } | ||
| 536 | return out, err | ||
| 537 | } | ||
| 538 | |||
| 539 | func equalStrings(a, b []string) bool { | ||
| 540 | if len(a) != len(b) { | ||
| 541 | return false | ||
| 542 | } | ||
| 543 | for i := range a { | ||
| 544 | if a[i] != b[i] { | ||
| 545 | return false | ||
| 546 | } | ||
| 547 | } | ||
| 548 | return true | ||
| 549 | } | ||