detached 74346a1

feat: add browser constants and fuzzy cache test utilities

Adds DirectionLeft/Right/Up/Down and NewTabTitle constants for workspace navigation, and browser_fuzzy_test for fuzzy search cache testing.
commit
74346a153e77e6e20ec5859c3af271a974823d06
author
bnema · · 2025-10-23 16:47 UTC
committer
bnema · · 2025-10-24 17:33 UTC
parent
2e8f954
signature
GPG Unverified2B82207D370127A0 The key is unknown
2 files · +331 -0
A internal/app/browser/constants.go +19 -0
@@ -0,0 +1,19 @@
1+package browser
2+
3+// Direction constants for pane navigation and splitting
4+const (
5+ DirectionLeft = "left"
6+ DirectionRight = "right"
7+ DirectionUp = "up"
8+ DirectionDown = "down"
9+)
10+
11+// UI string constants
12+const (
13+ NewTabTitle = "New Tab"
14+)
15+
16+// Theme constants
17+const (
18+ ThemeDefault = "default"
19+)
@@ -0,0 +1,19 @@
1+package browser
2+
3+// Direction constants for pane navigation and splitting
4+const (
5+ DirectionLeft = "left"
6+ DirectionRight = "right"
7+ DirectionUp = "up"
8+ DirectionDown = "down"
9+)
10+
11+// UI string constants
12+const (
13+ NewTabTitle = "New Tab"
14+)
15+
16+// Theme constants
17+const (
18+ ThemeDefault = "default"
19+)
A internal/services/browser_fuzzy_test.go +312 -0
@@ -0,0 +1,312 @@
1+package services
2+
3+import (
4+ "context"
5+ "database/sql"
6+ "testing"
7+ "time"
8+
9+ "github.com/bnema/dumber/internal/config"
10+ "github.com/bnema/dumber/internal/db"
11+ mock_db "github.com/bnema/dumber/internal/db/mocks"
12+ "go.uber.org/mock/gomock"
13+)
14+
15+// TestBrowserService_FuzzyCacheIntegration tests the fuzzy cache lifecycle in BrowserService
16+func TestBrowserService_FuzzyCacheIntegration(t *testing.T) {
17+ ctrl := gomock.NewController(t)
18+ defer ctrl.Finish()
19+
20+ mockDB := mock_db.NewMockDatabaseQuerier(ctrl)
21+ ctx := context.Background()
22+
23+ // Mock test history data
24+ testHistory := []db.History{
25+ {
26+ ID: 1,
27+ Url: "https://github.com",
28+ Title: sql.NullString{
29+ String: "GitHub",
30+ Valid: true,
31+ },
32+ VisitCount: sql.NullInt64{Int64: 5, Valid: true},
33+ },
34+ {
35+ ID: 2,
36+ Url: "https://google.com",
37+ Title: sql.NullString{
38+ String: "Google",
39+ Valid: true,
40+ },
41+ VisitCount: sql.NullInt64{Int64: 10, Valid: true},
42+ },
43+ {
44+ ID: 3,
45+ Url: "https://stackoverflow.com",
46+ Title: sql.NullString{
47+ String: "Stack Overflow",
48+ Valid: true,
49+ },
50+ VisitCount: sql.NullInt64{Int64: 3, Valid: true},
51+ },
52+ }
53+
54+ // Expect GetHistory to be called when loading fuzzy cache
55+ mockDB.EXPECT().
56+ GetHistory(gomock.Any(), gomock.Any()).
57+ Return(testHistory, nil).
58+ MinTimes(1)
59+
60+ // Create browser service (fuzzy cache initialized)
61+ cfg := &config.Config{}
62+ service := NewBrowserService(cfg, mockDB)
63+
64+ // Test 1: Verify fuzzy cache was initialized
65+ if service.fuzzyCache == nil {
66+ t.Fatal("Fuzzy cache should be initialized")
67+ }
68+
69+ // Test 2: Load fuzzy cache from DB
70+ err := service.LoadFuzzyCacheFromDB(ctx)
71+ if err != nil {
72+ t.Fatalf("Failed to load fuzzy cache: %v", err)
73+ }
74+
75+ // Test 3: Verify cache has entries
76+ stats := service.fuzzyCache.Stats()
77+ if stats.EntryCount == 0 {
78+ t.Error("Fuzzy cache should have entries after loading")
79+ }
80+
81+ t.Logf("Fuzzy cache loaded: %d entries, %d trigrams", stats.EntryCount, stats.TrigramCount)
82+
83+ // Test 4: Verify refresh tracking is initialized
84+ if service.historySinceRefresh != 0 {
85+ t.Error("historySinceRefresh should start at 0")
86+ }
87+
88+ if service.lastCacheRefresh.IsZero() {
89+ t.Error("lastCacheRefresh should be initialized")
90+ }
91+
92+ // Test 5: Flush all caches (should include fuzzy cache)
93+ err = service.FlushAllCaches(ctx)
94+ if err != nil {
95+ t.Fatalf("Failed to flush caches: %v", err)
96+ }
97+
98+ t.Log("Fuzzy cache integration test passed")
99+}
100+
101+// TestBrowserService_FuzzyCacheSmartRefresh tests the smart refresh logic
102+func TestBrowserService_FuzzyCacheSmartRefresh(t *testing.T) {
103+ ctrl := gomock.NewController(t)
104+ defer ctrl.Finish()
105+
106+ mockDB := mock_db.NewMockDatabaseQuerier(ctrl)
107+ cfg := &config.Config{}
108+
109+ // Expect GetHistory to be called when loading fuzzy cache
110+ mockDB.EXPECT().
111+ GetHistory(gomock.Any(), gomock.Any()).
112+ Return([]db.History{}, nil).
113+ AnyTimes()
114+
115+ service := NewBrowserService(cfg, mockDB)
116+
117+ // Load initial cache
118+ ctx := context.Background()
119+ err := service.LoadFuzzyCacheFromDB(ctx)
120+ if err != nil {
121+ t.Fatalf("Failed to load fuzzy cache: %v", err)
122+ }
123+
124+ // Test 1: Verify initial state
125+ if service.historySinceRefresh != 0 {
126+ t.Error("historySinceRefresh should start at 0")
127+ }
128+
129+ initialRefreshTime := service.lastCacheRefresh
130+
131+ // Test 2: Simulate adding less than threshold entries (< 10)
132+ service.historySinceRefresh = 5
133+ service.lastCacheRefresh = time.Now().Add(-10 * time.Minute) // Old enough
134+
135+ // This should NOT trigger refresh (not enough entries)
136+ // Manually check the condition
137+ const minEntries = 10
138+ const minInterval = 5 * time.Minute
139+
140+ shouldRefresh := service.historySinceRefresh >= minEntries &&
141+ time.Since(service.lastCacheRefresh) >= minInterval
142+
143+ if shouldRefresh {
144+ t.Error("Should not trigger refresh with only 5 entries")
145+ }
146+
147+ // Test 3: Simulate enough entries but too recent
148+ service.historySinceRefresh = 15
149+ service.lastCacheRefresh = time.Now() // Just refreshed
150+
151+ shouldRefresh = service.historySinceRefresh >= minEntries &&
152+ time.Since(service.lastCacheRefresh) >= minInterval
153+
154+ if shouldRefresh {
155+ t.Error("Should not trigger refresh when last refresh was too recent")
156+ }
157+
158+ // Test 4: Simulate conditions that SHOULD trigger refresh
159+ service.historySinceRefresh = 15
160+ service.lastCacheRefresh = time.Now().Add(-10 * time.Minute)
161+
162+ shouldRefresh = service.historySinceRefresh >= minEntries &&
163+ time.Since(service.lastCacheRefresh) >= minInterval
164+
165+ if !shouldRefresh {
166+ t.Error("Should trigger refresh with 15 entries and 10 minutes passed")
167+ }
168+
169+ t.Logf("Smart refresh logic validated: %d entries, %v since refresh, should refresh: %v",
170+ service.historySinceRefresh,
171+ time.Since(service.lastCacheRefresh),
172+ shouldRefresh)
173+
174+ // Test 5: Verify counter would reset after refresh
175+ if shouldRefresh {
176+ // Simulate the reset that happens in the actual code
177+ service.historySinceRefresh = 0
178+ service.lastCacheRefresh = time.Now()
179+
180+ if service.historySinceRefresh != 0 {
181+ t.Error("Counter should reset after refresh")
182+ }
183+ }
184+
185+ // Test 6: Verify old refresh time was updated
186+ if !service.lastCacheRefresh.After(initialRefreshTime) {
187+ t.Error("Refresh time should be updated")
188+ }
189+
190+ t.Log("Smart refresh logic test passed")
191+}
192+
193+// TestBrowserService_FuzzyCacheThresholds tests the threshold constants
194+func TestBrowserService_FuzzyCacheThresholds(t *testing.T) {
195+ // This test documents and validates the threshold values
196+
197+ const minEntries = 10 // From the actual code
198+ const minInterval = 5 * time.Minute // From the actual code
199+
200+ t.Logf("Fuzzy cache refresh thresholds:")
201+ t.Logf(" - Minimum entries: %d", minEntries)
202+ t.Logf(" - Minimum interval: %v", minInterval)
203+
204+ // Test reasonable threshold values
205+ if minEntries < 5 {
206+ t.Error("minEntries too low - will refresh too frequently")
207+ }
208+
209+ if minEntries > 100 {
210+ t.Error("minEntries too high - cache will be stale")
211+ }
212+
213+ if minInterval < 1*time.Minute {
214+ t.Error("minInterval too low - will refresh too frequently")
215+ }
216+
217+ if minInterval > 30*time.Minute {
218+ t.Error("minInterval too high - cache will be stale")
219+ }
220+
221+ // Test that both conditions are required
222+ // This prevents:
223+ // - Refreshing after 1 entry but 10 minutes (would be wasteful)
224+ // - Refreshing after 100 entries but 1 second (would be too frequent)
225+
226+ t.Log("Threshold validation passed - both conditions required for refresh")
227+}
228+
229+// TestBrowserService_FuzzyCacheLoadFromDB tests loading cache at startup
230+func TestBrowserService_FuzzyCacheLoadFromDB(t *testing.T) {
231+ ctrl := gomock.NewController(t)
232+ defer ctrl.Finish()
233+
234+ mockDB := mock_db.NewMockDatabaseQuerier(ctrl)
235+ ctx := context.Background()
236+
237+ // Mock test history entries
238+ testHistory := []db.History{
239+ {
240+ ID: 1,
241+ Url: "https://github.com/user/repo",
242+ Title: sql.NullString{String: "GitHub Repository", Valid: true},
243+ VisitCount: sql.NullInt64{Int64: 10, Valid: true},
244+ },
245+ {
246+ ID: 2,
247+ Url: "https://stackoverflow.com/questions/123",
248+ Title: sql.NullString{String: "Stack Overflow Question", Valid: true},
249+ VisitCount: sql.NullInt64{Int64: 5, Valid: true},
250+ },
251+ {
252+ ID: 3,
253+ Url: "https://golang.org/doc",
254+ Title: sql.NullString{String: "Go Documentation", Valid: true},
255+ VisitCount: sql.NullInt64{Int64: 15, Valid: true},
256+ },
257+ {
258+ ID: 4,
259+ Url: "https://reddit.com/r/golang",
260+ Title: sql.NullString{String: "Golang Subreddit", Valid: true},
261+ VisitCount: sql.NullInt64{Int64: 3, Valid: true},
262+ },
263+ {
264+ ID: 5,
265+ Url: "https://news.ycombinator.com",
266+ Title: sql.NullString{String: "Hacker News", Valid: true},
267+ VisitCount: sql.NullInt64{Int64: 20, Valid: true},
268+ },
269+ }
270+
271+ // Expect GetHistory to be called when loading fuzzy cache
272+ mockDB.EXPECT().
273+ GetHistory(gomock.Any(), gomock.Any()).
274+ Return(testHistory, nil).
275+ MinTimes(1)
276+
277+ // Create service and load cache
278+ cfg := &config.Config{}
279+ service := NewBrowserService(cfg, mockDB)
280+
281+ // Load cache from DB
282+ err := service.LoadFuzzyCacheFromDB(ctx)
283+ if err != nil {
284+ t.Fatalf("Failed to load fuzzy cache from DB: %v", err)
285+ }
286+
287+ // Verify cache has loaded entries
288+ stats := service.fuzzyCache.Stats()
289+ if stats.EntryCount < len(testHistory) {
290+ t.Errorf("Expected at least %d entries, got %d", len(testHistory), stats.EntryCount)
291+ }
292+
293+ if stats.TrigramCount == 0 {
294+ t.Error("Expected trigram index to be built")
295+ }
296+
297+ t.Logf("Loaded fuzzy cache: %d entries, %d trigrams", stats.EntryCount, stats.TrigramCount)
298+
299+ // Test search functionality
300+ result, err := service.fuzzyCache.Search(ctx, "github")
301+ if err != nil {
302+ t.Fatalf("Failed to search fuzzy cache: %v", err)
303+ }
304+
305+ if len(result.Matches) == 0 {
306+ t.Error("Expected to find matches for 'github'")
307+ }
308+
309+ t.Logf("Search for 'github' found %d matches", len(result.Matches))
310+
311+ t.Log("Load from DB test passed")
312+}
@@ -0,0 +1,312 @@
1+package services
2+
3+import (
4+ "context"
5+ "database/sql"
6+ "testing"
7+ "time"
8+
9+ "github.com/bnema/dumber/internal/config"
10+ "github.com/bnema/dumber/internal/db"
11+ mock_db "github.com/bnema/dumber/internal/db/mocks"
12+ "go.uber.org/mock/gomock"
13+)
14+
15+// TestBrowserService_FuzzyCacheIntegration tests the fuzzy cache lifecycle in BrowserService
16+func TestBrowserService_FuzzyCacheIntegration(t *testing.T) {
17+ ctrl := gomock.NewController(t)
18+ defer ctrl.Finish()
19+
20+ mockDB := mock_db.NewMockDatabaseQuerier(ctrl)
21+ ctx := context.Background()
22+
23+ // Mock test history data
24+ testHistory := []db.History{
25+ {
26+ ID: 1,
27+ Url: "https://github.com",
28+ Title: sql.NullString{
29+ String: "GitHub",
30+ Valid: true,
31+ },
32+ VisitCount: sql.NullInt64{Int64: 5, Valid: true},
33+ },
34+ {
35+ ID: 2,
36+ Url: "https://google.com",
37+ Title: sql.NullString{
38+ String: "Google",
39+ Valid: true,
40+ },
41+ VisitCount: sql.NullInt64{Int64: 10, Valid: true},
42+ },
43+ {
44+ ID: 3,
45+ Url: "https://stackoverflow.com",
46+ Title: sql.NullString{
47+ String: "Stack Overflow",
48+ Valid: true,
49+ },
50+ VisitCount: sql.NullInt64{Int64: 3, Valid: true},
51+ },
52+ }
53+
54+ // Expect GetHistory to be called when loading fuzzy cache
55+ mockDB.EXPECT().
56+ GetHistory(gomock.Any(), gomock.Any()).
57+ Return(testHistory, nil).
58+ MinTimes(1)
59+
60+ // Create browser service (fuzzy cache initialized)
61+ cfg := &config.Config{}
62+ service := NewBrowserService(cfg, mockDB)
63+
64+ // Test 1: Verify fuzzy cache was initialized
65+ if service.fuzzyCache == nil {
66+ t.Fatal("Fuzzy cache should be initialized")
67+ }
68+
69+ // Test 2: Load fuzzy cache from DB
70+ err := service.LoadFuzzyCacheFromDB(ctx)
71+ if err != nil {
72+ t.Fatalf("Failed to load fuzzy cache: %v", err)
73+ }
74+
75+ // Test 3: Verify cache has entries
76+ stats := service.fuzzyCache.Stats()
77+ if stats.EntryCount == 0 {
78+ t.Error("Fuzzy cache should have entries after loading")
79+ }
80+
81+ t.Logf("Fuzzy cache loaded: %d entries, %d trigrams", stats.EntryCount, stats.TrigramCount)
82+
83+ // Test 4: Verify refresh tracking is initialized
84+ if service.historySinceRefresh != 0 {
85+ t.Error("historySinceRefresh should start at 0")
86+ }
87+
88+ if service.lastCacheRefresh.IsZero() {
89+ t.Error("lastCacheRefresh should be initialized")
90+ }
91+
92+ // Test 5: Flush all caches (should include fuzzy cache)
93+ err = service.FlushAllCaches(ctx)
94+ if err != nil {
95+ t.Fatalf("Failed to flush caches: %v", err)
96+ }
97+
98+ t.Log("Fuzzy cache integration test passed")
99+}
100+
101+// TestBrowserService_FuzzyCacheSmartRefresh tests the smart refresh logic
102+func TestBrowserService_FuzzyCacheSmartRefresh(t *testing.T) {
103+ ctrl := gomock.NewController(t)
104+ defer ctrl.Finish()
105+
106+ mockDB := mock_db.NewMockDatabaseQuerier(ctrl)
107+ cfg := &config.Config{}
108+
109+ // Expect GetHistory to be called when loading fuzzy cache
110+ mockDB.EXPECT().
111+ GetHistory(gomock.Any(), gomock.Any()).
112+ Return([]db.History{}, nil).
113+ AnyTimes()
114+
115+ service := NewBrowserService(cfg, mockDB)
116+
117+ // Load initial cache
118+ ctx := context.Background()
119+ err := service.LoadFuzzyCacheFromDB(ctx)
120+ if err != nil {
121+ t.Fatalf("Failed to load fuzzy cache: %v", err)
122+ }
123+
124+ // Test 1: Verify initial state
125+ if service.historySinceRefresh != 0 {
126+ t.Error("historySinceRefresh should start at 0")
127+ }
128+
129+ initialRefreshTime := service.lastCacheRefresh
130+
131+ // Test 2: Simulate adding less than threshold entries (< 10)
132+ service.historySinceRefresh = 5
133+ service.lastCacheRefresh = time.Now().Add(-10 * time.Minute) // Old enough
134+
135+ // This should NOT trigger refresh (not enough entries)
136+ // Manually check the condition
137+ const minEntries = 10
138+ const minInterval = 5 * time.Minute
139+
140+ shouldRefresh := service.historySinceRefresh >= minEntries &&
141+ time.Since(service.lastCacheRefresh) >= minInterval
142+
143+ if shouldRefresh {
144+ t.Error("Should not trigger refresh with only 5 entries")
145+ }
146+
147+ // Test 3: Simulate enough entries but too recent
148+ service.historySinceRefresh = 15
149+ service.lastCacheRefresh = time.Now() // Just refreshed
150+
151+ shouldRefresh = service.historySinceRefresh >= minEntries &&
152+ time.Since(service.lastCacheRefresh) >= minInterval
153+
154+ if shouldRefresh {
155+ t.Error("Should not trigger refresh when last refresh was too recent")
156+ }
157+
158+ // Test 4: Simulate conditions that SHOULD trigger refresh
159+ service.historySinceRefresh = 15
160+ service.lastCacheRefresh = time.Now().Add(-10 * time.Minute)
161+
162+ shouldRefresh = service.historySinceRefresh >= minEntries &&
163+ time.Since(service.lastCacheRefresh) >= minInterval
164+
165+ if !shouldRefresh {
166+ t.Error("Should trigger refresh with 15 entries and 10 minutes passed")
167+ }
168+
169+ t.Logf("Smart refresh logic validated: %d entries, %v since refresh, should refresh: %v",
170+ service.historySinceRefresh,
171+ time.Since(service.lastCacheRefresh),
172+ shouldRefresh)
173+
174+ // Test 5: Verify counter would reset after refresh
175+ if shouldRefresh {
176+ // Simulate the reset that happens in the actual code
177+ service.historySinceRefresh = 0
178+ service.lastCacheRefresh = time.Now()
179+
180+ if service.historySinceRefresh != 0 {
181+ t.Error("Counter should reset after refresh")
182+ }
183+ }
184+
185+ // Test 6: Verify old refresh time was updated
186+ if !service.lastCacheRefresh.After(initialRefreshTime) {
187+ t.Error("Refresh time should be updated")
188+ }
189+
190+ t.Log("Smart refresh logic test passed")
191+}
192+
193+// TestBrowserService_FuzzyCacheThresholds tests the threshold constants
194+func TestBrowserService_FuzzyCacheThresholds(t *testing.T) {
195+ // This test documents and validates the threshold values
196+
197+ const minEntries = 10 // From the actual code
198+ const minInterval = 5 * time.Minute // From the actual code
199+
200+ t.Logf("Fuzzy cache refresh thresholds:")
201+ t.Logf(" - Minimum entries: %d", minEntries)
202+ t.Logf(" - Minimum interval: %v", minInterval)
203+
204+ // Test reasonable threshold values
205+ if minEntries < 5 {
206+ t.Error("minEntries too low - will refresh too frequently")
207+ }
208+
209+ if minEntries > 100 {
210+ t.Error("minEntries too high - cache will be stale")
211+ }
212+
213+ if minInterval < 1*time.Minute {
214+ t.Error("minInterval too low - will refresh too frequently")
215+ }
216+
217+ if minInterval > 30*time.Minute {
218+ t.Error("minInterval too high - cache will be stale")
219+ }
220+
221+ // Test that both conditions are required
222+ // This prevents:
223+ // - Refreshing after 1 entry but 10 minutes (would be wasteful)
224+ // - Refreshing after 100 entries but 1 second (would be too frequent)
225+
226+ t.Log("Threshold validation passed - both conditions required for refresh")
227+}
228+
229+// TestBrowserService_FuzzyCacheLoadFromDB tests loading cache at startup
230+func TestBrowserService_FuzzyCacheLoadFromDB(t *testing.T) {
231+ ctrl := gomock.NewController(t)
232+ defer ctrl.Finish()
233+
234+ mockDB := mock_db.NewMockDatabaseQuerier(ctrl)
235+ ctx := context.Background()
236+
237+ // Mock test history entries
238+ testHistory := []db.History{
239+ {
240+ ID: 1,
241+ Url: "https://github.com/user/repo",
242+ Title: sql.NullString{String: "GitHub Repository", Valid: true},
243+ VisitCount: sql.NullInt64{Int64: 10, Valid: true},
244+ },
245+ {
246+ ID: 2,
247+ Url: "https://stackoverflow.com/questions/123",
248+ Title: sql.NullString{String: "Stack Overflow Question", Valid: true},
249+ VisitCount: sql.NullInt64{Int64: 5, Valid: true},
250+ },
251+ {
252+ ID: 3,
253+ Url: "https://golang.org/doc",
254+ Title: sql.NullString{String: "Go Documentation", Valid: true},
255+ VisitCount: sql.NullInt64{Int64: 15, Valid: true},
256+ },
257+ {
258+ ID: 4,
259+ Url: "https://reddit.com/r/golang",
260+ Title: sql.NullString{String: "Golang Subreddit", Valid: true},
261+ VisitCount: sql.NullInt64{Int64: 3, Valid: true},
262+ },
263+ {
264+ ID: 5,
265+ Url: "https://news.ycombinator.com",
266+ Title: sql.NullString{String: "Hacker News", Valid: true},
267+ VisitCount: sql.NullInt64{Int64: 20, Valid: true},
268+ },
269+ }
270+
271+ // Expect GetHistory to be called when loading fuzzy cache
272+ mockDB.EXPECT().
273+ GetHistory(gomock.Any(), gomock.Any()).
274+ Return(testHistory, nil).
275+ MinTimes(1)
276+
277+ // Create service and load cache
278+ cfg := &config.Config{}
279+ service := NewBrowserService(cfg, mockDB)
280+
281+ // Load cache from DB
282+ err := service.LoadFuzzyCacheFromDB(ctx)
283+ if err != nil {
284+ t.Fatalf("Failed to load fuzzy cache from DB: %v", err)
285+ }
286+
287+ // Verify cache has loaded entries
288+ stats := service.fuzzyCache.Stats()
289+ if stats.EntryCount < len(testHistory) {
290+ t.Errorf("Expected at least %d entries, got %d", len(testHistory), stats.EntryCount)
291+ }
292+
293+ if stats.TrigramCount == 0 {
294+ t.Error("Expected trigram index to be built")
295+ }
296+
297+ t.Logf("Loaded fuzzy cache: %d entries, %d trigrams", stats.EntryCount, stats.TrigramCount)
298+
299+ // Test search functionality
300+ result, err := service.fuzzyCache.Search(ctx, "github")
301+ if err != nil {
302+ t.Fatalf("Failed to search fuzzy cache: %v", err)
303+ }
304+
305+ if len(result.Matches) == 0 {
306+ t.Error("Expected to find matches for 'github'")
307+ }
308+
309+ t.Logf("Search for 'github' found %d matches", len(result.Matches))
310+
311+ t.Log("Load from DB test passed")
312+}

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 8

Tags 42

↑↓ moveenter openesc close