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.

Code

395 commit(s) on main

update readme Keenan Nemetz · 38m ago 32290114
history blame raw 3.8 KiB
1package 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
11import (
12 "fmt"
13 "net/http"
14 "strconv"
15 "strings"
16
17 "rodent.software/vole/forge/repo"
18 "rodent.software/vole/metadata"
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`.
25func (w *Web) entityNumbers(memo *requestMemo, m *repo.Meta) map[string]int {
26 if err := w.ensureIndex(memo, m); err != nil {
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.
39func numberLabel(number int) string {
40 if number < 1 {
41 return "#?"
42 }
43 return "#" + strconv.Itoa(number)
44}
45
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
48// the item, then the repository. The chrome appends the site name, so a
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.
51func 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
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.
65func (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 {
85 return nil, fmt.Errorf("no issue or change is #%d", num)
86 }
87 if kind != metadata.KindIssue {
88 return nil, fmt.Errorf("#%d is a change, not an issue", num)
89 }
90 out = append(out, id)
91 }
92 return out, nil
93}
94
95// NumberAlias is /<name>/n/<number>: one link that works for an issue and for a
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.
98func (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 }
108 if err := w.ensureIndex(memoOf(r), m); err != nil {
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 {
119 section = "changes"
120 }
121 http.Redirect(rw, r, "/"+m.Name+"/"+section+"/"+id, http.StatusSeeOther)
122}
123