v0.31.0 bf0756b

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

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