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