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