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	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}