1package cache
2
3import (
4 "fmt"
5 "io"
6 "io/ioutil"
7 "os"
8 "path"
9 "path/filepath"
10 "strconv"
11 "sync"
12
13 "github.com/MichaelMure/git-bug/bug"
14 "github.com/MichaelMure/git-bug/entity"
15 "github.com/MichaelMure/git-bug/identity"
16 "github.com/MichaelMure/git-bug/repository"
17 "github.com/MichaelMure/git-bug/util/process"
18 "github.com/blevesearch/bleve"
19)
20
21// 1: original format
22// 2: added cache for identities with a reference in the bug cache
23// 3: no more legacy identity
24const formatVersion = 3
25
26// The maximum number of bugs loaded in memory. After that, eviction will be done.
27const defaultMaxLoadedBugs = 1000
28
29var _ repository.RepoCommon = &RepoCache{}
30var _ repository.RepoConfig = &RepoCache{}
31var _ repository.RepoKeyring = &RepoCache{}
32
33// RepoCache is a cache for a Repository. This cache has multiple functions:
34//
35// 1. After being loaded, a Bug is kept in memory in the cache, allowing for fast
36// access later.
37// 2. The cache maintain in memory and on disk a pre-digested excerpt for each bug,
38// allowing for fast querying the whole set of bugs without having to load
39// them individually.
40// 3. The cache guarantee that a single instance of a Bug is loaded at once, avoiding
41// loss of data that we could have with multiple copies in the same process.
42// 4. The same way, the cache maintain in memory a single copy of the loaded identities.
43//
44// The cache also protect the on-disk data by locking the git repository for its
45// own usage, by writing a lock file. Of course, normal git operations are not
46// affected, only git-bug related one.
47type RepoCache struct {
48 // the underlying repo
49 repo repository.ClockedRepo
50
51 // the name of the repository, as defined in the MultiRepoCache
52 name string
53
54 // maximum number of loaded bugs
55 maxLoadedBugs int
56
57 muBug sync.RWMutex
58 // excerpt of bugs data for all bugs
59 bugExcerpts map[entity.Id]*BugExcerpt
60 // searchable cache of all bugs
61 searchCache bleve.Index
62 // bug loaded in memory
63 bugs map[entity.Id]*BugCache
64 // loadedBugs is an LRU cache that records which bugs the cache has loaded in
65 loadedBugs *LRUIdCache
66
67 muIdentity sync.RWMutex
68 // excerpt of identities data for all identities
69 identitiesExcerpts map[entity.Id]*IdentityExcerpt
70 // identities loaded in memory
71 identities map[entity.Id]*IdentityCache
72
73 // the user identity's id, if known
74 userIdentityId entity.Id
75}
76
77func NewRepoCache(r repository.ClockedRepo) (*RepoCache, error) {
78 return NewNamedRepoCache(r, "")
79}
80
81func NewNamedRepoCache(r repository.ClockedRepo, name string) (*RepoCache, error) {
82 c := &RepoCache{
83 repo: r,
84 name: name,
85 maxLoadedBugs: defaultMaxLoadedBugs,
86 bugs: make(map[entity.Id]*BugCache),
87 loadedBugs: NewLRUIdCache(),
88 identities: make(map[entity.Id]*IdentityCache),
89 }
90
91 err := c.lock()
92 if err != nil {
93 return &RepoCache{}, err
94 }
95
96 err = c.load()
97 if err == nil {
98 return c, nil
99 }
100
101 // Cache is either missing, broken or outdated. Rebuilding.
102 err = c.buildCache()
103 if err != nil {
104 return nil, err
105 }
106
107 return c, c.write()
108}
109
110// setCacheSize change the maximum number of loaded bugs
111func (c *RepoCache) setCacheSize(size int) {
112 c.maxLoadedBugs = size
113 c.evictIfNeeded()
114}
115
116// load will try to read from the disk all the cache files
117func (c *RepoCache) load() error {
118 err := c.loadBugCache()
119 if err != nil {
120 return err
121 }
122
123 return c.loadIdentityCache()
124}
125
126// write will serialize on disk all the cache files
127func (c *RepoCache) write() error {
128 err := c.writeBugCache()
129 if err != nil {
130 return err
131 }
132 return c.writeIdentityCache()
133}
134
135func (c *RepoCache) lock() error {
136 lockPath := repoLockFilePath(c.repo)
137
138 err := repoIsAvailable(c.repo)
139 if err != nil {
140 return err
141 }
142
143 err = os.MkdirAll(filepath.Dir(lockPath), 0777)
144 if err != nil {
145 return err
146 }
147
148 f, err := os.Create(lockPath)
149 if err != nil {
150 return err
151 }
152
153 pid := fmt.Sprintf("%d", os.Getpid())
154 _, err = f.WriteString(pid)
155 if err != nil {
156 return err
157 }
158
159 return f.Close()
160}
161
162func (c *RepoCache) Close() error {
163 c.muBug.Lock()
164 defer c.muBug.Unlock()
165 c.muIdentity.Lock()
166 defer c.muIdentity.Unlock()
167
168 c.identities = make(map[entity.Id]*IdentityCache)
169 c.identitiesExcerpts = nil
170 c.bugs = make(map[entity.Id]*BugCache)
171 c.bugExcerpts = nil
172
173 if c.searchCache != nil {
174 c.searchCache.Close()
175 c.searchCache = nil
176 }
177
178 lockPath := repoLockFilePath(c.repo)
179 return os.Remove(lockPath)
180}
181
182func (c *RepoCache) buildCache() error {
183 c.muBug.Lock()
184 defer c.muBug.Unlock()
185 c.muIdentity.Lock()
186 defer c.muIdentity.Unlock()
187
188 _, _ = fmt.Fprintf(os.Stderr, "Building identity cache... ")
189
190 c.identitiesExcerpts = make(map[entity.Id]*IdentityExcerpt)
191
192 allIdentities := identity.ReadAllLocal(c.repo)
193
194 for i := range allIdentities {
195 if i.Err != nil {
196 return i.Err
197 }
198
199 c.identitiesExcerpts[i.Identity.Id()] = NewIdentityExcerpt(i.Identity)
200 }
201
202 _, _ = fmt.Fprintln(os.Stderr, "Done.")
203
204 _, _ = fmt.Fprintf(os.Stderr, "Building bug cache... ")
205
206 c.bugExcerpts = make(map[entity.Id]*BugExcerpt)
207
208 allBugs := bug.ReadAllLocal(c.repo)
209
210 err := c.ensureBleveIndex()
211 if err != nil {
212 return fmt.Errorf("Unable to create or open search cache. Error: %v", err)
213 }
214
215 for b := range allBugs {
216 if b.Err != nil {
217 return b.Err
218 }
219
220 snap := b.Bug.Compile()
221 c.bugExcerpts[b.Bug.Id()] = NewBugExcerpt(b.Bug, &snap)
222
223 if err := c.addBugToSearchIndex(&snap); err != nil {
224 return err
225 }
226 }
227
228 _, _ = fmt.Fprintln(os.Stderr, "Done.")
229
230 return nil
231}
232
233func (c *RepoCache) addBugToSearchIndex(snap *bug.Snapshot) error {
234 searchableBug := struct {
235 Text []string
236 }{}
237
238 for _, comment := range snap.Comments {
239 searchableBug.Text = append(searchableBug.Text, comment.Message)
240 }
241
242 err := c.searchCache.Index(snap.Id().String(), searchableBug)
243 if err != nil {
244 return err
245 }
246
247 return nil
248}
249
250func repoLockFilePath(repo repository.Repo) string {
251 return path.Join(repo.GetPath(), "git-bug", lockfile)
252}
253
254// repoIsAvailable check is the given repository is locked by a Cache.
255// Note: this is a smart function that will cleanup 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.Repo) error {
259 lockPath := repoLockFilePath(repo)
260
261 // Todo: this leave way for a racey access to the repo between the test
262 // if the file exist and the actual write. It's probably not a problem in
263 // practice because using a repository will be done from user interaction
264 // or in a context where a single instance of git-bug is already guaranteed
265 // (say, a server with the web UI running). But still, that might be nice to
266 // have a mutex or something to guard that.
267
268 // Todo: this will fail if somehow the filesystem is shared with another
269 // computer. Should add a configuration that prevent the cleaning of the
270 // lock file
271
272 f, err := os.Open(lockPath)
273
274 if err != nil && !os.IsNotExist(err) {
275 return err
276 }
277
278 if err == nil {
279 // lock file already exist
280 buf, err := ioutil.ReadAll(io.LimitReader(f, 10))
281 if err != nil {
282 return err
283 }
284 if len(buf) == 10 {
285 return fmt.Errorf("the lock file should be < 10 bytes")
286 }
287
288 pid, err := strconv.Atoi(string(buf))
289 if err != nil {
290 return err
291 }
292
293 if process.IsRunning(pid) {
294 return fmt.Errorf("the repository you want to access is already locked by the process pid %d", pid)
295 }
296
297 // The lock file is just laying there after a crash, clean it
298
299 fmt.Println("A lock file is present but the corresponding process is not, removing it.")
300 err = f.Close()
301 if err != nil {
302 return err
303 }
304
305 err = os.Remove(lockPath)
306 if err != nil {
307 return err
308 }
309 }
310
311 return nil
312}