v0.23.0 333c710

browse_stats.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package usecase
4
5import (
6	"context"
7	"sync"
8	"time"
9
10	"bnema.dev/ermite/internal/domain"
11	"bnema.dev/ermite/internal/ports"
12)
13
14const (
15	// maxStatsEntries bounds the cache of figures; when full it is emptied,
16	// and the entries of the current heads come back on the next read.
17	maxStatsEntries = 1024
18	// statsTTL is how long figures are served: the 30-day window moves
19	// with time even when the repository does not.
20	statsTTL = 10 * time.Minute
21)
22
23// statsKey names the figures of one state of a repository: its life (the
24// creation time), its last push or merge (which every ref change, tags
25// included, moves) and the head counted from.
26type statsKey struct {
27	repo    domain.RepoName
28	created int64
29	pushed  int64
30	head    domain.ObjectID
31}
32
33type statsEntry struct {
34	st  ports.RepoStats
35	at  time.Time
36	err error
37	// done is closed when st and err are set; until then the entry is the
38	// read in flight that the other callers wait for.
39	done chan struct{}
40}
41
42// statsCache keeps the figures of the home page, so that a request reads Git
43// once per repository state and per statsTTL, and concurrent requests share
44// one read.
45type statsCache struct {
46	mu sync.Mutex
47	m  map[statsKey]*statsEntry
48}
49
50// get returns the figures of key, computing them with read when they are
51// missing, failed or older than statsTTL.
52func (c *statsCache) get(ctx context.Context, key statsKey, now time.Time, read func() (ports.RepoStats, error)) (ports.RepoStats, error) {
53	c.mu.Lock()
54	e, ok := c.m[key]
55	if ok {
56		select {
57		case <-e.done:
58			if e.err != nil || now.Sub(e.at) >= statsTTL || now.Before(e.at) {
59				ok = false
60			}
61		default: // in flight
62		}
63	}
64	if !ok {
65		if c.m == nil || len(c.m) >= maxStatsEntries {
66			c.m = make(map[statsKey]*statsEntry)
67		}
68		e = &statsEntry{at: now, done: make(chan struct{})}
69		c.m[key] = e
70		c.mu.Unlock()
71		e.st, e.err = read()
72		close(e.done)
73		return cloneStats(e.st), e.err
74	}
75	c.mu.Unlock()
76	select {
77	case <-e.done:
78		return cloneStats(e.st), e.err
79	case <-ctx.Done():
80		return ports.RepoStats{}, ctx.Err()
81	}
82}
83
84func cloneStats(st ports.RepoStats) ports.RepoStats {
85	if st.LatestTag != nil {
86		t := *st.LatestTag
87		st.LatestTag = &t
88	}
89	return st
90}
91
92// RepoStats counts the commits reachable from head, all of them and those of
93// the last ports.RecentDays days before now, and names the latest tag. The
94// answer is kept for statsTTL, or until the repository is pushed to.
95func (b *Browse) RepoStats(ctx context.Context, p domain.Principal, repo domain.RepoName, head domain.ObjectID, now time.Time) (ports.RepoStats, error) {
96	r, err := b.authorize(ctx, p, repo)
97	if err != nil {
98		return ports.RepoStats{}, err
99	}
100	key := statsKey{repo: r.Name, created: repoEpoch(r), pushed: r.PushedAt.UnixNano(), head: head}
101	return b.stats.get(ctx, key, now, func() (ports.RepoStats, error) {
102		// The read outlives a caller that gives up: the others wait for it.
103		rctx := context.WithoutCancel(ctx)
104		var st ports.RepoStats
105		var err error
106		if st.Commits, err = b.reader.CountCommits(rctx, repo, head, time.Time{}); err != nil {
107			return ports.RepoStats{}, err
108		}
109		since := now.AddDate(0, 0, -ports.RecentDays)
110		if st.Recent, err = b.reader.CountCommits(rctx, repo, head, since); err != nil {
111			return ports.RepoStats{}, err
112		}
113		tag, ok, err := b.reader.LatestTag(rctx, repo)
114		if err != nil {
115			return ports.RepoStats{}, err
116		}
117		if ok {
118			st.LatestTag = &tag
119		}
120		return st, nil
121	})
122}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 45

↑↓ moveenter openesc close