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