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.
Read what a page needs once per request
5d440e37f16d06790ce87e2f8b7dcc7d9bf0532c · Keenan Nemetz · 10d ago · parent 34cbc99f
Read what a page needs once per request A page and its header are built from the same facts, and each of them was read separately: the repository (twice, each resolving HEAD's branch with a git call), the open change requests (once for the header's count, once for the page), and a change request looked up by reading every request. On a repository with 30 change requests the overview paid 21 git invocations after the metadata batching, and the requests list still paid 8. Give each read request one memo, installed by the middleware, and have the handlers and the header read through it. It lasts one request and covers reads only: a request that writes metadata gets no memo, so there is no TTL, no cross-request cache, and no invalidation protocol, and a push is still visible on the next page load. The header now takes the page it is rendering (`nav .Page "issues"`) instead of six fields the view already carries, which is also why the memo can reach it at all. Templates are unchanged in what they output.
server/web/page.go +38−14
| @@ -10,14 +10,16 @@ import ( | |||
| 10 | 10 | ||
| 11 | 11 | "github.com/rodent-software/vole/forge/access" | |
| 12 | 12 | "github.com/rodent-software/vole/forge/repo" | |
| 13 | "github.com/rodent-software/vole/metadata" | ||
| 14 | 13 | ) | |
| 15 | 14 | ||
| 16 | 15 | // Page is embedded by every view. Exported so html/template can promote its | |
| 17 | 16 | // fields; an unexported embedded type is not reachable from a template. | |
| 18 | 17 | type Page struct { | |
| 19 | Title string | ||
| 20 | Error string | ||
| 18 | Title string | ||
| 19 | Error string | ||
| 20 | // RepoName is the repository the path names, empty on instance-wide pages. | ||
| 21 | // The header reads it instead of parsing the URL again. | ||
| 22 | RepoName string | ||
| 21 | 23 | LoggedIn bool | |
| 22 | 24 | User string | |
| 23 | 25 | AccountName string | |
| @@ -46,6 +48,10 @@ type Page struct { | |||
| 46 | 48 | // Section is the global destination the current page belongs to, used by the | |
| 47 | 49 | // rail to mark the current item. It is presentation only. | |
| 48 | 50 | Section string | |
| 51 | // memo is what this request has already read. It is unexported because no | ||
| 52 | // template may depend on it: the header only needs it to stop repeating work | ||
| 53 | // the handler did. | ||
| 54 | memo *requestMemo | ||
| 49 | 55 | } | |
| 50 | 56 | ||
| 51 | 57 | // pageSection maps a request path to the global destination it belongs to. A | |
| @@ -84,10 +90,14 @@ func (w *Web) who(name string) string { | |||
| 84 | 90 | // advertise. Visibility comes from the same ACL rule the handlers use, so the | |
| 85 | 91 | // lock/globe icon cannot disagree with whether an anonymous clone works. | |
| 86 | 92 | // Templates call it as | |
| 87 | // {{template "repohead" (nav .Name "issues" .CanAdmin .CloneURL .SSHURL .Site)}}. | ||
| 88 | func (w *Web) repoNav(name, tab string, canAdmin bool, cloneURL, sshURL, site string) repoNavData { | ||
| 89 | n := repoNavData{Name: name, Tab: tab, Private: true, CanAdmin: canAdmin, CloneURL: cloneURL, SSHURL: sshURL, Site: site} | ||
| 90 | m, err := w.Repos.Open(name) | ||
| 93 | // {{template "repohead" (nav .Page "issues")}}. | ||
| 94 | func (w *Web) repoNav(p Page, tab string) repoNavData { | ||
| 95 | n := repoNavData{Name: p.RepoName, Tab: tab, Private: true, CanAdmin: p.CanAdmin, | ||
| 96 | CloneURL: p.CloneURL, SSHURL: p.SSHURL, Site: p.Site} | ||
| 97 | if p.RepoName == "" { | ||
| 98 | return n | ||
| 99 | } | ||
| 100 | m, err := w.repo(p.memo, p.RepoName) | ||
| 91 | 101 | if err != nil { | |
| 92 | 102 | return n | |
| 93 | 103 | } | |
| @@ -95,16 +105,25 @@ func (w *Web) repoNav(name, tab string, canAdmin bool, cloneURL, sshURL, site st | |||
| 95 | 105 | if acl, err := m.ACL(); err == nil { | |
| 96 | 106 | n.Private = !acl.Can("", access.Read) | |
| 97 | 107 | } | |
| 98 | n.OpenIssues, n.OpenRequests = w.openCounts(m) | ||
| 108 | n.OpenIssues, n.OpenRequests = w.openCounts(p.memo, m) | ||
| 99 | 109 | return n | |
| 100 | 110 | } | |
| 101 | 111 | ||
| 102 | 112 | // openCounts returns the counts the repository tabs show. They describe the | |
| 103 | // whole repository, not a filtered page: a badge that changed when you searched | ||
| 104 | // would be a lie. A read that fails leaves a zero rather than failing the page | ||
| 105 | // the reader asked for; the tab still links to the list, which reports its own | ||
| 106 | // error if the same read fails there. | ||
| 107 | func (w *Web) openCounts(m *repo.Meta) (issues, requests int) { | ||
| 113 | // whole repository, not a filtered page: an "open" badge that changes when you | ||
| 114 | // searched would be a lie. A read that fails leaves a zero rather than failing | ||
| 115 | // the page the reader asked for; the tab still links to the list, which reports | ||
| 116 | // its own error if the same read fails there. The counts are remembered for the | ||
| 117 | // rest of the request, because the page asked for the same list. | ||
| 118 | func (w *Web) openCounts(memo *requestMemo, m *repo.Meta) (issues, requests int) { | ||
| 119 | if memo != nil { | ||
| 120 | memo.mu.Lock() | ||
| 121 | c, ok := memo.counts[m.Name] | ||
| 122 | memo.mu.Unlock() | ||
| 123 | if ok { | ||
| 124 | return c.issues, c.requests | ||
| 125 | } | ||
| 126 | } | ||
| 108 | 127 | if w.Index != nil { | |
| 109 | 128 | if err := w.Index.Ensure(m); err == nil { | |
| 110 | 129 | if n, err := w.Index.IssueCount(m.ID, "open"); err == nil { | |
| @@ -112,13 +131,18 @@ func (w *Web) openCounts(m *repo.Meta) (issues, requests int) { | |||
| 112 | 131 | } | |
| 113 | 132 | } | |
| 114 | 133 | } | |
| 115 | if list, err := metadata.LoadRequests(m.Git(), metadata.Ref); err == nil { | ||
| 134 | if list, err := w.requestsAt(memo, m); err == nil { | ||
| 116 | 135 | for _, req := range list { | |
| 117 | 136 | if req.State == "open" { | |
| 118 | 137 | requests++ | |
| 119 | 138 | } | |
| 120 | 139 | } | |
| 121 | 140 | } | |
| 141 | if memo != nil { | ||
| 142 | memo.mu.Lock() | ||
| 143 | memo.counts[m.Name] = memoCounts{issues: issues, requests: requests} | ||
| 144 | memo.mu.Unlock() | ||
| 145 | } | ||
| 122 | 146 | return issues, requests | |
| 123 | 147 | } | |
| 124 | 148 | ||