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.
Drive the home rail from the events table
20442b5948ea72c5e84b6cf2ff922f3cc987ac13 · Keenan Nemetz · 10d ago · parent bca9ec24
Drive the home rail from the events table The landing page still built every visible repository's whole feed in memory to show the newest ten events across the instance. It now asks the events table for one bounded page, with the repository names attached, so the rail costs a page instead of the history. The cards are separated from the feed: - counts come from the index (issue and change totals); - the newest event time per card is one grouped query (`LastEventTimes`) instead of deriving it from the feed that was built anyway; - the release chip comes from the newest release event (`LastReleases`) instead of listing tags from git per card; - the head commit and language still come from git, which is the remaining per-repository work on the page. `visibleRepos` now builds handles with `Meta` rather than `Open`, which removes a `symbolic-ref` per repository from the landing page and the instance activity page. The all-git card builder stays as the fallback for a Web without an index. Load harness at concurrency 4 with three repositories: home goes from 178 to 243 req/s, p50 from 21 ms to 16 ms.
server/web/home.go +118−9
| @@ -96,7 +96,15 @@ func (w *Web) IndexPage(rw http.ResponseWriter, r *http.Request) { | |||
| 96 | 96 | } | |
| 97 | 97 | ||
| 98 | 98 | var events []activityEvent | |
| 99 | data.Cards, events = w.repoCards(memoOf(r), entries) | ||
| 99 | memo := memoOf(r) | ||
| 100 | if w.Index != nil { | ||
| 101 | data.Cards = w.repoCards(memo, entries) | ||
| 102 | events = w.homeEvents(memo, entries) | ||
| 103 | } else { | ||
| 104 | // A Web without an index (a test, or a CLI-shaped Web) builds the cards | ||
| 105 | // and the rail from git, as every page used to. | ||
| 106 | data.Cards, events = w.repoCardsFromGit(memo, entries) | ||
| 107 | } | ||
| 100 | 108 | if query != "" { | |
| 101 | 109 | sort.SliceStable(data.Cards, func(i, j int) bool { return data.Cards[i].Name < data.Cards[j].Name }) | |
| 102 | 110 | } else { | |
| @@ -167,7 +175,9 @@ func (w *Web) visibleRepos(r *http.Request, query string) ([]index.Entry, error) | |||
| 167 | 175 | needle := strings.ToLower(strings.TrimSpace(query)) | |
| 168 | 176 | var visible []index.Entry | |
| 169 | 177 | for _, e := range entries { | |
| 170 | m, err := w.repo(memoOf(r), e.Name) | ||
| 178 | // Meta rather than Open: visibility needs the id and the ACL, not the | ||
| 179 | // default branch, so do not spend a git call per repository on it. | ||
| 180 | m, err := w.Repos.Meta(e.Name) | ||
| 171 | 181 | if err != nil { | |
| 172 | 182 | continue | |
| 173 | 183 | } | |
| @@ -184,10 +194,112 @@ func (w *Web) visibleRepos(r *http.Request, query string) ([]index.Entry, error) | |||
| 184 | 194 | return visible, nil | |
| 185 | 195 | } | |
| 186 | 196 | ||
| 187 | // repoCards reads each repository once and returns its card plus its events with | ||
| 188 | // the repository named on them. It replaces the separate card and feed passes so | ||
| 189 | // the landing page does not walk each repository twice. | ||
| 190 | func (w *Web) repoCards(memo *requestMemo, entries []index.Entry) ([]repoCard, []activityEvent) { | ||
| 197 | // repoSummary is the card's data: the counts the index knows and the history the | ||
| 198 | // card shows. The rail's events are a separate query, so building a card does not | ||
| 199 | // walk the repository's issues and comments. | ||
| 200 | type repoSummary struct { | ||
| 201 | LastCommit *git.Commit | ||
| 202 | OpenIssues int | ||
| 203 | OpenChanges int | ||
| 204 | } | ||
| 205 | |||
| 206 | func (w *Web) repoSummary(memo *requestMemo, m *repo.Meta) repoSummary { | ||
| 207 | var s repoSummary | ||
| 208 | if err := w.ensureIndex(memo, m); err == nil { | ||
| 209 | s.OpenIssues, _ = w.Index.IssueCount(m.ID, "open") | ||
| 210 | s.OpenChanges, _ = w.Index.RequestCount(m.ID, "open") | ||
| 211 | } | ||
| 212 | g := m.Git() | ||
| 213 | if commit, _ := g.RevParse(m.DefaultBranch); commit != "" { | ||
| 214 | if commits, err := g.Log(commit, 1); err == nil && len(commits) > 0 { | ||
| 215 | s.LastCommit = &commits[0] | ||
| 216 | } | ||
| 217 | } | ||
| 218 | return s | ||
| 219 | } | ||
| 220 | |||
| 221 | // repoCards builds one card per repository from the index and the repository's | ||
| 222 | // head. The newest event time comes from the events table for the whole page in | ||
| 223 | // one grouped query. | ||
| 224 | func (w *Web) repoCards(memo *requestMemo, entries []index.Entry) []repoCard { | ||
| 225 | type item struct { | ||
| 226 | entry index.Entry | ||
| 227 | meta *repo.Meta | ||
| 228 | sum repoSummary | ||
| 229 | } | ||
| 230 | items := make([]item, 0, len(entries)) | ||
| 231 | ids := make([]string, 0, len(entries)) | ||
| 232 | for _, e := range entries { | ||
| 233 | m, err := w.repo(memo, e.Name) | ||
| 234 | if err != nil { | ||
| 235 | continue | ||
| 236 | } | ||
| 237 | items = append(items, item{entry: e, meta: m, sum: w.repoSummary(memo, m)}) | ||
| 238 | ids = append(ids, e.ID) | ||
| 239 | } | ||
| 240 | updated, _ := w.Index.LastEventTimes(ids) | ||
| 241 | releases, _ := w.Index.LastReleases(ids) | ||
| 242 | cards := make([]repoCard, 0, len(items)) | ||
| 243 | for _, it := range items { | ||
| 244 | card := repoCard{ | ||
| 245 | Name: it.entry.Name, Description: it.entry.Description, Branch: it.meta.DefaultBranch, | ||
| 246 | Private: true, Commit: it.sum.LastCommit, | ||
| 247 | OpenIssues: it.sum.OpenIssues, OpenChanges: it.sum.OpenChanges, | ||
| 248 | } | ||
| 249 | if name, ok := releases[it.entry.ID]; ok { | ||
| 250 | card.LastTag = &git.Tag{Name: name} | ||
| 251 | } | ||
| 252 | if it.sum.LastCommit != nil { | ||
| 253 | if tree, err := it.meta.Git().LsTreeR(it.sum.LastCommit.SHA); err == nil { | ||
| 254 | card.Language, card.LangClass = topLanguage(tree) | ||
| 255 | } | ||
| 256 | } | ||
| 257 | // Visibility is the same ACL rule the repository header uses, so the | ||
| 258 | // lock/globe on a card cannot disagree with whether an anonymous clone | ||
| 259 | // works. | ||
| 260 | if acl, err := it.meta.ACL(); err == nil { | ||
| 261 | card.Private = !acl.Can("", access.Read) | ||
| 262 | } | ||
| 263 | if t, ok := updated[it.entry.ID]; ok { | ||
| 264 | card.Updated = t | ||
| 265 | } else if it.sum.LastCommit != nil { | ||
| 266 | card.Updated = time.Unix(it.sum.LastCommit.When, 0).UTC() | ||
| 267 | } | ||
| 268 | cards = append(cards, card) | ||
| 269 | } | ||
| 270 | return cards | ||
| 271 | } | ||
| 272 | |||
| 273 | // homeEvents is the landing page rail: the newest events across every visible | ||
| 274 | // repository, one events-table query, bounded to what fits in the rail. | ||
| 275 | func (w *Web) homeEvents(memo *requestMemo, entries []index.Entry) []activityEvent { | ||
| 276 | repoIDs := make([]string, 0, len(entries)) | ||
| 277 | names := make(map[string]string, len(entries)) | ||
| 278 | for _, e := range entries { | ||
| 279 | m, err := w.Repos.Meta(e.Name) | ||
| 280 | if err != nil { | ||
| 281 | continue | ||
| 282 | } | ||
| 283 | if err := w.ensureIndex(memo, m); err != nil { | ||
| 284 | continue | ||
| 285 | } | ||
| 286 | repoIDs = append(repoIDs, e.ID) | ||
| 287 | names[e.ID] = e.Name | ||
| 288 | } | ||
| 289 | rows, err := w.Index.Events(repoIDs, "", "", "", homeEventLimit, 0) | ||
| 290 | if err != nil { | ||
| 291 | return nil | ||
| 292 | } | ||
| 293 | out := make([]activityEvent, 0, len(rows)) | ||
| 294 | for _, row := range rows { | ||
| 295 | out = append(out, w.activityEventFromRow(row, names[row.RepoID], true)) | ||
| 296 | } | ||
| 297 | return out | ||
| 298 | } | ||
| 299 | |||
| 300 | // repoCardsFromGit is the fallback for a Web without an index: one pass per | ||
| 301 | // repository builds the card and its events from git. | ||
| 302 | func (w *Web) repoCardsFromGit(memo *requestMemo, entries []index.Entry) ([]repoCard, []activityEvent) { | ||
| 191 | 303 | cards := make([]repoCard, 0, len(entries)) | |
| 192 | 304 | var events []activityEvent | |
| 193 | 305 | for _, e := range entries { | |
| @@ -206,9 +318,6 @@ func (w *Web) repoCards(memo *requestMemo, entries []index.Entry) ([]repoCard, [ | |||
| 206 | 318 | card.Language, card.LangClass = topLanguage(tree) | |
| 207 | 319 | } | |
| 208 | 320 | } | |
| 209 | // Visibility is the same ACL rule the repository header uses, so the | ||
| 210 | // lock/globe on a card cannot disagree with whether an anonymous clone | ||
| 211 | // works. | ||
| 212 | 321 | if acl, err := m.ACL(); err == nil { | |
| 213 | 322 | card.Private = !acl.Can("", access.Read) | |
| 214 | 323 | } | |