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