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