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.
| 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" |
| 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`. |
| 25 | func (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. |
| 39 | func 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. |
| 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 | |
| 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 { |
| 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. |
| 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 | } |
| 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 |