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