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.
Record the default branch summary when a push lands
8dc56e4e258dd56e2c20776f80d61dbed4993f31 · Keenan Nemetz · 10d ago · parent 20442b59
Record the default branch summary when a push lands The landing card showed the default branch tip and the language it is mostly made of by running `symbolic-ref`, `rev-parse`, `log -1` and `ls-tree -r` per repository on every home page read. Those facts change only when a push moves the branch, so the push now records them. - `repos` carries head_sha, head_subject, head_author, head_when, language and lang_class, and `Index.Entry` returns them with the rest of the row. - `Store.RefreshHead` recomputes the summary and skips the tree walk when the recorded tip is still the branch tip, so a metadata or tag push costs one rev-parse. It runs from post-receive, from the rebuild, and after the default branch changes in settings. - The language mapping moved to `internal/lang`, shared by the web pages and the summary; `fileClass` and `languageFor` are now one table. - The home cards read the head, language, counts, ACL and newest event time from the index. The home page starts no git process at all; 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 243 to 749 req/s, p50 from 16 ms to 5 ms, and the server runs no git child process for those requests.
server/web/home.go +33−82
| @@ -19,6 +19,7 @@ import ( | |||
| 19 | 19 | "github.com/rodent-software/vole/forge/access" | |
| 20 | 20 | "github.com/rodent-software/vole/forge/index" | |
| 21 | 21 | "github.com/rodent-software/vole/forge/repo" | |
| 22 | "github.com/rodent-software/vole/internal/lang" | ||
| 22 | 23 | ) | |
| 23 | 24 | ||
| 24 | 25 | const ( | |
| @@ -194,76 +195,58 @@ func (w *Web) visibleRepos(r *http.Request, query string) ([]index.Entry, error) | |||
| 194 | 195 | return visible, nil | |
| 195 | 196 | } | |
| 196 | 197 | ||
| 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. | ||
| 198 | // repoCards builds one card per repository from the index: the head summary and | ||
| 199 | // language were recorded when the push landed, the counts and the newest event | ||
| 200 | // time are index reads, and the ACL is the instance's rules. Nothing here starts | ||
| 201 | // a git process. | ||
| 224 | 202 | func (w *Web) repoCards(memo *requestMemo, entries []index.Entry) []repoCard { | |
| 225 | 203 | type item struct { | |
| 226 | entry index.Entry | ||
| 227 | meta *repo.Meta | ||
| 228 | sum repoSummary | ||
| 204 | entry index.Entry | ||
| 205 | issues, changes int | ||
| 206 | acl *access.ACL | ||
| 229 | 207 | } | |
| 230 | 208 | items := make([]item, 0, len(entries)) | |
| 231 | 209 | ids := make([]string, 0, len(entries)) | |
| 232 | 210 | for _, e := range entries { | |
| 233 | m, err := w.repo(memo, e.Name) | ||
| 211 | m, err := w.Repos.Meta(e.Name) | ||
| 234 | 212 | if err != nil { | |
| 235 | 213 | continue | |
| 236 | 214 | } | |
| 237 | items = append(items, item{entry: e, meta: m, sum: w.repoSummary(memo, m)}) | ||
| 215 | it := item{entry: e} | ||
| 216 | if err := w.ensureIndex(memo, m); err == nil { | ||
| 217 | it.issues, _ = w.Index.IssueCount(e.ID, "open") | ||
| 218 | it.changes, _ = w.Index.RequestCount(e.ID, "open") | ||
| 219 | } | ||
| 220 | it.acl, _ = m.ACL() | ||
| 221 | items = append(items, it) | ||
| 238 | 222 | ids = append(ids, e.ID) | |
| 239 | 223 | } | |
| 240 | 224 | updated, _ := w.Index.LastEventTimes(ids) | |
| 241 | 225 | releases, _ := w.Index.LastReleases(ids) | |
| 226 | |||
| 242 | 227 | cards := make([]repoCard, 0, len(items)) | |
| 243 | 228 | for _, it := range items { | |
| 229 | e := it.entry | ||
| 244 | 230 | 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} | ||
| 231 | Name: e.Name, Description: e.Description, Branch: e.DefaultBranch, | ||
| 232 | Private: true, OpenIssues: it.issues, OpenChanges: it.changes, | ||
| 251 | 233 | } | |
| 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) | ||
| 234 | if e.HeadSHA != "" { | ||
| 235 | card.Commit = &git.Commit{ | ||
| 236 | SHA: e.HeadSHA, Subject: e.HeadSubject, Author: e.HeadAuthor, When: e.HeadWhen, | ||
| 255 | 237 | } | |
| 238 | card.Language, card.LangClass = e.Language, e.LangClass | ||
| 256 | 239 | } | |
| 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) | ||
| 240 | if it.acl != nil { | ||
| 241 | card.Private = !it.acl.Can("", access.Read) | ||
| 242 | } | ||
| 243 | if name, ok := releases[e.ID]; ok { | ||
| 244 | card.LastTag = &git.Tag{Name: name} | ||
| 262 | 245 | } | |
| 263 | if t, ok := updated[it.entry.ID]; ok { | ||
| 246 | if t, ok := updated[e.ID]; ok { | ||
| 264 | 247 | card.Updated = t | |
| 265 | } else if it.sum.LastCommit != nil { | ||
| 266 | card.Updated = time.Unix(it.sum.LastCommit.When, 0).UTC() | ||
| 248 | } else if e.HeadSHA != "" { | ||
| 249 | card.Updated = time.Unix(e.HeadWhen, 0).UTC() | ||
| 267 | 250 | } | |
| 268 | 251 | cards = append(cards, card) | |
| 269 | 252 | } | |
| @@ -315,7 +298,7 @@ func (w *Web) repoCardsFromGit(memo *requestMemo, entries []index.Entry) ([]repo | |||
| 315 | 298 | } | |
| 316 | 299 | if feed.LastCommit != nil { | |
| 317 | 300 | if tree, err := m.Git().LsTreeR(feed.LastCommit.SHA); err == nil { | |
| 318 | card.Language, card.LangClass = topLanguage(tree) | ||
| 301 | card.Language, card.LangClass = lang.Top(tree) | ||
| 319 | 302 | } | |
| 320 | 303 | } | |
| 321 | 304 | if acl, err := m.ACL(); err == nil { | |
| @@ -364,38 +347,6 @@ func paginate(events []activityEvent, number, perPage int, urlFor func(int) stri | |||
| 364 | 347 | return events[from:to], p | |
| 365 | 348 | } | |
| 366 | 349 | ||
| 367 | // topLanguage guesses what a repository is mostly made of from the file names in | ||
| 368 | // its head tree. Counting files rather than bytes is deliberate: one generated | ||
| 369 | // binary or lock file should not decide the label, and the card only needs a | ||
| 370 | // hint. Unknown types are ignored unless the repository contains nothing else. | ||
| 371 | func topLanguage(tree []git.Entry) (name, class string) { | ||
| 372 | counts := map[string]int{} | ||
| 373 | sample := map[string]string{} | ||
| 374 | for _, e := range tree { | ||
| 375 | if e.Type != "blob" { | ||
| 376 | continue | ||
| 377 | } | ||
| 378 | lang := languageFor(e.Name) | ||
| 379 | counts[lang]++ | ||
| 380 | if _, ok := sample[lang]; !ok { | ||
| 381 | sample[lang] = e.Name | ||
| 382 | } | ||
| 383 | } | ||
| 384 | best := "" | ||
| 385 | for lang, n := range counts { | ||
| 386 | if lang == "Other" { | ||
| 387 | continue | ||
| 388 | } | ||
| 389 | if best == "" || n > counts[best] || (n == counts[best] && lang < best) { | ||
| 390 | best = lang | ||
| 391 | } | ||
| 392 | } | ||
| 393 | if best == "" { | ||
| 394 | return "", "" | ||
| 395 | } | ||
| 396 | return best, fileClass(sample[best]) | ||
| 397 | } | ||
| 398 | |||
| 399 | 350 | // sortCards puts the repository that changed most recently first. A repository | |
| 400 | 351 | // with no history sorts last, by name, so a fresh instance still has a stable | |
| 401 | 352 | // order. | |