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