Detailed changes
@@ -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),
},
}
}
@@ -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)
@@ -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),
},
}
}
@@ -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()
+}
@@ -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
}
@@ -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 {
@@ -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
@@ -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()
}
@@ -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
+ }
+}
@@ -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)) {
@@ -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"`
@@ -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(),
}
@@ -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)
+}
@@ -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())
@@ -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
+}
@@ -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)
@@ -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)
@@ -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
})
}
@@ -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) {
@@ -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
+}
@@ -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) {
@@ -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")
}
@@ -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 {
@@ -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,
@@ -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)
@@ -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)
@@ -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)
}
@@ -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)
@@ -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
@@ -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)
@@ -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) {
@@ -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())
}
@@ -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
+}
@@ -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
}