diff options
-rw-r--r-- | cache/repo_cache.go | 860 | ||||
-rw-r--r-- | cache/repo_cache_bug.go | 361 | ||||
-rw-r--r-- | cache/repo_cache_common.go | 231 | ||||
-rw-r--r-- | cache/repo_cache_identity.go | 275 |
4 files changed, 881 insertions, 846 deletions
diff --git a/cache/repo_cache.go b/cache/repo_cache.go index f778c944..7e605a6e 100644 --- a/cache/repo_cache.go +++ b/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 -} diff --git a/cache/repo_cache_bug.go b/cache/repo_cache_bug.go new file mode 100644 index 00000000..30692363 --- /dev/null +++ b/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 +} diff --git a/cache/repo_cache_common.go b/cache/repo_cache_common.go new file mode 100644 index 00000000..a931f2be --- /dev/null +++ b/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) +} diff --git a/cache/repo_cache_identity.go b/cache/repo_cache_identity.go new file mode 100644 index 00000000..8957d4aa --- /dev/null +++ b/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 +} |