Detailed changes
@@ -0,0 +1,106 @@
+package cache
+
+import (
+ "time"
+
+ "github.com/MichaelMure/git-bug/entities/board"
+ "github.com/MichaelMure/git-bug/entities/identity"
+ "github.com/MichaelMure/git-bug/entity"
+ "github.com/MichaelMure/git-bug/repository"
+)
+
+// BoardCache is a wrapper around a Board. It provides multiple functions:
+//
+// 1. Provide a higher level API to use than the raw API from Board.
+// 2. Maintain an up-to-date Snapshot available.
+// 3. Deal with concurrency.
+type BoardCache struct {
+ CachedEntityBase[*board.Snapshot, board.Operation]
+}
+
+func NewBoardCache(b *board.Board, repo repository.ClockedRepo, getUserIdentity getUserIdentityFunc, entityUpdated func(id entity.Id) error) *BoardCache {
+ return &BoardCache{
+ CachedEntityBase: CachedEntityBase[*board.Snapshot, board.Operation]{
+ repo: repo,
+ entityUpdated: entityUpdated,
+ getUserIdentity: getUserIdentity,
+ entity: &withSnapshot[*board.Snapshot, board.Operation]{Interface: b},
+ },
+ }
+}
+
+func (c *BoardCache) AddItemDraft(columnId entity.Id, title, message string, files []repository.Hash) (entity.CombinedId, *board.AddItemDraftOperation, error) {
+ author, err := c.getUserIdentity()
+ if err != nil {
+ return entity.UnsetCombinedId, nil, err
+ }
+
+ return c.AddItemDraftRaw(author, time.Now().Unix(), columnId, title, message, files, nil)
+}
+
+func (c *BoardCache) AddItemDraftRaw(author identity.Interface, unixTime int64, columnId entity.Id, title, message string, files []repository.Hash, metadata map[string]string) (entity.CombinedId, *board.AddItemDraftOperation, error) {
+ c.mu.Lock()
+ itemId, op, err := board.AddItemDraft(c.entity, author, unixTime, columnId, title, message, files, metadata)
+ c.mu.Unlock()
+ if err != nil {
+ return entity.UnsetCombinedId, nil, err
+ }
+ return itemId, op, c.notifyUpdated()
+}
+
+func (c *BoardCache) AddItemEntity(columnId entity.Id, e entity.Interface) (entity.CombinedId, *board.AddItemEntityOperation, error) {
+ author, err := c.getUserIdentity()
+ if err != nil {
+ return entity.UnsetCombinedId, nil, err
+ }
+
+ return c.AddItemEntityRaw(author, time.Now().Unix(), columnId, e, nil)
+}
+
+func (c *BoardCache) AddItemEntityRaw(author identity.Interface, unixTime int64, columnId entity.Id, e entity.Interface, metadata map[string]string) (entity.CombinedId, *board.AddItemEntityOperation, error) {
+ c.mu.Lock()
+ itemId, op, err := board.AddItemEntity(c.entity, author, unixTime, columnId, e, metadata)
+ c.mu.Unlock()
+ if err != nil {
+ return entity.UnsetCombinedId, nil, err
+ }
+ return itemId, op, c.notifyUpdated()
+}
+
+func (c *BoardCache) SetDescription(description string) (*board.SetDescriptionOperation, error) {
+ author, err := c.getUserIdentity()
+ if err != nil {
+ return nil, err
+ }
+
+ return c.SetDescriptionRaw(author, time.Now().Unix(), description, nil)
+}
+
+func (c *BoardCache) SetDescriptionRaw(author identity.Interface, unixTime int64, description string, metadata map[string]string) (*board.SetDescriptionOperation, error) {
+ c.mu.Lock()
+ op, err := board.SetDescription(c.entity, author, unixTime, description, metadata)
+ c.mu.Unlock()
+ if err != nil {
+ return nil, err
+ }
+ return op, c.notifyUpdated()
+}
+
+func (c *BoardCache) SetTitle(title string) (*board.SetTitleOperation, error) {
+ author, err := c.getUserIdentity()
+ if err != nil {
+ return nil, err
+ }
+
+ return c.SetTitleRaw(author, time.Now().Unix(), title, nil)
+}
+
+func (c *BoardCache) SetTitleRaw(author identity.Interface, unixTime int64, title string, metadata map[string]string) (*board.SetTitleOperation, error) {
+ c.mu.Lock()
+ op, err := board.SetTitle(c.entity, author, unixTime, title, metadata)
+ c.mu.Unlock()
+ if err != nil {
+ return nil, err
+ }
+ return op, c.notifyUpdated()
+}
@@ -0,0 +1,72 @@
+package cache
+
+import (
+ "encoding/gob"
+ "time"
+
+ "github.com/MichaelMure/git-bug/entity"
+ "github.com/MichaelMure/git-bug/util/lamport"
+)
+
+// Package initialisation used to register the type for (de)serialization
+func init() {
+ gob.Register(BoardExcerpt{})
+}
+
+var _ Excerpt = &BoardExcerpt{}
+
+// BoardExcerpt hold a subset of the board values to be able to sort and filter boards
+// efficiently without having to read and compile each raw boards.
+type BoardExcerpt struct {
+ id entity.Id
+
+ CreateLamportTime lamport.Time
+ EditLamportTime lamport.Time
+ CreateUnixTime int64
+ EditUnixTime int64
+
+ Title string
+ Description string
+ ItemCount int
+ Actors []entity.Id
+
+ CreateMetadata map[string]string
+}
+
+func NewBoardExcerpt(b *BoardCache) *BoardExcerpt {
+ snap := b.Snapshot()
+
+ actorsIds := make([]entity.Id, 0, len(snap.Actors))
+ for _, actor := range snap.Actors {
+ actorsIds = append(actorsIds, actor.Id())
+ }
+
+ return &BoardExcerpt{
+ id: b.Id(),
+ CreateLamportTime: b.CreateLamportTime(),
+ EditLamportTime: b.EditLamportTime(),
+ CreateUnixTime: b.FirstOp().Time().Unix(),
+ EditUnixTime: snap.EditTime().Unix(),
+ Title: snap.Title,
+ Description: snap.Description,
+ ItemCount: snap.ItemCount(),
+ Actors: actorsIds,
+ CreateMetadata: b.FirstOp().AllMetadata(),
+ }
+}
+
+func (b *BoardExcerpt) Id() entity.Id {
+ return b.id
+}
+
+func (b *BoardExcerpt) setId(id entity.Id) {
+ b.id = id
+}
+
+func (b *BoardExcerpt) CreateTime() time.Time {
+ return time.Unix(b.CreateUnixTime, 0)
+}
+
+func (b *BoardExcerpt) EditTime() time.Time {
+ return time.Unix(b.EditUnixTime, 0)
+}
@@ -0,0 +1,78 @@
+package cache
+
+import (
+ "time"
+
+ "github.com/MichaelMure/git-bug/entities/board"
+ "github.com/MichaelMure/git-bug/entities/identity"
+ "github.com/MichaelMure/git-bug/entity"
+ "github.com/MichaelMure/git-bug/repository"
+)
+
+type RepoCacheBoard struct {
+ *SubCache[*board.Board, *BoardExcerpt, *BoardCache]
+}
+
+func NewRepoCacheBoard(repo repository.ClockedRepo,
+ resolvers func() entity.Resolvers,
+ getUserIdentity getUserIdentityFunc) *RepoCacheBoard {
+
+ makeCached := func(b *board.Board, entityUpdated func(id entity.Id) error) *BoardCache {
+ return NewBoardCache(b, repo, getUserIdentity, entityUpdated)
+ }
+
+ makeIndexData := func(b *BoardCache) []string {
+ // no indexing
+ return nil
+ }
+
+ actions := Actions[*board.Board]{
+ ReadWithResolver: board.ReadWithResolver,
+ ReadAllWithResolver: board.ReadAllWithResolver,
+ Remove: board.Remove,
+ MergeAll: board.MergeAll,
+ }
+
+ sc := NewSubCache[*board.Board, *BoardExcerpt, *BoardCache](
+ repo, resolvers, getUserIdentity,
+ makeCached, NewBoardExcerpt, makeIndexData, actions,
+ board.Typename, board.Namespace,
+ formatVersion, defaultMaxLoadedBugs,
+ )
+
+ return &RepoCacheBoard{SubCache: sc}
+}
+
+func (c *RepoCacheBoard) New(title, description string, columns []string) (*BoardCache, *board.CreateOperation, error) {
+ author, err := c.getUserIdentity()
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return c.NewRaw(author, time.Now().Unix(), title, description, columns, nil)
+}
+
+func (c *RepoCacheBoard) NewDefaultColumns(title, description string) (*BoardCache, *board.CreateOperation, error) {
+ return c.New(title, description, board.DefaultColumns)
+}
+
+// NewRaw create a new board with the given title, description and columns.
+// The new board is written in the repository (commit).
+func (c *RepoCacheBoard) NewRaw(author identity.Interface, unixTime int64, title, description string, columns []string, metadata map[string]string) (*BoardCache, *board.CreateOperation, error) {
+ b, op, err := board.Create(author, unixTime, title, description, columns, metadata)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ err = b.Commit(c.repo)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ cached, err := c.add(b)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return cached, op, nil
+}
@@ -62,6 +62,7 @@ type RepoCache struct {
// resolvers for all known entities and excerpts
resolvers entity.Resolvers
+ boards *RepoCacheBoard
bugs *RepoCacheBug
identities *RepoCacheIdentity
@@ -94,11 +95,16 @@ func NewNamedRepoCache(r repository.ClockedRepo, name string) (*RepoCache, chan
c.bugs = NewRepoCacheBug(r, c.getResolvers, c.GetUserIdentity)
c.subcaches = append(c.subcaches, c.bugs)
+ c.boards = NewRepoCacheBoard(r, c.getResolvers, c.GetUserIdentity)
+ c.subcaches = append(c.subcaches, c.boards)
+
c.resolvers = entity.Resolvers{
&IdentityCache{}: entity.ResolverFunc[*IdentityCache](c.identities.Resolve),
&IdentityExcerpt{}: entity.ResolverFunc[*IdentityExcerpt](c.identities.ResolveExcerpt),
&BugCache{}: entity.ResolverFunc[*BugCache](c.bugs.Resolve),
&BugExcerpt{}: entity.ResolverFunc[*BugExcerpt](c.bugs.ResolveExcerpt),
+ &BoardCache{}: entity.ResolverFunc[*BoardCache](c.boards.Resolve),
+ &BoardExcerpt{}: entity.ResolverFunc[*BoardExcerpt](c.boards.ResolveExcerpt),
}
// small buffer so that below functions can emit an event without blocking
@@ -137,6 +143,11 @@ func NewRepoCacheNoEvents(r repository.ClockedRepo) (*RepoCache, error) {
return cache, nil
}
+// Boards gives access to the Board entities
+func (c *RepoCache) Boards() *RepoCacheBoard {
+ return c.boards
+}
+
// Bugs gives access to the Bug entities
func (c *RepoCache) Bugs() *RepoCacheBug {
return c.bugs
@@ -16,9 +16,12 @@ var _ Interface = &Board{}
// 1: original format
const formatVersion = 1
+const Typename = "board"
+const Namespace = "boards"
+
var def = dag.Definition{
- Typename: "board",
- Namespace: "boards",
+ Typename: Typename,
+ Namespace: Namespace,
OperationUnmarshaler: operationUnmarshaler,
FormatVersion: formatVersion,
}
@@ -41,6 +44,10 @@ func NewBoard() *Board {
}
}
+func wrapper(e *dag.Entity) *Board {
+ return &Board{Entity: e}
+}
+
func simpleResolvers(repo repository.ClockedRepo) entity.Resolvers {
return entity.Resolvers{
&identity.Identity{}: identity.NewSimpleResolver(repo),
@@ -55,11 +62,17 @@ func Read(repo repository.ClockedRepo, id entity.Id) (*Board, error) {
// ReadWithResolver will read a board from its Id, with a custom identity.Resolver
func ReadWithResolver(repo repository.ClockedRepo, resolvers entity.Resolvers, id entity.Id) (*Board, error) {
- e, err := dag.Read(def, repo, resolvers, id)
- if err != nil {
- return nil, err
- }
- return &Board{Entity: e}, nil
+ return dag.Read(def, wrapper, repo, resolvers, id)
+}
+
+// ReadAll read and parse all local boards
+func ReadAll(repo repository.ClockedRepo) <-chan entity.StreamedEntity[*Board] {
+ return dag.ReadAll(def, wrapper, repo, simpleResolvers(repo))
+}
+
+// ReadAllWithResolver read and parse all local boards
+func ReadAllWithResolver(repo repository.ClockedRepo, resolvers entity.Resolvers) <-chan entity.StreamedEntity[*Board] {
+ return dag.ReadAll(def, wrapper, repo, resolvers)
}
// Validate check if the Board data is valid
@@ -23,33 +23,14 @@ func Push(repo repository.Repo, remote string) (string, error) {
// Note: an author is necessary for the case where a merge commit is created, as this commit will
// have an author and may be signed if a signing key is available.
func Pull(repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, mergeAuthor identity.Interface) error {
- return dag.Pull(def, repo, resolvers, remote, mergeAuthor)
+ return dag.Pull(def, wrapper, repo, resolvers, remote, mergeAuthor)
}
// MergeAll will merge all the available remote board
// Note: an author is necessary for the case where a merge commit is created, as this commit will
// have an author and may be signed if a signing key is available.
func MergeAll(repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, mergeAuthor identity.Interface) <-chan entity.MergeResult {
- out := make(chan entity.MergeResult)
-
- go func() {
- defer close(out)
-
- results := dag.MergeAll(def, repo, resolvers, remote, mergeAuthor)
-
- // wrap the dag.Entity into a complete Bug
- for result := range results {
- result := result
- if result.Entity != nil {
- result.Entity = &Board{
- Entity: result.Entity.(*dag.Entity),
- }
- }
- out <- result
- }
- }()
-
- return out
+ return dag.MergeAll(def, wrapper, repo, resolvers, remote, mergeAuthor)
}
// Remove will remove a local bug from its entity.Id
@@ -88,14 +88,14 @@ func NewAddItemDraftOp(author identity.Interface, unixTime int64, columnId entit
}
// AddItemDraft is a convenience function to add a draft item to a Board
-func AddItemDraft(b *Board, author identity.Interface, unixTime int64, columnId entity.Id, title, message string, files []repository.Hash, metadata map[string]string) (*AddItemDraftOperation, error) {
+func AddItemDraft(b Interface, author identity.Interface, unixTime int64, columnId entity.Id, title, message string, files []repository.Hash, metadata map[string]string) (entity.CombinedId, *AddItemDraftOperation, error) {
op := NewAddItemDraftOp(author, unixTime, columnId, title, message, files)
for key, val := range metadata {
op.SetMetadata(key, val)
}
if err := op.Validate(); err != nil {
- return nil, err
+ return entity.UnsetCombinedId, nil, err
}
b.Append(op)
- return op, nil
+ return entity.CombineIds(b.Id(), op.Id()), op, nil
}
@@ -89,14 +89,14 @@ func NewAddItemEntityOp(author identity.Interface, unixTime int64, columnId enti
}
// AddItemEntity is a convenience function to add an entity item to a Board
-func AddItemEntity(b *Board, author identity.Interface, unixTime int64, columnId entity.Id, e entity.Interface, metadata map[string]string) (*AddItemEntityOperation, error) {
+func AddItemEntity(b Interface, author identity.Interface, unixTime int64, columnId entity.Id, e entity.Interface, metadata map[string]string) (entity.CombinedId, *AddItemEntityOperation, error) {
op := NewAddItemEntityOp(author, unixTime, columnId, e)
for key, val := range metadata {
op.SetMetadata(key, val)
}
if err := op.Validate(); err != nil {
- return nil, err
+ return entity.UnsetCombinedId, nil, err
}
b.Append(op)
- return op, nil
+ return entity.CombineIds(b.Id(), op.Id()), op, nil
}
@@ -100,14 +100,17 @@ func (op *CreateOperation) Apply(snap *Snapshot) {
}
// CreateDefaultColumns is a convenience function to create a board with the default columns
-func CreateDefaultColumns(author identity.Interface, unixTime int64, title, description string) (*Board, *CreateOperation, error) {
- return Create(author, unixTime, title, description, DefaultColumns)
+func CreateDefaultColumns(author identity.Interface, unixTime int64, title, description string, metadata map[string]string) (*Board, *CreateOperation, error) {
+ return Create(author, unixTime, title, description, DefaultColumns, metadata)
}
// Create is a convenience function to create a board
-func Create(author identity.Interface, unixTime int64, title, description string, columns []string) (*Board, *CreateOperation, error) {
+func Create(author identity.Interface, unixTime int64, title, description string, columns []string, metadata map[string]string) (*Board, *CreateOperation, error) {
b := NewBoard()
op := NewCreateOp(author, unixTime, title, description, columns)
+ for key, val := range metadata {
+ op.SetMetadata(key, val)
+ }
if err := op.Validate(); err != nil {
return nil, op, err
}
@@ -56,7 +56,7 @@ func NewSetDescriptionOp(author identity.Interface, unixTime int64, description
}
// SetDescription is a convenience function to change a board description
-func SetDescription(b *Board, author identity.Interface, unixTime int64, description string) (*SetDescriptionOperation, error) {
+func SetDescription(b Interface, author identity.Interface, unixTime int64, description string, metadata map[string]string) (*SetDescriptionOperation, error) {
var lastDescriptionOp *SetDescriptionOperation
for _, op := range b.Operations() {
switch op := op.(type) {
@@ -73,6 +73,9 @@ func SetDescription(b *Board, author identity.Interface, unixTime int64, descrip
}
op := NewSetDescriptionOp(author, unixTime, description, was)
+ for key, val := range metadata {
+ op.SetMetadata(key, val)
+ }
if err := op.Validate(); err != nil {
return nil, err
}
@@ -56,7 +56,7 @@ func NewSetTitleOp(author identity.Interface, unixTime int64, title string, was
}
// SetTitle is a convenience function to change a board title
-func SetTitle(b *Board, author identity.Interface, unixTime int64, title string) (*SetTitleOperation, error) {
+func SetTitle(b Interface, author identity.Interface, unixTime int64, title string, metadata map[string]string) (*SetTitleOperation, error) {
var lastTitleOp *SetTitleOperation
for _, op := range b.Operations() {
switch op := op.(type) {
@@ -73,6 +73,9 @@ func SetTitle(b *Board, author identity.Interface, unixTime int64, title string)
}
op := NewSetTitleOp(author, unixTime, title, was)
+ for key, val := range metadata {
+ op.SetMetadata(key, val)
+ }
if err := op.Validate(); err != nil {
return nil, err
}
@@ -33,10 +33,6 @@ type Snapshot struct {
Operations []dag.Operation
}
-func (snap *Snapshot) AllOperations() []dag.Operation {
- return snap.Operations
-}
-
// Id returns the Board identifier
func (snap *Snapshot) Id() entity.Id {
if snap.id == "" {
@@ -46,6 +42,14 @@ func (snap *Snapshot) Id() entity.Id {
return snap.id
}
+func (snap *Snapshot) AllOperations() []dag.Operation {
+ return snap.Operations
+}
+
+func (snap *Snapshot) AppendOperation(op dag.Operation) {
+ snap.Operations = append(snap.Operations, op)
+}
+
// EditTime returns the last time the board was modified
func (snap *Snapshot) EditTime() time.Time {
if len(snap.Operations) == 0 {
@@ -65,3 +69,34 @@ func (snap *Snapshot) addActor(actor identity.Interface) {
snap.Actors = append(snap.Actors, actor)
}
+
+// HasActor return true if the id is a actor
+func (snap *Snapshot) HasActor(id entity.Id) bool {
+ for _, p := range snap.Actors {
+ if p.Id() == id {
+ return true
+ }
+ }
+ return false
+}
+
+// HasAnyActor return true if one of the ids is a actor
+func (snap *Snapshot) HasAnyActor(ids ...entity.Id) bool {
+ for _, id := range ids {
+ if snap.HasActor(id) {
+ return true
+ }
+ }
+ return false
+}
+
+func (snap *Snapshot) ItemCount() int {
+ var count int
+ for _, column := range snap.Columns {
+ count += len(column.Items)
+ }
+ return count
+}
+
+// IsAuthored is a sign post method for gqlgen
+func (snap *Snapshot) IsAuthored() {}
@@ -3,6 +3,7 @@ package main
import (
"os"
+ "github.com/git-bug/git-bug/entities/board"
"github.com/git-bug/git-bug/entities/bug"
rb "github.com/git-bug/git-bug/misc/random_bugs"
"github.com/git-bug/git-bug/repository"
@@ -20,6 +21,7 @@ func main() {
loaders := []repository.ClockLoader{
bug.ClockLoader,
+ board.ClockLoader,
}
repo, err := repository.OpenGoGitRepo(dir, gitBugNamespace, loaders)