identity: working identity cache

Michael Muré created

Change summary

cache/bug_excerpt.go    |  35 +++++++++-
cache/filter.go         |  44 ++++++++-----
cache/identity_cache.go |  17 ++++
cache/repo_cache.go     | 137 ++++++++++++++++++++++++++++++++++++++++--
commands/user.go        |   2 
identity/identity.go    |   2 
6 files changed, 204 insertions(+), 33 deletions(-)

Detailed changes

cache/bug_excerpt.go 🔗

@@ -4,6 +4,7 @@ import (
 	"encoding/gob"
 
 	"github.com/MichaelMure/git-bug/bug"
+	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/util/lamport"
 )
 
@@ -17,25 +18,49 @@ type BugExcerpt struct {
 	CreateUnixTime    int64
 	EditUnixTime      int64
 
-	Status   bug.Status
-	AuthorId string
-	Labels   []bug.Label
+	Status bug.Status
+	Labels []bug.Label
+
+	// If author is identity.Bare, LegacyAuthor is set
+	// If author is identity.Identity, AuthorId is set and data is deported
+	// in a IdentityExcerpt
+	LegacyAuthor LegacyAuthorExcerpt
+	AuthorId     string
 
 	CreateMetadata map[string]string
 }
 
+// identity.Bare data are directly embedded in the bug excerpt
+type LegacyAuthorExcerpt struct {
+	Name  string
+	Login string
+}
+
 func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt {
-	return &BugExcerpt{
+	e := &BugExcerpt{
 		Id:                b.Id(),
 		CreateLamportTime: b.CreateLamportTime(),
 		EditLamportTime:   b.EditLamportTime(),
 		CreateUnixTime:    b.FirstOp().GetUnixTime(),
 		EditUnixTime:      snap.LastEditUnix(),
 		Status:            snap.Status,
-		AuthorId:          snap.Author.Id(),
 		Labels:            snap.Labels,
 		CreateMetadata:    b.FirstOp().AllMetadata(),
 	}
+
+	switch snap.Author.(type) {
+	case *identity.Identity:
+		e.AuthorId = snap.Author.Id()
+	case *identity.Bare:
+		e.LegacyAuthor = LegacyAuthorExcerpt{
+			Login: snap.Author.Login(),
+			Name:  snap.Author.Name(),
+		}
+	default:
+		panic("unhandled identity type")
+	}
+
+	return e
 }
 
 // Package initialisation used to register the type for (de)serialization

cache/filter.go 🔗

@@ -7,7 +7,7 @@ import (
 )
 
 // Filter is a functor that match a subset of bugs
-type Filter func(excerpt *BugExcerpt) bool
+type Filter func(repoCache *RepoCache, excerpt *BugExcerpt) bool
 
 // StatusFilter return a Filter that match a bug status
 func StatusFilter(query string) (Filter, error) {
@@ -16,24 +16,36 @@ func StatusFilter(query string) (Filter, error) {
 		return nil, err
 	}
 
-	return func(excerpt *BugExcerpt) bool {
+	return func(repoCache *RepoCache, excerpt *BugExcerpt) bool {
 		return excerpt.Status == status
 	}, nil
 }
 
 // AuthorFilter return a Filter that match a bug author
 func AuthorFilter(query string) Filter {
-	return func(excerpt *BugExcerpt) bool {
+	return func(repoCache *RepoCache, excerpt *BugExcerpt) bool {
 		query = strings.ToLower(query)
 
-		return strings.Contains(strings.ToLower(excerpt.Author.Name), query) ||
-			strings.Contains(strings.ToLower(excerpt.Author.Login), query)
+		// Normal identity
+		if excerpt.AuthorId != "" {
+			author, ok := repoCache.identitiesExcerpts[excerpt.AuthorId]
+			if !ok {
+				panic("missing identity in the cache")
+			}
+
+			return strings.Contains(strings.ToLower(author.Name), query) ||
+				strings.Contains(strings.ToLower(author.Login), query)
+		}
+
+		// Legacy identity support
+		return strings.Contains(strings.ToLower(excerpt.LegacyAuthor.Name), query) ||
+			strings.Contains(strings.ToLower(excerpt.LegacyAuthor.Login), query)
 	}
 }
 
 // LabelFilter return a Filter that match a label
 func LabelFilter(label string) Filter {
-	return func(excerpt *BugExcerpt) bool {
+	return func(repoCache *RepoCache, excerpt *BugExcerpt) bool {
 		for _, l := range excerpt.Labels {
 			if string(l) == label {
 				return true
@@ -45,7 +57,7 @@ func LabelFilter(label string) Filter {
 
 // NoLabelFilter return a Filter that match the absence of labels
 func NoLabelFilter() Filter {
-	return func(excerpt *BugExcerpt) bool {
+	return func(repoCache *RepoCache, excerpt *BugExcerpt) bool {
 		return len(excerpt.Labels) == 0
 	}
 }
@@ -59,20 +71,20 @@ type Filters struct {
 }
 
 // Match check if a bug match the set of filters
-func (f *Filters) Match(excerpt *BugExcerpt) bool {
-	if match := f.orMatch(f.Status, excerpt); !match {
+func (f *Filters) Match(repoCache *RepoCache, excerpt *BugExcerpt) bool {
+	if match := f.orMatch(f.Status, repoCache, excerpt); !match {
 		return false
 	}
 
-	if match := f.orMatch(f.Author, excerpt); !match {
+	if match := f.orMatch(f.Author, repoCache, excerpt); !match {
 		return false
 	}
 
-	if match := f.orMatch(f.Label, excerpt); !match {
+	if match := f.orMatch(f.Label, repoCache, excerpt); !match {
 		return false
 	}
 
-	if match := f.andMatch(f.NoFilters, excerpt); !match {
+	if match := f.andMatch(f.NoFilters, repoCache, excerpt); !match {
 		return false
 	}
 
@@ -80,28 +92,28 @@ func (f *Filters) Match(excerpt *BugExcerpt) bool {
 }
 
 // Check if any of the filters provided match the bug
-func (*Filters) orMatch(filters []Filter, excerpt *BugExcerpt) bool {
+func (*Filters) orMatch(filters []Filter, repoCache *RepoCache, excerpt *BugExcerpt) bool {
 	if len(filters) == 0 {
 		return true
 	}
 
 	match := false
 	for _, f := range filters {
-		match = match || f(excerpt)
+		match = match || f(repoCache, excerpt)
 	}
 
 	return match
 }
 
 // Check if all of the filters provided match the bug
-func (*Filters) andMatch(filters []Filter, excerpt *BugExcerpt) bool {
+func (*Filters) andMatch(filters []Filter, repoCache *RepoCache, excerpt *BugExcerpt) bool {
 	if len(filters) == 0 {
 		return true
 	}
 
 	match := true
 	for _, f := range filters {
-		match = match && f(excerpt)
+		match = match && f(repoCache, excerpt)
 	}
 
 	return match

cache/identity_cache.go 🔗

@@ -21,10 +21,23 @@ func (i *IdentityCache) notifyUpdated() error {
 	return i.repoCache.identityUpdated(i.Identity.Id())
 }
 
+func (i *IdentityCache) AddVersion(version *identity.Version) error {
+	i.Identity.AddVersion(version)
+	return i.notifyUpdated()
+}
+
 func (i *IdentityCache) Commit() error {
-	return i.Identity.Commit(i.repoCache.repo)
+	err := i.Identity.Commit(i.repoCache.repo)
+	if err != nil {
+		return err
+	}
+	return i.notifyUpdated()
 }
 
 func (i *IdentityCache) CommitAsNeeded() error {
-	return i.Identity.CommitAsNeeded(i.repoCache.repo)
+	err := i.Identity.CommitAsNeeded(i.repoCache.repo)
+	if err != nil {
+		return err
+	}
+	return i.notifyUpdated()
 }

cache/repo_cache.go 🔗

@@ -27,6 +27,14 @@ const identityCacheFile = "identity-cache"
 // 2: added cache for identities with a reference in the bug cache
 const formatVersion = 2
 
+type ErrInvalidCacheFormat struct {
+	message string
+}
+
+func (e ErrInvalidCacheFormat) Error() string {
+	return e.message
+}
+
 // RepoCache is a cache for a Repository. This cache has multiple functions:
 //
 // 1. After being loaded, a Bug is kept in memory in the cache, allowing for fast
@@ -75,6 +83,9 @@ func NewRepoCache(r repository.ClockedRepo) (*RepoCache, error) {
 	if err == nil {
 		return c, nil
 	}
+	if _, ok := err.(ErrInvalidCacheFormat); ok {
+		return nil, err
+	}
 
 	err = c.buildCache()
 	if err != nil {
@@ -156,7 +167,8 @@ func (c *RepoCache) bugUpdated(id string) error {
 
 	c.bugExcerpts[id] = NewBugExcerpt(b.bug, b.Snapshot())
 
-	return c.write()
+	// we only need to write the bug cache
+	return c.writeBugCache()
 }
 
 // identityUpdated is a callback to trigger when the excerpt of an identity
@@ -169,11 +181,21 @@ func (c *RepoCache) identityUpdated(id string) error {
 
 	c.identitiesExcerpts[id] = NewIdentityExcerpt(i.Identity)
 
-	return c.write()
+	// we only need to write the identity cache
+	return c.writeIdentityCache()
 }
 
-// load will try to read from the disk the bug cache file
+// 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 {
 	f, err := os.Open(bugCacheFilePath(c.repo))
 	if err != nil {
 		return err
@@ -191,16 +213,56 @@ func (c *RepoCache) load() error {
 		return err
 	}
 
-	if aux.Version != 1 {
-		return fmt.Errorf("unknown cache format version %v", aux.Version)
+	if aux.Version != 2 {
+		return ErrInvalidCacheFormat{
+			message: fmt.Sprintf("unknown cache format version %v", aux.Version),
+		}
 	}
 
 	c.bugExcerpts = aux.Excerpts
 	return nil
 }
 
-// write will serialize on disk the bug cache file
+// load will try to read from the disk the identity cache file
+func (c *RepoCache) loadIdentityCache() error {
+	f, err := os.Open(identityCacheFilePath(c.repo))
+	if err != nil {
+		return err
+	}
+
+	decoder := gob.NewDecoder(f)
+
+	aux := struct {
+		Version  uint
+		Excerpts map[string]*IdentityExcerpt
+	}{}
+
+	err = decoder.Decode(&aux)
+	if err != nil {
+		return err
+	}
+
+	if aux.Version != 2 {
+		return ErrInvalidCacheFormat{
+			message: fmt.Sprintf("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 {
 	var data bytes.Buffer
 
 	aux := struct {
@@ -231,15 +293,63 @@ func (c *RepoCache) write() error {
 	return f.Close()
 }
 
+// write will serialize on disk the identity cache file
+func (c *RepoCache) writeIdentityCache() error {
+	var data bytes.Buffer
+
+	aux := struct {
+		Version  uint
+		Excerpts map[string]*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", "git-bug", bugCacheFile)
 }
 
 func identityCacheFilePath(repo repository.Repo) string {
-	return path.Join(repo.GetPath(), ".git", "git-bug", bugCacheFile)
+	return path.Join(repo.GetPath(), ".git", "git-bug", identityCacheFile)
 }
 
 func (c *RepoCache) buildCache() error {
+	_, _ = fmt.Fprintf(os.Stderr, "Building identity cache... ")
+
+	c.identitiesExcerpts = make(map[string]*IdentityExcerpt)
+
+	allIdentities := identity.ReadAllLocalIdentities(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[string]*BugExcerpt)
@@ -333,7 +443,7 @@ func (c *RepoCache) QueryBugs(query *Query) []string {
 	var filtered []*BugExcerpt
 
 	for _, excerpt := range c.bugExcerpts {
-		if query.Match(excerpt) {
+		if query.Match(c, excerpt) {
 			filtered = append(filtered, excerpt)
 		}
 	}
@@ -463,11 +573,15 @@ func (c *RepoCache) NewBugRaw(author *IdentityCache, unixTime int64, title strin
 // Fetch retrieve update from a remote
 // This does not change the local bugs state
 func (c *RepoCache) Fetch(remote string) (string, error) {
+	// TODO: add identities
+
 	return bug.Fetch(c.repo, remote)
 }
 
 // MergeAll will merge all the available remote bug
 func (c *RepoCache) MergeAll(remote string) <-chan bug.MergeResult {
+	// TODO: add identities
+
 	out := make(chan bug.MergeResult)
 
 	// Intercept merge results to update the cache properly
@@ -505,6 +619,8 @@ func (c *RepoCache) MergeAll(remote string) <-chan bug.MergeResult {
 
 // Push update a remote with the local changes
 func (c *RepoCache) Push(remote string) (string, error) {
+	// TODO: add identities
+
 	return bug.Push(c.repo, remote)
 }
 
@@ -655,6 +771,11 @@ func (c *RepoCache) SetUserIdentity(i *IdentityCache) error {
 		return err
 	}
 
+	// 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

commands/user.go 🔗

@@ -38,7 +38,7 @@ func runUser(cmd *cobra.Command, args []string) error {
 	fmt.Printf("Name: %s\n", id.Name())
 	fmt.Printf("Login: %s\n", id.Login())
 	fmt.Printf("Email: %s\n", id.Email())
-	fmt.Printf("Protected: %v\n", id.IsProtected())
+	// fmt.Printf("Protected: %v\n", id.IsProtected())
 
 	return nil
 }

identity/identity.go 🔗

@@ -84,7 +84,7 @@ func ReadRemote(repo repository.Repo, remote string, id string) (*Identity, erro
 	return read(repo, ref)
 }
 
-// read will load and parse an identity frdm git
+// read will load and parse an identity from git
 func read(repo repository.Repo, ref string) (*Identity, error) {
 	refSplit := strings.Split(ref, "/")
 	id := refSplit[len(refSplit)-1]