diff options
author | Michael Muré <batolettre@gmail.com> | 2019-03-01 23:17:57 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-03-01 23:17:57 +0100 |
commit | 7260ca05bc3588c0572887a7d8f1b897c7fc13da (patch) | |
tree | 66854358df3cb9de651f7688556ec5a4b8ab1868 /cache | |
parent | 0aefae6fcca5786f2c898029c3d6282f760f2c63 (diff) | |
parent | b6bed784e5664819250aac20b2b9690879ee6ab1 (diff) | |
download | git-bug-7260ca05bc3588c0572887a7d8f1b897c7fc13da.tar.gz |
Merge pull request #89 from MichaelMure/identity
WIP identity in git
Diffstat (limited to 'cache')
-rw-r--r-- | cache/bug_cache.go | 109 | ||||
-rw-r--r-- | cache/bug_excerpt.go | 41 | ||||
-rw-r--r-- | cache/filter.go | 49 | ||||
-rw-r--r-- | cache/identity_cache.go | 43 | ||||
-rw-r--r-- | cache/identity_excerpt.go | 70 | ||||
-rw-r--r-- | cache/multi_repo_cache.go | 1 | ||||
-rw-r--r-- | cache/repo_cache.go | 402 |
7 files changed, 608 insertions, 107 deletions
diff --git a/cache/bug_cache.go b/cache/bug_cache.go index 52e9eafb..5fc76658 100644 --- a/cache/bug_cache.go +++ b/cache/bug_cache.go @@ -9,6 +9,10 @@ import ( "github.com/MichaelMure/git-bug/util/git" ) +// BugCache is a wrapper around a Bug. It provide multiple functions: +// +// 1. Provide a higher level API to use than the raw API from Bug. +// 2. Maintain an up to date Snapshot available. type BugCache struct { repoCache *RepoCache bug *bug.WithSnapshot @@ -53,8 +57,8 @@ func (e ErrMultipleMatchOp) Error() string { return fmt.Sprintf("Multiple matching operation found:\n%s", strings.Join(casted, "\n")) } -// ResolveTargetWithMetadata will find an operation that has the matching metadata -func (c *BugCache) ResolveTargetWithMetadata(key string, value string) (git.Hash, error) { +// ResolveOperationWithMetadata will find an operation that has the matching metadata +func (c *BugCache) ResolveOperationWithMetadata(key string, value string) (git.Hash, error) { // preallocate but empty matching := make([]git.Hash, 0, 5) @@ -82,45 +86,45 @@ func (c *BugCache) ResolveTargetWithMetadata(key string, value string) (git.Hash return matching[0], nil } -func (c *BugCache) AddComment(message string) error { +func (c *BugCache) AddComment(message string) (*bug.AddCommentOperation, error) { return c.AddCommentWithFiles(message, nil) } -func (c *BugCache) AddCommentWithFiles(message string, files []git.Hash) error { - author, err := bug.GetUser(c.repoCache.repo) +func (c *BugCache) AddCommentWithFiles(message string, files []git.Hash) (*bug.AddCommentOperation, error) { + author, err := c.repoCache.GetUserIdentity() if err != nil { - return err + return nil, err } return c.AddCommentRaw(author, time.Now().Unix(), message, files, nil) } -func (c *BugCache) AddCommentRaw(author bug.Person, unixTime int64, message string, files []git.Hash, metadata map[string]string) error { - op, err := bug.AddCommentWithFiles(c.bug, author, unixTime, message, files) +func (c *BugCache) AddCommentRaw(author *IdentityCache, unixTime int64, message string, files []git.Hash, metadata map[string]string) (*bug.AddCommentOperation, error) { + op, err := bug.AddCommentWithFiles(c.bug, author.Identity, unixTime, message, files) if err != nil { - return err + return nil, err } for key, value := range metadata { op.SetMetadata(key, value) } - return c.notifyUpdated() + return op, c.notifyUpdated() } -func (c *BugCache) ChangeLabels(added []string, removed []string) ([]bug.LabelChangeResult, error) { - author, err := bug.GetUser(c.repoCache.repo) +func (c *BugCache) ChangeLabels(added []string, removed []string) ([]bug.LabelChangeResult, *bug.LabelChangeOperation, error) { + author, err := c.repoCache.GetUserIdentity() if err != nil { - return nil, err + return nil, nil, err } return c.ChangeLabelsRaw(author, time.Now().Unix(), added, removed, nil) } -func (c *BugCache) ChangeLabelsRaw(author bug.Person, unixTime int64, added []string, removed []string, metadata map[string]string) ([]bug.LabelChangeResult, error) { - changes, op, err := bug.ChangeLabels(c.bug, author, unixTime, added, removed) +func (c *BugCache) ChangeLabelsRaw(author *IdentityCache, unixTime int64, added []string, removed []string, metadata map[string]string) ([]bug.LabelChangeResult, *bug.LabelChangeOperation, error) { + changes, op, err := bug.ChangeLabels(c.bug, author.Identity, unixTime, added, removed) if err != nil { - return changes, err + return changes, nil, err } for key, value := range metadata { @@ -129,107 +133,112 @@ func (c *BugCache) ChangeLabelsRaw(author bug.Person, unixTime int64, added []st err = c.notifyUpdated() if err != nil { - return nil, err + return nil, nil, err } - return changes, nil + return changes, op, nil } -func (c *BugCache) Open() error { - author, err := bug.GetUser(c.repoCache.repo) +func (c *BugCache) Open() (*bug.SetStatusOperation, error) { + author, err := c.repoCache.GetUserIdentity() if err != nil { - return err + return nil, err } return c.OpenRaw(author, time.Now().Unix(), nil) } -func (c *BugCache) OpenRaw(author bug.Person, unixTime int64, metadata map[string]string) error { - op, err := bug.Open(c.bug, author, unixTime) +func (c *BugCache) OpenRaw(author *IdentityCache, unixTime int64, metadata map[string]string) (*bug.SetStatusOperation, error) { + op, err := bug.Open(c.bug, author.Identity, unixTime) if err != nil { - return err + return nil, err } for key, value := range metadata { op.SetMetadata(key, value) } - return c.notifyUpdated() + return op, c.notifyUpdated() } -func (c *BugCache) Close() error { - author, err := bug.GetUser(c.repoCache.repo) +func (c *BugCache) Close() (*bug.SetStatusOperation, error) { + author, err := c.repoCache.GetUserIdentity() if err != nil { - return err + return nil, err } return c.CloseRaw(author, time.Now().Unix(), nil) } -func (c *BugCache) CloseRaw(author bug.Person, unixTime int64, metadata map[string]string) error { - op, err := bug.Close(c.bug, author, unixTime) +func (c *BugCache) CloseRaw(author *IdentityCache, unixTime int64, metadata map[string]string) (*bug.SetStatusOperation, error) { + op, err := bug.Close(c.bug, author.Identity, unixTime) if err != nil { - return err + return nil, err } for key, value := range metadata { op.SetMetadata(key, value) } - return c.notifyUpdated() + return op, c.notifyUpdated() } -func (c *BugCache) SetTitle(title string) error { - author, err := bug.GetUser(c.repoCache.repo) +func (c *BugCache) SetTitle(title string) (*bug.SetTitleOperation, error) { + author, err := c.repoCache.GetUserIdentity() if err != nil { - return err + return nil, err } return c.SetTitleRaw(author, time.Now().Unix(), title, nil) } -func (c *BugCache) SetTitleRaw(author bug.Person, unixTime int64, title string, metadata map[string]string) error { - op, err := bug.SetTitle(c.bug, author, unixTime, title) +func (c *BugCache) SetTitleRaw(author *IdentityCache, unixTime int64, title string, metadata map[string]string) (*bug.SetTitleOperation, error) { + op, err := bug.SetTitle(c.bug, author.Identity, unixTime, title) if err != nil { - return err + return nil, err } for key, value := range metadata { op.SetMetadata(key, value) } - return c.notifyUpdated() + return op, c.notifyUpdated() } -func (c *BugCache) EditComment(target git.Hash, message string) error { - author, err := bug.GetUser(c.repoCache.repo) +func (c *BugCache) EditComment(target git.Hash, message string) (*bug.EditCommentOperation, error) { + author, err := c.repoCache.GetUserIdentity() if err != nil { - return err + return nil, err } return c.EditCommentRaw(author, time.Now().Unix(), target, message, nil) } -func (c *BugCache) EditCommentRaw(author bug.Person, unixTime int64, target git.Hash, message string, metadata map[string]string) error { - op, err := bug.EditComment(c.bug, author, unixTime, target, message) +func (c *BugCache) EditCommentRaw(author *IdentityCache, unixTime int64, target git.Hash, message string, metadata map[string]string) (*bug.EditCommentOperation, error) { + op, err := bug.EditComment(c.bug, author.Identity, unixTime, target, message) if err != nil { - return err + return nil, err } for key, value := range metadata { op.SetMetadata(key, value) } - return c.notifyUpdated() + return op, c.notifyUpdated() } func (c *BugCache) Commit() error { - return c.bug.Commit(c.repoCache.repo) + err := c.bug.Commit(c.repoCache.repo) + if err != nil { + return err + } + return c.notifyUpdated() } func (c *BugCache) CommitAsNeeded() error { - if c.bug.HasPendingOp() { - return c.bug.Commit(c.repoCache.repo) + err := c.bug.CommitAsNeeded(c.repoCache.repo) + if err != nil { + return err } - return nil + return c.notifyUpdated() } diff --git a/cache/bug_excerpt.go b/cache/bug_excerpt.go index 7a11fa82..fd06e51b 100644 --- a/cache/bug_excerpt.go +++ b/cache/bug_excerpt.go @@ -4,9 +4,15 @@ import ( "encoding/gob" "github.com/MichaelMure/git-bug/bug" + "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/util/lamport" ) +// Package initialisation used to register the type for (de)serialization +func init() { + gob.Register(BugExcerpt{}) +} + // BugExcerpt hold a subset of the bug values to be able to sort and filter bugs // efficiently without having to read and compile each raw bugs. type BugExcerpt struct { @@ -18,29 +24,52 @@ type BugExcerpt struct { EditUnixTime int64 Status bug.Status - Author bug.Person 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, - Author: snap.Author, 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 -func init() { - gob.Register(BugExcerpt{}) +func (b *BugExcerpt) HumanId() string { + return bug.FormatHumanID(b.Id) } /* diff --git a/cache/filter.go b/cache/filter.go index 033df131..022a8ff2 100644 --- a/cache/filter.go +++ b/cache/filter.go @@ -1,11 +1,13 @@ package cache import ( + "strings" + "github.com/MichaelMure/git-bug/bug" ) -// Filter is a functor that match a subset of bugs -type Filter func(excerpt *BugExcerpt) bool +// Filter is a predicate that match a subset of bugs +type Filter func(repoCache *RepoCache, excerpt *BugExcerpt) bool // StatusFilter return a Filter that match a bug status func StatusFilter(query string) (Filter, error) { @@ -14,21 +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 excerpt.Author.Match(query) + return func(repoCache *RepoCache, excerpt *BugExcerpt) bool { + query = strings.ToLower(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 @@ -40,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 } } @@ -54,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 } @@ -75,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 diff --git a/cache/identity_cache.go b/cache/identity_cache.go new file mode 100644 index 00000000..2ae55f2d --- /dev/null +++ b/cache/identity_cache.go @@ -0,0 +1,43 @@ +package cache + +import ( + "github.com/MichaelMure/git-bug/identity" +) + +// IdentityCache is a wrapper around an Identity for caching. +type IdentityCache struct { + *identity.Identity + repoCache *RepoCache +} + +func NewIdentityCache(repoCache *RepoCache, id *identity.Identity) *IdentityCache { + return &IdentityCache{ + Identity: id, + repoCache: repoCache, + } +} + +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 { + err := i.Identity.Commit(i.repoCache.repo) + if err != nil { + return err + } + return i.notifyUpdated() +} + +func (i *IdentityCache) CommitAsNeeded() error { + err := i.Identity.CommitAsNeeded(i.repoCache.repo) + if err != nil { + return err + } + return i.notifyUpdated() +} diff --git a/cache/identity_excerpt.go b/cache/identity_excerpt.go new file mode 100644 index 00000000..2a13bc60 --- /dev/null +++ b/cache/identity_excerpt.go @@ -0,0 +1,70 @@ +package cache + +import ( + "encoding/gob" + "fmt" + + "github.com/MichaelMure/git-bug/identity" +) + +// Package initialisation used to register the type for (de)serialization +func init() { + gob.Register(IdentityExcerpt{}) +} + +// IdentityExcerpt hold a subset of the identity values to be able to sort and +// filter identities efficiently without having to read and compile each raw +// identity. +type IdentityExcerpt struct { + Id string + + Name string + Login string + ImmutableMetadata map[string]string +} + +func NewIdentityExcerpt(i *identity.Identity) *IdentityExcerpt { + return &IdentityExcerpt{ + Id: i.Id(), + Name: i.Name(), + Login: i.Login(), + ImmutableMetadata: i.ImmutableMetadata(), + } +} + +func (i *IdentityExcerpt) HumanId() string { + return identity.FormatHumanID(i.Id) +} + +// DisplayName return a non-empty string to display, representing the +// identity, based on the non-empty values. +func (i *IdentityExcerpt) DisplayName() string { + switch { + case i.Name == "" && i.Login != "": + return i.Login + case i.Name != "" && i.Login == "": + return i.Name + case i.Name != "" && i.Login != "": + return fmt.Sprintf("%s (%s)", i.Name, i.Login) + } + + panic("invalid person data") +} + +/* + * Sorting + */ + +type IdentityById []*IdentityExcerpt + +func (b IdentityById) Len() int { + return len(b) +} + +func (b IdentityById) Less(i, j int) bool { + return b[i].Id < b[j].Id +} + +func (b IdentityById) Swap(i, j int) { + b[i], b[j] = b[j], b[i] +} diff --git a/cache/multi_repo_cache.go b/cache/multi_repo_cache.go index ec435ff2..da1c26bd 100644 --- a/cache/multi_repo_cache.go +++ b/cache/multi_repo_cache.go @@ -8,6 +8,7 @@ import ( const lockfile = "lock" +// MultiRepoCache is the root cache, holding multiple RepoCache. type MultiRepoCache struct { repos map[string]*RepoCache } diff --git a/cache/repo_cache.go b/cache/repo_cache.go index 286e27a5..2b0fa360 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -14,27 +14,64 @@ import ( "time" "github.com/MichaelMure/git-bug/bug" + "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/git" "github.com/MichaelMure/git-bug/util/process" ) -const cacheFile = "cache" -const formatVersion = 1 +const bugCacheFile = "bug-cache" +const identityCacheFile = "identity-cache" +// 1: original format +// 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 +// access later. +// 2. The cache maintain on memory and on disk a pre-digested excerpt for each bug, +// allowing for fast querying the whole set of bugs without having to load +// them individually. +// 3. The cache guarantee that a single instance of a Bug is loaded at once, avoiding +// loss of data that we could have with multiple copies in the same process. +// 4. The same way, the cache maintain in memory a single copy of the loaded identities. +// +// The cache also protect the on-disk data by locking the git repository for its +// own usage, by writing a lock file. Of course, normal git operations are not +// affected, only git-bug related one. type RepoCache struct { // the underlying repo repo repository.ClockedRepo + // excerpt of bugs data for all bugs - excerpts map[string]*BugExcerpt + bugExcerpts map[string]*BugExcerpt // bug loaded in memory bugs map[string]*BugCache + + // excerpt of identities data for all identities + identitiesExcerpts map[string]*IdentityExcerpt + // identities loaded in memory + identities map[string]*IdentityCache + + // the user identity's id, if known + userIdentityId string } func NewRepoCache(r repository.ClockedRepo) (*RepoCache, error) { c := &RepoCache{ - repo: r, - bugs: make(map[string]*BugCache), + repo: r, + bugs: make(map[string]*BugCache), + identities: make(map[string]*IdentityCache), } err := c.lock() @@ -46,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 { @@ -125,14 +165,38 @@ func (c *RepoCache) bugUpdated(id string) error { panic("missing bug in the cache") } - c.excerpts[id] = NewBugExcerpt(b.bug, b.Snapshot()) + c.bugExcerpts[id] = NewBugExcerpt(b.bug, b.Snapshot()) - return c.write() + // we only need to write the bug cache + return c.writeBugCache() } -// load will try to read from the disk the bug cache file +// 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 string) error { + i, ok := c.identities[id] + if !ok { + panic("missing identity in the cache") + } + + c.identitiesExcerpts[id] = NewIdentityExcerpt(i.Identity) + + // 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 { - f, err := os.Open(cacheFilePath(c.repo)) + 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 } @@ -149,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.excerpts = aux.Excerpts + 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 { @@ -166,7 +270,7 @@ func (c *RepoCache) write() error { Excerpts map[string]*BugExcerpt }{ Version: formatVersion, - Excerpts: c.excerpts, + Excerpts: c.bugExcerpts, } encoder := gob.NewEncoder(&data) @@ -176,7 +280,7 @@ func (c *RepoCache) write() error { return err } - f, err := os.Create(cacheFilePath(c.repo)) + f, err := os.Create(bugCacheFilePath(c.repo)) if err != nil { return err } @@ -189,14 +293,66 @@ func (c *RepoCache) write() error { return f.Close() } -func cacheFilePath(repo repository.Repo) string { - return path.Join(repo.GetPath(), ".git", "git-bug", cacheFile) +// 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", 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.excerpts = make(map[string]*BugExcerpt) + c.bugExcerpts = make(map[string]*BugExcerpt) allBugs := bug.ReadAllLocalBugs(c.repo) @@ -206,7 +362,7 @@ func (c *RepoCache) buildCache() error { } snap := b.Bug.Compile() - c.excerpts[b.Bug.Id()] = NewBugExcerpt(b.Bug, &snap) + c.bugExcerpts[b.Bug.Id()] = NewBugExcerpt(b.Bug, &snap) } _, _ = fmt.Fprintln(os.Stderr, "Done.") @@ -231,13 +387,23 @@ func (c *RepoCache) ResolveBug(id string) (*BugCache, error) { return cached, nil } +// ResolveBugExcerpt retrieve a BugExcerpt matching the exact given id +func (c *RepoCache) ResolveBugExcerpt(id string) (*BugExcerpt, error) { + e, ok := c.bugExcerpts[id] + if !ok { + return nil, bug.ErrBugNotExist + } + + return e, nil +} + // ResolveBugPrefix retrieve a bug matching an id prefix. It fails if multiple // bugs match. func (c *RepoCache) ResolveBugPrefix(prefix string) (*BugCache, error) { // preallocate but empty matching := make([]string, 0, 5) - for id := range c.excerpts { + for id := range c.bugExcerpts { if strings.HasPrefix(id, prefix) { matching = append(matching, id) } @@ -261,7 +427,7 @@ func (c *RepoCache) ResolveBugCreateMetadata(key string, value string) (*BugCach // preallocate but empty matching := make([]string, 0, 5) - for id, excerpt := range c.excerpts { + for id, excerpt := range c.bugExcerpts { if excerpt.CreateMetadata[key] == value { matching = append(matching, id) } @@ -278,6 +444,7 @@ func (c *RepoCache) ResolveBugCreateMetadata(key string, value string) (*BugCach return c.ResolveBug(matching[0]) } +// QueryBugs return the id of all Bug matching the given Query func (c *RepoCache) QueryBugs(query *Query) []string { if query == nil { return c.AllBugsIds() @@ -285,8 +452,8 @@ func (c *RepoCache) QueryBugs(query *Query) []string { var filtered []*BugExcerpt - for _, excerpt := range c.excerpts { - if query.Match(excerpt) { + for _, excerpt := range c.bugExcerpts { + if query.Match(c, excerpt) { filtered = append(filtered, excerpt) } } @@ -321,10 +488,10 @@ func (c *RepoCache) QueryBugs(query *Query) []string { // AllBugsIds return all known bug ids func (c *RepoCache) AllBugsIds() []string { - result := make([]string, len(c.excerpts)) + result := make([]string, len(c.bugExcerpts)) i := 0 - for _, excerpt := range c.excerpts { + for _, excerpt := range c.bugExcerpts { result[i] = excerpt.Id i++ } @@ -332,11 +499,6 @@ func (c *RepoCache) AllBugsIds() []string { return result } -// ClearAllBugs clear all bugs kept in memory -func (c *RepoCache) ClearAllBugs() { - c.bugs = make(map[string]*BugCache) -} - // ValidLabels list valid labels // // Note: in the future, a proper label policy could be implemented where valid @@ -345,7 +507,7 @@ func (c *RepoCache) ClearAllBugs() { func (c *RepoCache) ValidLabels() []bug.Label { set := map[bug.Label]interface{}{} - for _, excerpt := range c.excerpts { + for _, excerpt := range c.bugExcerpts { for _, l := range excerpt.Labels { set[l] = nil } @@ -376,7 +538,7 @@ func (c *RepoCache) NewBug(title string, message string) (*BugCache, error) { // 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 []git.Hash) (*BugCache, error) { - author, err := bug.GetUser(c.repo) + author, err := c.GetUserIdentity() if err != nil { return nil, err } @@ -387,8 +549,8 @@ func (c *RepoCache) NewBugWithFiles(title string, message string, files []git.Ha // 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 bug.Person, unixTime int64, title string, message string, files []git.Hash, metadata map[string]string) (*BugCache, error) { - b, op, err := bug.CreateWithFiles(author, unixTime, title, message, files) +func (c *RepoCache) NewBugRaw(author *IdentityCache, unixTime int64, title string, message string, files []git.Hash, metadata map[string]string) (*BugCache, error) { + b, op, err := bug.CreateWithFiles(author.Identity, unixTime, title, message, files) if err != nil { return nil, err } @@ -402,9 +564,14 @@ func (c *RepoCache) NewBugRaw(author bug.Person, unixTime int64, title string, m return nil, err } + if _, has := c.bugs[b.Id()]; has { + return nil, fmt.Errorf("bug %s already exist in the cache", b.Id()) + } + cached := NewBugCache(c, b) c.bugs[b.Id()] = cached + // force the write of the excerpt err = c.bugUpdated(b.Id()) if err != nil { return nil, err @@ -421,6 +588,8 @@ func (c *RepoCache) Fetch(remote string) (string, error) { // 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 @@ -441,7 +610,7 @@ func (c *RepoCache) MergeAll(remote string) <-chan bug.MergeResult { case bug.MergeStatusNew, bug.MergeStatusUpdated: b := result.Bug snap := b.Compile() - c.excerpts[id] = NewBugExcerpt(b, &snap) + c.bugExcerpts[id] = NewBugExcerpt(b, &snap) } } @@ -524,3 +693,166 @@ func repoIsAvailable(repo repository.Repo) error { return nil } + +// ResolveIdentity retrieve an identity matching the exact given id +func (c *RepoCache) ResolveIdentity(id string) (*IdentityCache, error) { + cached, ok := c.identities[id] + if ok { + return cached, nil + } + + i, err := identity.ReadLocal(c.repo, id) + if err != nil { + return nil, err + } + + cached = NewIdentityCache(c, i) + c.identities[id] = cached + + return cached, nil +} + +// ResolveIdentityExcerpt retrieve a IdentityExcerpt matching the exact given id +func (c *RepoCache) ResolveIdentityExcerpt(id string) (*IdentityExcerpt, error) { + e, ok := c.identitiesExcerpts[id] + if !ok { + return nil, identity.ErrIdentityNotExist + } + + return e, nil +} + +// ResolveIdentityPrefix retrieve an Identity matching an id prefix. +// It fails if multiple identities match. +func (c *RepoCache) ResolveIdentityPrefix(prefix string) (*IdentityCache, error) { + // preallocate but empty + matching := make([]string, 0, 5) + + for id := range c.identitiesExcerpts { + if strings.HasPrefix(id, prefix) { + matching = append(matching, id) + } + } + + if len(matching) > 1 { + return nil, identity.ErrMultipleMatch{Matching: matching} + } + + if len(matching) == 0 { + return nil, identity.ErrIdentityNotExist + } + + return c.ResolveIdentity(matching[0]) +} + +// 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) { + // preallocate but empty + matching := make([]string, 0, 5) + + for id, i := range c.identitiesExcerpts { + if i.ImmutableMetadata[key] == value { + matching = append(matching, id) + } + } + + if len(matching) > 1 { + return nil, identity.ErrMultipleMatch{Matching: matching} + } + + if len(matching) == 0 { + return nil, identity.ErrIdentityNotExist + } + + return c.ResolveIdentity(matching[0]) +} + +// AllIdentityIds return all known identity ids +func (c *RepoCache) AllIdentityIds() []string { + result := make([]string, 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 + } + + // 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 + } + } + + 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 +} + +// 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) + + for key, value := range metadata { + i.SetMetadata(key, value) + } + + err := i.Commit(c.repo) + if err != nil { + return nil, err + } + + 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 + + // force the write of the excerpt + err = c.identityUpdated(i.Id()) + if err != nil { + return nil, err + } + + return cached, nil +} |