repo_cache.go

  1package cache
  2
  3import (
  4	"bytes"
  5	"encoding/gob"
  6	"fmt"
  7	"io"
  8	"io/ioutil"
  9	"os"
 10	"path"
 11	"sort"
 12	"strconv"
 13	"strings"
 14	"time"
 15
 16	"github.com/MichaelMure/git-bug/bug"
 17	"github.com/MichaelMure/git-bug/identity"
 18	"github.com/MichaelMure/git-bug/repository"
 19	"github.com/MichaelMure/git-bug/util/git"
 20	"github.com/MichaelMure/git-bug/util/process"
 21)
 22
 23const cacheFile = "cache"
 24const formatVersion = 1
 25
 26// RepoCache is a cache for a Repository. This cache has multiple functions:
 27//
 28// 1. After being loaded, a Bug is kept in memory in the cache, allowing for fast
 29// 		access later.
 30// 2. The cache maintain on memory and on disk a pre-digested excerpt for each bug,
 31// 		allowing for fast querying the whole set of bugs without having to load
 32//		them individually.
 33// 3. The cache guarantee that a single instance of a Bug is loaded at once, avoiding
 34// 		loss of data that we could have with multiple copies in the same process.
 35// 4. The same way, the cache maintain in memory a single copy of the loaded identities.
 36//
 37// The cache also protect the on-disk data by locking the git repository for its
 38// own usage, by writing a lock file. Of course, normal git operations are not
 39// affected, only git-bug related one.
 40type RepoCache struct {
 41	// the underlying repo
 42	repo repository.ClockedRepo
 43	// excerpt of bugs data for all bugs
 44	excerpts map[string]*BugExcerpt
 45	// bug loaded in memory
 46	bugs map[string]*BugCache
 47	// identities loaded in memory
 48	identities map[string]*identity.Identity
 49}
 50
 51func NewRepoCache(r repository.ClockedRepo) (*RepoCache, error) {
 52	c := &RepoCache{
 53		repo: r,
 54		bugs: make(map[string]*BugCache),
 55	}
 56
 57	err := c.lock()
 58	if err != nil {
 59		return &RepoCache{}, err
 60	}
 61
 62	err = c.load()
 63	if err == nil {
 64		return c, nil
 65	}
 66
 67	err = c.buildCache()
 68	if err != nil {
 69		return nil, err
 70	}
 71
 72	return c, c.write()
 73}
 74
 75// GetPath returns the path to the repo.
 76func (c *RepoCache) GetPath() string {
 77	return c.repo.GetPath()
 78}
 79
 80// GetPath returns the path to the repo.
 81func (c *RepoCache) GetCoreEditor() (string, error) {
 82	return c.repo.GetCoreEditor()
 83}
 84
 85// GetUserName returns the name the the user has used to configure git
 86func (c *RepoCache) GetUserName() (string, error) {
 87	return c.repo.GetUserName()
 88}
 89
 90// GetUserEmail returns the email address that the user has used to configure git.
 91func (c *RepoCache) GetUserEmail() (string, error) {
 92	return c.repo.GetUserEmail()
 93}
 94
 95// StoreConfig store a single key/value pair in the config of the repo
 96func (c *RepoCache) StoreConfig(key string, value string) error {
 97	return c.repo.StoreConfig(key, value)
 98}
 99
100// ReadConfigs read all key/value pair matching the key prefix
101func (c *RepoCache) ReadConfigs(keyPrefix string) (map[string]string, error) {
102	return c.repo.ReadConfigs(keyPrefix)
103}
104
105// RmConfigs remove all key/value pair matching the key prefix
106func (c *RepoCache) RmConfigs(keyPrefix string) error {
107	return c.repo.RmConfigs(keyPrefix)
108}
109
110func (c *RepoCache) lock() error {
111	lockPath := repoLockFilePath(c.repo)
112
113	err := repoIsAvailable(c.repo)
114	if err != nil {
115		return err
116	}
117
118	f, err := os.Create(lockPath)
119	if err != nil {
120		return err
121	}
122
123	pid := fmt.Sprintf("%d", os.Getpid())
124	_, err = f.WriteString(pid)
125	if err != nil {
126		return err
127	}
128
129	return f.Close()
130}
131
132func (c *RepoCache) Close() error {
133	lockPath := repoLockFilePath(c.repo)
134	return os.Remove(lockPath)
135}
136
137// bugUpdated is a callback to trigger when the excerpt of a bug changed,
138// that is each time a bug is updated
139func (c *RepoCache) bugUpdated(id string) error {
140	b, ok := c.bugs[id]
141	if !ok {
142		panic("missing bug in the cache")
143	}
144
145	c.excerpts[id] = NewBugExcerpt(b.bug, b.Snapshot())
146
147	return c.write()
148}
149
150// load will try to read from the disk the bug cache file
151func (c *RepoCache) load() error {
152	f, err := os.Open(cacheFilePath(c.repo))
153	if err != nil {
154		return err
155	}
156
157	decoder := gob.NewDecoder(f)
158
159	aux := struct {
160		Version  uint
161		Excerpts map[string]*BugExcerpt
162	}{}
163
164	err = decoder.Decode(&aux)
165	if err != nil {
166		return err
167	}
168
169	if aux.Version != 1 {
170		return fmt.Errorf("unknown cache format version %v", aux.Version)
171	}
172
173	c.excerpts = aux.Excerpts
174	return nil
175}
176
177// write will serialize on disk the bug cache file
178func (c *RepoCache) write() error {
179	var data bytes.Buffer
180
181	aux := struct {
182		Version  uint
183		Excerpts map[string]*BugExcerpt
184	}{
185		Version:  formatVersion,
186		Excerpts: c.excerpts,
187	}
188
189	encoder := gob.NewEncoder(&data)
190
191	err := encoder.Encode(aux)
192	if err != nil {
193		return err
194	}
195
196	f, err := os.Create(cacheFilePath(c.repo))
197	if err != nil {
198		return err
199	}
200
201	_, err = f.Write(data.Bytes())
202	if err != nil {
203		return err
204	}
205
206	return f.Close()
207}
208
209func cacheFilePath(repo repository.Repo) string {
210	return path.Join(repo.GetPath(), ".git", "git-bug", cacheFile)
211}
212
213func (c *RepoCache) buildCache() error {
214	_, _ = fmt.Fprintf(os.Stderr, "Building bug cache... ")
215
216	c.excerpts = make(map[string]*BugExcerpt)
217
218	allBugs := bug.ReadAllLocalBugs(c.repo)
219
220	for b := range allBugs {
221		if b.Err != nil {
222			return b.Err
223		}
224
225		snap := b.Bug.Compile()
226		c.excerpts[b.Bug.Id()] = NewBugExcerpt(b.Bug, &snap)
227	}
228
229	_, _ = fmt.Fprintln(os.Stderr, "Done.")
230	return nil
231}
232
233// ResolveBug retrieve a bug matching the exact given id
234func (c *RepoCache) ResolveBug(id string) (*BugCache, error) {
235	cached, ok := c.bugs[id]
236	if ok {
237		return cached, nil
238	}
239
240	b, err := bug.ReadLocalBug(c.repo, id)
241	if err != nil {
242		return nil, err
243	}
244
245	cached = NewBugCache(c, b)
246	c.bugs[id] = cached
247
248	return cached, nil
249}
250
251// ResolveBugPrefix retrieve a bug matching an id prefix. It fails if multiple
252// bugs match.
253func (c *RepoCache) ResolveBugPrefix(prefix string) (*BugCache, error) {
254	// preallocate but empty
255	matching := make([]string, 0, 5)
256
257	for id := range c.excerpts {
258		if strings.HasPrefix(id, prefix) {
259			matching = append(matching, id)
260		}
261	}
262
263	if len(matching) > 1 {
264		return nil, bug.ErrMultipleMatch{Matching: matching}
265	}
266
267	if len(matching) == 0 {
268		return nil, bug.ErrBugNotExist
269	}
270
271	return c.ResolveBug(matching[0])
272}
273
274// ResolveBugCreateMetadata retrieve a bug that has the exact given metadata on
275// its Create operation, that is, the first operation. It fails if multiple bugs
276// match.
277func (c *RepoCache) ResolveBugCreateMetadata(key string, value string) (*BugCache, error) {
278	// preallocate but empty
279	matching := make([]string, 0, 5)
280
281	for id, excerpt := range c.excerpts {
282		if excerpt.CreateMetadata[key] == value {
283			matching = append(matching, id)
284		}
285	}
286
287	if len(matching) > 1 {
288		return nil, bug.ErrMultipleMatch{Matching: matching}
289	}
290
291	if len(matching) == 0 {
292		return nil, bug.ErrBugNotExist
293	}
294
295	return c.ResolveBug(matching[0])
296}
297
298// QueryBugs return the id of all Bug matching the given Query
299func (c *RepoCache) QueryBugs(query *Query) []string {
300	if query == nil {
301		return c.AllBugsIds()
302	}
303
304	var filtered []*BugExcerpt
305
306	for _, excerpt := range c.excerpts {
307		if query.Match(excerpt) {
308			filtered = append(filtered, excerpt)
309		}
310	}
311
312	var sorter sort.Interface
313
314	switch query.OrderBy {
315	case OrderById:
316		sorter = BugsById(filtered)
317	case OrderByCreation:
318		sorter = BugsByCreationTime(filtered)
319	case OrderByEdit:
320		sorter = BugsByEditTime(filtered)
321	default:
322		panic("missing sort type")
323	}
324
325	if query.OrderDirection == OrderDescending {
326		sorter = sort.Reverse(sorter)
327	}
328
329	sort.Sort(sorter)
330
331	result := make([]string, len(filtered))
332
333	for i, val := range filtered {
334		result[i] = val.Id
335	}
336
337	return result
338}
339
340// AllBugsIds return all known bug ids
341func (c *RepoCache) AllBugsIds() []string {
342	result := make([]string, len(c.excerpts))
343
344	i := 0
345	for _, excerpt := range c.excerpts {
346		result[i] = excerpt.Id
347		i++
348	}
349
350	return result
351}
352
353// ClearAllBugs clear all bugs kept in memory
354func (c *RepoCache) ClearAllBugs() {
355	c.bugs = make(map[string]*BugCache)
356}
357
358// ValidLabels list valid labels
359//
360// Note: in the future, a proper label policy could be implemented where valid
361// labels are defined in a configuration file. Until that, the default behavior
362// is to return the list of labels already used.
363func (c *RepoCache) ValidLabels() []bug.Label {
364	set := map[bug.Label]interface{}{}
365
366	for _, excerpt := range c.excerpts {
367		for _, l := range excerpt.Labels {
368			set[l] = nil
369		}
370	}
371
372	result := make([]bug.Label, len(set))
373
374	i := 0
375	for l := range set {
376		result[i] = l
377		i++
378	}
379
380	// Sort
381	sort.Slice(result, func(i, j int) bool {
382		return string(result[i]) < string(result[j])
383	})
384
385	return result
386}
387
388// NewBug create a new bug
389// The new bug is written in the repository (commit)
390func (c *RepoCache) NewBug(title string, message string) (*BugCache, error) {
391	return c.NewBugWithFiles(title, message, nil)
392}
393
394// NewBugWithFiles create a new bug with attached files for the message
395// The new bug is written in the repository (commit)
396func (c *RepoCache) NewBugWithFiles(title string, message string, files []git.Hash) (*BugCache, error) {
397	author, err := identity.GetUserIdentity(c.repo)
398	if err != nil {
399		return nil, err
400	}
401
402	return c.NewBugRaw(author, time.Now().Unix(), title, message, files, nil)
403}
404
405// NewBugWithFilesMeta create a new bug with attached files for the message, as
406// well as metadata for the Create operation.
407// The new bug is written in the repository (commit)
408func (c *RepoCache) NewBugRaw(author *identity.Identity, unixTime int64, title string, message string, files []git.Hash, metadata map[string]string) (*BugCache, error) {
409	b, op, err := bug.CreateWithFiles(author, unixTime, title, message, files)
410	if err != nil {
411		return nil, err
412	}
413
414	for key, value := range metadata {
415		op.SetMetadata(key, value)
416	}
417
418	err = b.Commit(c.repo)
419	if err != nil {
420		return nil, err
421	}
422
423	if _, has := c.bugs[b.Id()]; has {
424		return nil, fmt.Errorf("bug %s already exist in the cache", b.Id())
425	}
426
427	cached := NewBugCache(c, b)
428	c.bugs[b.Id()] = cached
429
430	// force the write of the excerpt
431	err = c.bugUpdated(b.Id())
432	if err != nil {
433		return nil, err
434	}
435
436	return cached, nil
437}
438
439// Fetch retrieve update from a remote
440// This does not change the local bugs state
441func (c *RepoCache) Fetch(remote string) (string, error) {
442	return bug.Fetch(c.repo, remote)
443}
444
445// MergeAll will merge all the available remote bug
446func (c *RepoCache) MergeAll(remote string) <-chan bug.MergeResult {
447	out := make(chan bug.MergeResult)
448
449	// Intercept merge results to update the cache properly
450	go func() {
451		defer close(out)
452
453		results := bug.MergeAll(c.repo, remote)
454		for result := range results {
455			out <- result
456
457			if result.Err != nil {
458				continue
459			}
460
461			id := result.Id
462
463			switch result.Status {
464			case bug.MergeStatusNew, bug.MergeStatusUpdated:
465				b := result.Bug
466				snap := b.Compile()
467				c.excerpts[id] = NewBugExcerpt(b, &snap)
468			}
469		}
470
471		err := c.write()
472
473		// No easy way out here ..
474		if err != nil {
475			panic(err)
476		}
477	}()
478
479	return out
480}
481
482// Push update a remote with the local changes
483func (c *RepoCache) Push(remote string) (string, error) {
484	return bug.Push(c.repo, remote)
485}
486
487func repoLockFilePath(repo repository.Repo) string {
488	return path.Join(repo.GetPath(), ".git", "git-bug", lockfile)
489}
490
491// repoIsAvailable check is the given repository is locked by a Cache.
492// Note: this is a smart function that will cleanup the lock file if the
493// corresponding process is not there anymore.
494// If no error is returned, the repo is free to edit.
495func repoIsAvailable(repo repository.Repo) error {
496	lockPath := repoLockFilePath(repo)
497
498	// Todo: this leave way for a racey access to the repo between the test
499	// if the file exist and the actual write. It's probably not a problem in
500	// practice because using a repository will be done from user interaction
501	// or in a context where a single instance of git-bug is already guaranteed
502	// (say, a server with the web UI running). But still, that might be nice to
503	// have a mutex or something to guard that.
504
505	// Todo: this will fail if somehow the filesystem is shared with another
506	// computer. Should add a configuration that prevent the cleaning of the
507	// lock file
508
509	f, err := os.Open(lockPath)
510
511	if err != nil && !os.IsNotExist(err) {
512		return err
513	}
514
515	if err == nil {
516		// lock file already exist
517		buf, err := ioutil.ReadAll(io.LimitReader(f, 10))
518		if err != nil {
519			return err
520		}
521		if len(buf) == 10 {
522			return fmt.Errorf("the lock file should be < 10 bytes")
523		}
524
525		pid, err := strconv.Atoi(string(buf))
526		if err != nil {
527			return err
528		}
529
530		if process.IsRunning(pid) {
531			return fmt.Errorf("the repository you want to access is already locked by the process pid %d", pid)
532		}
533
534		// The lock file is just laying there after a crash, clean it
535
536		fmt.Println("A lock file is present but the corresponding process is not, removing it.")
537		err = f.Close()
538		if err != nil {
539			return err
540		}
541
542		err = os.Remove(lockPath)
543		if err != nil {
544			return err
545		}
546	}
547
548	return nil
549}
550
551// ResolveIdentity retrieve an identity matching the exact given id
552func (c *RepoCache) ResolveIdentity(id string) (*identity.Identity, error) {
553	cached, ok := c.identities[id]
554	if ok {
555		return cached, nil
556	}
557
558	i, err := identity.Read(c.repo, id)
559	if err != nil {
560		return nil, err
561	}
562
563	c.identities[id] = i
564
565	return i, nil
566}
567
568// ResolveIdentityPrefix retrieve an Identity matching an id prefix.
569// It fails if multiple identities match.
570func (c *RepoCache) ResolveIdentityPrefix(prefix string) (*identity.Identity, error) {
571	// preallocate but empty
572	matching := make([]string, 0, 5)
573
574	for id := range c.identities {
575		if strings.HasPrefix(id, prefix) {
576			matching = append(matching, id)
577		}
578	}
579
580	if len(matching) > 1 {
581		return nil, identity.ErrMultipleMatch{Matching: matching}
582	}
583
584	if len(matching) == 0 {
585		return nil, identity.ErrIdentityNotExist
586	}
587
588	return c.ResolveIdentity(matching[0])
589}
590
591// ResolveIdentityImmutableMetadata retrieve an Identity that has the exact given metadata on
592// one of it's version. If multiple version have the same key, the first defined take precedence.
593func (c *RepoCache) ResolveIdentityImmutableMetadata(key string, value string) (*identity.Identity, error) {
594	// preallocate but empty
595	matching := make([]string, 0, 5)
596
597	for id, i := range c.identities {
598		if i.ImmutableMetadata()[key] == value {
599			matching = append(matching, id)
600		}
601	}
602
603	if len(matching) > 1 {
604		return nil, identity.ErrMultipleMatch{Matching: matching}
605	}
606
607	if len(matching) == 0 {
608		return nil, identity.ErrIdentityNotExist
609	}
610
611	return c.ResolveIdentity(matching[0])
612}
613
614// NewIdentity create a new identity
615// The new identity is written in the repository (commit)
616func (c *RepoCache) NewIdentity(name string, email string) (*identity.Identity, error) {
617	return c.NewIdentityRaw(name, email, "", "", nil)
618}
619
620// NewIdentityFull create a new identity
621// The new identity is written in the repository (commit)
622func (c *RepoCache) NewIdentityFull(name string, email string, login string, avatarUrl string) (*identity.Identity, error) {
623	return c.NewIdentityRaw(name, email, login, avatarUrl, nil)
624}
625
626func (c *RepoCache) NewIdentityRaw(name string, email string, login string, avatarUrl string, metadata map[string]string) (*identity.Identity, error) {
627	i := identity.NewIdentityFull(name, email, login, avatarUrl)
628
629	for key, value := range metadata {
630		i.SetMetadata(key, value)
631	}
632
633	err := i.Commit(c.repo)
634	if err != nil {
635		return nil, err
636	}
637
638	if _, has := c.identities[i.Id()]; has {
639		return nil, fmt.Errorf("identity %s already exist in the cache", i.Id())
640	}
641
642	c.identities[i.Id()] = i
643
644	return i, nil
645}