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