aboutsummaryrefslogtreecommitdiffstats
path: root/cache/repo_cache.go
diff options
context:
space:
mode:
Diffstat (limited to 'cache/repo_cache.go')
-rw-r--r--cache/repo_cache.go402
1 files changed, 367 insertions, 35 deletions
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
+}