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