Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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.