1package cache
2
3import (
4 "bytes"
5 "encoding/gob"
6 "fmt"
7 "os"
8 "path"
9 "sort"
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)
17
18const bugCacheFile = "bug-cache"
19
20func bugCacheFilePath(repo repository.Repo) string {
21 return path.Join(repo.GetPath(), "git-bug", bugCacheFile)
22}
23
24// bugUpdated is a callback to trigger when the excerpt of a bug changed,
25// that is each time a bug is updated
26func (c *RepoCache) bugUpdated(id entity.Id) error {
27 err := c.ensureBugLoaded(id)
28 if err != nil {
29 return err
30 }
31
32 c.muBug.Lock()
33 b, _ := c.bugs[id]
34 c.bugExcerpts[id] = NewBugExcerpt(b.bug, b.Snapshot())
35 c.muBug.Unlock()
36
37 // we only need to write the bug cache
38 return c.writeBugCache()
39}
40
41// load will try to read from the disk the bug cache file
42func (c *RepoCache) loadBugCache() error {
43 c.muBug.Lock()
44 defer c.muBug.Unlock()
45
46 f, err := os.Open(bugCacheFilePath(c.repo))
47 if err != nil {
48 return err
49 }
50
51 decoder := gob.NewDecoder(f)
52
53 aux := struct {
54 Version uint
55 Excerpts map[entity.Id]*BugExcerpt
56 }{}
57
58 err = decoder.Decode(&aux)
59 if err != nil {
60 return err
61 }
62
63 if aux.Version != formatVersion {
64 return fmt.Errorf("unknown cache format version %v", aux.Version)
65 }
66
67 c.bugExcerpts = aux.Excerpts
68 return nil
69}
70
71// write will serialize on disk the bug cache file
72func (c *RepoCache) writeBugCache() error {
73 c.muBug.RLock()
74 defer c.muBug.RUnlock()
75
76 var data bytes.Buffer
77
78 aux := struct {
79 Version uint
80 Excerpts map[entity.Id]*BugExcerpt
81 }{
82 Version: formatVersion,
83 Excerpts: c.bugExcerpts,
84 }
85
86 encoder := gob.NewEncoder(&data)
87
88 err := encoder.Encode(aux)
89 if err != nil {
90 return err
91 }
92
93 f, err := os.Create(bugCacheFilePath(c.repo))
94 if err != nil {
95 return err
96 }
97
98 _, err = f.Write(data.Bytes())
99 if err != nil {
100 return err
101 }
102
103 return f.Close()
104}
105
106// ResolveBugExcerpt retrieve a BugExcerpt matching the exact given id
107func (c *RepoCache) ResolveBugExcerpt(id entity.Id) (*BugExcerpt, error) {
108 err := c.ensureBugLoaded(id)
109 if err != nil {
110 return nil, err
111 }
112
113 c.muBug.RLock()
114 defer c.muBug.RUnlock()
115 return c.bugExcerpts[id], nil
116}
117
118// ResolveBug retrieve a bug matching the exact given id
119func (c *RepoCache) ResolveBug(id entity.Id) (*BugCache, error) {
120 err := c.ensureBugLoaded(id)
121 if err != nil {
122 return nil, err
123 }
124
125 c.muBug.RLock()
126 defer c.muBug.RUnlock()
127 return c.bugs[id], nil
128}
129
130func (c *RepoCache) ensureBugLoaded(id entity.Id) error {
131 if c.presentBugs.Get(id) {
132 return nil
133 }
134
135 b, err := bug.ReadLocalBug(c.repo, id)
136 if err != nil {
137 return err
138 }
139
140 bugCache := NewBugCache(c, b)
141
142 c.muBug.Lock()
143 if c.presentBugs.Len() == c.presentBugs.maxSize {
144 for _, id := range c.presentBugs.GetAll() {
145 if b := c.bugs[id]; !b.NeedCommit() {
146 b.mu.Lock()
147 c.presentBugs.Remove(id)
148 delete(c.bugExcerpts, id)
149 delete(c.bugs, id)
150 }
151 }
152 }
153
154 c.presentBugs.Add(id)
155 c.bugs[id] = bugCache
156 excerpt := NewBugExcerpt(b, bugCache.Snapshot()) // TODO: Is this needed?
157 c.bugExcerpts[id] = excerpt
158
159 c.muBug.Unlock()
160 return nil
161}
162
163// ResolveBugExcerptPrefix retrieve a BugExcerpt matching an id prefix. It fails if multiple
164// bugs match.
165func (c *RepoCache) ResolveBugExcerptPrefix(prefix string) (*BugExcerpt, error) {
166 return c.ResolveBugExcerptMatcher(func(excerpt *BugExcerpt) bool {
167 return excerpt.Id.HasPrefix(prefix)
168 })
169}
170
171// ResolveBugPrefix retrieve a bug matching an id prefix. It fails if multiple
172// bugs match.
173func (c *RepoCache) ResolveBugPrefix(prefix string) (*BugCache, error) {
174 return c.ResolveBugMatcher(func(excerpt *BugExcerpt) bool {
175 return excerpt.Id.HasPrefix(prefix)
176 })
177}
178
179// ResolveBugCreateMetadata retrieve a bug that has the exact given metadata on
180// its Create operation, that is, the first operation. It fails if multiple bugs
181// match.
182func (c *RepoCache) ResolveBugCreateMetadata(key string, value string) (*BugCache, error) {
183 return c.ResolveBugMatcher(func(excerpt *BugExcerpt) bool {
184 return excerpt.CreateMetadata[key] == value
185 })
186}
187
188func (c *RepoCache) ResolveBugExcerptMatcher(f func(*BugExcerpt) bool) (*BugExcerpt, error) {
189 id, err := c.resolveBugMatcher(f)
190 if err != nil {
191 return nil, err
192 }
193 return c.ResolveBugExcerpt(id)
194}
195
196func (c *RepoCache) ResolveBugMatcher(f func(*BugExcerpt) bool) (*BugCache, error) {
197 id, err := c.resolveBugMatcher(f)
198 if err != nil {
199 return nil, err
200 }
201 return c.ResolveBug(id)
202}
203
204func (c *RepoCache) resolveBugMatcher(f func(*BugExcerpt) bool) (entity.Id, error) {
205 c.muBug.RLock()
206 defer c.muBug.RUnlock()
207
208 // preallocate but empty
209 matching := make([]entity.Id, 0, 5)
210
211 for _, excerpt := range c.bugExcerpts {
212 if f(excerpt) {
213 matching = append(matching, excerpt.Id)
214 }
215 }
216
217 if len(matching) > 1 {
218 return entity.UnsetId, bug.NewErrMultipleMatchBug(matching)
219 }
220
221 if len(matching) == 0 {
222 return entity.UnsetId, bug.ErrBugNotExist
223 }
224
225 return matching[0], nil
226}
227
228// QueryBugs return the id of all Bug matching the given Query
229func (c *RepoCache) QueryBugs(q *query.Query) []entity.Id {
230 c.muBug.RLock()
231 defer c.muBug.RUnlock()
232
233 if q == nil {
234 return c.AllBugsIds()
235 }
236
237 matcher := compileMatcher(q.Filters)
238
239 var filtered []*BugExcerpt
240
241 for _, excerpt := range c.bugExcerpts {
242 if matcher.Match(excerpt, c) {
243 filtered = append(filtered, excerpt)
244 }
245 }
246
247 var sorter sort.Interface
248
249 switch q.OrderBy {
250 case query.OrderById:
251 sorter = BugsById(filtered)
252 case query.OrderByCreation:
253 sorter = BugsByCreationTime(filtered)
254 case query.OrderByEdit:
255 sorter = BugsByEditTime(filtered)
256 default:
257 panic("missing sort type")
258 }
259
260 switch q.OrderDirection {
261 case query.OrderAscending:
262 // Nothing to do
263 case query.OrderDescending:
264 sorter = sort.Reverse(sorter)
265 default:
266 panic("missing sort direction")
267 }
268
269 sort.Sort(sorter)
270
271 result := make([]entity.Id, len(filtered))
272
273 for i, val := range filtered {
274 result[i] = val.Id
275 }
276
277 return result
278}
279
280// AllBugsIds return all known bug ids
281func (c *RepoCache) AllBugsIds() []entity.Id {
282 c.muBug.RLock()
283 defer c.muBug.RUnlock()
284
285 result := make([]entity.Id, len(c.bugExcerpts))
286
287 i := 0
288 for _, excerpt := range c.bugExcerpts {
289 result[i] = excerpt.Id
290 i++
291 }
292
293 return result
294}
295
296// ValidLabels list valid labels
297//
298// Note: in the future, a proper label policy could be implemented where valid
299// labels are defined in a configuration file. Until that, the default behavior
300// is to return the list of labels already used.
301func (c *RepoCache) ValidLabels() []bug.Label {
302 c.muBug.RLock()
303 defer c.muBug.RUnlock()
304
305 set := map[bug.Label]interface{}{}
306
307 for _, excerpt := range c.bugExcerpts {
308 for _, l := range excerpt.Labels {
309 set[l] = nil
310 }
311 }
312
313 result := make([]bug.Label, len(set))
314
315 i := 0
316 for l := range set {
317 result[i] = l
318 i++
319 }
320
321 // Sort
322 sort.Slice(result, func(i, j int) bool {
323 return string(result[i]) < string(result[j])
324 })
325
326 return result
327}
328
329// NewBug create a new bug
330// The new bug is written in the repository (commit)
331func (c *RepoCache) NewBug(title string, message string) (*BugCache, *bug.CreateOperation, error) {
332 return c.NewBugWithFiles(title, message, nil)
333}
334
335// NewBugWithFiles create a new bug with attached files for the message
336// The new bug is written in the repository (commit)
337func (c *RepoCache) NewBugWithFiles(title string, message string, files []repository.Hash) (*BugCache, *bug.CreateOperation, error) {
338 author, err := c.GetUserIdentity()
339 if err != nil {
340 return nil, nil, err
341 }
342
343 return c.NewBugRaw(author, time.Now().Unix(), title, message, files, nil)
344}
345
346// NewBugWithFilesMeta create a new bug with attached files for the message, as
347// well as metadata for the Create operation.
348// The new bug is written in the repository (commit)
349func (c *RepoCache) NewBugRaw(author *IdentityCache, unixTime int64, title string, message string, files []repository.Hash, metadata map[string]string) (*BugCache, *bug.CreateOperation, error) {
350 b, op, err := bug.CreateWithFiles(author.Identity, unixTime, title, message, files)
351 if err != nil {
352 return nil, nil, err
353 }
354
355 for key, value := range metadata {
356 op.SetMetadata(key, value)
357 }
358
359 err = b.Commit(c.repo)
360 if err != nil {
361 return nil, nil, err
362 }
363
364 c.muBug.Lock()
365 if _, has := c.bugs[b.Id()]; has {
366 c.muBug.Unlock()
367 return nil, nil, fmt.Errorf("bug %s already exist in the cache", b.Id())
368 }
369
370 cached := NewBugCache(c, b)
371 c.bugs[b.Id()] = cached
372 c.muBug.Unlock()
373
374 // force the write of the excerpt
375 err = c.bugUpdated(b.Id())
376 if err != nil {
377 return nil, nil, err
378 }
379
380 return cached, op, nil
381}
382
383// RemoveBug removes a bug from the cache and repo given a bug id prefix
384func (c *RepoCache) RemoveBug(prefix string) error {
385 b, err := c.ResolveBugPrefix(prefix)
386 if err != nil {
387 return err
388 }
389
390 err = c.ensureBugLoaded(b.Id())
391 if err != nil {
392 return err
393 }
394
395 c.muBug.Lock()
396 b.mu.Lock()
397 fmt.Println("got lock")
398 err = bug.RemoveBug(c.repo, b.Id())
399 if err != nil {
400 c.muBug.Unlock()
401 b.mu.Unlock()
402 return err
403 }
404 fmt.Println("noerr")
405 c.presentBugs.Remove(b.Id())
406 fmt.Println("removing1")
407 delete(c.bugs, b.Id())
408 fmt.Println("removed2")
409 delete(c.bugExcerpts, b.Id())
410 fmt.Println("unlocking")
411
412 c.muBug.Unlock()
413 return c.writeBugCache()
414}
415
416// onEvict will update the bugs and bugExcerpts when a bug is evicted from the cache
417func (c *RepoCache) onEvict(id entity.Id) { // TODO: Do we need this?
418 delete(c.bugs, id)
419 delete(c.bugExcerpts, id)
420}