1package cache
2
3import (
4 "fmt"
5 "io"
6 "os"
7 "strconv"
8 "sync"
9
10 "github.com/git-bug/git-bug/entities/bug"
11 "github.com/git-bug/git-bug/entities/identity"
12 "github.com/git-bug/git-bug/entity"
13 "github.com/git-bug/git-bug/repository"
14 "github.com/git-bug/git-bug/util/multierr"
15 "github.com/git-bug/git-bug/util/process"
16)
17
18// 1: original format
19// 2: added cache for identities with a reference in the bug cache
20// 3: no more legacy identity
21// 4: entities make their IDs from data, not git commit
22const formatVersion = 4
23
24// The maximum number of bugs loaded in memory. After that, eviction will be done.
25const defaultMaxLoadedBugs = 1000
26
27var _ repository.RepoCommon = &RepoCache{}
28var _ repository.RepoConfig = &RepoCache{}
29var _ repository.RepoKeyring = &RepoCache{}
30
31// cacheMgmt is the expected interface for a sub-cache.
32type cacheMgmt interface {
33 Typename() string
34 Load() error
35 Build() <-chan BuildEvent
36 SetCacheSize(size int)
37 RemoveAll() error
38 MergeAll(remote string) <-chan entity.MergeResult
39 GetNamespace() string
40 Close() error
41}
42
43// RepoCache is a cache for a Repository. This cache has multiple functions:
44//
45// 1. After being loaded, a Bug is kept in memory in the cache, allowing for fast
46// access later.
47// 2. The cache maintains in memory and on disk a pre-digested excerpt for each bug,
48// allowing for fast querying the whole set of bugs without having to load
49// them individually.
50// 3. The cache guarantees that a single instance of a Bug is loaded at once, avoiding
51// loss of data that we could have with multiple copies in the same process.
52// 4. The same way, the cache maintains in memory a single copy of the loaded identities.
53//
54// The cache also protects the on-disk data by locking the git repository for its
55// own usage, by writing a lock file. Of course, normal git operations are not
56// affected, only git-bug related one.
57type RepoCache struct {
58 // the underlying repo
59 repo repository.ClockedRepo
60
61 // the name of the repository, as defined in the MultiRepoCache
62 name string
63
64 // resolvers for all known entities and excerpts
65 resolvers entity.Resolvers
66
67 bugs *RepoCacheBug
68 identities *RepoCacheIdentity
69
70 subcaches []cacheMgmt
71
72 // the user identity's id, if known
73 muUserIdentity sync.RWMutex
74 userIdentityId entity.Id
75}
76
77// NewRepoCache create or open a cache on top of a raw repository.
78// The caller is expected to read all returned events before the cache is considered
79// ready to use.
80func NewRepoCache(r repository.ClockedRepo) (*RepoCache, chan BuildEvent) {
81 return NewNamedRepoCache(r, defaultRepoName)
82}
83
84// NewNamedRepoCache create or open a named cache on top of a raw repository.
85// The caller is expected to read all returned events before the cache is considered
86// ready to use.
87func NewNamedRepoCache(r repository.ClockedRepo, name string) (*RepoCache, chan BuildEvent) {
88 c := &RepoCache{
89 repo: r,
90 name: name,
91 }
92
93 c.identities = NewRepoCacheIdentity(r, c.getResolvers, c.GetUserIdentity)
94 c.subcaches = append(c.subcaches, c.identities)
95
96 c.bugs = NewRepoCacheBug(r, c.getResolvers, c.GetUserIdentity)
97 c.subcaches = append(c.subcaches, c.bugs)
98
99 c.resolvers = entity.Resolvers{
100 &IdentityCache{}: entity.ResolverFunc[*IdentityCache](c.identities.Resolve),
101 &IdentityExcerpt{}: entity.ResolverFunc[*IdentityExcerpt](c.identities.ResolveExcerpt),
102 &BugCache{}: entity.ResolverFunc[*BugCache](c.bugs.Resolve),
103 &BugExcerpt{}: entity.ResolverFunc[*BugExcerpt](c.bugs.ResolveExcerpt),
104 }
105
106 // small buffer so that the functions below can emit an event without blocking
107 events := make(chan BuildEvent)
108
109 go func() {
110 defer close(events)
111
112 err := c.lock(events)
113 if err != nil {
114 events <- BuildEvent{Err: err}
115 return
116 }
117
118 err = c.load()
119 if err == nil {
120 return
121 }
122
123 // Cache is either missing, broken or outdated. Rebuilding.
124 c.buildCache(events)
125 }()
126
127 return c, events
128}
129
130func NewRepoCacheNoEvents(r repository.ClockedRepo) (*RepoCache, error) {
131 cache, events := NewRepoCache(r)
132 for event := range events {
133 if event.Err != nil {
134 for range events {
135 }
136 return nil, event.Err
137 }
138 }
139 return cache, nil
140}
141
142func (c *RepoCache) registerObserver(repoRef string, typename string, observer Observer) {
143 switch typename {
144 case bug.Typename:
145 c.bugs.RegisterObserver(repoRef, observer)
146 case identity.Typename:
147 c.identities.RegisterObserver(repoRef, observer)
148 default:
149 panic(fmt.Sprintf("unknown typename %q", typename))
150 }
151}
152
153func (c *RepoCache) unregisterObserver(typename string, observer Observer) {
154 switch typename {
155 case bug.Typename:
156 c.bugs.UnregisterObserver(observer)
157 case identity.Typename:
158 c.identities.UnregisterObserver(observer)
159 default:
160 panic(fmt.Sprintf("unknown typename %q", typename))
161 }
162}
163
164// Bugs gives access to the Bug entities
165func (c *RepoCache) Bugs() *RepoCacheBug {
166 return c.bugs
167}
168
169// Identities gives access to the Identity entities
170func (c *RepoCache) Identities() *RepoCacheIdentity {
171 return c.identities
172}
173
174func (c *RepoCache) getResolvers() entity.Resolvers {
175 return c.resolvers
176}
177
178// setCacheSize change the maximum number of loaded bugs
179func (c *RepoCache) setCacheSize(size int) {
180 for _, subcache := range c.subcaches {
181 subcache.SetCacheSize(size)
182 }
183}
184
185// load will try to read from the disk all the cache files
186func (c *RepoCache) load() error {
187 var errWait multierr.ErrWaitGroup
188 for _, mgmt := range c.subcaches {
189 errWait.Go(mgmt.Load)
190 }
191 return errWait.Wait()
192}
193
194func (c *RepoCache) lock(events chan BuildEvent) error {
195 err := repoIsAvailable(c.repo, events)
196 if err != nil {
197 return err
198 }
199
200 f, err := c.repo.LocalStorage().Create(lockfile)
201 if err != nil {
202 return err
203 }
204
205 pid := fmt.Sprintf("%d", os.Getpid())
206 _, err = f.Write([]byte(pid))
207 if err != nil {
208 _ = f.Close()
209 return err
210 }
211
212 return f.Close()
213}
214
215func (c *RepoCache) Close() error {
216 var errWait multierr.ErrWaitGroup
217 for _, mgmt := range c.subcaches {
218 errWait.Go(mgmt.Close)
219 }
220 err := errWait.Wait()
221 if err != nil {
222 return err
223 }
224
225 err = c.repo.Close()
226 if err != nil {
227 return err
228 }
229
230 return c.repo.LocalStorage().Remove(lockfile)
231}
232
233func (c *RepoCache) buildCache(events chan BuildEvent) {
234 events <- BuildEvent{Event: BuildEventCacheIsBuilt}
235
236 var wg sync.WaitGroup
237 for _, subcache := range c.subcaches {
238 wg.Add(1)
239 go func(subcache cacheMgmt) {
240 defer wg.Done()
241
242 buildEvents := subcache.Build()
243 for buildEvent := range buildEvents {
244 events <- buildEvent
245 if buildEvent.Err != nil {
246 return
247 }
248 }
249 }(subcache)
250 }
251 wg.Wait()
252}
253
254// repoIsAvailable check is the given repository is locked by a Cache.
255// Note: this is a smart function that will clean the lock file if the
256// corresponding process is not there anymore.
257// If no error is returned, the repo is free to edit.
258func repoIsAvailable(repo repository.RepoStorage, events chan BuildEvent) error {
259 // Todo: this leave way for a racey access to the repo between the test
260 // if the file exist and the actual write. It's probably not a problem in
261 // practice because using a repository will be done from user interaction
262 // or in a context where a single instance of git-bug is already guaranteed
263 // (say, a server with the web UI running). But still, that might be nice to
264 // have a mutex or something to guard that.
265
266 // Todo: this will fail if somehow the filesystem is shared with another
267 // computer. Should add a configuration that prevent the cleaning of the
268 // lock file
269
270 f, err := repo.LocalStorage().Open(lockfile)
271 if err != nil && !os.IsNotExist(err) {
272 return err
273 }
274
275 if err == nil {
276 // lock file already exist
277 buf, err := io.ReadAll(io.LimitReader(f, 10))
278 if err != nil {
279 _ = f.Close()
280 return err
281 }
282
283 err = f.Close()
284 if err != nil {
285 return err
286 }
287
288 if len(buf) >= 10 {
289 return fmt.Errorf("the lock file should be < 10 bytes")
290 }
291
292 pid, err := strconv.Atoi(string(buf))
293 if err != nil {
294 return err
295 }
296
297 if process.IsRunning(pid) {
298 return fmt.Errorf("the repository you want to access is already locked by the process pid %d", pid)
299 }
300
301 // The lock file is just laying there after a crash, clean it
302
303 events <- BuildEvent{Event: BuildEventRemoveLock}
304
305 err = repo.LocalStorage().Remove(lockfile)
306 if err != nil {
307 return err
308 }
309 }
310
311 return nil
312}