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