v0.26.2 3cd9d2e

lazy_repos.go RawHistoryFolder

1// Package sqlite provides SQLite implementations of domain repositories.
2//
3// # Lazy Repository Infrastructure
4//
5// This file contains lazy-loading repository wrappers that defer database initialization
6// until first access. While currently unused (the application uses eager initialization
7// via OpenDatabase in RunParallelDBWebKit), this infrastructure is kept for potential
8// future optimization scenarios:
9//
10//   - CLI commands that may not need database access
11//   - Further startup latency reduction by deferring DB init past first paint
12//   - Testing with mock DatabaseProvider implementations
13//
14// The lazy wrappers implement the same repository interfaces as their eager counterparts,
15// making them drop-in replacements when lazy initialization becomes beneficial.
16package sqlite
17
18import (
19	"context"
20	"database/sql"
21	"sync"
22	"time"
23
24	"github.com/bnema/dumber/internal/application/port"
25	"github.com/bnema/dumber/internal/domain/entity"
26	"github.com/bnema/dumber/internal/domain/repository"
27)
28
29// LazyHistoryRepository wraps a history repository with lazy database initialization.
30type LazyHistoryRepository struct {
31	provider port.DatabaseProvider
32	repo     repository.HistoryRepository
33	once     sync.Once
34	initErr  error
35}
36
37// NewLazyHistoryRepository creates a lazy-loading history repository.
38func NewLazyHistoryRepository(provider port.DatabaseProvider) repository.HistoryRepository {
39	return &LazyHistoryRepository{provider: provider}
40}
41
42func (r *LazyHistoryRepository) init(ctx context.Context) error {
43	r.once.Do(func() {
44		db, err := r.provider.DB(ctx)
45		if err != nil {
46			r.initErr = err
47			return
48		}
49		r.repo = NewHistoryRepository(db)
50	})
51	return r.initErr
52}
53
54func (r *LazyHistoryRepository) Save(ctx context.Context, entry *entity.HistoryEntry) error {
55	if err := r.init(ctx); err != nil {
56		return err
57	}
58	return r.repo.Save(ctx, entry)
59}
60
61func (r *LazyHistoryRepository) FindByURL(ctx context.Context, url string) (*entity.HistoryEntry, error) {
62	if err := r.init(ctx); err != nil {
63		return nil, err
64	}
65	return r.repo.FindByURL(ctx, url)
66}
67
68func (r *LazyHistoryRepository) Search(ctx context.Context, query string, limit int) ([]entity.HistoryMatch, error) {
69	if err := r.init(ctx); err != nil {
70		return nil, err
71	}
72	return r.repo.Search(ctx, query, limit)
73}
74
75func (r *LazyHistoryRepository) GetRecent(ctx context.Context, limit, offset int) ([]*entity.HistoryEntry, error) {
76	if err := r.init(ctx); err != nil {
77		return nil, err
78	}
79	return r.repo.GetRecent(ctx, limit, offset)
80}
81
82func (r *LazyHistoryRepository) GetRecentSince(ctx context.Context, days int) ([]*entity.HistoryEntry, error) {
83	if err := r.init(ctx); err != nil {
84		return nil, err
85	}
86	return r.repo.GetRecentSince(ctx, days)
87}
88
89func (r *LazyHistoryRepository) GetMostVisited(ctx context.Context, days int) ([]*entity.HistoryEntry, error) {
90	if err := r.init(ctx); err != nil {
91		return nil, err
92	}
93	return r.repo.GetMostVisited(ctx, days)
94}
95
96func (r *LazyHistoryRepository) GetAllRecentHistory(ctx context.Context) ([]*entity.HistoryEntry, error) {
97	if err := r.init(ctx); err != nil {
98		return nil, err
99	}
100	return r.repo.GetAllRecentHistory(ctx)
101}
102
103func (r *LazyHistoryRepository) GetAllMostVisited(ctx context.Context) ([]*entity.HistoryEntry, error) {
104	if err := r.init(ctx); err != nil {
105		return nil, err
106	}
107	return r.repo.GetAllMostVisited(ctx)
108}
109
110func (r *LazyHistoryRepository) IncrementVisitCount(ctx context.Context, url string) error {
111	if err := r.init(ctx); err != nil {
112		return err
113	}
114	return r.repo.IncrementVisitCount(ctx, url)
115}
116
117func (r *LazyHistoryRepository) Delete(ctx context.Context, id int64) error {
118	if err := r.init(ctx); err != nil {
119		return err
120	}
121	return r.repo.Delete(ctx, id)
122}
123
124func (r *LazyHistoryRepository) DeleteOlderThan(ctx context.Context, before time.Time) error {
125	if err := r.init(ctx); err != nil {
126		return err
127	}
128	return r.repo.DeleteOlderThan(ctx, before)
129}
130
131func (r *LazyHistoryRepository) DeleteAll(ctx context.Context) error {
132	if err := r.init(ctx); err != nil {
133		return err
134	}
135	return r.repo.DeleteAll(ctx)
136}
137
138func (r *LazyHistoryRepository) DeleteByDomain(ctx context.Context, domain string) error {
139	if err := r.init(ctx); err != nil {
140		return err
141	}
142	return r.repo.DeleteByDomain(ctx, domain)
143}
144
145func (r *LazyHistoryRepository) GetStats(ctx context.Context) (*entity.HistoryStats, error) {
146	if err := r.init(ctx); err != nil {
147		return nil, err
148	}
149	return r.repo.GetStats(ctx)
150}
151
152func (r *LazyHistoryRepository) GetDomainStats(ctx context.Context, limit int) ([]*entity.DomainStat, error) {
153	if err := r.init(ctx); err != nil {
154		return nil, err
155	}
156	return r.repo.GetDomainStats(ctx, limit)
157}
158
159func (r *LazyHistoryRepository) GetHourlyDistribution(ctx context.Context) ([]*entity.HourlyDistribution, error) {
160	if err := r.init(ctx); err != nil {
161		return nil, err
162	}
163	return r.repo.GetHourlyDistribution(ctx)
164}
165
166func (r *LazyHistoryRepository) GetDailyVisitCount(ctx context.Context, daysAgo string) ([]*entity.DailyVisitCount, error) {
167	if err := r.init(ctx); err != nil {
168		return nil, err
169	}
170	return r.repo.GetDailyVisitCount(ctx, daysAgo)
171}
172
173// LazyFavoriteRepository wraps a favorite repository with lazy database initialization.
174type LazyFavoriteRepository struct {
175	provider port.DatabaseProvider
176	repo     repository.FavoriteRepository
177	once     sync.Once
178	initErr  error
179}
180
181// NewLazyFavoriteRepository creates a lazy-loading favorite repository.
182func NewLazyFavoriteRepository(provider port.DatabaseProvider) repository.FavoriteRepository {
183	return &LazyFavoriteRepository{provider: provider}
184}
185
186func (r *LazyFavoriteRepository) init(ctx context.Context) error {
187	r.once.Do(func() {
188		db, err := r.provider.DB(ctx)
189		if err != nil {
190			r.initErr = err
191			return
192		}
193		r.repo = NewFavoriteRepository(db)
194	})
195	return r.initErr
196}
197
198func (r *LazyFavoriteRepository) Save(ctx context.Context, fav *entity.Favorite) error {
199	if err := r.init(ctx); err != nil {
200		return err
201	}
202	return r.repo.Save(ctx, fav)
203}
204
205func (r *LazyFavoriteRepository) FindByID(ctx context.Context, id entity.FavoriteID) (*entity.Favorite, error) {
206	if err := r.init(ctx); err != nil {
207		return nil, err
208	}
209	return r.repo.FindByID(ctx, id)
210}
211
212func (r *LazyFavoriteRepository) FindByURL(ctx context.Context, url string) (*entity.Favorite, error) {
213	if err := r.init(ctx); err != nil {
214		return nil, err
215	}
216	return r.repo.FindByURL(ctx, url)
217}
218
219func (r *LazyFavoriteRepository) GetAll(ctx context.Context) ([]*entity.Favorite, error) {
220	if err := r.init(ctx); err != nil {
221		return nil, err
222	}
223	return r.repo.GetAll(ctx)
224}
225
226func (r *LazyFavoriteRepository) GetByFolder(ctx context.Context, folderID *entity.FolderID) ([]*entity.Favorite, error) {
227	if err := r.init(ctx); err != nil {
228		return nil, err
229	}
230	return r.repo.GetByFolder(ctx, folderID)
231}
232
233func (r *LazyFavoriteRepository) GetByShortcut(ctx context.Context, key int) (*entity.Favorite, error) {
234	if err := r.init(ctx); err != nil {
235		return nil, err
236	}
237	return r.repo.GetByShortcut(ctx, key)
238}
239
240func (r *LazyFavoriteRepository) UpdatePosition(ctx context.Context, id entity.FavoriteID, position int) error {
241	if err := r.init(ctx); err != nil {
242		return err
243	}
244	return r.repo.UpdatePosition(ctx, id, position)
245}
246
247func (r *LazyFavoriteRepository) SetFolder(ctx context.Context, id entity.FavoriteID, folderID *entity.FolderID) error {
248	if err := r.init(ctx); err != nil {
249		return err
250	}
251	return r.repo.SetFolder(ctx, id, folderID)
252}
253
254func (r *LazyFavoriteRepository) SetShortcut(ctx context.Context, id entity.FavoriteID, key *int) error {
255	if err := r.init(ctx); err != nil {
256		return err
257	}
258	return r.repo.SetShortcut(ctx, id, key)
259}
260
261func (r *LazyFavoriteRepository) Delete(ctx context.Context, id entity.FavoriteID) error {
262	if err := r.init(ctx); err != nil {
263		return err
264	}
265	return r.repo.Delete(ctx, id)
266}
267
268// LazyFolderRepository wraps a folder repository with lazy database initialization.
269type LazyFolderRepository struct {
270	provider port.DatabaseProvider
271	repo     repository.FolderRepository
272	once     sync.Once
273	initErr  error
274}
275
276// NewLazyFolderRepository creates a lazy-loading folder repository.
277func NewLazyFolderRepository(provider port.DatabaseProvider) repository.FolderRepository {
278	return &LazyFolderRepository{provider: provider}
279}
280
281func (r *LazyFolderRepository) init(ctx context.Context) error {
282	r.once.Do(func() {
283		db, err := r.provider.DB(ctx)
284		if err != nil {
285			r.initErr = err
286			return
287		}
288		r.repo = NewFolderRepository(db)
289	})
290	return r.initErr
291}
292
293func (r *LazyFolderRepository) Save(ctx context.Context, folder *entity.Folder) error {
294	if err := r.init(ctx); err != nil {
295		return err
296	}
297	return r.repo.Save(ctx, folder)
298}
299
300func (r *LazyFolderRepository) FindByID(ctx context.Context, id entity.FolderID) (*entity.Folder, error) {
301	if err := r.init(ctx); err != nil {
302		return nil, err
303	}
304	return r.repo.FindByID(ctx, id)
305}
306
307func (r *LazyFolderRepository) GetAll(ctx context.Context) ([]*entity.Folder, error) {
308	if err := r.init(ctx); err != nil {
309		return nil, err
310	}
311	return r.repo.GetAll(ctx)
312}
313
314func (r *LazyFolderRepository) GetChildren(ctx context.Context, parentID *entity.FolderID) ([]*entity.Folder, error) {
315	if err := r.init(ctx); err != nil {
316		return nil, err
317	}
318	return r.repo.GetChildren(ctx, parentID)
319}
320
321func (r *LazyFolderRepository) UpdatePosition(ctx context.Context, id entity.FolderID, position int) error {
322	if err := r.init(ctx); err != nil {
323		return err
324	}
325	return r.repo.UpdatePosition(ctx, id, position)
326}
327
328func (r *LazyFolderRepository) Delete(ctx context.Context, id entity.FolderID) error {
329	if err := r.init(ctx); err != nil {
330		return err
331	}
332	return r.repo.Delete(ctx, id)
333}
334
335// LazyTagRepository wraps a tag repository with lazy database initialization.
336type LazyTagRepository struct {
337	provider port.DatabaseProvider
338	repo     repository.TagRepository
339	once     sync.Once
340	initErr  error
341}
342
343// NewLazyTagRepository creates a lazy-loading tag repository.
344func NewLazyTagRepository(provider port.DatabaseProvider) repository.TagRepository {
345	return &LazyTagRepository{provider: provider}
346}
347
348func (r *LazyTagRepository) init(ctx context.Context) error {
349	r.once.Do(func() {
350		db, err := r.provider.DB(ctx)
351		if err != nil {
352			r.initErr = err
353			return
354		}
355		r.repo = NewTagRepository(db)
356	})
357	return r.initErr
358}
359
360func (r *LazyTagRepository) Save(ctx context.Context, tag *entity.Tag) error {
361	if err := r.init(ctx); err != nil {
362		return err
363	}
364	return r.repo.Save(ctx, tag)
365}
366
367func (r *LazyTagRepository) FindByID(ctx context.Context, id entity.TagID) (*entity.Tag, error) {
368	if err := r.init(ctx); err != nil {
369		return nil, err
370	}
371	return r.repo.FindByID(ctx, id)
372}
373
374func (r *LazyTagRepository) FindByName(ctx context.Context, name string) (*entity.Tag, error) {
375	if err := r.init(ctx); err != nil {
376		return nil, err
377	}
378	return r.repo.FindByName(ctx, name)
379}
380
381func (r *LazyTagRepository) GetAll(ctx context.Context) ([]*entity.Tag, error) {
382	if err := r.init(ctx); err != nil {
383		return nil, err
384	}
385	return r.repo.GetAll(ctx)
386}
387
388func (r *LazyTagRepository) AssignToFavorite(ctx context.Context, tagID entity.TagID, favID entity.FavoriteID) error {
389	if err := r.init(ctx); err != nil {
390		return err
391	}
392	return r.repo.AssignToFavorite(ctx, tagID, favID)
393}
394
395func (r *LazyTagRepository) RemoveFromFavorite(ctx context.Context, tagID entity.TagID, favID entity.FavoriteID) error {
396	if err := r.init(ctx); err != nil {
397		return err
398	}
399	return r.repo.RemoveFromFavorite(ctx, tagID, favID)
400}
401
402func (r *LazyTagRepository) GetForFavorite(ctx context.Context, favID entity.FavoriteID) ([]*entity.Tag, error) {
403	if err := r.init(ctx); err != nil {
404		return nil, err
405	}
406	return r.repo.GetForFavorite(ctx, favID)
407}
408
409func (r *LazyTagRepository) Delete(ctx context.Context, id entity.TagID) error {
410	if err := r.init(ctx); err != nil {
411		return err
412	}
413	return r.repo.Delete(ctx, id)
414}
415
416// LazyZoomRepository wraps a zoom repository with lazy database initialization.
417type LazyZoomRepository struct {
418	provider port.DatabaseProvider
419	repo     repository.ZoomRepository
420	once     sync.Once
421	initErr  error
422}
423
424// NewLazyZoomRepository creates a lazy-loading zoom repository.
425func NewLazyZoomRepository(provider port.DatabaseProvider) repository.ZoomRepository {
426	return &LazyZoomRepository{provider: provider}
427}
428
429func (r *LazyZoomRepository) init(ctx context.Context) error {
430	r.once.Do(func() {
431		db, err := r.provider.DB(ctx)
432		if err != nil {
433			r.initErr = err
434			return
435		}
436		r.repo = NewZoomRepository(db)
437	})
438	return r.initErr
439}
440
441func (r *LazyZoomRepository) Get(ctx context.Context, domain string) (*entity.ZoomLevel, error) {
442	if err := r.init(ctx); err != nil {
443		return nil, err
444	}
445	return r.repo.Get(ctx, domain)
446}
447
448func (r *LazyZoomRepository) Set(ctx context.Context, zoom *entity.ZoomLevel) error {
449	if err := r.init(ctx); err != nil {
450		return err
451	}
452	return r.repo.Set(ctx, zoom)
453}
454
455func (r *LazyZoomRepository) Delete(ctx context.Context, domain string) error {
456	if err := r.init(ctx); err != nil {
457		return err
458	}
459	return r.repo.Delete(ctx, domain)
460}
461
462func (r *LazyZoomRepository) GetAll(ctx context.Context) ([]*entity.ZoomLevel, error) {
463	if err := r.init(ctx); err != nil {
464		return nil, err
465	}
466	return r.repo.GetAll(ctx)
467}
468
469// LazySessionRepository wraps a session repository with lazy database initialization.
470type LazySessionRepository struct {
471	provider port.DatabaseProvider
472	repo     repository.SessionRepository
473	once     sync.Once
474	initErr  error
475}
476
477// NewLazySessionRepository creates a lazy-loading session repository.
478func NewLazySessionRepository(provider port.DatabaseProvider) repository.SessionRepository {
479	return &LazySessionRepository{provider: provider}
480}
481
482func (r *LazySessionRepository) init(ctx context.Context) error {
483	r.once.Do(func() {
484		db, err := r.provider.DB(ctx)
485		if err != nil {
486			r.initErr = err
487			return
488		}
489		r.repo = NewSessionRepository(db)
490	})
491	return r.initErr
492}
493
494func (r *LazySessionRepository) Save(ctx context.Context, session *entity.Session) error {
495	if err := r.init(ctx); err != nil {
496		return err
497	}
498	return r.repo.Save(ctx, session)
499}
500
501func (r *LazySessionRepository) FindByID(ctx context.Context, id entity.SessionID) (*entity.Session, error) {
502	if err := r.init(ctx); err != nil {
503		return nil, err
504	}
505	return r.repo.FindByID(ctx, id)
506}
507
508func (r *LazySessionRepository) GetActive(ctx context.Context) (*entity.Session, error) {
509	if err := r.init(ctx); err != nil {
510		return nil, err
511	}
512	return r.repo.GetActive(ctx)
513}
514
515func (r *LazySessionRepository) GetRecent(ctx context.Context, limit int) ([]*entity.Session, error) {
516	if err := r.init(ctx); err != nil {
517		return nil, err
518	}
519	return r.repo.GetRecent(ctx, limit)
520}
521
522func (r *LazySessionRepository) MarkEnded(ctx context.Context, id entity.SessionID, endedAt time.Time) error {
523	if err := r.init(ctx); err != nil {
524		return err
525	}
526	return r.repo.MarkEnded(ctx, id, endedAt)
527}
528
529func (r *LazySessionRepository) Delete(ctx context.Context, id entity.SessionID) error {
530	if err := r.init(ctx); err != nil {
531		return err
532	}
533	return r.repo.Delete(ctx, id)
534}
535
536func (r *LazySessionRepository) DeleteOldestExited(ctx context.Context, keepCount int) (int64, error) {
537	if err := r.init(ctx); err != nil {
538		return 0, err
539	}
540	return r.repo.DeleteOldestExited(ctx, keepCount)
541}
542
543func (r *LazySessionRepository) DeleteExitedBefore(ctx context.Context, cutoff time.Time) (int64, error) {
544	if err := r.init(ctx); err != nil {
545		return 0, err
546	}
547	return r.repo.DeleteExitedBefore(ctx, cutoff)
548}
549
550// LazySessionStateRepository wraps a session state repository with lazy database initialization.
551type LazySessionStateRepository struct {
552	provider port.DatabaseProvider
553	repo     repository.SessionStateRepository
554	once     sync.Once
555	initErr  error
556}
557
558// NewLazySessionStateRepository creates a lazy-loading session state repository.
559func NewLazySessionStateRepository(provider port.DatabaseProvider) repository.SessionStateRepository {
560	return &LazySessionStateRepository{provider: provider}
561}
562
563func (r *LazySessionStateRepository) init(ctx context.Context) error {
564	r.once.Do(func() {
565		db, err := r.provider.DB(ctx)
566		if err != nil {
567			r.initErr = err
568			return
569		}
570		r.repo = NewSessionStateRepository(db)
571	})
572	return r.initErr
573}
574
575func (r *LazySessionStateRepository) SaveSnapshot(ctx context.Context, state *entity.SessionState) error {
576	if err := r.init(ctx); err != nil {
577		return err
578	}
579	return r.repo.SaveSnapshot(ctx, state)
580}
581
582func (r *LazySessionStateRepository) GetSnapshot(ctx context.Context, sessionID entity.SessionID) (*entity.SessionState, error) {
583	if err := r.init(ctx); err != nil {
584		return nil, err
585	}
586	return r.repo.GetSnapshot(ctx, sessionID)
587}
588
589func (r *LazySessionStateRepository) DeleteSnapshot(ctx context.Context, sessionID entity.SessionID) error {
590	if err := r.init(ctx); err != nil {
591		return err
592	}
593	return r.repo.DeleteSnapshot(ctx, sessionID)
594}
595
596func (r *LazySessionStateRepository) GetAllSnapshots(ctx context.Context) ([]*entity.SessionState, error) {
597	if err := r.init(ctx); err != nil {
598		return nil, err
599	}
600	return r.repo.GetAllSnapshots(ctx)
601}
602
603func (r *LazySessionStateRepository) GetTotalSnapshotsSize(ctx context.Context) (int64, error) {
604	if err := r.init(ctx); err != nil {
605		return 0, err
606	}
607	return r.repo.GetTotalSnapshotsSize(ctx)
608}
609
610// LazyContentWhitelistRepository wraps a content whitelist repository with lazy database initialization.
611type LazyContentWhitelistRepository struct {
612	provider port.DatabaseProvider
613	repo     repository.ContentWhitelistRepository
614	once     sync.Once
615	initErr  error
616}
617
618// NewLazyContentWhitelistRepository creates a lazy-loading content whitelist repository.
619func NewLazyContentWhitelistRepository(provider port.DatabaseProvider) repository.ContentWhitelistRepository {
620	return &LazyContentWhitelistRepository{provider: provider}
621}
622
623func (r *LazyContentWhitelistRepository) init(ctx context.Context) error {
624	r.once.Do(func() {
625		db, err := r.provider.DB(ctx)
626		if err != nil {
627			r.initErr = err
628			return
629		}
630		r.repo = NewContentWhitelistRepository(db)
631	})
632	return r.initErr
633}
634
635func (r *LazyContentWhitelistRepository) Add(ctx context.Context, domain string) error {
636	if err := r.init(ctx); err != nil {
637		return err
638	}
639	return r.repo.Add(ctx, domain)
640}
641
642func (r *LazyContentWhitelistRepository) Remove(ctx context.Context, domain string) error {
643	if err := r.init(ctx); err != nil {
644		return err
645	}
646	return r.repo.Remove(ctx, domain)
647}
648
649func (r *LazyContentWhitelistRepository) Contains(ctx context.Context, domain string) (bool, error) {
650	if err := r.init(ctx); err != nil {
651		return false, err
652	}
653	return r.repo.Contains(ctx, domain)
654}
655
656func (r *LazyContentWhitelistRepository) GetAll(ctx context.Context) ([]string, error) {
657	if err := r.init(ctx); err != nil {
658		return nil, err
659	}
660	return r.repo.GetAll(ctx)
661}
662
663// LazyRepositories holds all lazy-loaded repositories.
664type LazyRepositories struct {
665	History      repository.HistoryRepository
666	Favorite     repository.FavoriteRepository
667	Folder       repository.FolderRepository
668	Tag          repository.TagRepository
669	Zoom         repository.ZoomRepository
670	Session      repository.SessionRepository
671	SessionState repository.SessionStateRepository
672	Filter       repository.ContentWhitelistRepository
673}
674
675// NewLazyRepositories creates all lazy repositories from a database provider.
676func NewLazyRepositories(provider port.DatabaseProvider) *LazyRepositories {
677	return &LazyRepositories{
678		History:      NewLazyHistoryRepository(provider),
679		Favorite:     NewLazyFavoriteRepository(provider),
680		Folder:       NewLazyFolderRepository(provider),
681		Tag:          NewLazyTagRepository(provider),
682		Zoom:         NewLazyZoomRepository(provider),
683		Session:      NewLazySessionRepository(provider),
684		SessionState: NewLazySessionStateRepository(provider),
685		Filter:       NewLazyContentWhitelistRepository(provider),
686	}
687}
688
689// GetDB returns the underlying *sql.DB for operations that need it directly.
690// Returns nil if the database hasn't been initialized yet.
691func GetDB(provider port.DatabaseProvider, ctx context.Context) *sql.DB {
692	db, err := provider.DB(ctx)
693	if err != nil {
694		return nil
695	}
696	return db
697}

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