repo_cache_bug.go

  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}