cache: split into multiple files for readability

Michael Muré created

Change summary

cache/repo_cache.go          | 860 -------------------------------------
cache/repo_cache_bug.go      | 361 +++++++++++++++
cache/repo_cache_common.go   | 231 ++++++++++
cache/repo_cache_identity.go | 275 ++++++++++++
4 files changed, 881 insertions(+), 846 deletions(-)

Detailed changes

cache/repo_cache.go 🔗

@@ -1,32 +1,22 @@
 package cache
 
 import (
-	"bytes"
-	"encoding/gob"
 	"fmt"
 	"io"
 	"io/ioutil"
 	"os"
 	"path"
 	"path/filepath"
-	"sort"
 	"strconv"
 	"sync"
-	"time"
-
-	"github.com/pkg/errors"
 
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/identity"
-	"github.com/MichaelMure/git-bug/query"
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/MichaelMure/git-bug/util/process"
 )
 
-const bugCacheFile = "bug-cache"
-const identityCacheFile = "identity-cache"
-
 // 1: original format
 // 2: added cache for identities with a reference in the bug cache
 // 3: CreateUnixTime --> createUnixTime, EditUnixTime --> editUnixTime
@@ -102,53 +92,22 @@ func NewNamedRepoCache(r repository.ClockedRepo, name string) (*RepoCache, error
 	return c, c.write()
 }
 
-func (c *RepoCache) Name() string {
-	return c.name
-}
-
-// LocalConfig give access to the repository scoped configuration
-func (c *RepoCache) LocalConfig() repository.Config {
-	return c.repo.LocalConfig()
-}
-
-// GlobalConfig give access to the git global configuration
-func (c *RepoCache) GlobalConfig() repository.Config {
-	return c.repo.GlobalConfig()
-}
-
-// GetPath returns the path to the repo.
-func (c *RepoCache) GetPath() string {
-	return c.repo.GetPath()
-}
-
-// GetCoreEditor returns the name of the editor that the user has used to configure git.
-func (c *RepoCache) GetCoreEditor() (string, error) {
-	return c.repo.GetCoreEditor()
-}
-
-// GetRemotes returns the configured remotes repositories.
-func (c *RepoCache) GetRemotes() (map[string]string, error) {
-	return c.repo.GetRemotes()
-}
-
-// GetUserName returns the name the the user has used to configure git
-func (c *RepoCache) GetUserName() (string, error) {
-	return c.repo.GetUserName()
-}
-
-// GetUserEmail returns the email address that the user has used to configure git.
-func (c *RepoCache) GetUserEmail() (string, error) {
-	return c.repo.GetUserEmail()
-}
-
-// ReadData will attempt to read arbitrary data from the given hash
-func (c *RepoCache) ReadData(hash repository.Hash) ([]byte, error) {
-	return c.repo.ReadData(hash)
+// load will try to read from the disk all the cache files
+func (c *RepoCache) load() error {
+	err := c.loadBugCache()
+	if err != nil {
+		return err
+	}
+	return c.loadIdentityCache()
 }
 
-// StoreData will store arbitrary data and return the corresponding hash
-func (c *RepoCache) StoreData(data []byte) (repository.Hash, error) {
-	return c.repo.StoreData(data)
+// write will serialize on disk all the cache files
+func (c *RepoCache) write() error {
+	err := c.writeBugCache()
+	if err != nil {
+		return err
+	}
+	return c.writeIdentityCache()
 }
 
 func (c *RepoCache) lock() error {
@@ -193,198 +152,6 @@ func (c *RepoCache) Close() error {
 	return os.Remove(lockPath)
 }
 
-// bugUpdated is a callback to trigger when the excerpt of a bug changed,
-// that is each time a bug is updated
-func (c *RepoCache) bugUpdated(id entity.Id) error {
-	c.muBug.Lock()
-
-	b, ok := c.bugs[id]
-	if !ok {
-		c.muBug.Unlock()
-		panic("missing bug in the cache")
-	}
-
-	c.bugExcerpts[id] = NewBugExcerpt(b.bug, b.Snapshot())
-	c.muBug.Unlock()
-
-	// we only need to write the bug cache
-	return c.writeBugCache()
-}
-
-// identityUpdated is a callback to trigger when the excerpt of an identity
-// changed, that is each time an identity is updated
-func (c *RepoCache) identityUpdated(id entity.Id) error {
-	c.muIdentity.Lock()
-
-	i, ok := c.identities[id]
-	if !ok {
-		c.muIdentity.Unlock()
-		panic("missing identity in the cache")
-	}
-
-	c.identitiesExcerpts[id] = NewIdentityExcerpt(i.Identity)
-	c.muIdentity.Unlock()
-
-	// we only need to write the identity cache
-	return c.writeIdentityCache()
-}
-
-// load will try to read from the disk all the cache files
-func (c *RepoCache) load() error {
-	err := c.loadBugCache()
-	if err != nil {
-		return err
-	}
-	return c.loadIdentityCache()
-}
-
-// load will try to read from the disk the bug cache file
-func (c *RepoCache) loadBugCache() error {
-	c.muBug.Lock()
-	defer c.muBug.Unlock()
-
-	f, err := os.Open(bugCacheFilePath(c.repo))
-	if err != nil {
-		return err
-	}
-
-	decoder := gob.NewDecoder(f)
-
-	aux := struct {
-		Version  uint
-		Excerpts map[entity.Id]*BugExcerpt
-	}{}
-
-	err = decoder.Decode(&aux)
-	if err != nil {
-		return err
-	}
-
-	if aux.Version != formatVersion {
-		return fmt.Errorf("unknown cache format version %v", aux.Version)
-	}
-
-	c.bugExcerpts = aux.Excerpts
-	return nil
-}
-
-// load will try to read from the disk the identity cache file
-func (c *RepoCache) loadIdentityCache() error {
-	c.muIdentity.Lock()
-	defer c.muIdentity.Unlock()
-
-	f, err := os.Open(identityCacheFilePath(c.repo))
-	if err != nil {
-		return err
-	}
-
-	decoder := gob.NewDecoder(f)
-
-	aux := struct {
-		Version  uint
-		Excerpts map[entity.Id]*IdentityExcerpt
-	}{}
-
-	err = decoder.Decode(&aux)
-	if err != nil {
-		return err
-	}
-
-	if aux.Version != formatVersion {
-		return fmt.Errorf("unknown cache format version %v", aux.Version)
-	}
-
-	c.identitiesExcerpts = aux.Excerpts
-	return nil
-}
-
-// write will serialize on disk all the cache files
-func (c *RepoCache) write() error {
-	err := c.writeBugCache()
-	if err != nil {
-		return err
-	}
-	return c.writeIdentityCache()
-}
-
-// write will serialize on disk the bug cache file
-func (c *RepoCache) writeBugCache() error {
-	c.muBug.RLock()
-	defer c.muBug.RUnlock()
-
-	var data bytes.Buffer
-
-	aux := struct {
-		Version  uint
-		Excerpts map[entity.Id]*BugExcerpt
-	}{
-		Version:  formatVersion,
-		Excerpts: c.bugExcerpts,
-	}
-
-	encoder := gob.NewEncoder(&data)
-
-	err := encoder.Encode(aux)
-	if err != nil {
-		return err
-	}
-
-	f, err := os.Create(bugCacheFilePath(c.repo))
-	if err != nil {
-		return err
-	}
-
-	_, err = f.Write(data.Bytes())
-	if err != nil {
-		return err
-	}
-
-	return f.Close()
-}
-
-// write will serialize on disk the identity cache file
-func (c *RepoCache) writeIdentityCache() error {
-	c.muIdentity.RLock()
-	defer c.muIdentity.RUnlock()
-
-	var data bytes.Buffer
-
-	aux := struct {
-		Version  uint
-		Excerpts map[entity.Id]*IdentityExcerpt
-	}{
-		Version:  formatVersion,
-		Excerpts: c.identitiesExcerpts,
-	}
-
-	encoder := gob.NewEncoder(&data)
-
-	err := encoder.Encode(aux)
-	if err != nil {
-		return err
-	}
-
-	f, err := os.Create(identityCacheFilePath(c.repo))
-	if err != nil {
-		return err
-	}
-
-	_, err = f.Write(data.Bytes())
-	if err != nil {
-		return err
-	}
-
-	return f.Close()
-}
-
-func bugCacheFilePath(repo repository.Repo) string {
-	return path.Join(repo.GetPath(), "git-bug", bugCacheFile)
-}
-
-func identityCacheFilePath(repo repository.Repo) string {
-	return path.Join(repo.GetPath(), "git-bug", identityCacheFile)
-}
-
 func (c *RepoCache) buildCache() error {
 	c.muBug.Lock()
 	defer c.muBug.Unlock()
@@ -426,367 +193,6 @@ func (c *RepoCache) buildCache() error {
 	return nil
 }
 
-// ResolveBugExcerpt retrieve a BugExcerpt matching the exact given id
-func (c *RepoCache) ResolveBugExcerpt(id entity.Id) (*BugExcerpt, error) {
-	c.muBug.RLock()
-	defer c.muBug.RUnlock()
-
-	e, ok := c.bugExcerpts[id]
-	if !ok {
-		return nil, bug.ErrBugNotExist
-	}
-
-	return e, nil
-}
-
-// ResolveBug retrieve a bug matching the exact given id
-func (c *RepoCache) ResolveBug(id entity.Id) (*BugCache, error) {
-	c.muBug.RLock()
-	cached, ok := c.bugs[id]
-	c.muBug.RUnlock()
-	if ok {
-		return cached, nil
-	}
-
-	b, err := bug.ReadLocalBug(c.repo, id)
-	if err != nil {
-		return nil, err
-	}
-
-	cached = NewBugCache(c, b)
-
-	c.muBug.Lock()
-	c.bugs[id] = cached
-	c.muBug.Unlock()
-
-	return cached, nil
-}
-
-// ResolveBugExcerptPrefix retrieve a BugExcerpt matching an id prefix. It fails if multiple
-// bugs match.
-func (c *RepoCache) ResolveBugExcerptPrefix(prefix string) (*BugExcerpt, error) {
-	return c.ResolveBugExcerptMatcher(func(excerpt *BugExcerpt) bool {
-		return excerpt.Id.HasPrefix(prefix)
-	})
-}
-
-// ResolveBugPrefix retrieve a bug matching an id prefix. It fails if multiple
-// bugs match.
-func (c *RepoCache) ResolveBugPrefix(prefix string) (*BugCache, error) {
-	return c.ResolveBugMatcher(func(excerpt *BugExcerpt) bool {
-		return excerpt.Id.HasPrefix(prefix)
-	})
-}
-
-// ResolveBugCreateMetadata retrieve a bug that has the exact given metadata on
-// its Create operation, that is, the first operation. It fails if multiple bugs
-// match.
-func (c *RepoCache) ResolveBugCreateMetadata(key string, value string) (*BugCache, error) {
-	return c.ResolveBugMatcher(func(excerpt *BugExcerpt) bool {
-		return excerpt.CreateMetadata[key] == value
-	})
-}
-
-func (c *RepoCache) ResolveBugExcerptMatcher(f func(*BugExcerpt) bool) (*BugExcerpt, error) {
-	id, err := c.resolveBugMatcher(f)
-	if err != nil {
-		return nil, err
-	}
-	return c.ResolveBugExcerpt(id)
-}
-
-func (c *RepoCache) ResolveBugMatcher(f func(*BugExcerpt) bool) (*BugCache, error) {
-	id, err := c.resolveBugMatcher(f)
-	if err != nil {
-		return nil, err
-	}
-	return c.ResolveBug(id)
-}
-
-func (c *RepoCache) resolveBugMatcher(f func(*BugExcerpt) bool) (entity.Id, error) {
-	c.muBug.RLock()
-	defer c.muBug.RUnlock()
-
-	// preallocate but empty
-	matching := make([]entity.Id, 0, 5)
-
-	for _, excerpt := range c.bugExcerpts {
-		if f(excerpt) {
-			matching = append(matching, excerpt.Id)
-		}
-	}
-
-	if len(matching) > 1 {
-		return entity.UnsetId, bug.NewErrMultipleMatchBug(matching)
-	}
-
-	if len(matching) == 0 {
-		return entity.UnsetId, bug.ErrBugNotExist
-	}
-
-	return matching[0], nil
-}
-
-// QueryBugs return the id of all Bug matching the given Query
-func (c *RepoCache) QueryBugs(q *query.Query) []entity.Id {
-	c.muBug.RLock()
-	defer c.muBug.RUnlock()
-
-	if q == nil {
-		return c.AllBugsIds()
-	}
-
-	matcher := compileMatcher(q.Filters)
-
-	var filtered []*BugExcerpt
-
-	for _, excerpt := range c.bugExcerpts {
-		if matcher.Match(excerpt, c) {
-			filtered = append(filtered, excerpt)
-		}
-	}
-
-	var sorter sort.Interface
-
-	switch q.OrderBy {
-	case query.OrderById:
-		sorter = BugsById(filtered)
-	case query.OrderByCreation:
-		sorter = BugsByCreationTime(filtered)
-	case query.OrderByEdit:
-		sorter = BugsByEditTime(filtered)
-	default:
-		panic("missing sort type")
-	}
-
-	switch q.OrderDirection {
-	case query.OrderAscending:
-		// Nothing to do
-	case query.OrderDescending:
-		sorter = sort.Reverse(sorter)
-	default:
-		panic("missing sort direction")
-	}
-
-	sort.Sort(sorter)
-
-	result := make([]entity.Id, len(filtered))
-
-	for i, val := range filtered {
-		result[i] = val.Id
-	}
-
-	return result
-}
-
-// AllBugsIds return all known bug ids
-func (c *RepoCache) AllBugsIds() []entity.Id {
-	c.muBug.RLock()
-	defer c.muBug.RUnlock()
-
-	result := make([]entity.Id, len(c.bugExcerpts))
-
-	i := 0
-	for _, excerpt := range c.bugExcerpts {
-		result[i] = excerpt.Id
-		i++
-	}
-
-	return result
-}
-
-// ValidLabels list valid labels
-//
-// Note: in the future, a proper label policy could be implemented where valid
-// labels are defined in a configuration file. Until that, the default behavior
-// is to return the list of labels already used.
-func (c *RepoCache) ValidLabels() []bug.Label {
-	c.muBug.RLock()
-	defer c.muBug.RUnlock()
-
-	set := map[bug.Label]interface{}{}
-
-	for _, excerpt := range c.bugExcerpts {
-		for _, l := range excerpt.Labels {
-			set[l] = nil
-		}
-	}
-
-	result := make([]bug.Label, len(set))
-
-	i := 0
-	for l := range set {
-		result[i] = l
-		i++
-	}
-
-	// Sort
-	sort.Slice(result, func(i, j int) bool {
-		return string(result[i]) < string(result[j])
-	})
-
-	return result
-}
-
-// NewBug create a new bug
-// The new bug is written in the repository (commit)
-func (c *RepoCache) NewBug(title string, message string) (*BugCache, *bug.CreateOperation, error) {
-	return c.NewBugWithFiles(title, message, nil)
-}
-
-// NewBugWithFiles create a new bug with attached files for the message
-// The new bug is written in the repository (commit)
-func (c *RepoCache) NewBugWithFiles(title string, message string, files []repository.Hash) (*BugCache, *bug.CreateOperation, error) {
-	author, err := c.GetUserIdentity()
-	if err != nil {
-		return nil, nil, err
-	}
-
-	return c.NewBugRaw(author, time.Now().Unix(), title, message, files, nil)
-}
-
-// NewBugWithFilesMeta create a new bug with attached files for the message, as
-// well as metadata for the Create operation.
-// The new bug is written in the repository (commit)
-func (c *RepoCache) NewBugRaw(author *IdentityCache, unixTime int64, title string, message string, files []repository.Hash, metadata map[string]string) (*BugCache, *bug.CreateOperation, error) {
-	b, op, err := bug.CreateWithFiles(author.Identity, unixTime, title, message, files)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	for key, value := range metadata {
-		op.SetMetadata(key, value)
-	}
-
-	err = b.Commit(c.repo)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	c.muBug.Lock()
-	if _, has := c.bugs[b.Id()]; has {
-		c.muBug.Unlock()
-		return nil, nil, fmt.Errorf("bug %s already exist in the cache", b.Id())
-	}
-
-	cached := NewBugCache(c, b)
-	c.bugs[b.Id()] = cached
-	c.muBug.Unlock()
-
-	// force the write of the excerpt
-	err = c.bugUpdated(b.Id())
-	if err != nil {
-		return nil, nil, err
-	}
-
-	return cached, op, nil
-}
-
-// Fetch retrieve updates from a remote
-// This does not change the local bugs or identities state
-func (c *RepoCache) Fetch(remote string) (string, error) {
-	stdout1, err := identity.Fetch(c.repo, remote)
-	if err != nil {
-		return stdout1, err
-	}
-
-	stdout2, err := bug.Fetch(c.repo, remote)
-	if err != nil {
-		return stdout2, err
-	}
-
-	return stdout1 + stdout2, nil
-}
-
-// MergeAll will merge all the available remote bug and identities
-func (c *RepoCache) MergeAll(remote string) <-chan entity.MergeResult {
-	out := make(chan entity.MergeResult)
-
-	// Intercept merge results to update the cache properly
-	go func() {
-		defer close(out)
-
-		results := identity.MergeAll(c.repo, remote)
-		for result := range results {
-			out <- result
-
-			if result.Err != nil {
-				continue
-			}
-
-			switch result.Status {
-			case entity.MergeStatusNew, entity.MergeStatusUpdated:
-				i := result.Entity.(*identity.Identity)
-				c.muIdentity.Lock()
-				c.identitiesExcerpts[result.Id] = NewIdentityExcerpt(i)
-				c.muIdentity.Unlock()
-			}
-		}
-
-		results = bug.MergeAll(c.repo, remote)
-		for result := range results {
-			out <- result
-
-			if result.Err != nil {
-				continue
-			}
-
-			switch result.Status {
-			case entity.MergeStatusNew, entity.MergeStatusUpdated:
-				b := result.Entity.(*bug.Bug)
-				snap := b.Compile()
-				c.muBug.Lock()
-				c.bugExcerpts[result.Id] = NewBugExcerpt(b, &snap)
-				c.muBug.Unlock()
-			}
-		}
-
-		err := c.write()
-
-		// No easy way out here ..
-		if err != nil {
-			panic(err)
-		}
-	}()
-
-	return out
-}
-
-// Push update a remote with the local changes
-func (c *RepoCache) Push(remote string) (string, error) {
-	stdout1, err := identity.Push(c.repo, remote)
-	if err != nil {
-		return stdout1, err
-	}
-
-	stdout2, err := bug.Push(c.repo, remote)
-	if err != nil {
-		return stdout2, err
-	}
-
-	return stdout1 + stdout2, nil
-}
-
-// Pull will do a Fetch + MergeAll
-// This function will return an error if a merge fail
-func (c *RepoCache) Pull(remote string) error {
-	_, err := c.Fetch(remote)
-	if err != nil {
-		return err
-	}
-
-	for merge := range c.MergeAll(remote) {
-		if merge.Err != nil {
-			return merge.Err
-		}
-		if merge.Status == entity.MergeStatusInvalid {
-			return errors.Errorf("merge failure: %s", merge.Reason)
-		}
-	}
-
-	return nil
-}
-
 func repoLockFilePath(repo repository.Repo) string {
 	return path.Join(repo.GetPath(), "git-bug", lockfile)
 }
@@ -850,241 +256,3 @@ func repoIsAvailable(repo repository.Repo) error {
 
 	return nil
 }
-
-// ResolveIdentityExcerpt retrieve a IdentityExcerpt matching the exact given id
-func (c *RepoCache) ResolveIdentityExcerpt(id entity.Id) (*IdentityExcerpt, error) {
-	c.muIdentity.RLock()
-	defer c.muIdentity.RUnlock()
-
-	e, ok := c.identitiesExcerpts[id]
-	if !ok {
-		return nil, identity.ErrIdentityNotExist
-	}
-
-	return e, nil
-}
-
-// ResolveIdentity retrieve an identity matching the exact given id
-func (c *RepoCache) ResolveIdentity(id entity.Id) (*IdentityCache, error) {
-	c.muIdentity.RLock()
-	cached, ok := c.identities[id]
-	c.muIdentity.RUnlock()
-	if ok {
-		return cached, nil
-	}
-
-	i, err := identity.ReadLocal(c.repo, id)
-	if err != nil {
-		return nil, err
-	}
-
-	cached = NewIdentityCache(c, i)
-
-	c.muIdentity.Lock()
-	c.identities[id] = cached
-	c.muIdentity.Unlock()
-
-	return cached, nil
-}
-
-// ResolveIdentityExcerptPrefix retrieve a IdentityExcerpt matching an id prefix.
-// It fails if multiple identities match.
-func (c *RepoCache) ResolveIdentityExcerptPrefix(prefix string) (*IdentityExcerpt, error) {
-	return c.ResolveIdentityExcerptMatcher(func(excerpt *IdentityExcerpt) bool {
-		return excerpt.Id.HasPrefix(prefix)
-	})
-}
-
-// ResolveIdentityPrefix retrieve an Identity matching an id prefix.
-// It fails if multiple identities match.
-func (c *RepoCache) ResolveIdentityPrefix(prefix string) (*IdentityCache, error) {
-	return c.ResolveIdentityMatcher(func(excerpt *IdentityExcerpt) bool {
-		return excerpt.Id.HasPrefix(prefix)
-	})
-}
-
-// ResolveIdentityImmutableMetadata retrieve an Identity that has the exact given metadata on
-// one of it's version. If multiple version have the same key, the first defined take precedence.
-func (c *RepoCache) ResolveIdentityImmutableMetadata(key string, value string) (*IdentityCache, error) {
-	return c.ResolveIdentityMatcher(func(excerpt *IdentityExcerpt) bool {
-		return excerpt.ImmutableMetadata[key] == value
-	})
-}
-
-func (c *RepoCache) ResolveIdentityExcerptMatcher(f func(*IdentityExcerpt) bool) (*IdentityExcerpt, error) {
-	id, err := c.resolveIdentityMatcher(f)
-	if err != nil {
-		return nil, err
-	}
-	return c.ResolveIdentityExcerpt(id)
-}
-
-func (c *RepoCache) ResolveIdentityMatcher(f func(*IdentityExcerpt) bool) (*IdentityCache, error) {
-	id, err := c.resolveIdentityMatcher(f)
-	if err != nil {
-		return nil, err
-	}
-	return c.ResolveIdentity(id)
-}
-
-func (c *RepoCache) resolveIdentityMatcher(f func(*IdentityExcerpt) bool) (entity.Id, error) {
-	c.muIdentity.RLock()
-	defer c.muIdentity.RUnlock()
-
-	// preallocate but empty
-	matching := make([]entity.Id, 0, 5)
-
-	for _, excerpt := range c.identitiesExcerpts {
-		if f(excerpt) {
-			matching = append(matching, excerpt.Id)
-		}
-	}
-
-	if len(matching) > 1 {
-		return entity.UnsetId, identity.NewErrMultipleMatch(matching)
-	}
-
-	if len(matching) == 0 {
-		return entity.UnsetId, identity.ErrIdentityNotExist
-	}
-
-	return matching[0], nil
-}
-
-// AllIdentityIds return all known identity ids
-func (c *RepoCache) AllIdentityIds() []entity.Id {
-	c.muIdentity.RLock()
-	defer c.muIdentity.RUnlock()
-
-	result := make([]entity.Id, len(c.identitiesExcerpts))
-
-	i := 0
-	for _, excerpt := range c.identitiesExcerpts {
-		result[i] = excerpt.Id
-		i++
-	}
-
-	return result
-}
-
-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()
-
-	// Make sure that everything is fine
-	if _, ok := c.identities[i.Id()]; !ok {
-		panic("SetUserIdentity while the identity is not from the cache, something is wrong")
-	}
-
-	c.userIdentityId = i.Id()
-
-	return nil
-}
-
-func (c *RepoCache) GetUserIdentity() (*IdentityCache, error) {
-	if c.userIdentityId != "" {
-		i, ok := c.identities[c.userIdentityId]
-		if ok {
-			return i, nil
-		}
-	}
-
-	c.muIdentity.Lock()
-	defer c.muIdentity.Unlock()
-
-	i, err := identity.GetUserIdentity(c.repo)
-	if err != nil {
-		return nil, err
-	}
-
-	cached := NewIdentityCache(c, i)
-	c.identities[i.Id()] = cached
-	c.userIdentityId = i.Id()
-
-	return cached, nil
-}
-
-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.muIdentity.RLock()
-	defer c.muIdentity.RUnlock()
-
-	excerpt, ok := c.identitiesExcerpts[c.userIdentityId]
-	if !ok {
-		return nil, fmt.Errorf("cache: missing identity excerpt %v", c.userIdentityId)
-	}
-	return excerpt, nil
-}
-
-func (c *RepoCache) IsUserIdentitySet() (bool, error) {
-	return identity.IsUserIdentitySet(c.repo)
-}
-
-func (c *RepoCache) NewIdentityFromGitUser() (*IdentityCache, error) {
-	return c.NewIdentityFromGitUserRaw(nil)
-}
-
-func (c *RepoCache) NewIdentityFromGitUserRaw(metadata map[string]string) (*IdentityCache, error) {
-	i, err := identity.NewFromGitUser(c.repo)
-	if err != nil {
-		return nil, err
-	}
-	return c.finishIdentity(i, metadata)
-}
-
-// NewIdentity create a new identity
-// The new identity is written in the repository (commit)
-func (c *RepoCache) NewIdentity(name string, email string) (*IdentityCache, error) {
-	return c.NewIdentityRaw(name, email, "", "", nil)
-}
-
-// NewIdentityFull create a new identity
-// The new identity is written in the repository (commit)
-func (c *RepoCache) NewIdentityFull(name string, email string, login string, avatarUrl string) (*IdentityCache, error) {
-	return c.NewIdentityRaw(name, email, login, avatarUrl, nil)
-}
-
-func (c *RepoCache) NewIdentityRaw(name string, email string, login string, avatarUrl string, metadata map[string]string) (*IdentityCache, error) {
-	i := identity.NewIdentityFull(name, email, login, avatarUrl)
-	return c.finishIdentity(i, metadata)
-}
-
-func (c *RepoCache) finishIdentity(i *identity.Identity, metadata map[string]string) (*IdentityCache, error) {
-	for key, value := range metadata {
-		i.SetMetadata(key, value)
-	}
-
-	err := i.Commit(c.repo)
-	if err != nil {
-		return nil, err
-	}
-
-	c.muIdentity.Lock()
-	if _, has := c.identities[i.Id()]; has {
-		return nil, fmt.Errorf("identity %s already exist in the cache", i.Id())
-	}
-
-	cached := NewIdentityCache(c, i)
-	c.identities[i.Id()] = cached
-	c.muIdentity.Unlock()
-
-	// force the write of the excerpt
-	err = c.identityUpdated(i.Id())
-	if err != nil {
-		return nil, err
-	}
-
-	return cached, nil
-}

cache/repo_cache_bug.go 🔗

@@ -0,0 +1,361 @@
+package cache
+
+import (
+	"bytes"
+	"encoding/gob"
+	"fmt"
+	"os"
+	"path"
+	"sort"
+	"time"
+
+	"github.com/MichaelMure/git-bug/bug"
+	"github.com/MichaelMure/git-bug/entity"
+	"github.com/MichaelMure/git-bug/query"
+	"github.com/MichaelMure/git-bug/repository"
+)
+
+const bugCacheFile = "bug-cache"
+
+func bugCacheFilePath(repo repository.Repo) string {
+	return path.Join(repo.GetPath(), "git-bug", bugCacheFile)
+}
+
+// bugUpdated is a callback to trigger when the excerpt of a bug changed,
+// that is each time a bug is updated
+func (c *RepoCache) bugUpdated(id entity.Id) error {
+	c.muBug.Lock()
+
+	b, ok := c.bugs[id]
+	if !ok {
+		c.muBug.Unlock()
+		panic("missing bug in the cache")
+	}
+
+	c.bugExcerpts[id] = NewBugExcerpt(b.bug, b.Snapshot())
+	c.muBug.Unlock()
+
+	// we only need to write the bug cache
+	return c.writeBugCache()
+}
+
+// load will try to read from the disk the bug cache file
+func (c *RepoCache) loadBugCache() error {
+	c.muBug.Lock()
+	defer c.muBug.Unlock()
+
+	f, err := os.Open(bugCacheFilePath(c.repo))
+	if err != nil {
+		return err
+	}
+
+	decoder := gob.NewDecoder(f)
+
+	aux := struct {
+		Version  uint
+		Excerpts map[entity.Id]*BugExcerpt
+	}{}
+
+	err = decoder.Decode(&aux)
+	if err != nil {
+		return err
+	}
+
+	if aux.Version != formatVersion {
+		return fmt.Errorf("unknown cache format version %v", aux.Version)
+	}
+
+	c.bugExcerpts = aux.Excerpts
+	return nil
+}
+
+// write will serialize on disk the bug cache file
+func (c *RepoCache) writeBugCache() error {
+	c.muBug.RLock()
+	defer c.muBug.RUnlock()
+
+	var data bytes.Buffer
+
+	aux := struct {
+		Version  uint
+		Excerpts map[entity.Id]*BugExcerpt
+	}{
+		Version:  formatVersion,
+		Excerpts: c.bugExcerpts,
+	}
+
+	encoder := gob.NewEncoder(&data)
+
+	err := encoder.Encode(aux)
+	if err != nil {
+		return err
+	}
+
+	f, err := os.Create(bugCacheFilePath(c.repo))
+	if err != nil {
+		return err
+	}
+
+	_, err = f.Write(data.Bytes())
+	if err != nil {
+		return err
+	}
+
+	return f.Close()
+}
+
+// ResolveBugExcerpt retrieve a BugExcerpt matching the exact given id
+func (c *RepoCache) ResolveBugExcerpt(id entity.Id) (*BugExcerpt, error) {
+	c.muBug.RLock()
+	defer c.muBug.RUnlock()
+
+	e, ok := c.bugExcerpts[id]
+	if !ok {
+		return nil, bug.ErrBugNotExist
+	}
+
+	return e, nil
+}
+
+// ResolveBug retrieve a bug matching the exact given id
+func (c *RepoCache) ResolveBug(id entity.Id) (*BugCache, error) {
+	c.muBug.RLock()
+	cached, ok := c.bugs[id]
+	c.muBug.RUnlock()
+	if ok {
+		return cached, nil
+	}
+
+	b, err := bug.ReadLocalBug(c.repo, id)
+	if err != nil {
+		return nil, err
+	}
+
+	cached = NewBugCache(c, b)
+
+	c.muBug.Lock()
+	c.bugs[id] = cached
+	c.muBug.Unlock()
+
+	return cached, nil
+}
+
+// ResolveBugExcerptPrefix retrieve a BugExcerpt matching an id prefix. It fails if multiple
+// bugs match.
+func (c *RepoCache) ResolveBugExcerptPrefix(prefix string) (*BugExcerpt, error) {
+	return c.ResolveBugExcerptMatcher(func(excerpt *BugExcerpt) bool {
+		return excerpt.Id.HasPrefix(prefix)
+	})
+}
+
+// ResolveBugPrefix retrieve a bug matching an id prefix. It fails if multiple
+// bugs match.
+func (c *RepoCache) ResolveBugPrefix(prefix string) (*BugCache, error) {
+	return c.ResolveBugMatcher(func(excerpt *BugExcerpt) bool {
+		return excerpt.Id.HasPrefix(prefix)
+	})
+}
+
+// ResolveBugCreateMetadata retrieve a bug that has the exact given metadata on
+// its Create operation, that is, the first operation. It fails if multiple bugs
+// match.
+func (c *RepoCache) ResolveBugCreateMetadata(key string, value string) (*BugCache, error) {
+	return c.ResolveBugMatcher(func(excerpt *BugExcerpt) bool {
+		return excerpt.CreateMetadata[key] == value
+	})
+}
+
+func (c *RepoCache) ResolveBugExcerptMatcher(f func(*BugExcerpt) bool) (*BugExcerpt, error) {
+	id, err := c.resolveBugMatcher(f)
+	if err != nil {
+		return nil, err
+	}
+	return c.ResolveBugExcerpt(id)
+}
+
+func (c *RepoCache) ResolveBugMatcher(f func(*BugExcerpt) bool) (*BugCache, error) {
+	id, err := c.resolveBugMatcher(f)
+	if err != nil {
+		return nil, err
+	}
+	return c.ResolveBug(id)
+}
+
+func (c *RepoCache) resolveBugMatcher(f func(*BugExcerpt) bool) (entity.Id, error) {
+	c.muBug.RLock()
+	defer c.muBug.RUnlock()
+
+	// preallocate but empty
+	matching := make([]entity.Id, 0, 5)
+
+	for _, excerpt := range c.bugExcerpts {
+		if f(excerpt) {
+			matching = append(matching, excerpt.Id)
+		}
+	}
+
+	if len(matching) > 1 {
+		return entity.UnsetId, bug.NewErrMultipleMatchBug(matching)
+	}
+
+	if len(matching) == 0 {
+		return entity.UnsetId, bug.ErrBugNotExist
+	}
+
+	return matching[0], nil
+}
+
+// QueryBugs return the id of all Bug matching the given Query
+func (c *RepoCache) QueryBugs(q *query.Query) []entity.Id {
+	c.muBug.RLock()
+	defer c.muBug.RUnlock()
+
+	if q == nil {
+		return c.AllBugsIds()
+	}
+
+	matcher := compileMatcher(q.Filters)
+
+	var filtered []*BugExcerpt
+
+	for _, excerpt := range c.bugExcerpts {
+		if matcher.Match(excerpt, c) {
+			filtered = append(filtered, excerpt)
+		}
+	}
+
+	var sorter sort.Interface
+
+	switch q.OrderBy {
+	case query.OrderById:
+		sorter = BugsById(filtered)
+	case query.OrderByCreation:
+		sorter = BugsByCreationTime(filtered)
+	case query.OrderByEdit:
+		sorter = BugsByEditTime(filtered)
+	default:
+		panic("missing sort type")
+	}
+
+	switch q.OrderDirection {
+	case query.OrderAscending:
+		// Nothing to do
+	case query.OrderDescending:
+		sorter = sort.Reverse(sorter)
+	default:
+		panic("missing sort direction")
+	}
+
+	sort.Sort(sorter)
+
+	result := make([]entity.Id, len(filtered))
+
+	for i, val := range filtered {
+		result[i] = val.Id
+	}
+
+	return result
+}
+
+// AllBugsIds return all known bug ids
+func (c *RepoCache) AllBugsIds() []entity.Id {
+	c.muBug.RLock()
+	defer c.muBug.RUnlock()
+
+	result := make([]entity.Id, len(c.bugExcerpts))
+
+	i := 0
+	for _, excerpt := range c.bugExcerpts {
+		result[i] = excerpt.Id
+		i++
+	}
+
+	return result
+}
+
+// ValidLabels list valid labels
+//
+// Note: in the future, a proper label policy could be implemented where valid
+// labels are defined in a configuration file. Until that, the default behavior
+// is to return the list of labels already used.
+func (c *RepoCache) ValidLabels() []bug.Label {
+	c.muBug.RLock()
+	defer c.muBug.RUnlock()
+
+	set := map[bug.Label]interface{}{}
+
+	for _, excerpt := range c.bugExcerpts {
+		for _, l := range excerpt.Labels {
+			set[l] = nil
+		}
+	}
+
+	result := make([]bug.Label, len(set))
+
+	i := 0
+	for l := range set {
+		result[i] = l
+		i++
+	}
+
+	// Sort
+	sort.Slice(result, func(i, j int) bool {
+		return string(result[i]) < string(result[j])
+	})
+
+	return result
+}
+
+// NewBug create a new bug
+// The new bug is written in the repository (commit)
+func (c *RepoCache) NewBug(title string, message string) (*BugCache, *bug.CreateOperation, error) {
+	return c.NewBugWithFiles(title, message, nil)
+}
+
+// NewBugWithFiles create a new bug with attached files for the message
+// The new bug is written in the repository (commit)
+func (c *RepoCache) NewBugWithFiles(title string, message string, files []repository.Hash) (*BugCache, *bug.CreateOperation, error) {
+	author, err := c.GetUserIdentity()
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return c.NewBugRaw(author, time.Now().Unix(), title, message, files, nil)
+}
+
+// NewBugWithFilesMeta create a new bug with attached files for the message, as
+// well as metadata for the Create operation.
+// The new bug is written in the repository (commit)
+func (c *RepoCache) NewBugRaw(author *IdentityCache, unixTime int64, title string, message string, files []repository.Hash, metadata map[string]string) (*BugCache, *bug.CreateOperation, error) {
+	b, op, err := bug.CreateWithFiles(author.Identity, unixTime, title, message, files)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	for key, value := range metadata {
+		op.SetMetadata(key, value)
+	}
+
+	err = b.Commit(c.repo)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	c.muBug.Lock()
+	if _, has := c.bugs[b.Id()]; has {
+		c.muBug.Unlock()
+		return nil, nil, fmt.Errorf("bug %s already exist in the cache", b.Id())
+	}
+
+	cached := NewBugCache(c, b)
+	c.bugs[b.Id()] = cached
+	c.muBug.Unlock()
+
+	// force the write of the excerpt
+	err = c.bugUpdated(b.Id())
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return cached, op, nil
+}

cache/repo_cache_common.go 🔗

@@ -0,0 +1,231 @@
+package cache
+
+import (
+	"fmt"
+
+	"github.com/pkg/errors"
+
+	"github.com/MichaelMure/git-bug/bug"
+	"github.com/MichaelMure/git-bug/entity"
+	"github.com/MichaelMure/git-bug/identity"
+	"github.com/MichaelMure/git-bug/repository"
+)
+
+func (c *RepoCache) Name() string {
+	return c.name
+}
+
+// LocalConfig give access to the repository scoped configuration
+func (c *RepoCache) LocalConfig() repository.Config {
+	return c.repo.LocalConfig()
+}
+
+// GlobalConfig give access to the git global configuration
+func (c *RepoCache) GlobalConfig() repository.Config {
+	return c.repo.GlobalConfig()
+}
+
+// GetPath returns the path to the repo.
+func (c *RepoCache) GetPath() string {
+	return c.repo.GetPath()
+}
+
+// GetCoreEditor returns the name of the editor that the user has used to configure git.
+func (c *RepoCache) GetCoreEditor() (string, error) {
+	return c.repo.GetCoreEditor()
+}
+
+// GetRemotes returns the configured remotes repositories.
+func (c *RepoCache) GetRemotes() (map[string]string, error) {
+	return c.repo.GetRemotes()
+}
+
+// GetUserName returns the name the the user has used to configure git
+func (c *RepoCache) GetUserName() (string, error) {
+	return c.repo.GetUserName()
+}
+
+// GetUserEmail returns the email address that the user has used to configure git.
+func (c *RepoCache) GetUserEmail() (string, error) {
+	return c.repo.GetUserEmail()
+}
+
+// ReadData will attempt to read arbitrary data from the given hash
+func (c *RepoCache) ReadData(hash repository.Hash) ([]byte, error) {
+	return c.repo.ReadData(hash)
+}
+
+// StoreData will store arbitrary data and return the corresponding hash
+func (c *RepoCache) StoreData(data []byte) (repository.Hash, error) {
+	return c.repo.StoreData(data)
+}
+
+// Fetch retrieve updates from a remote
+// This does not change the local bugs or identities state
+func (c *RepoCache) Fetch(remote string) (string, error) {
+	stdout1, err := identity.Fetch(c.repo, remote)
+	if err != nil {
+		return stdout1, err
+	}
+
+	stdout2, err := bug.Fetch(c.repo, remote)
+	if err != nil {
+		return stdout2, err
+	}
+
+	return stdout1 + stdout2, nil
+}
+
+// MergeAll will merge all the available remote bug and identities
+func (c *RepoCache) MergeAll(remote string) <-chan entity.MergeResult {
+	out := make(chan entity.MergeResult)
+
+	// Intercept merge results to update the cache properly
+	go func() {
+		defer close(out)
+
+		results := identity.MergeAll(c.repo, remote)
+		for result := range results {
+			out <- result
+
+			if result.Err != nil {
+				continue
+			}
+
+			switch result.Status {
+			case entity.MergeStatusNew, entity.MergeStatusUpdated:
+				i := result.Entity.(*identity.Identity)
+				c.muIdentity.Lock()
+				c.identitiesExcerpts[result.Id] = NewIdentityExcerpt(i)
+				c.muIdentity.Unlock()
+			}
+		}
+
+		results = bug.MergeAll(c.repo, remote)
+		for result := range results {
+			out <- result
+
+			if result.Err != nil {
+				continue
+			}
+
+			switch result.Status {
+			case entity.MergeStatusNew, entity.MergeStatusUpdated:
+				b := result.Entity.(*bug.Bug)
+				snap := b.Compile()
+				c.muBug.Lock()
+				c.bugExcerpts[result.Id] = NewBugExcerpt(b, &snap)
+				c.muBug.Unlock()
+			}
+		}
+
+		err := c.write()
+
+		// No easy way out here ..
+		if err != nil {
+			panic(err)
+		}
+	}()
+
+	return out
+}
+
+// Push update a remote with the local changes
+func (c *RepoCache) Push(remote string) (string, error) {
+	stdout1, err := identity.Push(c.repo, remote)
+	if err != nil {
+		return stdout1, err
+	}
+
+	stdout2, err := bug.Push(c.repo, remote)
+	if err != nil {
+		return stdout2, err
+	}
+
+	return stdout1 + stdout2, nil
+}
+
+// Pull will do a Fetch + MergeAll
+// This function will return an error if a merge fail
+func (c *RepoCache) Pull(remote string) error {
+	_, err := c.Fetch(remote)
+	if err != nil {
+		return err
+	}
+
+	for merge := range c.MergeAll(remote) {
+		if merge.Err != nil {
+			return merge.Err
+		}
+		if merge.Status == entity.MergeStatusInvalid {
+			return errors.Errorf("merge failure: %s", merge.Reason)
+		}
+	}
+
+	return nil
+}
+
+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()
+
+	// Make sure that everything is fine
+	if _, ok := c.identities[i.Id()]; !ok {
+		panic("SetUserIdentity while the identity is not from the cache, something is wrong")
+	}
+
+	c.userIdentityId = i.Id()
+
+	return nil
+}
+
+func (c *RepoCache) GetUserIdentity() (*IdentityCache, error) {
+	if c.userIdentityId != "" {
+		i, ok := c.identities[c.userIdentityId]
+		if ok {
+			return i, nil
+		}
+	}
+
+	c.muIdentity.Lock()
+	defer c.muIdentity.Unlock()
+
+	i, err := identity.GetUserIdentity(c.repo)
+	if err != nil {
+		return nil, err
+	}
+
+	cached := NewIdentityCache(c, i)
+	c.identities[i.Id()] = cached
+	c.userIdentityId = i.Id()
+
+	return cached, nil
+}
+
+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.muIdentity.RLock()
+	defer c.muIdentity.RUnlock()
+
+	excerpt, ok := c.identitiesExcerpts[c.userIdentityId]
+	if !ok {
+		return nil, fmt.Errorf("cache: missing identity excerpt %v", c.userIdentityId)
+	}
+	return excerpt, nil
+}
+
+func (c *RepoCache) IsUserIdentitySet() (bool, error) {
+	return identity.IsUserIdentitySet(c.repo)
+}

cache/repo_cache_identity.go 🔗

@@ -0,0 +1,275 @@
+package cache
+
+import (
+	"bytes"
+	"encoding/gob"
+	"fmt"
+	"os"
+	"path"
+
+	"github.com/MichaelMure/git-bug/entity"
+	"github.com/MichaelMure/git-bug/identity"
+	"github.com/MichaelMure/git-bug/repository"
+)
+
+const identityCacheFile = "identity-cache"
+
+func identityCacheFilePath(repo repository.Repo) string {
+	return path.Join(repo.GetPath(), "git-bug", identityCacheFile)
+}
+
+// identityUpdated is a callback to trigger when the excerpt of an identity
+// changed, that is each time an identity is updated
+func (c *RepoCache) identityUpdated(id entity.Id) error {
+	c.muIdentity.Lock()
+
+	i, ok := c.identities[id]
+	if !ok {
+		c.muIdentity.Unlock()
+		panic("missing identity in the cache")
+	}
+
+	c.identitiesExcerpts[id] = NewIdentityExcerpt(i.Identity)
+	c.muIdentity.Unlock()
+
+	// we only need to write the identity cache
+	return c.writeIdentityCache()
+}
+
+// load will try to read from the disk the identity cache file
+func (c *RepoCache) loadIdentityCache() error {
+	c.muIdentity.Lock()
+	defer c.muIdentity.Unlock()
+
+	f, err := os.Open(identityCacheFilePath(c.repo))
+	if err != nil {
+		return err
+	}
+
+	decoder := gob.NewDecoder(f)
+
+	aux := struct {
+		Version  uint
+		Excerpts map[entity.Id]*IdentityExcerpt
+	}{}
+
+	err = decoder.Decode(&aux)
+	if err != nil {
+		return err
+	}
+
+	if aux.Version != formatVersion {
+		return fmt.Errorf("unknown cache format version %v", aux.Version)
+	}
+
+	c.identitiesExcerpts = aux.Excerpts
+	return nil
+}
+
+// write will serialize on disk the identity cache file
+func (c *RepoCache) writeIdentityCache() error {
+	c.muIdentity.RLock()
+	defer c.muIdentity.RUnlock()
+
+	var data bytes.Buffer
+
+	aux := struct {
+		Version  uint
+		Excerpts map[entity.Id]*IdentityExcerpt
+	}{
+		Version:  formatVersion,
+		Excerpts: c.identitiesExcerpts,
+	}
+
+	encoder := gob.NewEncoder(&data)
+
+	err := encoder.Encode(aux)
+	if err != nil {
+		return err
+	}
+
+	f, err := os.Create(identityCacheFilePath(c.repo))
+	if err != nil {
+		return err
+	}
+
+	_, err = f.Write(data.Bytes())
+	if err != nil {
+		return err
+	}
+
+	return f.Close()
+}
+
+// ResolveIdentityExcerpt retrieve a IdentityExcerpt matching the exact given id
+func (c *RepoCache) ResolveIdentityExcerpt(id entity.Id) (*IdentityExcerpt, error) {
+	c.muIdentity.RLock()
+	defer c.muIdentity.RUnlock()
+
+	e, ok := c.identitiesExcerpts[id]
+	if !ok {
+		return nil, identity.ErrIdentityNotExist
+	}
+
+	return e, nil
+}
+
+// ResolveIdentity retrieve an identity matching the exact given id
+func (c *RepoCache) ResolveIdentity(id entity.Id) (*IdentityCache, error) {
+	c.muIdentity.RLock()
+	cached, ok := c.identities[id]
+	c.muIdentity.RUnlock()
+	if ok {
+		return cached, nil
+	}
+
+	i, err := identity.ReadLocal(c.repo, id)
+	if err != nil {
+		return nil, err
+	}
+
+	cached = NewIdentityCache(c, i)
+
+	c.muIdentity.Lock()
+	c.identities[id] = cached
+	c.muIdentity.Unlock()
+
+	return cached, nil
+}
+
+// ResolveIdentityExcerptPrefix retrieve a IdentityExcerpt matching an id prefix.
+// It fails if multiple identities match.
+func (c *RepoCache) ResolveIdentityExcerptPrefix(prefix string) (*IdentityExcerpt, error) {
+	return c.ResolveIdentityExcerptMatcher(func(excerpt *IdentityExcerpt) bool {
+		return excerpt.Id.HasPrefix(prefix)
+	})
+}
+
+// ResolveIdentityPrefix retrieve an Identity matching an id prefix.
+// It fails if multiple identities match.
+func (c *RepoCache) ResolveIdentityPrefix(prefix string) (*IdentityCache, error) {
+	return c.ResolveIdentityMatcher(func(excerpt *IdentityExcerpt) bool {
+		return excerpt.Id.HasPrefix(prefix)
+	})
+}
+
+// ResolveIdentityImmutableMetadata retrieve an Identity that has the exact given metadata on
+// one of it's version. If multiple version have the same key, the first defined take precedence.
+func (c *RepoCache) ResolveIdentityImmutableMetadata(key string, value string) (*IdentityCache, error) {
+	return c.ResolveIdentityMatcher(func(excerpt *IdentityExcerpt) bool {
+		return excerpt.ImmutableMetadata[key] == value
+	})
+}
+
+func (c *RepoCache) ResolveIdentityExcerptMatcher(f func(*IdentityExcerpt) bool) (*IdentityExcerpt, error) {
+	id, err := c.resolveIdentityMatcher(f)
+	if err != nil {
+		return nil, err
+	}
+	return c.ResolveIdentityExcerpt(id)
+}
+
+func (c *RepoCache) ResolveIdentityMatcher(f func(*IdentityExcerpt) bool) (*IdentityCache, error) {
+	id, err := c.resolveIdentityMatcher(f)
+	if err != nil {
+		return nil, err
+	}
+	return c.ResolveIdentity(id)
+}
+
+func (c *RepoCache) resolveIdentityMatcher(f func(*IdentityExcerpt) bool) (entity.Id, error) {
+	c.muIdentity.RLock()
+	defer c.muIdentity.RUnlock()
+
+	// preallocate but empty
+	matching := make([]entity.Id, 0, 5)
+
+	for _, excerpt := range c.identitiesExcerpts {
+		if f(excerpt) {
+			matching = append(matching, excerpt.Id)
+		}
+	}
+
+	if len(matching) > 1 {
+		return entity.UnsetId, identity.NewErrMultipleMatch(matching)
+	}
+
+	if len(matching) == 0 {
+		return entity.UnsetId, identity.ErrIdentityNotExist
+	}
+
+	return matching[0], nil
+}
+
+// AllIdentityIds return all known identity ids
+func (c *RepoCache) AllIdentityIds() []entity.Id {
+	c.muIdentity.RLock()
+	defer c.muIdentity.RUnlock()
+
+	result := make([]entity.Id, len(c.identitiesExcerpts))
+
+	i := 0
+	for _, excerpt := range c.identitiesExcerpts {
+		result[i] = excerpt.Id
+		i++
+	}
+
+	return result
+}
+
+func (c *RepoCache) NewIdentityFromGitUser() (*IdentityCache, error) {
+	return c.NewIdentityFromGitUserRaw(nil)
+}
+
+func (c *RepoCache) NewIdentityFromGitUserRaw(metadata map[string]string) (*IdentityCache, error) {
+	i, err := identity.NewFromGitUser(c.repo)
+	if err != nil {
+		return nil, err
+	}
+	return c.finishIdentity(i, metadata)
+}
+
+// NewIdentity create a new identity
+// The new identity is written in the repository (commit)
+func (c *RepoCache) NewIdentity(name string, email string) (*IdentityCache, error) {
+	return c.NewIdentityRaw(name, email, "", "", nil)
+}
+
+// NewIdentityFull create a new identity
+// The new identity is written in the repository (commit)
+func (c *RepoCache) NewIdentityFull(name string, email string, login string, avatarUrl string) (*IdentityCache, error) {
+	return c.NewIdentityRaw(name, email, login, avatarUrl, nil)
+}
+
+func (c *RepoCache) NewIdentityRaw(name string, email string, login string, avatarUrl string, metadata map[string]string) (*IdentityCache, error) {
+	i := identity.NewIdentityFull(name, email, login, avatarUrl)
+	return c.finishIdentity(i, metadata)
+}
+
+func (c *RepoCache) finishIdentity(i *identity.Identity, metadata map[string]string) (*IdentityCache, error) {
+	for key, value := range metadata {
+		i.SetMetadata(key, value)
+	}
+
+	err := i.Commit(c.repo)
+	if err != nil {
+		return nil, err
+	}
+
+	c.muIdentity.Lock()
+	if _, has := c.identities[i.Id()]; has {
+		return nil, fmt.Errorf("identity %s already exist in the cache", i.Id())
+	}
+
+	cached := NewIdentityCache(c, i)
+	c.identities[i.Id()] = cached
+	c.muIdentity.Unlock()
+
+	// force the write of the excerpt
+	err = c.identityUpdated(i.Id())
+	if err != nil {
+		return nil, err
+	}
+
+	return cached, nil
+}