v0.22.0 4c072b1

browsecache.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package usecase
4
5import (
6	"container/list"
7	"slices"
8	"sync"
9
10	"bnema.dev/ermite/internal/domain"
11	"bnema.dev/ermite/internal/ports"
12)
13
14// Default bounds of the history cache of Browse.
15const (
16	// DefaultHistoryCacheEntries bounds the cached answers.
17	DefaultHistoryCacheEntries = 256
18	// DefaultHistoryCacheBytes bounds their estimated size.
19	DefaultHistoryCacheBytes = 8 << 20
20)
21
22// historyKey names an answer that depends only on immutable data: a commit
23// designated by its full id, the path inside it, and for a page of history
24// where it starts and how long it is. The repository is part of the key so
25// that an answer is never served across a name, and so is its creation time
26// (in nanoseconds): a repository deleted and created again under the same
27// name is another repository, whose commit ids must not hit the old answers.
28type historyKey struct {
29	repo    domain.RepoName
30	created int64
31	rev     string // full commit id
32	path    string
33	kind    historyKind
34	limit   int
35}
36
37type historyKind uint8
38
39const (
40	kindTreeLast historyKind = iota // a directory with the last commit of its entries
41	kindPage                        // the first page of the history of a path
42)
43
44// historyCache is a small LRU of answers about commits, bounded by entry count
45// and by estimated bytes. The answers are immutable (a commit id fixes its
46// whole history), so there is no invalidation: entries leave by eviction only.
47// It is safe for concurrent use. Values are stored and returned as copies, so
48// a caller cannot alter what another one gets.
49type historyCache struct {
50	maxEntries int
51	maxBytes   int64
52
53	mu    sync.Mutex
54	order *list.List // front is the most recently used
55	items map[historyKey]*list.Element
56	bytes int64
57}
58
59type historyEntry struct {
60	key  historyKey
61	tree domain.Tree
62	page ports.CommitPage
63	size int64
64}
65
66// newHistoryCache returns nil (no cache) unless both bounds are positive.
67func newHistoryCache(maxEntries int, maxBytes int64) *historyCache {
68	if maxEntries <= 0 || maxBytes <= 0 {
69		return nil
70	}
71	return &historyCache{
72		maxEntries: maxEntries, maxBytes: maxBytes,
73		order: list.New(), items: map[historyKey]*list.Element{},
74	}
75}
76
77// get returns the entry of key and marks it recently used.
78func (c *historyCache) get(key historyKey) (*historyEntry, bool) {
79	if c == nil {
80		return nil, false
81	}
82	c.mu.Lock()
83	defer c.mu.Unlock()
84	el, ok := c.items[key]
85	if !ok {
86		return nil, false
87	}
88	c.order.MoveToFront(el)
89	return el.Value.(*historyEntry), true
90}
91
92// put stores e, then evicts the least recently used entries until both bounds
93// hold. An entry larger than the byte bound is not stored.
94func (c *historyCache) put(e *historyEntry) {
95	if c == nil || e.size > c.maxBytes {
96		return
97	}
98	c.mu.Lock()
99	defer c.mu.Unlock()
100	if el, ok := c.items[e.key]; ok {
101		c.bytes -= el.Value.(*historyEntry).size
102		c.order.Remove(el)
103	}
104	c.items[e.key] = c.order.PushFront(e)
105	c.bytes += e.size
106	for c.order.Len() > c.maxEntries || c.bytes > c.maxBytes {
107		oldest := c.order.Back()
108		old := oldest.Value.(*historyEntry)
109		c.order.Remove(oldest)
110		delete(c.items, old.key)
111		c.bytes -= old.size
112	}
113}
114
115// treeEntry builds the entry of a tree; the tree is copied.
116func treeEntry(key historyKey, t domain.Tree) *historyEntry {
117	t = cloneTree(t)
118	size := int64(256 + len(t.Entries)*160)
119	for _, e := range t.Entries {
120		size += int64(len(e.Name) + len(e.Path))
121		if e.LastCommit != nil {
122			size += commitSize(*e.LastCommit)
123		}
124	}
125	return &historyEntry{key: key, tree: t, size: size}
126}
127
128// pageEntry builds the entry of a page of history; the page is copied.
129func pageEntry(key historyKey, p ports.CommitPage) *historyEntry {
130	p = clonePage(p)
131	size := int64(128)
132	for _, c := range p.Commits {
133		size += commitSize(c)
134	}
135	return &historyEntry{key: key, page: p, size: size}
136}
137
138func commitSize(c domain.Commit) int64 {
139	n := 384 + len(c.Parents)*48 + len(c.Summary) + len(c.Message)
140	n += len(c.Author.Name) + len(c.Author.Email) + len(c.Committer.Name) + len(c.Committer.Email)
141	return int64(n)
142}
143
144func cloneCommit(c domain.Commit) domain.Commit {
145	c.Parents = slices.Clone(c.Parents)
146	return c
147}
148
149func cloneTree(t domain.Tree) domain.Tree {
150	t.Entries = slices.Clone(t.Entries)
151	for i := range t.Entries {
152		if lc := t.Entries[i].LastCommit; lc != nil {
153			c := cloneCommit(*lc)
154			t.Entries[i].LastCommit = &c
155		}
156	}
157	return t
158}
159
160func clonePage(p ports.CommitPage) ports.CommitPage {
161	p.Commits = slices.Clone(p.Commits)
162	for i := range p.Commits {
163		p.Commits[i] = cloneCommit(p.Commits[i])
164	}
165	return p
166}

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 44

↑↓ moveenter openesc close