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