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

Blame · server/web/numbers.go

Line attribution on main · 122 line(s)

e05f1c48
Keenan Nemetz
13d ago
1 package web
2
3 // The `#42` alias at the web boundary.
4 //
5 // A number is not stored and not authoritative: it is the creation rank of the
6 // entity in the metadata branch's history (metadata.Numbers). The index keeps a
7 // copy so a page render does not walk history, and Ensure refreshes that copy
8 // whenever the branch moves. Every surface here reads the same table, so the
9 // number on the list page, the number in the URL and the number in a body agree.
10
11 import (
12 "fmt"
13 "net/http"
14 "strconv"
de84c32b
Keenan Nemetz
5d ago
15 "strings"
e05f1c48
Keenan Nemetz
13d ago
16
d37d301f
Keenan Nemetz
15h ago
17 "rodent.software/vole/forge/repo"
18 "rodent.software/vole/metadata"
e05f1c48
Keenan Nemetz
13d ago
19 )
20
21 // entityNumbers returns a repo's numbers, keyed by entity id, refreshing the
22 // derived rows if the metadata branch moved. A repo whose branch cannot be read
23 // gets an empty map: an unnumbered page is better than a failed one, and the
24 // templates render a missing number as `#?` rather than `#0`.
73ec800b
Keenan Nemetz
10d ago
25 func (w *Web) entityNumbers(memo *requestMemo, m *repo.Meta) map[string]int {
26 if err := w.ensureIndex(memo, m); err != nil {
e05f1c48
Keenan Nemetz
13d ago
27 return map[string]int{}
28 }
29 nums, err := w.Index.Numbers(m.ID)
30 if err != nil {
31 return map[string]int{}
32 }
33 return nums
34 }
35
36 // numberLabel renders a number the way every surface writes it. Zero means the
37 // branch did not number the entity, which is a fact worth showing as unknown
38 // rather than as a number nobody assigned.
39 func numberLabel(number int) string {
40 if number < 1 {
41 return "#?"
42 }
43 return "#" + strconv.Itoa(number)
44 }
45
de84c32b
Keenan Nemetz
5d ago
46 // numberedTitle names a page from the part the reader asked for to the place it
47 // lives: the number and title first, then a section when the page is a tab of
6ce635a8
Keenan Nemetz
1d ago
48 // the item, then the repository. The chrome appends the site name, so a
de84c32b
Keenan Nemetz
5d ago
49 // browser tab or a bookmark reads most specific first. An empty section or
50 // repository is left out rather than rendered as a dangling separator.
51 func numberedTitle(number int, title, section, repo string) string {
52 parts := []string{numberLabel(number) + " " + title}
53 if section != "" {
54 parts = append(parts, section)
55 }
56 if repo != "" {
57 parts = append(parts, repo)
58 }
59 return strings.Join(parts, " · ")
60 }
61
e05f1c48
Keenan Nemetz
13d ago
62 // resolveCloses turns `42` and `#42` in a Closes field into the issue's ULID, the
63 // form the file stores. A number that names a change request is refused: a request
64 // cannot close a request, and the author is still looking at the form.
65 func (w *Web) resolveCloses(m *repo.Meta, values []string) ([]string, error) {
66 out := make([]string, 0, len(values))
67 nums := false
68 for _, v := range values {
69 num, ok := metadata.ParseNumber(v)
70 if !ok {
71 if v = metadata.TrimReference(v); v != "" {
72 out = append(out, v)
73 }
74 continue
75 }
76 if !nums {
77 // One refresh for the whole field, not one per number.
78 if err := w.Index.Ensure(m); err != nil {
79 return nil, fmt.Errorf("issue numbering unavailable: %w", err)
80 }
81 nums = true
82 }
83 id, kind, ok := w.Index.ByNumber(m.ID, num)
84 if !ok {
b06184d1
Keenan Nemetz
11d ago
85 return nil, fmt.Errorf("no issue or change is #%d", num)
e05f1c48
Keenan Nemetz
13d ago
86 }
87 if kind != metadata.KindIssue {
b06184d1
Keenan Nemetz
11d ago
88 return nil, fmt.Errorf("#%d is a change, not an issue", num)
e05f1c48
Keenan Nemetz
13d ago
89 }
90 out = append(out, id)
91 }
92 return out, nil
93 }
94
ed03b3d1
Keenan Nemetz
3h ago
95 // NumberAlias is /<name>/n/<number>: one link that works for an issue and for a
e05f1c48
Keenan Nemetz
13d ago
96 // change request. A body can say `#42` before anyone knows which kind it is, and
97 // the redirect resolves the question from the index instead of guessing.
98 func (w *Web) NumberAlias(rw http.ResponseWriter, r *http.Request) {
99 m, ok := w.open(rw, r)
100 if !ok {
101 return
102 }
103 num, ok := metadata.ParseNumber(r.PathValue("num"))
104 if !ok {
105 w.notFound(rw, r)
106 return
107 }
73ec800b
Keenan Nemetz
10d ago
108 if err := w.ensureIndex(memoOf(r), m); err != nil {
e05f1c48
Keenan Nemetz
13d ago
109 w.serverError(rw, r, "number index unavailable: "+err.Error())
110 return
111 }
112 id, kind, ok := w.Index.ByNumber(m.ID, num)
113 if !ok {
114 w.notFound(rw, r)
115 return
116 }
117 section := "issues"
118 if kind == metadata.KindRequest {
8fb7aabf
Keenan Nemetz
3d ago
119 section = "changes"
e05f1c48
Keenan Nemetz
13d ago
120 }
ed03b3d1
Keenan Nemetz
3h ago
121 http.Redirect(rw, r, "/"+m.Name+"/"+section+"/"+id, http.StatusSeeOther)
e05f1c48
Keenan Nemetz
13d ago
122 }