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