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