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}