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}