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
}