WIP

Michael Muré created

Change summary

cache/bug_cache.go         |  6 +-
cache/bug_subcache.go      | 96 +++++++++++++++------------------------
cache/cached.go            |  5 -
cache/identity_cache.go    | 10 ++--
cache/identity_subcache.go | 33 ++++++++++++
cache/repo_cache.go        | 86 +++++++++--------------------------
cache/repo_cache_common.go | 64 ++++++++++++-------------
cache/subcache.go          | 80 ++++++++++++++++++++-------------
commands/execenv/env.go    | 21 ++++++++
entity/resolver.go         |  8 +-
10 files changed, 204 insertions(+), 205 deletions(-)

Detailed changes

cache/bug_cache.go 🔗

@@ -22,12 +22,12 @@ type BugCache struct {
 	CachedEntityBase[*bug.Snapshot, bug.Operation]
 }
 
-func NewBugCache(subcache *RepoCacheBug, getUserIdentity func() (identity.Interface, error), b *bug.Bug) *BugCache {
+func NewBugCache(b *bug.Bug, repo repository.ClockedRepo, getUserIdentity getUserIdentityFunc, entityUpdated func(id entity.Id) error) *BugCache {
 	return &BugCache{
 		CachedEntityBase: CachedEntityBase[*bug.Snapshot, bug.Operation]{
-			entityUpdated:   subcache.entityUpdated,
+			repo:            repo,
+			entityUpdated:   entityUpdated,
 			getUserIdentity: getUserIdentity,
-			repo:            subcache.repo,
 			entity:          &bug.WithSnapshot{Bug: b},
 		},
 	}

cache/bug_subcache.go 🔗

@@ -2,13 +2,8 @@ package cache
 
 import (
 	"errors"
-	"fmt"
 	"sort"
-	"strings"
 	"time"
-	"unicode/utf8"
-
-	"github.com/blevesearch/bleve"
 
 	"github.com/MichaelMure/git-bug/entities/bug"
 	"github.com/MichaelMure/git-bug/entities/identity"
@@ -18,7 +13,39 @@ import (
 )
 
 type RepoCacheBug struct {
-	*SubCache[*BugExcerpt, *BugCache, bug.Interface]
+	*SubCache[*bug.Bug, *BugExcerpt, *BugCache]
+}
+
+func NewRepoCacheBug(repo repository.ClockedRepo,
+	resolvers func() entity.Resolvers,
+	getUserIdentity getUserIdentityFunc) *RepoCacheBug {
+
+	makeCached := func(b *bug.Bug, entityUpdated func(id entity.Id) error) *BugCache {
+		return NewBugCache(b, repo, getUserIdentity, entityUpdated)
+	}
+
+	makeExcerpt := func(b *bug.Bug) *BugExcerpt {
+		return NewBugExcerpt(b, b.Compile())
+	}
+
+	makeIndex := func(b *BugCache) []string {
+		snap := b.Snapshot()
+		var res []string
+		for _, comment := range snap.Comments {
+			res = append(res, comment.Message)
+		}
+		res = append(res, snap.Title)
+		return res
+	}
+
+	sc := NewSubCache[*bug.Bug, *BugExcerpt, *BugCache](
+		repo, resolvers, getUserIdentity,
+		makeCached, makeExcerpt, makeIndex,
+		"bug", "bugs",
+		formatVersion, defaultMaxLoadedBugs,
+	)
+
+	return &RepoCacheBug{SubCache: sc}
 }
 
 // ResolveBugCreateMetadata retrieve a bug that has the exact given metadata on
@@ -94,29 +121,19 @@ func (c *RepoCacheBug) QueryBugs(q *query.Query) ([]entity.Id, error) {
 	if q.Search != nil {
 		foundBySearch = map[entity.Id]*BugExcerpt{}
 
-		terms := make([]string, len(q.Search))
-		copy(terms, q.Search)
-		for i, search := range q.Search {
-			if strings.Contains(search, " ") {
-				terms[i] = fmt.Sprintf("\"%s\"", search)
-			}
-		}
-
-		bleveQuery := bleve.NewQueryStringQuery(strings.Join(terms, " "))
-		bleveSearch := bleve.NewSearchRequest(bleveQuery)
-
-		index, err := c.repo.GetBleveIndex("bug")
+		index, err := c.repo.GetIndex("bug")
 		if err != nil {
 			return nil, err
 		}
 
-		searchResults, err := index.Search(bleveSearch)
+		res, err := index.Search(q.Search)
 		if err != nil {
 			return nil, err
 		}
 
-		for _, hit := range searchResults.Hits {
-			foundBySearch[entity.Id(hit.ID)] = c.excerpts[entity.Id(hit.ID)]
+		for _, hit := range res {
+			id := entity.Id(hit)
+			foundBySearch[id] = c.excerpts[id]
 		}
 	} else {
 		foundBySearch = c.excerpts
@@ -232,40 +249,3 @@ func (c *RepoCacheBug) NewRaw(author identity.Interface, unixTime int64, title s
 
 	return cached, op, nil
 }
-
-func (c *RepoCacheBug) addBugToSearchIndex(snap *bug.Snapshot) error {
-	searchableBug := struct {
-		Text []string
-	}{}
-
-	// See https://github.com/blevesearch/bleve/issues/1576
-	var sb strings.Builder
-	normalize := func(text string) string {
-		sb.Reset()
-		for _, field := range strings.Fields(text) {
-			if utf8.RuneCountInString(field) < 100 {
-				sb.WriteString(field)
-				sb.WriteRune(' ')
-			}
-		}
-		return sb.String()
-	}
-
-	for _, comment := range snap.Comments {
-		searchableBug.Text = append(searchableBug.Text, normalize(comment.Message))
-	}
-
-	searchableBug.Text = append(searchableBug.Text, normalize(snap.Title))
-
-	index, err := c.repo.GetBleveIndex("bug")
-	if err != nil {
-		return err
-	}
-
-	err = index.Index(snap.Id().String(), searchableBug)
-	if err != nil {
-		return err
-	}
-
-	return nil
-}

cache/cached.go 🔗

@@ -4,7 +4,6 @@ import (
 	"sync"
 
 	"github.com/MichaelMure/git-bug/entities/bug"
-	"github.com/MichaelMure/git-bug/entities/identity"
 	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/entity/dag"
 	"github.com/MichaelMure/git-bug/repository"
@@ -57,9 +56,9 @@ import (
 // }
 
 type CachedEntityBase[SnapT dag.Snapshot, OpT dag.Operation] struct {
-	entityUpdated   func(id entity.Id) error
-	getUserIdentity func() (identity.Interface, error)
 	repo            repository.ClockedRepo
+	entityUpdated   func(id entity.Id) error
+	getUserIdentity getUserIdentityFunc
 
 	mu     sync.RWMutex
 	entity dag.Interface[SnapT, OpT]

cache/identity_cache.go 🔗

@@ -10,17 +10,17 @@ var _ identity.Interface = &IdentityCache{}
 
 // IdentityCache is a wrapper around an Identity for caching.
 type IdentityCache struct {
-	entityUpdated func(id entity.Id) error
 	repo          repository.ClockedRepo
+	entityUpdated func(id entity.Id) error
 
 	*identity.Identity
 }
 
-func NewIdentityCache(subcache *RepoCacheIdentity, id *identity.Identity) *IdentityCache {
+func NewIdentityCache(i *identity.Identity, repo repository.ClockedRepo, entityUpdated func(id entity.Id) error) *IdentityCache {
 	return &IdentityCache{
-		entityUpdated: subcache.entityUpdated,
-		repo:          subcache.repo,
-		Identity:      id,
+		repo:          repo,
+		entityUpdated: entityUpdated,
+		Identity:      i,
 	}
 }
 

cache/identity_subcache.go 🔗

@@ -4,10 +4,39 @@ import (
 	"fmt"
 
 	"github.com/MichaelMure/git-bug/entities/identity"
+	"github.com/MichaelMure/git-bug/entity"
+	"github.com/MichaelMure/git-bug/repository"
 )
 
 type RepoCacheIdentity struct {
-	SubCache[*IdentityExcerpt, *IdentityCache, identity.Interface]
+	*SubCache[*identity.Identity, *IdentityExcerpt, *IdentityCache]
+}
+
+func NewRepoCacheIdentity(repo repository.ClockedRepo,
+	resolvers func() entity.Resolvers,
+	getUserIdentity getUserIdentityFunc) *RepoCacheIdentity {
+
+	makeCached := func(i *identity.Identity, entityUpdated func(id entity.Id) error) *IdentityCache {
+		return NewIdentityCache(i, repo, entityUpdated)
+	}
+
+	makeExcerpt := func(i *identity.Identity) *IdentityExcerpt {
+		return NewIdentityExcerpt(i)
+	}
+
+	makeIndex := func(i *IdentityCache) []string {
+		// no indexing
+		return nil
+	}
+
+	sc := NewSubCache[*identity.Identity, *IdentityExcerpt, *IdentityCache](
+		repo, resolvers, getUserIdentity,
+		makeCached, makeExcerpt, makeIndex,
+		"identity", "identities",
+		formatVersion, defaultMaxLoadedBugs,
+	)
+
+	return &RepoCacheIdentity{SubCache: sc}
 }
 
 // ResolveIdentityImmutableMetadata retrieve an Identity that has the exact given metadata on
@@ -65,7 +94,7 @@ func (c *RepoCacheIdentity) finishIdentity(i *identity.Identity, metadata map[st
 		return nil, fmt.Errorf("identity %s already exist in the cache", i.Id())
 	}
 
-	cached := NewIdentityCache(c, i)
+	cached := NewIdentityCache(i, c.repo, c.entityUpdated)
 	c.cached[i.Id()] = cached
 	c.mu.Unlock()
 

cache/repo_cache.go 🔗

@@ -8,8 +8,6 @@ import (
 	"strconv"
 	"sync"
 
-	"github.com/MichaelMure/git-bug/entities/bug"
-	"github.com/MichaelMure/git-bug/entities/identity"
 	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/MichaelMure/git-bug/util/multierr"
@@ -67,6 +65,7 @@ type RepoCache struct {
 	subcaches []cacheMgmt
 
 	// the user identity's id, if known
+	muUserIdentity sync.RWMutex
 	userIdentityId entity.Id
 }
 
@@ -80,17 +79,15 @@ func NewNamedRepoCache(r repository.ClockedRepo, name string) (*RepoCache, chan
 		name: name,
 	}
 
-	bugs := NewSubCache[*BugExcerpt, *BugCache, bug.Interface](r,
-		c.getResolvers, c.GetUserIdentity,
-		"bug", "bugs",
-		formatVersion, defaultMaxLoadedBugs)
+	c.identities = NewRepoCacheIdentity(r, c.getResolvers, c.GetUserIdentity)
+	c.subcaches = append(c.subcaches, c.identities)
 
-	c.subcaches = append(c.subcaches, bugs)
-	c.bugs = &RepoCacheBug{SubCache: *bugs}
+	c.bugs = NewRepoCacheBug(r, c.getResolvers, c.GetUserIdentity)
+	c.subcaches = append(c.subcaches, c.bugs)
 
 	c.resolvers = entity.Resolvers{
-		&IdentityCache{}: entity.ResolverFunc((func(id entity.Id) (entity.Interface, error)(c.identities.Resolve)),
-		&BugCache{}:      c.bugs,
+		&IdentityCache{}: entity.ResolverFunc[*IdentityCache](c.identities.Resolve),
+		&BugCache{}:      entity.ResolverFunc[*BugCache](c.bugs.Resolve),
 	}
 
 	err := c.lock()
@@ -104,12 +101,9 @@ func NewNamedRepoCache(r repository.ClockedRepo, name string) (*RepoCache, chan
 	}
 
 	// Cache is either missing, broken or outdated. Rebuilding.
-	err = c.buildCache()
-	if err != nil {
-		return nil, err
-	}
+	events := c.buildCache()
 
-	return c, c.write()
+	return c, events, nil
 }
 
 // Bugs gives access to the Bug entities
@@ -198,8 +192,8 @@ const (
 
 type BuildEvent struct {
 	Typename string
-	Event BuildEventType
-	Err error
+	Event    BuildEventType
+	Err      error
 }
 
 func (c *RepoCache) buildCache() chan BuildEvent {
@@ -222,14 +216,23 @@ func (c *RepoCache) buildCache() chan BuildEvent {
 				if err != nil {
 					out <- BuildEvent{
 						Typename: subcache.Typename(),
-						Err: err,
+						Err:      err,
+					}
+					return
+				}
+
+				err = subcache.Write()
+				if err != nil {
+					out <- BuildEvent{
+						Typename: subcache.Typename(),
+						Err:      err,
 					}
 					return
 				}
 
 				out <- BuildEvent{
 					Typename: subcache.Typename(),
-					Event: BuildEventFinished,
+					Event:    BuildEventFinished,
 				}
 			}(subcache)
 		}
@@ -237,51 +240,6 @@ func (c *RepoCache) buildCache() chan BuildEvent {
 	}()
 
 	return out
-
-	_, _ = fmt.Fprintf(os.Stderr, "Building identity cache... ")
-
-	c.identitiesExcerpts = make(map[entity.Id]*IdentityExcerpt)
-
-	allIdentities := identity.ReadAllLocal(c.repo)
-
-	for i := range allIdentities {
-		if i.Err != nil {
-			return i.Err
-		}
-
-		c.identitiesExcerpts[i.Identity.Id()] = NewIdentityExcerpt(i.Identity)
-	}
-
-	_, _ = fmt.Fprintln(os.Stderr, "Done.")
-
-	_, _ = fmt.Fprintf(os.Stderr, "Building bug cache... ")
-
-	c.bugExcerpts = make(map[entity.Id]*BugExcerpt)
-
-	allBugs := bug.ReadAllWithResolver(c.repo, c.resolvers)
-
-	// wipe the index just to be sure
-	err := c.repo.ClearBleveIndex("bug")
-	if err != nil {
-		return err
-	}
-
-	for b := range allBugs {
-		if b.Err != nil {
-			return b.Err
-		}
-
-		snap := b.Bug.Compile()
-		c.bugExcerpts[b.Bug.Id()] = NewBugExcerpt(b.Bug, snap)
-
-		if err := c.addBugToSearchIndex(snap); err != nil {
-			return err
-		}
-	}
-
-	_, _ = fmt.Fprintln(os.Stderr, "Done.")
-
-	return nil
 }
 
 // repoIsAvailable check is the given repository is locked by a Cache.

cache/repo_cache_common.go 🔗

@@ -1,8 +1,6 @@
 package cache
 
 import (
-	"fmt"
-
 	"github.com/go-git/go-billy/v5"
 	"github.com/pkg/errors"
 
@@ -186,64 +184,64 @@ func (c *RepoCache) Pull(remote string) error {
 }
 
 func (c *RepoCache) SetUserIdentity(i *IdentityCache) error {
-	err := identity.SetUserIdentity(c.repo, i.Identity)
-	if err != nil {
-		return err
-	}
-
-	c.muIdentity.RLock()
-	defer c.muIdentity.RUnlock()
+	c.muUserIdentity.RLock()
+	defer c.muUserIdentity.RUnlock()
 
 	// Make sure that everything is fine
-	if _, ok := c.identities[i.Id()]; !ok {
+	if _, err := c.identities.Resolve(i.Id()); err != nil {
 		panic("SetUserIdentity while the identity is not from the cache, something is wrong")
 	}
 
+	err := identity.SetUserIdentity(c.repo, i.Identity)
+	if err != nil {
+		return err
+	}
+
 	c.userIdentityId = i.Id()
 
 	return nil
 }
 
 func (c *RepoCache) GetUserIdentity() (*IdentityCache, error) {
+	c.muUserIdentity.RLock()
 	if c.userIdentityId != "" {
-		i, ok := c.identities[c.userIdentityId]
-		if ok {
-			return i, nil
-		}
+		defer c.muUserIdentity.RUnlock()
+		return c.identities.Resolve(c.userIdentityId)
 	}
+	c.muUserIdentity.RUnlock()
 
-	c.muIdentity.Lock()
-	defer c.muIdentity.Unlock()
+	c.muUserIdentity.Lock()
+	defer c.muUserIdentity.Unlock()
 
-	i, err := identity.GetUserIdentity(c.repo)
+	i, err := identity.GetUserIdentityId(c.repo)
 	if err != nil {
 		return nil, err
 	}
 
-	cached := NewIdentityCache(c, i)
-	c.identities[i.Id()] = cached
-	c.userIdentityId = i.Id()
+	c.userIdentityId = i
 
-	return cached, nil
+	return c.identities.Resolve(i)
 }
 
 func (c *RepoCache) GetUserIdentityExcerpt() (*IdentityExcerpt, error) {
-	if c.userIdentityId == "" {
-		id, err := identity.GetUserIdentityId(c.repo)
-		if err != nil {
-			return nil, err
-		}
-		c.userIdentityId = id
+	c.muUserIdentity.RLock()
+	if c.userIdentityId != "" {
+		defer c.muUserIdentity.RUnlock()
+		return c.identities.ResolveExcerpt(c.userIdentityId)
 	}
+	c.muUserIdentity.RUnlock()
 
-	c.muIdentity.RLock()
-	defer c.muIdentity.RUnlock()
+	c.muUserIdentity.Lock()
+	defer c.muUserIdentity.Unlock()
 
-	excerpt, ok := c.identitiesExcerpts[c.userIdentityId]
-	if !ok {
-		return nil, fmt.Errorf("cache: missing identity excerpt %v", c.userIdentityId)
+	i, err := identity.GetUserIdentityId(c.repo)
+	if err != nil {
+		return nil, err
 	}
-	return excerpt, nil
+
+	c.userIdentityId = i
+
+	return c.identities.ResolveExcerpt(i)
 }
 
 func (c *RepoCache) IsUserIdentitySet() (bool, error) {

cache/subcache.go 🔗

@@ -19,20 +19,21 @@ type Excerpt interface {
 }
 
 type CacheEntity interface {
+	Id() entity.Id
 	NeedCommit() bool
 }
 
 type getUserIdentityFunc func() (*IdentityCache, error)
 
-type SubCache[ExcerptT Excerpt, CacheT CacheEntity, EntityT entity.Interface] struct {
+type SubCache[EntityT entity.Interface, ExcerptT Excerpt, CacheT CacheEntity] struct {
 	repo      repository.ClockedRepo
 	resolvers func() entity.Resolvers
 
 	getUserIdentity  getUserIdentityFunc
 	readWithResolver func(repository.ClockedRepo, entity.Resolvers, entity.Id) (EntityT, error)
-	makeCached       func(*SubCache[ExcerptT, CacheT, EntityT], getUserIdentityFunc, EntityT) CacheT
-	makeExcerpt      func() Excerpt
-	indexingCallback func(CacheT) error
+	makeCached       func(entity EntityT, entityUpdated func(id entity.Id) error) CacheT
+	makeExcerpt      func(EntityT) ExcerptT
+	makeIndex        func(CacheT) []string
 
 	typename  string
 	namespace string
@@ -45,13 +46,15 @@ type SubCache[ExcerptT Excerpt, CacheT CacheEntity, EntityT entity.Interface] st
 	lru      *lruIdCache
 }
 
-func NewSubCache[ExcerptT Excerpt, CacheT CacheEntity, EntityT entity.Interface](
+func NewSubCache[EntityT entity.Interface, ExcerptT Excerpt, CacheT CacheEntity](
 	repo repository.ClockedRepo,
-	resolvers func() entity.Resolvers,
-	getUserIdentity getUserIdentityFunc,
+	resolvers func() entity.Resolvers, getUserIdentity getUserIdentityFunc,
+	makeCached func(entity EntityT, entityUpdated func(id entity.Id) error) CacheT,
+	makeExcerpt func(EntityT) ExcerptT,
+	makeIndex func(CacheT) []string,
 	typename, namespace string,
-	version uint, maxLoaded int) *SubCache[ExcerptT, CacheT, EntityT] {
-	return &SubCache[ExcerptT, CacheT, EntityT]{
+	version uint, maxLoaded int) *SubCache[EntityT, ExcerptT, CacheT] {
+	return &SubCache[EntityT, ExcerptT, CacheT]{
 		repo:            repo,
 		resolvers:       resolvers,
 		getUserIdentity: getUserIdentity,
@@ -65,12 +68,12 @@ func NewSubCache[ExcerptT Excerpt, CacheT CacheEntity, EntityT entity.Interface]
 	}
 }
 
-func (sc *SubCache[ExcerptT, CacheT, EntityT]) Typename() string {
+func (sc *SubCache[EntityT, ExcerptT, CacheT]) Typename() string {
 	return sc.typename
 }
 
 // Load will try to read from the disk the entity cache file
-func (sc *SubCache[ExcerptT, CacheT, EntityT]) Load() error {
+func (sc *SubCache[EntityT, ExcerptT, CacheT]) Load() error {
 	sc.mu.Lock()
 	defer sc.mu.Unlock()
 
@@ -97,7 +100,7 @@ func (sc *SubCache[ExcerptT, CacheT, EntityT]) Load() error {
 
 	sc.excerpts = aux.Excerpts
 
-	index, err := sc.repo.GetBleveIndex("bug")
+	index, err := sc.repo.GetIndex(sc.typename)
 	if err != nil {
 		return err
 	}
@@ -115,7 +118,7 @@ func (sc *SubCache[ExcerptT, CacheT, EntityT]) Load() error {
 }
 
 // Write will serialize on disk the entity cache file
-func (sc *SubCache[ExcerptT, CacheT, EntityT]) Write() error {
+func (sc *SubCache[EntityT, ExcerptT, CacheT]) Write() error {
 	sc.mu.RLock()
 	defer sc.mu.RUnlock()
 
@@ -149,19 +152,26 @@ func (sc *SubCache[ExcerptT, CacheT, EntityT]) Write() error {
 	return f.Close()
 }
 
-func (sc *SubCache[ExcerptT, CacheT, EntityT]) Build() error {
+func (sc *SubCache[EntityT, ExcerptT, CacheT]) Build() error {
 	sc.excerpts = make(map[entity.Id]ExcerptT)
 
 	sc.readWithResolver
 
 	allBugs := bug.ReadAllWithResolver(c.repo, c.resolvers)
 
+	index, err := sc.repo.GetIndex(sc.typename)
+	if err != nil {
+		return err
+	}
+
 	// wipe the index just to be sure
-	err := c.repo.ClearBleveIndex("bug")
+	err = index.Clear()
 	if err != nil {
 		return err
 	}
 
+	indexer, indexEnd := index.IndexBatch()
+
 	for b := range allBugs {
 		if b.Err != nil {
 			return b.Err
@@ -170,15 +180,21 @@ func (sc *SubCache[ExcerptT, CacheT, EntityT]) Build() error {
 		snap := b.Bug.Compile()
 		c.bugExcerpts[b.Bug.Id()] = NewBugExcerpt(b.Bug, snap)
 
-		if err := c.addBugToSearchIndex(snap); err != nil {
+		if err := indexer(snap); err != nil {
 			return err
 		}
 	}
 
+	err = indexEnd()
+	if err != nil {
+		return err
+	}
+
 	_, _ = fmt.Fprintln(os.Stderr, "Done.")
+	return nil
 }
 
-func (sc *SubCache[ExcerptT, CacheT, EntityT]) Close() error {
+func (sc *SubCache[EntityT, ExcerptT, CacheT]) Close() error {
 	sc.mu.Lock()
 	defer sc.mu.Unlock()
 	sc.excerpts = nil
@@ -187,7 +203,7 @@ func (sc *SubCache[ExcerptT, CacheT, EntityT]) Close() error {
 }
 
 // AllIds return all known bug ids
-func (sc *SubCache[ExcerptT, CacheT, EntityT]) AllIds() []entity.Id {
+func (sc *SubCache[EntityT, ExcerptT, CacheT]) AllIds() []entity.Id {
 	sc.mu.RLock()
 	defer sc.mu.RUnlock()
 
@@ -203,7 +219,7 @@ func (sc *SubCache[ExcerptT, CacheT, EntityT]) AllIds() []entity.Id {
 }
 
 // Resolve retrieve an entity matching the exact given id
-func (sc *SubCache[ExcerptT, CacheT, EntityT]) Resolve(id entity.Id) (CacheT, error) {
+func (sc *SubCache[EntityT, ExcerptT, CacheT]) Resolve(id entity.Id) (CacheT, error) {
 	sc.mu.RLock()
 	cached, ok := sc.cached[id]
 	if ok {
@@ -213,12 +229,12 @@ func (sc *SubCache[ExcerptT, CacheT, EntityT]) Resolve(id entity.Id) (CacheT, er
 	}
 	sc.mu.RUnlock()
 
-	b, err := sc.readWithResolver(sc.repo, sc.resolvers(), id)
+	e, err := sc.readWithResolver(sc.repo, sc.resolvers(), id)
 	if err != nil {
 		return *new(CacheT), err
 	}
 
-	cached = sc.makeCached(sc, sc.getUserIdentity, b)
+	cached = sc.makeCached(e, sc.entityUpdated)
 
 	sc.mu.Lock()
 	sc.cached[id] = cached
@@ -232,13 +248,13 @@ func (sc *SubCache[ExcerptT, CacheT, EntityT]) Resolve(id entity.Id) (CacheT, er
 
 // ResolvePrefix retrieve an entity matching an id prefix. It fails if multiple
 // entity match.
-func (sc *SubCache[ExcerptT, CacheT, EntityT]) ResolvePrefix(prefix string) (CacheT, error) {
+func (sc *SubCache[EntityT, ExcerptT, CacheT]) ResolvePrefix(prefix string) (CacheT, error) {
 	return sc.ResolveMatcher(func(excerpt ExcerptT) bool {
 		return excerpt.Id().HasPrefix(prefix)
 	})
 }
 
-func (sc *SubCache[ExcerptT, CacheT, EntityT]) ResolveMatcher(f func(ExcerptT) bool) (CacheT, error) {
+func (sc *SubCache[EntityT, ExcerptT, CacheT]) ResolveMatcher(f func(ExcerptT) bool) (CacheT, error) {
 	id, err := sc.resolveMatcher(f)
 	if err != nil {
 		return *new(CacheT), err
@@ -247,7 +263,7 @@ func (sc *SubCache[ExcerptT, CacheT, EntityT]) ResolveMatcher(f func(ExcerptT) b
 }
 
 // ResolveExcerpt retrieve an Excerpt matching the exact given id
-func (sc *SubCache[ExcerptT, CacheT, EntityT]) ResolveExcerpt(id entity.Id) (ExcerptT, error) {
+func (sc *SubCache[EntityT, ExcerptT, CacheT]) ResolveExcerpt(id entity.Id) (ExcerptT, error) {
 	sc.mu.RLock()
 	defer sc.mu.RUnlock()
 
@@ -261,13 +277,13 @@ func (sc *SubCache[ExcerptT, CacheT, EntityT]) ResolveExcerpt(id entity.Id) (Exc
 
 // ResolveExcerptPrefix retrieve an Excerpt matching an id prefix. It fails if multiple
 // entity match.
-func (sc *SubCache[ExcerptT, CacheT, EntityT]) ResolveExcerptPrefix(prefix string) (ExcerptT, error) {
+func (sc *SubCache[EntityT, ExcerptT, CacheT]) ResolveExcerptPrefix(prefix string) (ExcerptT, error) {
 	return sc.ResolveExcerptMatcher(func(excerpt ExcerptT) bool {
 		return excerpt.Id().HasPrefix(prefix)
 	})
 }
 
-func (sc *SubCache[ExcerptT, CacheT, EntityT]) ResolveExcerptMatcher(f func(ExcerptT) bool) (ExcerptT, error) {
+func (sc *SubCache[EntityT, ExcerptT, CacheT]) ResolveExcerptMatcher(f func(ExcerptT) bool) (ExcerptT, error) {
 	id, err := sc.resolveMatcher(f)
 	if err != nil {
 		return *new(ExcerptT), err
@@ -275,7 +291,7 @@ func (sc *SubCache[ExcerptT, CacheT, EntityT]) ResolveExcerptMatcher(f func(Exce
 	return sc.ResolveExcerpt(id)
 }
 
-func (sc *SubCache[ExcerptT, CacheT, EntityT]) resolveMatcher(f func(ExcerptT) bool) (entity.Id, error) {
+func (sc *SubCache[EntityT, ExcerptT, CacheT]) resolveMatcher(f func(ExcerptT) bool) (entity.Id, error) {
 	sc.mu.RLock()
 	defer sc.mu.RUnlock()
 
@@ -301,14 +317,14 @@ func (sc *SubCache[ExcerptT, CacheT, EntityT]) resolveMatcher(f func(ExcerptT) b
 
 var errNotInCache = errors.New("entity missing from cache")
 
-func (sc *SubCache[ExcerptT, CacheT, EntityT]) add(e EntityT) (CacheT, error) {
+func (sc *SubCache[EntityT, ExcerptT, CacheT]) add(e EntityT) (CacheT, error) {
 	sc.mu.Lock()
 	if _, has := sc.cached[e.Id()]; has {
 		sc.mu.Unlock()
 		return *new(CacheT), fmt.Errorf("entity %s already exist in the cache", e.Id())
 	}
 
-	cached := sc.makeCached(sc, sc.getUserIdentity, e)
+	cached := sc.makeCached(e, sc.entityUpdated)
 	sc.cached[e.Id()] = cached
 	sc.lru.Add(e.Id())
 	sc.mu.Unlock()
@@ -324,7 +340,7 @@ func (sc *SubCache[ExcerptT, CacheT, EntityT]) add(e EntityT) (CacheT, error) {
 	return cached, nil
 }
 
-func (sc *SubCache[ExcerptT, CacheT, EntityT]) Remove(prefix string) error {
+func (sc *SubCache[EntityT, ExcerptT, CacheT]) Remove(prefix string) error {
 	e, err := sc.ResolvePrefix(prefix)
 	if err != nil {
 		return err
@@ -349,7 +365,7 @@ func (sc *SubCache[ExcerptT, CacheT, EntityT]) Remove(prefix string) error {
 }
 
 // entityUpdated is a callback to trigger when the excerpt of an entity changed
-func (sc *SubCache[ExcerptT, CacheT, EntityT]) entityUpdated(id entity.Id) error {
+func (sc *SubCache[EntityT, ExcerptT, CacheT]) entityUpdated(id entity.Id) error {
 	sc.mu.Lock()
 	b, ok := sc.cached[id]
 	if !ok {
@@ -376,7 +392,7 @@ func (sc *SubCache[ExcerptT, CacheT, EntityT]) entityUpdated(id entity.Id) error
 }
 
 // evictIfNeeded will evict an entity from the cache if needed
-func (sc *SubCache[ExcerptT, CacheT, EntityT]) evictIfNeeded() {
+func (sc *SubCache[EntityT, ExcerptT, CacheT]) evictIfNeeded() {
 	sc.mu.Lock()
 	defer sc.mu.Unlock()
 	if sc.lru.Len() <= sc.maxLoaded {

commands/execenv/env.go 🔗

@@ -128,11 +128,30 @@ func LoadBackend(env *Env) func(*cobra.Command, []string) error {
 			return err
 		}
 
-		env.Backend, err = cache.NewRepoCache(env.Repo)
+		var events chan cache.BuildEvent
+		env.Backend, events, err = cache.NewRepoCache(env.Repo)
 		if err != nil {
 			return err
 		}
 
+		if events != nil {
+			_, _ = fmt.Fprintln(os.Stderr, "Building cache... ")
+		}
+
+		for event := range events {
+			if event.Err != nil {
+				_, _ = fmt.Fprintf(os.Stderr, "Cache building error [%s]: %v\n", event.Typename, event.Err)
+				continue
+			}
+
+			switch event.Event {
+			case cache.BuildEventStarted:
+				_, _ = fmt.Fprintf(os.Stderr, "[%s] started\n", event.Typename)
+			case cache.BuildEventFinished:
+				_, _ = fmt.Fprintf(os.Stderr, "[%s] done\n", event.Typename)
+			}
+		}
+
 		cleaner := func(env *Env) interrupt.CleanerFunc {
 			return func() error {
 				if env.Backend != nil {

entity/resolver.go 🔗

@@ -64,18 +64,18 @@ func (c *CachedResolver) Resolve(id Id) (Interface, error) {
 	return i, nil
 }
 
-var _ Resolver = ResolverFunc(nil)
+var _ Resolver = ResolverFunc[Interface](nil)
 
 // ResolverFunc is a helper to morph a function resolver into a Resolver
-type ResolverFunc func(id Id) (Interface, error)
+type ResolverFunc[T Interface] func(id Id) (T, error)
 
-func (fn ResolverFunc) Resolve(id Id) (Interface, error) {
+func (fn ResolverFunc[T]) Resolve(id Id) (Interface, error) {
 	return fn(id)
 }
 
 // MakeResolver create a resolver able to return the given entities.
 func MakeResolver(entities ...Interface) Resolver {
-	return ResolverFunc(func(id Id) (Interface, error) {
+	return ResolverFunc[Interface](func(id Id) (Interface, error) {
 		for _, entity := range entities {
 			if entity.Id() == id {
 				return entity, nil