v0.12.1 1453d21

store.go RawHistoryFolder

1package filtering
2
3import (
4	"crypto/sha256"
5	"encoding/json"
6	"fmt"
7	"os"
8	"path/filepath"
9	"time"
10
11	"github.com/bnema/dumber/internal/config"
12	"github.com/bnema/dumber/internal/logging"
13)
14
15const (
16	cacheVersion    = 1    // Cache format version for compatibility
17	dirPermissions  = 0755 // Directory permissions
18	filePermissions = 0644 // File permissions
19)
20
21// FileFilterStore implements FilterStore using filesystem storage
22type FileFilterStore struct {
23	cacheFile string
24	metaFile  string
25}
26
27// CacheMetadata stores information about the cache
28type CacheMetadata struct {
29	Version      int       `json:"version"`
30	CreatedAt    time.Time `json:"created_at"`
31	LastUsed     time.Time `json:"last_used"`
32	DataHash     string    `json:"data_hash"`
33	FilterHashes []string  `json:"filter_hashes"` // Hashes of source filter lists
34}
35
36// NewFileFilterStore creates a new file-based filter store
37func NewFileFilterStore() (*FileFilterStore, error) {
38	cacheFile, err := config.GetFilterCacheFile()
39	if err != nil {
40		return nil, fmt.Errorf("failed to get cache file path: %w", err)
41	}
42
43	cacheDir := filepath.Dir(cacheFile)
44	metaFile := filepath.Join(cacheDir, "metadata.json")
45
46	// Ensure cache directory exists
47	if err := os.MkdirAll(cacheDir, dirPermissions); err != nil {
48		return nil, fmt.Errorf("failed to create cache directory: %w", err)
49	}
50
51	return &FileFilterStore{
52		cacheFile: cacheFile,
53		metaFile:  metaFile,
54	}, nil
55}
56
57// LoadCached loads compiled filters from cache
58func (fs *FileFilterStore) LoadCached() (*CompiledFilters, error) {
59	// Check if cache file exists
60	if _, err := os.Stat(fs.cacheFile); os.IsNotExist(err) {
61		return nil, fmt.Errorf("cache file does not exist")
62	}
63
64	// Load metadata
65	metadata, err := fs.loadMetadata()
66	if err != nil {
67		return nil, fmt.Errorf("failed to load cache metadata: %w", err)
68	}
69
70	// Verify cache version compatibility
71	if metadata.Version != cacheVersion {
72		return nil, fmt.Errorf("cache version mismatch: got %d, expected %d", metadata.Version, cacheVersion)
73	}
74
75	// Load cache data
76	data, err := os.ReadFile(fs.cacheFile)
77	if err != nil {
78		return nil, fmt.Errorf("failed to read cache file: %w", err)
79	}
80
81	// Verify data integrity
82	dataHash := fmt.Sprintf("%x", sha256.Sum256(data))
83	if dataHash != metadata.DataHash {
84		return nil, fmt.Errorf("cache data corrupted: hash mismatch")
85	}
86
87	// Deserialize compiled filters
88	var compiled CompiledFilters
89	if err := json.Unmarshal(data, &compiled); err != nil {
90		return nil, fmt.Errorf("failed to deserialize cache data: %w", err)
91	}
92
93	// Update last used time
94	metadata.LastUsed = time.Now()
95	if err := fs.saveMetadata(metadata); err != nil {
96		logging.Error(fmt.Sprintf("Failed to update cache metadata: %v", err))
97	}
98
99	logging.Info(fmt.Sprintf("Loaded filter cache from %s", fs.cacheFile))
100	return &compiled, nil
101}
102
103// SaveCache saves compiled filters to cache
104func (fs *FileFilterStore) SaveCache(filters *CompiledFilters) error {
105	// Serialize filters
106	data, err := json.Marshal(filters)
107	if err != nil {
108		return fmt.Errorf("failed to serialize filters: %w", err)
109	}
110
111	// Create metadata
112	metadata := &CacheMetadata{
113		Version:   cacheVersion,
114		CreatedAt: time.Now(),
115		LastUsed:  time.Now(),
116		DataHash:  fmt.Sprintf("%x", sha256.Sum256(data)),
117		// FilterHashes would be populated with source list hashes in a full implementation
118		FilterHashes: []string{},
119	}
120
121	// Write cache data atomically
122	tempFile := fs.cacheFile + ".tmp"
123	if err := os.WriteFile(tempFile, data, filePermissions); err != nil {
124		return fmt.Errorf("failed to write cache data: %w", err)
125	}
126
127	if err := os.Rename(tempFile, fs.cacheFile); err != nil {
128		if removeErr := os.Remove(tempFile); removeErr != nil {
129			logging.Error(fmt.Sprintf("Failed to cleanup temp file %s: %v", tempFile, removeErr))
130		}
131		return fmt.Errorf("failed to move cache file: %w", err)
132	}
133
134	// Save metadata
135	if err := fs.saveMetadata(metadata); err != nil {
136		return fmt.Errorf("failed to save cache metadata: %w", err)
137	}
138
139	logging.Info(fmt.Sprintf("Saved filter cache to %s", fs.cacheFile))
140	return nil
141}
142
143// GetCacheInfo returns cache existence and modification time
144func (fs *FileFilterStore) GetCacheInfo() (bool, time.Time, error) {
145	info, err := os.Stat(fs.cacheFile)
146	if os.IsNotExist(err) {
147		return false, time.Time{}, nil
148	}
149	if err != nil {
150		return false, time.Time{}, err
151	}
152
153	return true, info.ModTime(), nil
154}
155
156// InvalidateCache removes the cache file
157func (fs *FileFilterStore) InvalidateCache() error {
158	// Remove cache file
159	if err := os.Remove(fs.cacheFile); err != nil && !os.IsNotExist(err) {
160		return fmt.Errorf("failed to remove cache file: %w", err)
161	}
162
163	// Remove metadata file
164	if err := os.Remove(fs.metaFile); err != nil && !os.IsNotExist(err) {
165		return fmt.Errorf("failed to remove metadata file: %w", err)
166	}
167
168	logging.Info("Filter cache invalidated")
169	return nil
170}
171
172// loadMetadata loads cache metadata from file
173func (fs *FileFilterStore) loadMetadata() (*CacheMetadata, error) {
174	data, err := os.ReadFile(fs.metaFile)
175	if err != nil {
176		return nil, err
177	}
178
179	var metadata CacheMetadata
180	if err := json.Unmarshal(data, &metadata); err != nil {
181		return nil, err
182	}
183
184	return &metadata, nil
185}
186
187// saveMetadata saves cache metadata to file
188func (fs *FileFilterStore) saveMetadata(metadata *CacheMetadata) error {
189	data, err := json.Marshal(metadata)
190	if err != nil {
191		return err
192	}
193
194	// Write metadata atomically
195	tempFile := fs.metaFile + ".tmp"
196	if err := os.WriteFile(tempFile, data, filePermissions); err != nil {
197		return err
198	}
199
200	if err := os.Rename(tempFile, fs.metaFile); err != nil {
201		if removeErr := os.Remove(tempFile); removeErr != nil {
202			logging.Error(fmt.Sprintf("Failed to cleanup temp metadata file %s: %v", tempFile, removeErr))
203		}
204		return err
205	}
206
207	return nil
208}

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