1package cache
2
3import (
4 "bytes"
5 "encoding/gob"
6 "errors"
7 "fmt"
8 "os"
9 "path"
10 "sort"
11 "time"
12
13 "github.com/MichaelMure/git-bug/bug"
14 "github.com/MichaelMure/git-bug/entity"
15 "github.com/MichaelMure/git-bug/query"
16 "github.com/MichaelMure/git-bug/repository"
17 "github.com/blevesearch/bleve"
18)
19
20const (
21 bugCacheFile = "bug-cache"
22 searchCacheDir = "search-cache"
23)
24
25var errBugNotInCache = errors.New("bug missing from cache")
26
27func bugCacheFilePath(repo repository.Repo) string {
28 return path.Join(repo.GetPath(), "git-bug", bugCacheFile)
29}
30
31func searchCacheDirPath(repo repository.Repo) string {
32 return path.Join(repo.GetPath(), "git-bug", searchCacheDir)
33}
34
35// bugUpdated is a callback to trigger when the excerpt of a bug changed,
36// that is each time a bug is updated
37func (c *RepoCache) bugUpdated(id entity.Id) error {
38 c.muBug.Lock()
39 b, ok := c.bugs[id]
40 if !ok {
41 c.muBug.Unlock()
42
43 // if the bug is not loaded at this point, it means it was loaded before
44 // but got evicted. Which means we potentially have multiple copies in
45 // memory and thus concurrent write.
46 // Failing immediately here is the simple and safe solution to avoid
47 // complicated data loss.
48 return errBugNotInCache
49 }
50 c.loadedBugs.Get(id)
51 c.bugExcerpts[id] = NewBugExcerpt(b.bug, b.Snapshot())
52 c.muBug.Unlock()
53
54 if err := c.addBugToSearchIndex(b.Snapshot()); err != nil {
55 return err
56 }
57
58 // we only need to write the bug cache
59 return c.writeBugCache()
60}
61
62// load will try to read from the disk the bug cache file
63func (c *RepoCache) loadBugCache() error {
64 c.muBug.Lock()
65 defer c.muBug.Unlock()
66
67 f, err := os.Open(bugCacheFilePath(c.repo))
68 if err != nil {
69 return err
70 }
71
72 decoder := gob.NewDecoder(f)
73
74 aux := struct {
75 Version uint
76 Excerpts map[entity.Id]*BugExcerpt
77 }{}
78
79 err = decoder.Decode(&aux)
80 if err != nil {
81 return err
82 }
83
84 if aux.Version != formatVersion {
85 return fmt.Errorf("unknown cache format version %v", aux.Version)
86 }
87
88 err = c.ensureBleveIndex()
89 if err != nil {
90 return fmt.Errorf("Unable to create or open search cache. Error: %v", err)
91 }
92 count, err := c.searchCache.DocCount()
93 if err != nil {
94 return err
95 }
96 if count != uint64(len(c.bugExcerpts)) {
97 return fmt.Errorf("count mismatch between bleve and bug excerpts")
98 }
99
100 c.bugExcerpts = aux.Excerpts
101 return nil
102}
103
104func (c *RepoCache) ensureBleveIndex() error {
105 blevePath := searchCacheDirPath(c.repo)
106
107 // Try to open the bleve index. If there is _any_ error, whether it be that
108 // the bleve index does not exist or is corrupt, handle that by nuking the
109 // bleve index and recreating it.
110 bleveIndex, err := bleve.Open(blevePath)
111 if err != nil {
112 // If the index does not exist, we don't care. We're going to create it
113 // next.
114 _ = os.RemoveAll(blevePath)
115
116 mapping := bleve.NewIndexMapping()
117 dir := searchCacheDirPath(c.repo)
118
119 bleveIndex, err := bleve.New(dir, mapping)
120 if err != nil {
121 return err
122 }
123
124 c.searchCache = bleveIndex
125
126 return nil
127 }
128
129 c.searchCache = bleveIndex
130
131 return nil
132}
133
134// write will serialize on disk the bug cache file
135func (c *RepoCache) writeBugCache() error {
136 c.muBug.RLock()
137 defer c.muBug.RUnlock()
138
139 var data bytes.Buffer
140
141 aux := struct {
142 Version uint
143 Excerpts map[entity.Id]*BugExcerpt
144 }{
145 Version: formatVersion,
146 Excerpts: c.bugExcerpts,
147 }
148
149 encoder := gob.NewEncoder(&data)
150
151 err := encoder.Encode(aux)
152 if err != nil {
153 return err
154 }
155
156 f, err := os.Create(bugCacheFilePath(c.repo))
157 if err != nil {
158 return err
159 }
160
161 _, err = f.Write(data.Bytes())
162 if err != nil {
163 return err
164 }
165
166 return f.Close()
167}
168
169// ResolveBugExcerpt retrieve a BugExcerpt matching the exact given id
170func (c *RepoCache) ResolveBugExcerpt(id entity.Id) (*BugExcerpt, error) {
171 c.muBug.RLock()
172 defer c.muBug.RUnlock()
173
174 excerpt, ok := c.bugExcerpts[id]
175 if !ok {
176 return nil, bug.ErrBugNotExist
177 }
178
179 return excerpt, nil
180}
181
182// ResolveBug retrieve a bug matching the exact given id
183func (c *RepoCache) ResolveBug(id entity.Id) (*BugCache, error) {
184 c.muBug.RLock()
185 cached, ok := c.bugs[id]
186 if ok {
187 c.loadedBugs.Get(id)
188 c.muBug.RUnlock()
189 return cached, nil
190 }
191 c.muBug.RUnlock()
192
193 b, err := bug.ReadLocalWithResolver(c.repo, newIdentityCacheResolver(c), id)
194 if err != nil {
195 return nil, err
196 }
197
198 cached = NewBugCache(c, b)
199
200 c.muBug.Lock()
201 c.bugs[id] = cached
202 c.loadedBugs.Add(id)
203 c.muBug.Unlock()
204
205 c.evictIfNeeded()
206
207 return cached, nil
208}
209
210// evictIfNeeded will evict a bug from the cache if needed
211// it also removes references of the bug from the bugs
212func (c *RepoCache) evictIfNeeded() {
213 c.muBug.Lock()
214 defer c.muBug.Unlock()
215 if c.loadedBugs.Len() <= c.maxLoadedBugs {
216 return
217 }
218
219 for _, id := range c.loadedBugs.GetOldestToNewest() {
220 b := c.bugs[id]
221 if b.NeedCommit() {
222 continue
223 }
224
225 b.mu.Lock()
226 c.loadedBugs.Remove(id)
227 delete(c.bugs, id)
228
229 if c.loadedBugs.Len() <= c.maxLoadedBugs {
230 return
231 }
232 }
233}
234
235// ResolveBugExcerptPrefix retrieve a BugExcerpt matching an id prefix. It fails if multiple
236// bugs match.
237func (c *RepoCache) ResolveBugExcerptPrefix(prefix string) (*BugExcerpt, error) {
238 return c.ResolveBugExcerptMatcher(func(excerpt *BugExcerpt) bool {
239 return excerpt.Id.HasPrefix(prefix)
240 })
241}
242
243// ResolveBugPrefix retrieve a bug matching an id prefix. It fails if multiple
244// bugs match.
245func (c *RepoCache) ResolveBugPrefix(prefix string) (*BugCache, error) {
246 return c.ResolveBugMatcher(func(excerpt *BugExcerpt) bool {
247 return excerpt.Id.HasPrefix(prefix)
248 })
249}
250
251// ResolveBugCreateMetadata retrieve a bug that has the exact given metadata on
252// its Create operation, that is, the first operation. It fails if multiple bugs
253// match.
254func (c *RepoCache) ResolveBugCreateMetadata(key string, value string) (*BugCache, error) {
255 return c.ResolveBugMatcher(func(excerpt *BugExcerpt) bool {
256 return excerpt.CreateMetadata[key] == value
257 })
258}
259
260func (c *RepoCache) ResolveBugExcerptMatcher(f func(*BugExcerpt) bool) (*BugExcerpt, error) {
261 id, err := c.resolveBugMatcher(f)
262 if err != nil {
263 return nil, err
264 }
265 return c.ResolveBugExcerpt(id)
266}
267
268func (c *RepoCache) ResolveBugMatcher(f func(*BugExcerpt) bool) (*BugCache, error) {
269 id, err := c.resolveBugMatcher(f)
270 if err != nil {
271 return nil, err
272 }
273 return c.ResolveBug(id)
274}
275
276func (c *RepoCache) resolveBugMatcher(f func(*BugExcerpt) bool) (entity.Id, error) {
277 c.muBug.RLock()
278 defer c.muBug.RUnlock()
279
280 // preallocate but empty
281 matching := make([]entity.Id, 0, 5)
282
283 for _, excerpt := range c.bugExcerpts {
284 if f(excerpt) {
285 matching = append(matching, excerpt.Id)
286 }
287 }
288
289 if len(matching) > 1 {
290 return entity.UnsetId, bug.NewErrMultipleMatchBug(matching)
291 }
292
293 if len(matching) == 0 {
294 return entity.UnsetId, bug.ErrBugNotExist
295 }
296
297 return matching[0], nil
298}
299
300// QueryBugs return the id of all Bug matching the given Query
301func (c *RepoCache) QueryBugs(q *query.Query) []entity.Id {
302 c.muBug.RLock()
303 defer c.muBug.RUnlock()
304
305 if q == nil {
306 return c.AllBugsIds()
307 }
308
309 matcher := compileMatcher(q.Filters)
310
311 var filtered []*BugExcerpt
312
313 //if q.Search != nil {
314 // booleanQuery := bleve.NewBooleanQuery()
315 // for _, term := range q.Search {
316 // query := bleve.NewMatchQuery(term)
317 // booleanQuery.AddMust(query)
318 // }
319
320 // search := bleve.NewSearchRequest(booleanQuery)
321 // searchResults, _ := c.searchCache.Search(search)
322 //}
323
324 for _, excerpt := range c.bugExcerpts {
325 if matcher.Match(excerpt, c) {
326 filtered = append(filtered, excerpt)
327 }
328 }
329
330 var sorter sort.Interface
331
332 switch q.OrderBy {
333 case query.OrderById:
334 sorter = BugsById(filtered)
335 case query.OrderByCreation:
336 sorter = BugsByCreationTime(filtered)
337 case query.OrderByEdit:
338 sorter = BugsByEditTime(filtered)
339 default:
340 panic("missing sort type")
341 }
342
343 switch q.OrderDirection {
344 case query.OrderAscending:
345 // Nothing to do
346 case query.OrderDescending:
347 sorter = sort.Reverse(sorter)
348 default:
349 panic("missing sort direction")
350 }
351
352 sort.Sort(sorter)
353
354 result := make([]entity.Id, len(filtered))
355
356 for i, val := range filtered {
357 result[i] = val.Id
358 }
359
360 return result
361}
362
363// AllBugsIds return all known bug ids
364func (c *RepoCache) AllBugsIds() []entity.Id {
365 c.muBug.RLock()
366 defer c.muBug.RUnlock()
367
368 result := make([]entity.Id, len(c.bugExcerpts))
369
370 i := 0
371 for _, excerpt := range c.bugExcerpts {
372 result[i] = excerpt.Id
373 i++
374 }
375
376 return result
377}
378
379// ValidLabels list valid labels
380//
381// Note: in the future, a proper label policy could be implemented where valid
382// labels are defined in a configuration file. Until that, the default behavior
383// is to return the list of labels already used.
384func (c *RepoCache) ValidLabels() []bug.Label {
385 c.muBug.RLock()
386 defer c.muBug.RUnlock()
387
388 set := map[bug.Label]interface{}{}
389
390 for _, excerpt := range c.bugExcerpts {
391 for _, l := range excerpt.Labels {
392 set[l] = nil
393 }
394 }
395
396 result := make([]bug.Label, len(set))
397
398 i := 0
399 for l := range set {
400 result[i] = l
401 i++
402 }
403
404 // Sort
405 sort.Slice(result, func(i, j int) bool {
406 return string(result[i]) < string(result[j])
407 })
408
409 return result
410}
411
412// NewBug create a new bug
413// The new bug is written in the repository (commit)
414func (c *RepoCache) NewBug(title string, message string) (*BugCache, *bug.CreateOperation, error) {
415 return c.NewBugWithFiles(title, message, nil)
416}
417
418// NewBugWithFiles create a new bug with attached files for the message
419// The new bug is written in the repository (commit)
420func (c *RepoCache) NewBugWithFiles(title string, message string, files []repository.Hash) (*BugCache, *bug.CreateOperation, error) {
421 author, err := c.GetUserIdentity()
422 if err != nil {
423 return nil, nil, err
424 }
425
426 return c.NewBugRaw(author, time.Now().Unix(), title, message, files, nil)
427}
428
429// NewBugWithFilesMeta create a new bug with attached files for the message, as
430// well as metadata for the Create operation.
431// The new bug is written in the repository (commit)
432func (c *RepoCache) NewBugRaw(author *IdentityCache, unixTime int64, title string, message string, files []repository.Hash, metadata map[string]string) (*BugCache, *bug.CreateOperation, error) {
433 b, op, err := bug.CreateWithFiles(author.Identity, unixTime, title, message, files)
434 if err != nil {
435 return nil, nil, err
436 }
437
438 for key, value := range metadata {
439 op.SetMetadata(key, value)
440 }
441
442 err = b.Commit(c.repo)
443 if err != nil {
444 return nil, nil, err
445 }
446
447 c.muBug.Lock()
448 if _, has := c.bugs[b.Id()]; has {
449 c.muBug.Unlock()
450 return nil, nil, fmt.Errorf("bug %s already exist in the cache", b.Id())
451 }
452
453 cached := NewBugCache(c, b)
454 c.bugs[b.Id()] = cached
455 c.loadedBugs.Add(b.Id())
456 c.muBug.Unlock()
457
458 c.evictIfNeeded()
459
460 // force the write of the excerpt
461 err = c.bugUpdated(b.Id())
462 if err != nil {
463 return nil, nil, err
464 }
465
466 return cached, op, nil
467}
468
469// RemoveBug removes a bug from the cache and repo given a bug id prefix
470func (c *RepoCache) RemoveBug(prefix string) error {
471 c.muBug.RLock()
472
473 b, err := c.ResolveBugPrefix(prefix)
474 if err != nil {
475 c.muBug.RUnlock()
476 return err
477 }
478 c.muBug.RUnlock()
479
480 c.muBug.Lock()
481 err = bug.RemoveBug(c.repo, b.Id())
482
483 delete(c.bugs, b.Id())
484 delete(c.bugExcerpts, b.Id())
485 c.loadedBugs.Remove(b.Id())
486
487 c.muBug.Unlock()
488
489 return c.writeBugCache()
490}