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.

Read the activity feed as a SQL stream

73a732b1b84a308f0739e4c8325e9c2a832097cb · Keenan Nemetz · 10d ago · parent c8cdb6ac

Read the activity feed as a SQL stream

A page of the feed used to be built in memory: every issue, comment,
approval and state change became an event, was sorted, and was then sliced
to fifty rows. The page cost the whole history, and the instance feed did
it for every visible repository.

The sources are rows in the index, so the stream is a UNION over them
(forge/index.Activity and ActivityCount): the database filters, orders and
limits, and only the page's rows cross back into the process. Releases are
the one event source that is not an index row — a tag is a ref — so they
are read from git (one call per repository, only when the feed can show a
release) and merged with the page in time order.

The handlers refresh each repository before reading, because a browser
write never passes through the push hook. The repository feed keeps the
in-memory path as a fallback for a Web without an index. `visibleRepos`
now builds a repository handle without a HEAD lookup, which removes one
git process per repository from the instance activity page too.

The count and the page each scan the union, so the database still sorts
the events rather than reading an index in order; a materialised events
table is the next step if a large history makes that visible. What is
gone is the per-request materialisation of every event in Go, which is
what this follow-up set out to remove.

server/web/activity.go +49−19

@@ -374,6 +374,30 @@ func (w *Web) activityRequests(memo *requestMemo, m *repo.Meta) []activityReques
374 374 return out
375 375 }
376 376
377 // repoActivityURL builds a page link for a repository's feed, keeping the
378 // filters so paging never widens what the reader asked for.
379 func repoActivityURL(name, kind, actor, assignee string) func(int) string {
380 return func(n int) string {
381 v := url.Values{}
382 if kind != "" {
383 v.Set("kind", kind)
384 }
385 if actor != "" {
386 v.Set("actor", actor)
387 }
388 if assignee != "" {
389 v.Set("assignee", assignee)
390 }
391 if n > 1 {
392 v.Set("page", strconv.Itoa(n))
393 }
394 if q := v.Encode(); q != "" {
395 return "/r/" + name + "/activity?" + q
396 }
397 return "/r/" + name + "/activity"
398 }
399 }
400
377 401 // activityHistory is how far back the feed looks for state changes. The feed
378 402 // itself keeps fewer events; this is the window a closed issue has to be seen in.
379 403 const activityHistory = 200
@@ -450,32 +474,38 @@ func (w *Web) Activity(rw http.ResponseWriter, r *http.Request) {
450 474 kind := normalizeActivityKind(r.URL.Query().Get("kind"))
451 475 actor := strings.TrimSpace(r.URL.Query().Get("actor"))
452 476 assignee := strings.TrimSpace(r.URL.Query().Get("assignee"))
453 feed := w.repoActivity(memoOf(r), m, kind, actor, assignee, 0)
454 477 number, _ := strconv.Atoi(r.URL.Query().Get("page"))
455 events, pages := paginate(feed.Events, number, activityPage, func(n int) string {
456 v := url.Values{}
457 if kind != "" {
458 v.Set("kind", kind)
459 }
460 if actor != "" {
461 v.Set("actor", actor)
462 }
463 if assignee != "" {
464 v.Set("assignee", assignee)
465 }
466 if n > 1 {
467 v.Set("page", strconv.Itoa(n))
478 urlFor := repoActivityURL(m.Name, kind, actor, assignee)
479
480 var events []activityEvent
481 var pages pager
482 var assignees []string
483 if w.Index != nil {
484 if err := w.ensureIndex(memoOf(r), m); err != nil {
485 w.serverError(rw, r, "cannot read activity: "+err.Error())
486 return
468 487 }
469 if q := v.Encode(); q != "" {
470 return "/r/" + m.Name + "/activity?" + q
488 var err error
489 events, pages, err = w.activityPage(
490 []string{m.ID}, []*repo.Meta{m}, map[string]string{m.ID: m.Name},
491 kind, actor, assignee, false, number, activityPage, urlFor, "Activity pages")
492 if err != nil {
493 w.serverError(rw, r, "cannot read activity: "+err.Error())
494 return
471 495 }
472 return "/r/" + m.Name + "/activity"
473 }, "Activity pages")
496 assignees, _ = w.Index.Assignees(m.ID)
497 } else {
498 // A Web without an index (a test, or a CLI-shaped Web) builds the feed
499 // in memory, as every page used to.
500 feed := w.repoActivity(memoOf(r), m, kind, actor, assignee, 0)
501 events, pages = paginate(feed.Events, number, activityPage, urlFor, "Activity pages")
502 assignees = feed.Assignees
503 }
474 504 pages.Target = "#activity-feed"
475 505 view := activityView{
476 506 Page: w.page(r, m.Name+" · activity"), Name: m.Name, Kind: kind,
477 507 Actor: actor, Assignee: assignee,
478 Actors: w.activityActors(), Assignees: feed.Assignees, Events: events, Pager: pages,
508 Actors: w.activityActors(), Assignees: assignees, Events: events, Pager: pages,
479 509 }
480 510 if htmxRequest(r) {
481 511 w.render(rw, "activity_feed", view, 0)