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