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