diff --git a/cache/board_cache.go b/cache/board_cache.go index ac7c283b0d0f896e68fa895a41b6a1e667f075f6..54b231645bc0efe32f35f8a0827c95a65fa87757 100644 --- a/cache/board_cache.go +++ b/cache/board_cache.go @@ -11,7 +11,7 @@ import ( // 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. +// 1. Provides 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 { @@ -24,7 +24,7 @@ func NewBoardCache(b *board.Board, repo repository.ClockedRepo, getUserIdentity repo: repo, entityUpdated: entityUpdated, getUserIdentity: getUserIdentity, - entity: &withSnapshot[*board.Snapshot, board.Operation]{Interface: b}, + entity: newWithSnapshot[*board.Snapshot, board.Operation](b), }, } } diff --git a/cache/board_subcache.go b/cache/board_subcache.go index bf33dfbea804e49e964029b6bea383458b0072db..a2b34a5896ed0cc693ccd1d707ddcd4bb89b7cae 100644 --- a/cache/board_subcache.go +++ b/cache/board_subcache.go @@ -31,6 +31,7 @@ func NewRepoCacheBoard(repo repository.ClockedRepo, ReadWithResolver: board.ReadWithResolver, ReadAllWithResolver: board.ReadAllWithResolver, Remove: board.Remove, + RemoveAll: board.RemoveAll, MergeAll: board.MergeAll, } @@ -44,6 +45,15 @@ func NewRepoCacheBoard(repo repository.ClockedRepo, return &RepoCacheBoard{SubCache: sc} } +// ResolveBoardCreateMetadata retrieve a board that has the exact given metadata on its Create operation, that is, the first operation. +// It fails if multiple bugs match. +func (c *RepoCacheBoard) ResolveBoardCreateMetadata(key string, value string) (*BoardCache, error) { + return c.ResolveMatcher(func(excerpt *BoardExcerpt) bool { + return excerpt.CreateMetadata[key] == value + }) +} + +// ResolveColumn finds the board and column id that matches the given prefix. func (c *RepoCacheBoard) ResolveColumn(prefix string) (*BoardCache, entity.CombinedId, error) { boardPrefix, _ := entity.SeparateIds(prefix) boardCandidate := make([]entity.Id, 0, 5) @@ -88,6 +98,10 @@ func (c *RepoCacheBoard) ResolveColumn(prefix string) (*BoardCache, entity.Combi return matchingBoard, matchingColumnId, nil } +// TODO: resolve item? + +// New creates a new board. +// The new board is written in the repository (commit) func (c *RepoCacheBoard) New(title, description string, columns []string) (*BoardCache, *board.CreateOperation, error) { author, err := c.getUserIdentity() if err != nil { @@ -97,11 +111,13 @@ func (c *RepoCacheBoard) New(title, description string, columns []string) (*Boar return c.NewRaw(author, time.Now().Unix(), title, description, columns, nil) } +// NewDefaultColumns creates a new board with the default columns. +// The new board is written in the repository (commit) 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. +// 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) diff --git a/cache/bug_cache.go b/cache/bug_cache.go index a06709596d245c097f4ac82caa5f680d4fcc7d5f..6c1a92d72f738aefae4d7a51fe653205c24add73 100644 --- a/cache/bug_cache.go +++ b/cache/bug_cache.go @@ -28,7 +28,7 @@ func NewBugCache(b *bug.Bug, repo repository.ClockedRepo, getUserIdentity getUse repo: repo, entityUpdated: entityUpdated, getUserIdentity: getUserIdentity, - entity: &withSnapshot[*bug.Snapshot, bug.Operation]{Interface: b}, + entity: newWithSnapshot[*bug.Snapshot, bug.Operation](b), }, } } diff --git a/cache/cached.go b/cache/cached.go index dc40d9480835534e70942d7b096095c267a93c87..1626d0fd7500e716daa0b76697e836c337ac6744 100644 --- a/cache/cached.go +++ b/cache/cached.go @@ -9,6 +9,7 @@ import ( "github.com/git-bug/git-bug/util/lamport" ) +var _ dag.ReadOnly[dag.Snapshot, dag.Operation] = &CachedEntityBase[dag.Snapshot, dag.Operation]{} var _ CacheEntity = &CachedEntityBase[dag.Snapshot, dag.Operation]{} // CachedEntityBase provide the base function of an entity managed by the cache. @@ -18,7 +19,7 @@ type CachedEntityBase[SnapT dag.Snapshot, OpT dag.Operation] struct { getUserIdentity getUserIdentityFunc mu sync.RWMutex - entity dag.Interface[SnapT, OpT] + entity dag.ReadWrite[SnapT, OpT] } func (e *CachedEntityBase[SnapT, OpT]) Id() entity.Id { @@ -28,7 +29,7 @@ func (e *CachedEntityBase[SnapT, OpT]) Id() entity.Id { func (e *CachedEntityBase[SnapT, OpT]) Snapshot() SnapT { e.mu.RLock() defer e.mu.RUnlock() - return e.entity.Compile() + return e.entity.Snapshot() } func (e *CachedEntityBase[SnapT, OpT]) notifyUpdated() error { @@ -109,3 +110,7 @@ func (e *CachedEntityBase[SnapT, OpT]) EditLamportTime() lamport.Time { func (e *CachedEntityBase[SnapT, OpT]) FirstOp() OpT { return e.entity.FirstOp() } + +func (e *CachedEntityBase[SnapT, OpT]) LastOp() OpT { + return e.entity.LastOp() +} diff --git a/cache/repo_cache.go b/cache/repo_cache.go index 0eda1f7fbe0ad30672ab29f102d5cfdc980333cd..be3f3722fbe6c64e23e8e5b5424d44314e88a702 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -105,12 +105,12 @@ func NewNamedRepoCache(r repository.ClockedRepo, name string) (*RepoCache, chan identity.Interface(nil): entity.ResolverFunc[*IdentityCache](c.identities.Resolve), &IdentityCache{}: entity.ResolverFunc[*IdentityCache](c.identities.Resolve), &IdentityExcerpt{}: entity.ResolverFunc[*IdentityExcerpt](c.identities.ResolveExcerpt), - bug.Interface(nil): entity.ResolverFunc[*BugCache](c.bugs.Resolve), + bug.ReadOnly(nil): entity.ResolverFunc[*BugCache](c.bugs.Resolve), &bug.Bug{}: entity.ResolverFunc[*BugCache](c.bugs.Resolve), &BugCache{}: entity.ResolverFunc[*BugCache](c.bugs.Resolve), &BugExcerpt{}: entity.ResolverFunc[*BugExcerpt](c.bugs.ResolveExcerpt), - board.Interface(nil): entity.ResolverFunc[*BoardCache](c.boards.Resolve), - &bug.Bug{}: entity.ResolverFunc[*BoardCache](c.boards.Resolve), + board.ReadOnly(nil): entity.ResolverFunc[*BoardCache](c.boards.Resolve), + &board.Board{}: entity.ResolverFunc[*BoardCache](c.boards.Resolve), &BoardCache{}: entity.ResolverFunc[*BoardCache](c.boards.Resolve), &BoardExcerpt{}: entity.ResolverFunc[*BoardExcerpt](c.boards.ResolveExcerpt), } @@ -249,9 +249,9 @@ type BuildEvent struct { Typename string // Event is the type of the event. Event BuildEventType - // Total is the total number of element being built. Set if Event is BuildEventStarted. + // Total is the total number of elements being built. Set if Event is BuildEventStarted. Total int64 - // Progress is the current count of processed element. Set if Event is BuildEventProgress. + // Progress is the current count of processed elements. Set if Event is BuildEventProgress. Progress int64 } diff --git a/cache/subcache.go b/cache/subcache.go index d9b6db8d4e875c8a62920bec05f3f491bc081325..4c61f49fffdd011aa3fcb563655a08e646beea7c 100644 --- a/cache/subcache.go +++ b/cache/subcache.go @@ -40,6 +40,7 @@ type Actions[EntityT entity.Interface] struct { var _ cacheMgmt = &SubCache[entity.Interface, Excerpt, CacheEntity]{} +// SubCache provides caching management for one type of Entity. type SubCache[EntityT entity.Interface, ExcerptT Excerpt, CacheT CacheEntity] struct { repo repository.ClockedRepo resolvers func() entity.Resolvers @@ -348,7 +349,7 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) AllIds() []entity.Id { return result } -// Resolve retrieve an entity matching the exact given id +// Resolve retrieves an entity matching the exact given id func (sc *SubCache[EntityT, ExcerptT, CacheT]) Resolve(id entity.Id) (CacheT, error) { sc.mu.RLock() cached, ok := sc.cached[id] @@ -376,14 +377,14 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) Resolve(id entity.Id) (CacheT, er return cached, nil } -// ResolvePrefix retrieve an entity matching an id prefix. It fails if multiple -// entities match. +// ResolvePrefix retrieves an entity matching an id prefix. It fails if multiple entities match. func (sc *SubCache[EntityT, ExcerptT, CacheT]) ResolvePrefix(prefix string) (CacheT, error) { return sc.ResolveMatcher(func(excerpt ExcerptT) bool { return excerpt.Id().HasPrefix(prefix) }) } +// ResolveMatcher retrieves an entity matching the given matched. It fails if multiple entities match. func (sc *SubCache[EntityT, ExcerptT, CacheT]) ResolveMatcher(f func(ExcerptT) bool) (CacheT, error) { id, err := sc.resolveMatcher(f) if err != nil { @@ -405,14 +406,14 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) ResolveExcerpt(id entity.Id) (Exc return excerpt, nil } -// ResolveExcerptPrefix retrieve an Excerpt matching an id prefix. It fails if multiple -// entities match. +// ResolveExcerptPrefix retrieve an Excerpt matching an id prefix. It fails if multiple entities match. func (sc *SubCache[EntityT, ExcerptT, CacheT]) ResolveExcerptPrefix(prefix string) (ExcerptT, error) { return sc.ResolveExcerptMatcher(func(excerpt ExcerptT) bool { return excerpt.Id().HasPrefix(prefix) }) } +// ResolveExcerptMatcher retrieve an Excerpt matching a given matcher. It fails if multiple entities match. func (sc *SubCache[EntityT, ExcerptT, CacheT]) ResolveExcerptMatcher(f func(ExcerptT) bool) (ExcerptT, error) { id, err := sc.resolveMatcher(f) if err != nil { @@ -421,6 +422,23 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) ResolveExcerptMatcher(f func(Exce return sc.ResolveExcerpt(id) } +// QueryExcerptMatcher finds all the Excerpt matching the given matcher. +func (sc *SubCache[EntityT, ExcerptT, CacheT]) QueryExcerptMatcher(f func(ExcerptT) bool) ([]ExcerptT, error) { + ids, err := sc.queryMatcher(f) + if err != nil { + return nil, err + } + res := make([]ExcerptT, len(ids)) + for i, id := range ids { + res[i], err = sc.ResolveExcerpt(id) + if err != nil { + return nil, err + } + } + return res, nil +} + +// resolveMatcher finds the id of the entity matching the given matcher. It fails if multiple entities match. func (sc *SubCache[EntityT, ExcerptT, CacheT]) resolveMatcher(f func(ExcerptT) bool) (entity.Id, error) { sc.mu.RLock() defer sc.mu.RUnlock() @@ -445,6 +463,25 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) resolveMatcher(f func(ExcerptT) b return matching[0], nil } +// queryMatcher find the ids of all the entities matching the given matcher. +func (sc *SubCache[EntityT, ExcerptT, CacheT]) queryMatcher(f func(ExcerptT) bool) ([]entity.Id, error) { + // TODO: this might use some pagination, or better: a go1.23 iterator? + + sc.mu.RLock() + defer sc.mu.RUnlock() + + // preallocate but empty + matching := make([]entity.Id, 0, 5) + + for _, excerpt := range sc.excerpts { + if f(excerpt) { + matching = append(matching, excerpt.Id()) + } + } + + return matching, nil +} + func (sc *SubCache[EntityT, ExcerptT, CacheT]) add(e EntityT) (CacheT, error) { sc.mu.Lock() if _, has := sc.cached[e.Id()]; has { diff --git a/cache/with_snapshot.go b/cache/with_snapshot.go index af61971af4ca61ce6eda6b19d794d9afa9547418..5e23a186e1ec504fd66c255babee5cf7f4871ee0 100644 --- a/cache/with_snapshot.go +++ b/cache/with_snapshot.go @@ -7,20 +7,24 @@ import ( "github.com/git-bug/git-bug/repository" ) -var _ dag.Interface[dag.Snapshot, dag.OperationWithApply[dag.Snapshot]] = &withSnapshot[dag.Snapshot, dag.OperationWithApply[dag.Snapshot]]{} +var _ dag.ReadWrite[dag.Snapshot, dag.OperationWithApply[dag.Snapshot]] = &withSnapshot[dag.Snapshot, dag.OperationWithApply[dag.Snapshot]]{} // withSnapshot encapsulate an entity and maintain a snapshot efficiently. type withSnapshot[SnapT dag.Snapshot, OpT dag.OperationWithApply[SnapT]] struct { - dag.Interface[SnapT, OpT] + dag.ReadWrite[SnapT, OpT] mu sync.Mutex snap *SnapT } -func (ws *withSnapshot[SnapT, OpT]) Compile() SnapT { +func newWithSnapshot[SnapT dag.Snapshot, OpT dag.OperationWithApply[SnapT]](readWrite dag.ReadWrite[SnapT, OpT]) *withSnapshot[SnapT, OpT] { + return &withSnapshot[SnapT, OpT]{ReadWrite: readWrite} +} + +func (ws *withSnapshot[SnapT, OpT]) Snapshot() SnapT { ws.mu.Lock() defer ws.mu.Unlock() if ws.snap == nil { - snap := ws.Interface.Compile() + snap := ws.ReadWrite.Snapshot() ws.snap = &snap } return *ws.snap @@ -31,7 +35,7 @@ func (ws *withSnapshot[SnapT, OpT]) Append(op OpT) { ws.mu.Lock() defer ws.mu.Unlock() - ws.Interface.Append(op) + ws.ReadWrite.Append(op) if ws.snap == nil { return @@ -46,7 +50,7 @@ func (ws *withSnapshot[SnapT, OpT]) Commit(repo repository.ClockedRepo) error { ws.mu.Lock() defer ws.mu.Unlock() - err := ws.Interface.Commit(repo) + err := ws.ReadWrite.Commit(repo) if err != nil { ws.snap = nil return err diff --git a/commands/board/board_addbug.go b/commands/board/board_addbug.go index dbbd3a8297de4d8966b9c3c08c5af7dc2b4b12d0..f153e4ec0f35bb11cf6a6fb378fa975ba25de4a6 100644 --- a/commands/board/board_addbug.go +++ b/commands/board/board_addbug.go @@ -1,15 +1,10 @@ package boardcmd import ( - "fmt" - "strconv" - "github.com/spf13/cobra" bugcmd "github.com/git-bug/git-bug/commands/bug" "github.com/git-bug/git-bug/commands/execenv" - _select "github.com/git-bug/git-bug/commands/select" - "github.com/git-bug/git-bug/entity" ) type boardAddBugOptions struct { @@ -41,46 +36,22 @@ func newBoardAddBugCommand() *cobra.Command { } func runBoardAddBug(env *execenv.Env, opts boardAddBugOptions, args []string) error { - board, args, err := ResolveSelected(env.Backend, args) + b, columnId, err := resolveColumnId(env, opts.column, args) if err != nil { return err } - var columnId entity.CombinedId - - switch { - case err == nil: - // try to parse as column number - index, err := strconv.Atoi(opts.column) - if err == nil { - if index-1 >= 0 && index-1 < len(board.Snapshot().Columns) { - columnId = board.Snapshot().Columns[index-1].CombinedId - } else { - return fmt.Errorf("invalid column") - } - } - fallthrough // could be an Id - case _select.IsErrNoValidId(err): - board, columnId, err = env.Backend.Boards().ResolveColumn(opts.column) - if err != nil { - return err - } - default: - // actual error - return err - } - bug, _, err := bugcmd.ResolveSelected(env.Backend, args) if err != nil { return err } - id, _, err := board.AddItemEntity(columnId, bug) + id, _, err := b.AddItemEntity(columnId, bug) if err != nil { return err } env.Out.Printf("%s created\n", id.Human()) - return board.Commit() + return b.Commit() } diff --git a/commands/board/board_adddraft.go b/commands/board/board_adddraft.go index 604bc0758e9458b7652015669e7822c5c0fa5700..efa181e93f1bb99368366f76d5468b25d0e256da 100644 --- a/commands/board/board_adddraft.go +++ b/commands/board/board_adddraft.go @@ -6,6 +6,7 @@ import ( "github.com/spf13/cobra" + "github.com/git-bug/git-bug/cache" buginput "github.com/git-bug/git-bug/commands/bug/input" "github.com/git-bug/git-bug/commands/execenv" _select "github.com/git-bug/git-bug/commands/select" @@ -52,29 +53,8 @@ func newBoardAddDraftCommand() *cobra.Command { } func runBoardAddDraft(env *execenv.Env, opts boardAddDraftOptions, args []string) error { - b, args, err := ResolveSelected(env.Backend, args) - - var columnId entity.CombinedId - - switch { - case err == nil: - // try to parse as column number - index, err := strconv.Atoi(opts.column) - if err == nil { - if index-1 >= 0 && index-1 < len(b.Snapshot().Columns) { - columnId = b.Snapshot().Columns[index-1].CombinedId - } else { - return fmt.Errorf("invalid column") - } - } - fallthrough // could be an Id - case _select.IsErrNoValidId(err): - b, columnId, err = env.Backend.Boards().ResolveColumn(opts.column) - if err != nil { - return err - } - default: - // actual error + b, columnId, err := resolveColumnId(env, opts.column, args) + if err != nil { return err } @@ -106,3 +86,27 @@ func runBoardAddDraft(env *execenv.Env, opts boardAddDraftOptions, args []string return b.Commit() } + +func resolveColumnId(env *execenv.Env, column string, args []string) (*cache.BoardCache, entity.CombinedId, error) { + if column == "" { + return nil, entity.UnsetCombinedId, fmt.Errorf("flag --column is required") + } + + b, args, err := ResolveSelected(env.Backend, args) + + switch { + case err == nil: + // we have a pre-selected board, try to parse as column number + index, err := strconv.Atoi(column) + if err == nil && index-1 >= 0 && index-1 < len(b.Snapshot().Columns) { + return b, b.Snapshot().Columns[index-1].CombinedId, nil + } + fallthrough // could be an Id + case _select.IsErrNoValidId(err): + return env.Backend.Boards().ResolveColumn(column) + + default: + // actual error + return nil, entity.UnsetCombinedId, err + } +} diff --git a/commands/bug/completion.go b/commands/bug/completion.go index 20c67a42e6917ee4b34ea436d090281861c456cf..f04468a87eb60f510c07dc7877e155e249ad111b 100644 --- a/commands/bug/completion.go +++ b/commands/bug/completion.go @@ -12,7 +12,7 @@ import ( "github.com/git-bug/git-bug/entities/common" ) -// BugCompletion complete a bug id +// BugCompletion perform bug completion (id, title) on the environment backend func BugCompletion(env *execenv.Env) completion.ValidArgsFunction { return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) { if err := execenv.LoadBackend(env)(cmd, args); err != nil { @@ -26,6 +26,7 @@ func BugCompletion(env *execenv.Env) completion.ValidArgsFunction { } } +// BugWithBackend perform bug completion (id, title) on the given backend func BugWithBackend(backend *cache.RepoCache, toComplete string) (completions []string, directives cobra.ShellCompDirective) { for _, id := range backend.Bugs().AllIds() { if strings.Contains(id.String(), strings.TrimSpace(toComplete)) { diff --git a/commands/cmdjson/board.go b/commands/cmdjson/board.go index 11f77a4facadfe7e0eb6fb0d4a9a6225e6d3e7e5..0ca9196fb8b1243b8a231d8d66ff0a3fecadcd69 100644 --- a/commands/cmdjson/board.go +++ b/commands/cmdjson/board.go @@ -59,7 +59,7 @@ func NewBoardColumn(column *board.Column) BoardColumn { case *board.Draft: jsonColumn.Items[j] = NewBoardDraftItem(item) case *board.BugItem: - jsonColumn.Items[j] = NewBugSnapshot(item.Bug.Compile()) + jsonColumn.Items[j] = NewBoardBugItem(item) default: panic("unknown item type") } @@ -68,6 +68,7 @@ func NewBoardColumn(column *board.Column) BoardColumn { } type BoardDraftItem struct { + Type string `json:"type"` Id string `json:"id"` HumanId string `json:"human_id"` Author Identity `json:"author"` @@ -77,14 +78,34 @@ type BoardDraftItem struct { func NewBoardDraftItem(item *board.Draft) BoardDraftItem { return BoardDraftItem{ + Type: "draft", Id: item.CombinedId().String(), HumanId: item.CombinedId().Human(), - Author: NewIdentity(item.Author), - Title: item.Title, + Author: NewIdentity(item.Author()), + Title: item.Title(), Message: item.Message, } } +type BoardBugItem struct { + Type string `json:"type"` + Id string `json:"id"` + HumanId string `json:"human_id"` + Author Identity `json:"author"` + BugId string `json:"bug_id"` +} + +func NewBoardBugItem(item *board.BugItem) BoardBugItem { + return BoardBugItem{ + Type: "bug", + Id: item.CombinedId().String(), + HumanId: item.CombinedId().Human(), + Author: NewIdentity(item.Author()), + BugId: item.Bug.Snapshot().Id().String(), + // TODO: add more? + } +} + type BoardExcerpt struct { Id string `json:"id"` HumanId string `json:"human_id"` diff --git a/entities/board/board.go b/entities/board/board.go index ecc7ff20ad7a0a29d4e9358459559e1b4ded00dc..0a841501690f2d8b4c155c93575adef06819fb2e 100644 --- a/entities/board/board.go +++ b/entities/board/board.go @@ -11,7 +11,9 @@ import ( "github.com/git-bug/git-bug/repository" ) -var _ Interface = &Board{} +var _ ReadOnly = &Board{} +var _ ReadWrite = &Board{} +var _ entity.Interface = &Board{} // 1: original format const formatVersion = 1 @@ -28,9 +30,8 @@ var def = dag.Definition{ var ClockLoader = dag.ClockLoader(def) -type Interface interface { - dag.Interface[*Snapshot, Operation] -} +type ReadOnly dag.ReadOnly[*Snapshot, Operation] +type ReadWrite dag.ReadWrite[*Snapshot, Operation] // Board holds the data of a project board. type Board struct { @@ -115,8 +116,8 @@ func (board *Board) Operations() []Operation { return result } -// Compile a board in an easily usable snapshot -func (board *Board) Compile() *Snapshot { +// Snapshot compiles a board in an easily usable snapshot +func (board *Board) Snapshot() *Snapshot { snap := &Snapshot{ id: board.Id(), } diff --git a/entities/board/board_actions.go b/entities/board/board_actions.go index 65feda5a9b2c75e6b08952a0c5f571202b877a5f..95c989fcfd3c88ee0569e75444f95450dcb36811 100644 --- a/entities/board/board_actions.go +++ b/entities/board/board_actions.go @@ -33,7 +33,13 @@ func MergeAll(repo repository.ClockedRepo, resolvers entity.Resolvers, remote st return dag.MergeAll(def, wrapper, repo, resolvers, remote, mergeAuthor) } -// Remove will remove a local bug from its entity.Id +// Remove will remove a local board from its entity.Id func Remove(repo repository.ClockedRepo, id entity.Id) error { return dag.Remove(def, repo, id) } + +// RemoveAll will remove all local boards. +// RemoveAll is idempotent. +func RemoveAll(repo repository.ClockedRepo) error { + return dag.RemoveAll(def, repo) +} diff --git a/entities/board/item_draft.go b/entities/board/item_draft.go index 30171afd8d74caacb217e9d89284ea419c9daff8..fe8410f9d16d3d50ea4e25e028f2f9807554c9cc 100644 --- a/entities/board/item_draft.go +++ b/entities/board/item_draft.go @@ -16,8 +16,8 @@ type Draft struct { // of the Operation that created the Draft combinedId entity.CombinedId - Author identity.Interface - Title string + author identity.Interface + title string Message string // Creation time of the comment. @@ -33,6 +33,14 @@ func (d *Draft) CombinedId() entity.CombinedId { return d.combinedId } +func (d *Draft) Author() identity.Interface { + return d.author +} + +func (d *Draft) Title() string { + return d.title +} + // FormatTimeRel format the UnixTime of the comment for human consumption func (d *Draft) FormatTimeRel() string { return humanize.Time(d.unixTime.Time()) diff --git a/entities/board/item_entity.go b/entities/board/item_entity.go index 497f6869c0ff39e03560e03e74bb5395ead59e1c..d1f90fdca75174d1563fa127fc2e128d8618fb25 100644 --- a/entities/board/item_entity.go +++ b/entities/board/item_entity.go @@ -2,14 +2,16 @@ package board import ( "github.com/git-bug/git-bug/entities/bug" + "github.com/git-bug/git-bug/entities/identity" "github.com/git-bug/git-bug/entity" + "github.com/git-bug/git-bug/entity/dag" ) var _ Item = &BugItem{} type BugItem struct { combinedId entity.CombinedId - Bug bug.Interface + Bug dag.CompileTo[*bug.Snapshot] } func (e *BugItem) CombinedId() entity.CombinedId { @@ -19,3 +21,11 @@ func (e *BugItem) CombinedId() entity.CombinedId { } return e.combinedId } + +func (e *BugItem) Author() identity.Interface { + return e.Bug.Snapshot().Author +} + +func (e *BugItem) Title() string { + return e.Bug.Snapshot().Title +} diff --git a/entities/board/op_add_item_draft.go b/entities/board/op_add_item_draft.go index 253dfc4b0d8096489b6ae595895226c554d1d29a..47528cf525b0de6d4dcecb25684bb44cb481fc8b 100644 --- a/entities/board/op_add_item_draft.go +++ b/entities/board/op_add_item_draft.go @@ -62,12 +62,13 @@ func (op *AddItemDraftOperation) Apply(snapshot *Snapshot) { // Recreate the combined Id to match on combinedId := entity.CombineIds(snapshot.Id(), op.ColumnId) + // search the column for _, column := range snapshot.Columns { if column.CombinedId == combinedId { column.Items = append(column.Items, &Draft{ combinedId: entity.CombineIds(snapshot.id, op.Id()), - Author: op.Author(), - Title: op.Title, + author: op.Author(), + title: op.Title, Message: op.Message, unixTime: timestamp.Timestamp(op.UnixTime), }) @@ -89,7 +90,7 @@ 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 Interface, author identity.Interface, unixTime int64, columnId entity.Id, title, message string, files []repository.Hash, metadata map[string]string) (entity.CombinedId, *AddItemDraftOperation, error) { +func AddItemDraft(b ReadWrite, 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) diff --git a/entities/board/op_add_item_entity.go b/entities/board/op_add_item_entity.go index 91df207cf7424ef0c6ff2feea94c7f7204468fc4..7502eaffa347bc78e6653941c9df3effda3ef643 100644 --- a/entities/board/op_add_item_entity.go +++ b/entities/board/op_add_item_entity.go @@ -54,19 +54,21 @@ func (op *AddItemEntityOperation) Validate() error { func (op *AddItemEntityOperation) Apply(snapshot *Snapshot) { if op.entity == nil { + // entity was not found while unmarshalling/resolving return } // Recreate the combined Id to match on combinedId := entity.CombineIds(snapshot.Id(), op.ColumnId) + // search the column for _, column := range snapshot.Columns { if column.CombinedId == combinedId { switch op.EntityType { case EntityTypeBug: column.Items = append(column.Items, &BugItem{ - combinedId: entity.CombineIds(snapshot.Id(), op.entity.Id()), - Bug: op.entity.(bug.Interface), + combinedId: entity.CombineIds(snapshot.Id(), op.Id()), + Bug: op.entity.(dag.CompileTo[*bug.Snapshot]), }) } snapshot.addParticipant(op.Author()) @@ -76,7 +78,7 @@ func (op *AddItemEntityOperation) Apply(snapshot *Snapshot) { } func NewAddItemEntityOp(author identity.Interface, unixTime int64, columnId entity.Id, entityType ItemEntityType, e entity.Interface) *AddItemEntityOperation { - // Note: due to import cycle we are not able to properly check the type of the entity here; + // Note: due to import cycle we are not able to sanity check the type of the entity here; // proceed with caution! return &AddItemEntityOperation{ OpBase: dag.NewOpBase(AddItemEntityOp, author, unixTime), @@ -88,7 +90,7 @@ 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 Interface, author identity.Interface, unixTime int64, columnId entity.Id, entityType ItemEntityType, e entity.Interface, metadata map[string]string) (entity.CombinedId, *AddItemEntityOperation, error) { +func AddItemEntity(b ReadWrite, author identity.Interface, unixTime int64, columnId entity.Id, entityType ItemEntityType, e entity.Interface, metadata map[string]string) (entity.CombinedId, *AddItemEntityOperation, error) { op := NewAddItemEntityOp(author, unixTime, columnId, entityType, e) for key, val := range metadata { op.SetMetadata(key, val) diff --git a/entities/board/op_add_item_entity_test.go b/entities/board/op_add_item_entity_test.go index 248ee7836f57873daa456c4a785175431579c76d..13b8566f50972fcd23cea6926e01808e4ace0f59 100644 --- a/entities/board/op_add_item_entity_test.go +++ b/entities/board/op_add_item_entity_test.go @@ -20,6 +20,6 @@ func TestAddItemEntityOpSerialize(t *testing.T) { &bug.Bug{}: entity.MakeResolver(b), } - return NewAddItemEntityOp(author, unixTime, "foo", b), resolvers + return NewAddItemEntityOp(author, unixTime, "foo", EntityTypeBug, b), resolvers }) } diff --git a/entities/board/op_set_description.go b/entities/board/op_set_description.go index 7483de811e6102401fe48cdc962cb857c5234f84..94bf7fda7c86032d1233cbf99939a8db307cbfeb 100644 --- a/entities/board/op_set_description.go +++ b/entities/board/op_set_description.go @@ -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 Interface, author identity.Interface, unixTime int64, description string, metadata map[string]string) (*SetDescriptionOperation, error) { +func SetDescription(b ReadWrite, 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) { diff --git a/entities/board/op_set_metadata.go b/entities/board/op_set_metadata.go new file mode 100644 index 0000000000000000000000000000000000000000..4407c47a4f18802c7f6539f6375bb433f11f6165 --- /dev/null +++ b/entities/board/op_set_metadata.go @@ -0,0 +1,21 @@ +package board + +import ( + "github.com/git-bug/git-bug/entities/identity" + "github.com/git-bug/git-bug/entity" + "github.com/git-bug/git-bug/entity/dag" +) + +func NewSetMetadataOp(author identity.Interface, unixTime int64, target entity.Id, newMetadata map[string]string) *dag.SetMetadataOperation[*Snapshot] { + return dag.NewSetMetadataOp[*Snapshot](SetMetadataOp, author, unixTime, target, newMetadata) +} + +// SetMetadata is a convenience function to add metadata on another operation +func SetMetadata(b ReadWrite, author identity.Interface, unixTime int64, target entity.Id, newMetadata map[string]string) (*dag.SetMetadataOperation[*Snapshot], error) { + op := NewSetMetadataOp(author, unixTime, target, newMetadata) + if err := op.Validate(); err != nil { + return nil, err + } + b.Append(op) + return op, nil +} diff --git a/entities/board/op_set_title.go b/entities/board/op_set_title.go index 1e03f8c2000c960237060ea4fdeeb8bb57017009..35c7ef8e0f4a4dafbbf4e6ca9f805206e4749c75 100644 --- a/entities/board/op_set_title.go +++ b/entities/board/op_set_title.go @@ -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 Interface, author identity.Interface, unixTime int64, title string, metadata map[string]string) (*SetTitleOperation, error) { +func SetTitle(b ReadWrite, 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) { diff --git a/entities/board/operation.go b/entities/board/operation.go index 0c0e5c35b776351f179d3c446aa899671fbe49e1..80c3428eaa8f4fe84b49ee8a1d825d00a927ea52 100644 --- a/entities/board/operation.go +++ b/entities/board/operation.go @@ -15,6 +15,7 @@ type OperationType dag.OperationType const ( _ dag.OperationType = iota CreateOp + SetMetadataOp SetTitleOp SetDescriptionOp AddItemEntityOp @@ -45,6 +46,8 @@ func operationUnmarshaler(raw json.RawMessage, resolvers entity.Resolvers) (dag. switch t.OperationType { case CreateOp: op = &CreateOperation{} + case SetMetadataOp: + op = &dag.SetMetadataOperation[*Snapshot]{} case SetTitleOp: op = &SetTitleOperation{} case SetDescriptionOp: @@ -66,7 +69,7 @@ func operationUnmarshaler(raw json.RawMessage, resolvers entity.Resolvers) (dag. case *AddItemEntityOperation: switch op.EntityType { case EntityTypeBug: - op.entity, err = entity.Resolve[bug.Interface](resolvers, op.EntityId) + op.entity, err = entity.Resolve[bug.ReadOnly](resolvers, op.EntityId) default: return nil, fmt.Errorf("unknown entity type") } diff --git a/entities/board/snapshot.go b/entities/board/snapshot.go index 3500c47a5f628f7bdbe72b1e98bb77883294a09c..9a87709f2847f69eaefd812084c5b96d6529d612 100644 --- a/entities/board/snapshot.go +++ b/entities/board/snapshot.go @@ -20,6 +20,10 @@ type Column struct { type Item interface { CombinedId() entity.CombinedId + + Author() identity.Interface + Title() string + // TODO: all items have status? // Status() common.Status } @@ -106,6 +110,7 @@ func (snap *Snapshot) HasAnyParticipant(ids ...entity.Id) bool { return false } +// ItemCount returns the number of items (draft, entity) in the board. func (snap *Snapshot) ItemCount() int { var count int for _, column := range snap.Columns { diff --git a/entities/bug/bug.go b/entities/bug/bug.go index 8958fbd0e1b93498dd46631b3f3696fb421bf5b7..201f1380216872c6fe73062352dfce49b9bd7900 100644 --- a/entities/bug/bug.go +++ b/entities/bug/bug.go @@ -11,7 +11,8 @@ import ( "github.com/git-bug/git-bug/repository" ) -var _ Interface = &Bug{} +var _ ReadOnly = &Bug{} +var _ ReadWrite = &Bug{} var _ entity.Interface = &Bug{} // 1: original format @@ -32,9 +33,8 @@ var def = dag.Definition{ var ClockLoader = dag.ClockLoader(def) -type Interface interface { - dag.Interface[*Snapshot, Operation] -} +type ReadOnly dag.ReadOnly[*Snapshot, Operation] +type ReadWrite dag.ReadWrite[*Snapshot, Operation] // Bug holds the data of a bug thread, organized in a way close to // how it will be persisted inside Git. This is the data structure @@ -123,8 +123,8 @@ func (bug *Bug) Operations() []Operation { return result } -// Compile a bug in an easily usable snapshot -func (bug *Bug) Compile() *Snapshot { +// Snapshot compiles a bug in an easily usable snapshot +func (bug *Bug) Snapshot() *Snapshot { snap := &Snapshot{ id: bug.Id(), Status: common.OpenStatus, diff --git a/entities/bug/op_add_comment.go b/entities/bug/op_add_comment.go index 166348a674e15262502207a989ced0f591e67c2f..6e272fefa556fc04635f2181dbbd57ec43b33963 100644 --- a/entities/bug/op_add_comment.go +++ b/entities/bug/op_add_comment.go @@ -89,7 +89,7 @@ type AddCommentTimelineItem struct { func (a *AddCommentTimelineItem) IsAuthored() {} // AddComment is a convenience function to add a comment to a bug -func AddComment(b Interface, author identity.Interface, unixTime int64, message string, files []repository.Hash, metadata map[string]string) (entity.CombinedId, *AddCommentOperation, error) { +func AddComment(b ReadWrite, author identity.Interface, unixTime int64, message string, files []repository.Hash, metadata map[string]string) (entity.CombinedId, *AddCommentOperation, error) { op := NewAddCommentOp(author, unixTime, message, files) for key, val := range metadata { op.SetMetadata(key, val) diff --git a/entities/bug/op_create_test.go b/entities/bug/op_create_test.go index a7367acc892e569555bbdbf9070852460cf1f103..9a5374cbbd98dfbb8afd2e2808af6975a5a23d26 100644 --- a/entities/bug/op_create_test.go +++ b/entities/bug/op_create_test.go @@ -26,7 +26,7 @@ func TestCreate(t *testing.T) { require.Equal(t, "message", op.Message) // Create generate the initial operation and create a new timeline item - snap := b.Compile() + snap := b.Snapshot() require.Equal(t, common.OpenStatus, snap.Status) require.Equal(t, rene, snap.Author) require.Equal(t, "title", snap.Title) diff --git a/entities/bug/op_edit_comment.go b/entities/bug/op_edit_comment.go index 9b1b61688c928e3a50abf1d8c308ac93ee58ff98..cd5eadfdc5c7a01fae9053c1375a36daeb9d52fb 100644 --- a/entities/bug/op_edit_comment.go +++ b/entities/bug/op_edit_comment.go @@ -117,7 +117,7 @@ func NewEditCommentOp(author identity.Interface, unixTime int64, target entity.I } // EditComment is a convenience function to apply the operation -func EditComment(b Interface, author identity.Interface, unixTime int64, target entity.Id, message string, files []repository.Hash, metadata map[string]string) (entity.CombinedId, *EditCommentOperation, error) { +func EditComment(b ReadWrite, author identity.Interface, unixTime int64, target entity.Id, message string, files []repository.Hash, metadata map[string]string) (entity.CombinedId, *EditCommentOperation, error) { op := NewEditCommentOp(author, unixTime, target, message, files) for key, val := range metadata { op.SetMetadata(key, val) @@ -130,7 +130,7 @@ func EditComment(b Interface, author identity.Interface, unixTime int64, target } // EditCreateComment is a convenience function to edit the body of a bug (the first comment) -func EditCreateComment(b Interface, author identity.Interface, unixTime int64, message string, files []repository.Hash, metadata map[string]string) (entity.CombinedId, *EditCommentOperation, error) { +func EditCreateComment(b ReadWrite, author identity.Interface, unixTime int64, message string, files []repository.Hash, metadata map[string]string) (entity.CombinedId, *EditCommentOperation, error) { createOp := b.FirstOp().(*CreateOperation) return EditComment(b, author, unixTime, createOp.Id(), message, files, metadata) } diff --git a/entities/bug/op_label_change.go b/entities/bug/op_label_change.go index cf8adfb75bacf5d0477b1be027d0531a26bb227c..56c2cb8a1c8c04041078e4d5ee2f0b355333ba6a 100644 --- a/entities/bug/op_label_change.go +++ b/entities/bug/op_label_change.go @@ -121,11 +121,11 @@ func (l LabelChangeTimelineItem) CombinedId() entity.CombinedId { func (l *LabelChangeTimelineItem) IsAuthored() {} // ChangeLabels is a convenience function to change labels on a bug -func ChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string, metadata map[string]string) ([]LabelChangeResult, *LabelChangeOperation, error) { +func ChangeLabels(b ReadWrite, author identity.Interface, unixTime int64, add, remove []string, metadata map[string]string) ([]LabelChangeResult, *LabelChangeOperation, error) { var added, removed []common.Label var results []LabelChangeResult - snap := b.Compile() + snap := b.Snapshot() for _, str := range add { label := common.Label(str) @@ -187,7 +187,7 @@ func ChangeLabels(b Interface, author identity.Interface, unixTime int64, add, r // responsible for what you are doing. In the general case, you want to use ChangeLabels instead. // The intended use of this function is to allow importers to create legal but unexpected label changes, // like removing a label with no information of when it was added before. -func ForceChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string, metadata map[string]string) (*LabelChangeOperation, error) { +func ForceChangeLabels(b ReadWrite, author identity.Interface, unixTime int64, add, remove []string, metadata map[string]string) (*LabelChangeOperation, error) { added := make([]common.Label, len(add)) for i, str := range add { added[i] = common.Label(str) diff --git a/entities/bug/op_set_metadata.go b/entities/bug/op_set_metadata.go index fd5cc94b595834d237b5fd2326e9e5fe8fb0a137..0bde2d86d9ce78e37ab78dbcfa51dd12224e1c35 100644 --- a/entities/bug/op_set_metadata.go +++ b/entities/bug/op_set_metadata.go @@ -11,7 +11,7 @@ func NewSetMetadataOp(author identity.Interface, unixTime int64, target entity.I } // SetMetadata is a convenience function to add metadata on another operation -func SetMetadata(b Interface, author identity.Interface, unixTime int64, target entity.Id, newMetadata map[string]string) (*dag.SetMetadataOperation[*Snapshot], error) { +func SetMetadata(b ReadWrite, author identity.Interface, unixTime int64, target entity.Id, newMetadata map[string]string) (*dag.SetMetadataOperation[*Snapshot], error) { op := NewSetMetadataOp(author, unixTime, target, newMetadata) if err := op.Validate(); err != nil { return nil, err diff --git a/entities/bug/op_set_status.go b/entities/bug/op_set_status.go index 641065a03879a0ac5803251e68c34ae7f487c3ec..9955ad8836a25f593e5c555e23690a3524104f77 100644 --- a/entities/bug/op_set_status.go +++ b/entities/bug/op_set_status.go @@ -72,7 +72,7 @@ func (s SetStatusTimelineItem) CombinedId() entity.CombinedId { func (s *SetStatusTimelineItem) IsAuthored() {} // Open is a convenience function to change a bugs state to Open -func Open(b Interface, author identity.Interface, unixTime int64, metadata map[string]string) (*SetStatusOperation, error) { +func Open(b ReadWrite, author identity.Interface, unixTime int64, metadata map[string]string) (*SetStatusOperation, error) { op := NewSetStatusOp(author, unixTime, common.OpenStatus) for key, value := range metadata { op.SetMetadata(key, value) @@ -85,7 +85,7 @@ func Open(b Interface, author identity.Interface, unixTime int64, metadata map[s } // Close is a convenience function to change a bugs state to Close -func Close(b Interface, author identity.Interface, unixTime int64, metadata map[string]string) (*SetStatusOperation, error) { +func Close(b ReadWrite, author identity.Interface, unixTime int64, metadata map[string]string) (*SetStatusOperation, error) { op := NewSetStatusOp(author, unixTime, common.ClosedStatus) for key, value := range metadata { op.SetMetadata(key, value) diff --git a/entities/bug/op_set_title.go b/entities/bug/op_set_title.go index 7ec98281cd7234e19bb94f7ae2415f7df94ac91d..f927cfba12986e52a10a0142dfbf9e262b30711b 100644 --- a/entities/bug/op_set_title.go +++ b/entities/bug/op_set_title.go @@ -84,7 +84,7 @@ func (s SetTitleTimelineItem) CombinedId() entity.CombinedId { func (s *SetTitleTimelineItem) IsAuthored() {} // SetTitle is a convenience function to change a bugs title -func SetTitle(b Interface, author identity.Interface, unixTime int64, title string, metadata map[string]string) (*SetTitleOperation, error) { +func SetTitle(b ReadWrite, 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) { diff --git a/entity/dag/example_test.go b/entity/dag/example_test.go index 3ffdb4fcfa9bcddc8706c6dd9a2c3237b8009d96..9f8425e4fc63d110ce083031b6b7ae6994ce153b 100644 --- a/entity/dag/example_test.go +++ b/entity/dag/example_test.go @@ -269,9 +269,9 @@ func operationUnmarshaler(raw json.RawMessage, resolvers entity.Resolvers) (dag. return op, nil } -// Compile compute a view of the final state. This is what we would use to display the state +// Snapshot computes a view of the final state. This is what we would use to display the state // in a user interface. -func (pc ProjectConfig) Compile() *Snapshot { +func (pc ProjectConfig) Snapshot() *Snapshot { // Note: this would benefit from caching, but it's a simple example snap := &Snapshot{ // default value @@ -335,7 +335,7 @@ func Example_entity() { confIsaac, _ := Read(repoIsaac, confRene.Id()) // Compile gives the current state of the config - snapshot := confIsaac.Compile() + snapshot := confIsaac.Snapshot() for admin, _ := range snapshot.Administrator { fmt.Println(admin.DisplayName()) } diff --git a/entity/dag/interface.go b/entity/dag/interface.go index dfa32a482f450e6bec44fc955d459a6783621376..1f322acfcbfa9490d257c1cd73395b003477ce44 100644 --- a/entity/dag/interface.go +++ b/entity/dag/interface.go @@ -6,42 +6,48 @@ import ( "github.com/git-bug/git-bug/util/lamport" ) -// Interface define the extended interface of a dag.Entity -type Interface[SnapT Snapshot, OpT Operation] interface { - entity.Interface - - // Validate checks if the Entity data is valid - Validate() error +type CompileTo[SnapT Snapshot] interface { + // Snapshot compiles an Entity in an easily usable snapshot + Snapshot() SnapT +} - // Append an operation into the staging area, to be committed later - Append(op OpT) +// ReadOnly defines the extended read-only interface of a dag.Entity +type ReadOnly[SnapT Snapshot, OpT Operation] interface { + entity.Interface - // Operations returns the ordered operations - Operations() []OpT + CompileTo[SnapT] // NeedCommit indicates that the in-memory state changed and need to be committed in the repository NeedCommit() bool - // Commit writes the staging area in Git and move the operations to the packs - Commit(repo repository.ClockedRepo) error - - // CommitAsNeeded execute a Commit only if necessary. This function is useful to avoid getting an error if the Entity - // is already in sync with the repository. - CommitAsNeeded(repo repository.ClockedRepo) error - // FirstOp lookup for the very first operation of the Entity. FirstOp() OpT // LastOp lookup for the very last operation of the Entity. - // For a valid Entity, should never be nil + // For a valid Entity, it should never be nil. LastOp() OpT - // Compile an Entity in an easily usable snapshot - Compile() SnapT - // CreateLamportTime return the Lamport time of creation CreateLamportTime() lamport.Time // EditLamportTime return the Lamport time of the last edit EditLamportTime() lamport.Time } + +// ReadWrite is an entity interface that includes the direct manipulation of operations. +type ReadWrite[SnapT Snapshot, OpT Operation] interface { + ReadOnly[SnapT, OpT] + + // Commit writes the staging area in Git and move the operations to the packs + Commit(repo repository.ClockedRepo) error + + // CommitAsNeeded execute a Commit only if necessary. This function is useful to avoid getting an error if the Entity + // is already in sync with the repository. + CommitAsNeeded(repo repository.ClockedRepo) error + + // Append an operation into the staging area, to be committed later + Append(op OpT) + + // Operations return the ordered operations + Operations() []OpT +} diff --git a/entity/interface.go b/entity/interface.go index 3035ac88d4a4328381ad13d2b449681d242a9f61..cb022c3cd528e25c8f4d437d39754511068d70e2 100644 --- a/entity/interface.go +++ b/entity/interface.go @@ -9,6 +9,7 @@ type Interface interface { // the root of the entity. // It is acceptable to use such a hash and keep mutating that data as long as Id() is not called. Id() Id + // Validate check if the Entity data is valid Validate() error }