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