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