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