From d6a45e7bdb664958b2228a5f0aba1369d40846e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Fri, 23 Aug 2024 22:02:34 +0200 Subject: [PATCH] board: make the basic commands work Also rework of entity generic interfaces, in a way that allows to handle low level or cache entities the same way (ReadOnly), and another ReadWrite interface that also allow to mutate the entity. This should fix some long long standing issue around that, and notably fix the resolvers. This is the first time an entity really load another one, which is what required that fix. Hopefully this opens the way for Identities to use the dag framework. --- cache/board_cache.go | 4 +- cache/board_subcache.go | 18 +++++++- cache/bug_cache.go | 2 +- cache/cached.go | 9 +++- cache/repo_cache.go | 10 ++--- cache/subcache.go | 47 ++++++++++++++++++--- cache/with_snapshot.go | 16 +++++--- commands/board/board_addbug.go | 35 ++-------------- commands/board/board_adddraft.go | 50 ++++++++++++----------- commands/bug/completion.go | 3 +- commands/cmdjson/board.go | 27 ++++++++++-- entities/board/board.go | 13 +++--- entities/board/board_actions.go | 8 +++- entities/board/item_draft.go | 12 +++++- entities/board/item_entity.go | 12 +++++- entities/board/op_add_item_draft.go | 7 ++-- entities/board/op_add_item_entity.go | 10 +++-- entities/board/op_add_item_entity_test.go | 2 +- entities/board/op_set_description.go | 2 +- entities/board/op_set_metadata.go | 21 ++++++++++ entities/board/op_set_title.go | 2 +- entities/board/operation.go | 5 ++- entities/board/snapshot.go | 5 +++ entities/bug/bug.go | 12 +++--- entities/bug/op_add_comment.go | 2 +- entities/bug/op_create_test.go | 2 +- entities/bug/op_edit_comment.go | 4 +- entities/bug/op_label_change.go | 6 +-- entities/bug/op_set_metadata.go | 2 +- entities/bug/op_set_status.go | 4 +- entities/bug/op_set_title.go | 2 +- entity/dag/example_test.go | 6 +-- entity/dag/interface.go | 48 ++++++++++++---------- entity/interface.go | 1 + 34 files changed, 266 insertions(+), 143 deletions(-) create mode 100644 entities/board/op_set_metadata.go 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 }