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