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.

Number issues and change requests for human references

e05f1c48d4ee55635cd7b77889f0545137198106 · Keenan Nemetz · 13d ago · parent 41d1fee9

Number issues and change requests for human references

A ULID is an unreadable handle, and the 8-character prefix the list showed
is mostly timestamp: this repository's 27 issues shared seven distinct
six-character prefixes and two pairs already collided at eight. The part
people actually type carried almost no information.

Issues and change requests now share one gap-free sequence (#1, #42, ...)
derived from the creation order in the metadata branch's history, the way
one forge counts both in a single run.

The number is not stored. It is the rank of the entity's creation commit,
so nothing is written when an entity is created, a repository needs no
backfill, the same history gives the same numbers on every clone, and
`vole admin index rebuild` reproduces them from git. A ULID stays the
identity: paths, front matter, `closes:` and audit entries are unchanged,
and data/index.db only caches the mapping so a page render does not walk
history.

Every id-taking command accepts a number, `#42` links in a body through a
kind-agnostic /r/<repo>/n/42 route, and --closes resolves an alias to the
ULID before writing.

Closes #45

cmd/vole/change.go +67−8

@@ -28,8 +28,10 @@ func changeUsage() string {
28 28 merge <id>
29 29 close|reopen <id>
30 30
31 --no-push commits locally without pushing; -- ends option parsing. A comment is
32 yours to edit or delete; an admin may repair somebody else's, audited.`
31 --no-push commits locally without pushing; -- ends option parsing. An id takes a
32 prefix or the #42 number: numbers are one sequence shared with issues, oldest
33 first. --closes takes an issue id or its number. A comment is yours to edit or
34 delete; an admin may repair somebody else's, audited.`
33 35 }
34 36
35 37 // cmdChange implements the change-request commands. A request is a promise to
@@ -146,7 +148,9 @@ func cmdChange(args []string) error {
146 148 // A linked issue is a promise that landing this request closes it, so the ids
147 149 // are checked when the request is written rather than at the merge: a typo
148 150 // should fail while the author is still looking at the command.
149 r.Closes = closesFlag(f.all("closes"))
151 if r.Closes, err = resolveCloses(c, f.all("closes")); err != nil {
152 return err
153 }
150 154 reapply := func() error { return metadata.CreateRequest(c.Repo, r, author) }
151 155 if err := reapply(); err != nil {
152 156 return err
@@ -158,8 +162,14 @@ func cmdChange(args []string) error {
158 162 for _, id := range r.Closes {
159 163 fmt.Fprintf(os.Stderr, "closes %s when merged\n", id)
160 164 }
165 label := ""
166 if nums, nerr := c.Numbers(); nerr == nil {
167 label = " as " + numberLabel(nums, r.ID)
168 }
161 169 if u := who.Page("requests", r.ID, ""); u != "" {
162 fmt.Fprintf(os.Stderr, "opened at %s\n", u)
170 fmt.Fprintf(os.Stderr, "opened%s at %s\n", label, u)
171 } else {
172 fmt.Fprintf(os.Stderr, "opened%s\n", label)
163 173 }
164 174 return nil
165 175
@@ -280,6 +290,10 @@ func cmdChange(args []string) error {
280 290 return usagef("--state must be open, closed or all")
281 291 }
282 292 format := f.one("format")
293 nums, err := c.Numbers()
294 if err != nil {
295 return err
296 }
283 297 n := 0
284 298 for _, r := range list {
285 299 if want != "" && want != "all" && r.State != want {
@@ -288,10 +302,10 @@ func cmdChange(args []string) error {
288 302 n++
289 303 switch format {
290 304 case "json":
291 fmt.Printf(`{"id":%q,"title":%q,"state":%q,"base":%q,"head":%q,"author":%q,"approvals":%d}`+"\n",
292 r.ID, r.Title, r.State, r.Base, r.Head, r.Author, len(r.Approvals))
305 fmt.Printf(`{"id":%q,"number":%d,"title":%q,"state":%q,"base":%q,"head":%q,"author":%q,"approvals":%d}`+"\n",
306 r.ID, numberOf(nums, r.ID), r.Title, r.State, r.Base, r.Head, r.Author, len(r.Approvals))
293 307 default:
294 fmt.Printf("%s\t%s\t%s\t%d/%d\t%s\n", r.ID, r.State, r.Base, r.CurrentApprovals(), 1, r.Title)
308 fmt.Printf("%s\t%s\t%s\t%s\t%d/%d\t%s\n", numberLabel(nums, r.ID), r.ID, r.State, r.Base, r.CurrentApprovals(), 1, r.Title)
295 309 }
296 310 }
297 311 if n == 0 {
@@ -307,6 +321,11 @@ func cmdChange(args []string) error {
307 321 if err != nil {
308 322 return err
309 323 }
324 nums, err := c.Numbers()
325 if err != nil {
326 return err
327 }
328 fmt.Printf("number: %s\n", numberLabel(nums, r.ID))
310 329 fmt.Printf("id: %s\n", r.ID)
311 330 fmt.Printf("title: %s\n", r.Title)
312 331 fmt.Printf("state: %s\n", r.State)
@@ -379,7 +398,9 @@ func cmdChange(args []string) error {
379 398 case f.has("no-closes"):
380 399 closes, linksChanged = nil, len(r.Closes) > 0
381 400 case f.given("closes"):
382 closes = closesFlag(f.all("closes"))
401 if closes, err = resolveCloses(c, f.all("closes")); err != nil {
402 return err
403 }
383 404 linksChanged = !equalIDs(closes, r.Closes)
384 405 }
385 406 if !baseChanged && !headMoved && !linksChanged {
@@ -523,6 +544,44 @@ func closesFlag(values []string) []string {
523 544 return out
524 545 }
525 546
547 // resolveCloses turns `#42` and `42` into the issue's ULID before the link is
548 // written. A number that names a change request is refused here rather than at the
549 // merge: a request cannot close a request, and the author is still looking.
550 func resolveCloses(c *client.Branch, values []string) ([]string, error) {
551 ids := closesFlag(values)
552 var nums *metadata.Numbers
553 out := make([]string, 0, len(ids))
554 for _, id := range ids {
555 num, ok := metadata.ParseNumber(id)
556 if !ok {
557 out = append(out, metadata.TrimReference(id))
558 continue
559 }
560 if nums == nil {
561 var err error
562 if nums, err = c.Numbers(); err != nil {
563 return nil, err
564 }
565 }
566 found, kind, ok := nums.At(num)
567 if !ok {
568 return nil, fmt.Errorf("no issue or change request is #%d", num)
569 }
570 if kind != metadata.KindIssue {
571 return nil, fmt.Errorf("#%d is a change request, not an issue: %s", num, found)
572 }
573 out = append(out, found)
574 }
575 return out, nil
576 }
577
578 // numberOf is the alias as a number, for the JSON form: 0 says the branch has not
579 // numbered this entity, which is what a consumer can test without parsing `#?`.
580 func numberOf(nums *metadata.Numbers, id string) int {
581 num, _ := nums.Of(id)
582 return num
583 }
584
526 585 // equalIDs compares two id lists as sets after normalisation, which is what deciding
527 586 // "did the links change" needs: order and duplicates are not edits.
528 587 func equalIDs(a, b []string) bool {