repo_cache.go

  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}