diff --git a/cache/board_cache.go b/cache/board_cache.go index ecf7e735d94aead4f0bdd6c0958e8cde6cae61d3..ac7c283b0d0f896e68fa895a41b6a1e667f075f6 100644 --- a/cache/board_cache.go +++ b/cache/board_cache.go @@ -29,7 +29,7 @@ func NewBoardCache(b *board.Board, repo repository.ClockedRepo, getUserIdentity } } -func (c *BoardCache) AddItemDraft(columnId entity.Id, title, message string, files []repository.Hash) (entity.CombinedId, *board.AddItemDraftOperation, error) { +func (c *BoardCache) AddItemDraft(columnId entity.CombinedId, title, message string, files []repository.Hash) (entity.CombinedId, *board.AddItemDraftOperation, error) { author, err := c.getUserIdentity() if err != nil { return entity.UnsetCombinedId, nil, err @@ -38,9 +38,14 @@ func (c *BoardCache) AddItemDraft(columnId entity.Id, title, message string, fil return c.AddItemDraftRaw(author, time.Now().Unix(), columnId, title, message, files, nil) } -func (c *BoardCache) AddItemDraftRaw(author identity.Interface, unixTime int64, columnId entity.Id, title, message string, files []repository.Hash, metadata map[string]string) (entity.CombinedId, *board.AddItemDraftOperation, error) { +func (c *BoardCache) AddItemDraftRaw(author identity.Interface, unixTime int64, columnId entity.CombinedId, title, message string, files []repository.Hash, metadata map[string]string) (entity.CombinedId, *board.AddItemDraftOperation, error) { + column, err := c.Snapshot().SearchColumn(columnId) + if err != nil { + return entity.UnsetCombinedId, nil, err + } + c.mu.Lock() - itemId, op, err := board.AddItemDraft(c.entity, author, unixTime, columnId, title, message, files, metadata) + itemId, op, err := board.AddItemDraft(c.entity, author, unixTime, column.Id, title, message, files, metadata) c.mu.Unlock() if err != nil { return entity.UnsetCombinedId, nil, err @@ -48,7 +53,7 @@ func (c *BoardCache) AddItemDraftRaw(author identity.Interface, unixTime int64, return itemId, op, c.notifyUpdated() } -func (c *BoardCache) AddItemEntity(columnId entity.Id, e entity.Interface) (entity.CombinedId, *board.AddItemEntityOperation, error) { +func (c *BoardCache) AddItemEntity(columnId entity.CombinedId, e entity.Interface) (entity.CombinedId, *board.AddItemEntityOperation, error) { author, err := c.getUserIdentity() if err != nil { return entity.UnsetCombinedId, nil, err @@ -57,9 +62,22 @@ func (c *BoardCache) AddItemEntity(columnId entity.Id, e entity.Interface) (enti return c.AddItemEntityRaw(author, time.Now().Unix(), columnId, e, nil) } -func (c *BoardCache) AddItemEntityRaw(author identity.Interface, unixTime int64, columnId entity.Id, e entity.Interface, metadata map[string]string) (entity.CombinedId, *board.AddItemEntityOperation, error) { +func (c *BoardCache) AddItemEntityRaw(author identity.Interface, unixTime int64, columnId entity.CombinedId, e entity.Interface, metadata map[string]string) (entity.CombinedId, *board.AddItemEntityOperation, error) { + column, err := c.Snapshot().SearchColumn(columnId) + if err != nil { + return entity.UnsetCombinedId, nil, err + } + + var entityType board.ItemEntityType + switch e.(type) { + case *BugCache: + entityType = board.EntityTypeBug + default: + panic("unknown entity type") + } + c.mu.Lock() - itemId, op, err := board.AddItemEntity(c.entity, author, unixTime, columnId, e, metadata) + itemId, op, err := board.AddItemEntity(c.entity, author, unixTime, column.Id, entityType, e, metadata) c.mu.Unlock() if err != nil { return entity.UnsetCombinedId, nil, err diff --git a/cache/board_subcache.go b/cache/board_subcache.go index bc33fb3b1448b040414a2d79b11495af21e9151a..bf33dfbea804e49e964029b6bea383458b0072db 100644 --- a/cache/board_subcache.go +++ b/cache/board_subcache.go @@ -1,6 +1,7 @@ package cache import ( + "errors" "time" "github.com/git-bug/git-bug/entities/board" @@ -43,6 +44,50 @@ func NewRepoCacheBoard(repo repository.ClockedRepo, return &RepoCacheBoard{SubCache: sc} } +func (c *RepoCacheBoard) ResolveColumn(prefix string) (*BoardCache, entity.CombinedId, error) { + boardPrefix, _ := entity.SeparateIds(prefix) + boardCandidate := make([]entity.Id, 0, 5) + + // build a list of possible matching boards + c.mu.RLock() + for _, excerpt := range c.excerpts { + if excerpt.Id().HasPrefix(boardPrefix) { + boardCandidate = append(boardCandidate, excerpt.Id()) + } + } + c.mu.RUnlock() + + matchingBoardIds := make([]entity.Id, 0, 5) + matchingColumnId := entity.UnsetCombinedId + var matchingBoard *BoardCache + + // search for matching columns + // searching every board candidate allow for some collision with the board prefix only, + // before being refined with the full column prefix + for _, boardId := range boardCandidate { + b, err := c.Resolve(boardId) + if err != nil { + return nil, entity.UnsetCombinedId, err + } + + for _, column := range b.Snapshot().Columns { + if column.CombinedId.HasPrefix(prefix) { + matchingBoardIds = append(matchingBoardIds, boardId) + matchingBoard = b + matchingColumnId = column.CombinedId + } + } + } + + if len(matchingBoardIds) > 1 { + return nil, entity.UnsetCombinedId, entity.NewErrMultipleMatch("board/column", matchingBoardIds) + } else if len(matchingBoardIds) == 0 { + return nil, entity.UnsetCombinedId, errors.New("column doesn't exist") + } + + return matchingBoard, matchingColumnId, nil +} + func (c *RepoCacheBoard) New(title, description string, columns []string) (*BoardCache, *board.CreateOperation, error) { author, err := c.getUserIdentity() if err != nil { diff --git a/cache/repo_cache.go b/cache/repo_cache.go index acecabf3f279b4208c93627d114df90fa84db46a..0eda1f7fbe0ad30672ab29f102d5cfdc980333cd 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -7,6 +7,9 @@ import ( "strconv" "sync" + "github.com/git-bug/git-bug/entities/board" + "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/repository" "github.com/git-bug/git-bug/util/multierr" @@ -99,12 +102,17 @@ func NewNamedRepoCache(r repository.ClockedRepo, name string) (*RepoCache, chan c.subcaches = append(c.subcaches, c.boards) c.resolvers = entity.Resolvers{ - &IdentityCache{}: entity.ResolverFunc[*IdentityCache](c.identities.Resolve), - &IdentityExcerpt{}: entity.ResolverFunc[*IdentityExcerpt](c.identities.ResolveExcerpt), - &BugCache{}: entity.ResolverFunc[*BugCache](c.bugs.Resolve), - &BugExcerpt{}: entity.ResolverFunc[*BugExcerpt](c.bugs.ResolveExcerpt), - &BoardCache{}: entity.ResolverFunc[*BoardCache](c.boards.Resolve), - &BoardExcerpt{}: entity.ResolverFunc[*BoardExcerpt](c.boards.ResolveExcerpt), + 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.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), + &BoardCache{}: entity.ResolverFunc[*BoardCache](c.boards.Resolve), + &BoardExcerpt{}: entity.ResolverFunc[*BoardExcerpt](c.boards.ResolveExcerpt), } // small buffer so that below functions can emit an event without blocking diff --git a/commands/board/board.go b/commands/board/board.go index 52cf9d39b8dae3a404ace3a22fb22565c5face01..fb11385b2d886428ebde049b699f5266f0a1c523 100644 --- a/commands/board/board.go +++ b/commands/board/board.go @@ -67,6 +67,7 @@ func NewBoardCommand() *cobra.Command { cmd.AddCommand(newBoardDescriptionCommand()) cmd.AddCommand(newBoardTitleCommand()) cmd.AddCommand(newBoardAddDraftCommand()) + cmd.AddCommand(newBoardAddBugCommand()) return cmd } diff --git a/commands/board/board_addbug.go b/commands/board/board_addbug.go new file mode 100644 index 0000000000000000000000000000000000000000..dbbd3a8297de4d8966b9c3c08c5af7dc2b4b12d0 --- /dev/null +++ b/commands/board/board_addbug.go @@ -0,0 +1,86 @@ +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 { + column string +} + +func newBoardAddBugCommand() *cobra.Command { + env := execenv.NewEnv() + options := boardAddBugOptions{} + + cmd := &cobra.Command{ + Use: "add-bug [BOARD_ID] [BUG_ID]", + Short: "Add a bug to a board", + PreRunE: execenv.LoadBackend(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runBoardAddBug(env, options, args) + }), + ValidArgsFunction: BoardAndBugCompletion(env), + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringVarP(&options.column, "column", "c", "1", + "The column to add to. Either a column Id or prefix, or the column number starting from 1.") + _ = cmd.RegisterFlagCompletionFunc("column", ColumnCompletion(env)) + + return cmd +} + +func runBoardAddBug(env *execenv.Env, opts boardAddBugOptions, args []string) error { + board, args, err := ResolveSelected(env.Backend, 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) + if err != nil { + return err + } + + env.Out.Printf("%s created\n", id.Human()) + + return board.Commit() +} diff --git a/commands/board/board_adddraft.go b/commands/board/board_adddraft.go index 0c67e75700b4589b7b3516c3ee420e0ed3413ac7..604bc0758e9458b7652015669e7822c5c0fa5700 100644 --- a/commands/board/board_adddraft.go +++ b/commands/board/board_adddraft.go @@ -1,12 +1,14 @@ package boardcmd import ( + "fmt" "strconv" "github.com/spf13/cobra" 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" "github.com/git-bug/git-bug/entity" ) @@ -43,7 +45,6 @@ func newBoardAddDraftCommand() *cobra.Command { "Take the message from the given file. Use - to read the message from the standard input") flags.StringVarP(&options.column, "column", "c", "1", "The column to add to. Either a column Id or prefix, or the column number starting from 1.") - // _ = cmd.MarkFlagRequired("column") _ = cmd.RegisterFlagCompletionFunc("column", ColumnCompletion(env)) flags.BoolVar(&options.nonInteractive, "non-interactive", false, "Do not ask for user input") @@ -52,18 +53,29 @@ func newBoardAddDraftCommand() *cobra.Command { func runBoardAddDraft(env *execenv.Env, opts boardAddDraftOptions, args []string) error { b, args, err := ResolveSelected(env.Backend, args) - if err != nil { - return err - } - var columnId entity.Id - - index, err := strconv.Atoi(opts.column) - if err == nil && index-1 >= 0 && index-1 < len(b.Snapshot().Columns) { - columnId = b.Snapshot().Columns[index-1].Id - } else { - // TODO: ID or combined ID? - // TODO: resolve + 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 + return err } if opts.messageFile != "" && opts.message == "" { diff --git a/commands/board/completion.go b/commands/board/completion.go index fe802f570160caf96880183b713374f599e818bd..a8d1bab87296fd7217b94d344dd8b307eb4389f8 100644 --- a/commands/board/completion.go +++ b/commands/board/completion.go @@ -5,8 +5,11 @@ import ( "github.com/spf13/cobra" + "github.com/git-bug/git-bug/cache" + bugcmd "github.com/git-bug/git-bug/commands/bug" "github.com/git-bug/git-bug/commands/completion" "github.com/git-bug/git-bug/commands/execenv" + _select "github.com/git-bug/git-bug/commands/select" ) // BoardCompletion complete a board id @@ -19,20 +22,25 @@ func BoardCompletion(env *execenv.Env) completion.ValidArgsFunction { _ = env.Backend.Close() }() - for _, id := range env.Backend.Boards().AllIds() { - if strings.Contains(id.String(), strings.TrimSpace(toComplete)) { - excerpt, err := env.Backend.Boards().ResolveExcerpt(id) - if err != nil { - return completion.HandleError(err) - } - completions = append(completions, id.Human()+"\t"+excerpt.Title) + return boardWithBackend(env.Backend, toComplete) + } +} + +func boardWithBackend(backend *cache.RepoCache, toComplete string) (completions []string, directives cobra.ShellCompDirective) { + for _, id := range backend.Boards().AllIds() { + if strings.Contains(id.String(), strings.TrimSpace(toComplete)) { + excerpt, err := backend.Boards().ResolveExcerpt(id) + if err != nil { + return completion.HandleError(err) } + completions = append(completions, id.Human()+"\t"+excerpt.Title) } - - return completions, cobra.ShellCompDirectiveNoFileComp } + + return completions, cobra.ShellCompDirectiveNoFileComp } +// ColumnCompletion complete a board's column id func ColumnCompletion(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 { @@ -43,14 +51,39 @@ func ColumnCompletion(env *execenv.Env) completion.ValidArgsFunction { }() b, _, err := ResolveSelected(env.Backend, args) - if err != nil { + switch { + case _select.IsErrNoValidId(err): + // no completion + case err == nil: + for _, column := range b.Snapshot().Columns { + completions = append(completions, column.CombinedId.Human()+"\t"+column.Name) + } + default: return completion.HandleError(err) } - for _, column := range b.Snapshot().Columns { - completions = append(completions, column.Id.Human()+"\t"+column.Name) + return completions, cobra.ShellCompDirectiveNoFileComp + } +} + +func BoardAndBugCompletion(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 { + return completion.HandleError(err) + } + defer func() { + _ = env.Backend.Close() + }() + + _, _, err := ResolveSelected(env.Backend, args) + switch { + case _select.IsErrNoValidId(err): + return boardWithBackend(env.Backend, toComplete) + case err == nil: + return bugcmd.BugWithBackend(env.Backend, toComplete) + default: + return completion.HandleError(err) } - return completions, cobra.ShellCompDirectiveNoFileComp } } diff --git a/commands/bug/completion.go b/commands/bug/completion.go index 4329829ef8c8e8900e92a654b903833b45c8e2ab..20c67a42e6917ee4b34ea436d090281861c456cf 100644 --- a/commands/bug/completion.go +++ b/commands/bug/completion.go @@ -22,11 +22,11 @@ func BugCompletion(env *execenv.Env) completion.ValidArgsFunction { _ = env.Backend.Close() }() - return bugWithBackend(env.Backend, toComplete) + return BugWithBackend(env.Backend, toComplete) } } -func bugWithBackend(backend *cache.RepoCache, toComplete string) (completions []string, directives cobra.ShellCompDirective) { +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)) { excerpt, err := backend.Bugs().ResolveExcerpt(id) @@ -53,7 +53,7 @@ func BugAndLabelsCompletion(env *execenv.Env, addOrRemove bool) completion.Valid b, cleanArgs, err := ResolveSelected(env.Backend, args) if _select.IsErrNoValidId(err) { // we need a bug first to complete labels - return bugWithBackend(env.Backend, toComplete) + return BugWithBackend(env.Backend, toComplete) } if err != nil { return completion.HandleError(err) diff --git a/commands/cmdjson/board.go b/commands/cmdjson/board.go index 01826cd037f5162dfcba3f0d5dbf15bdd5a27f5a..11f77a4facadfe7e0eb6fb0d4a9a6225e6d3e7e5 100644 --- a/commands/cmdjson/board.go +++ b/commands/cmdjson/board.go @@ -49,8 +49,8 @@ type BoardColumn struct { func NewBoardColumn(column *board.Column) BoardColumn { jsonColumn := BoardColumn{ - Id: column.Id.String(), - HumanId: column.Id.Human(), + Id: column.CombinedId.String(), + HumanId: column.CombinedId.Human(), Name: column.Name, } jsonColumn.Items = make([]any, len(column.Items)) diff --git a/commands/select/select.go b/commands/select/select.go index 0e6ee8723345f56153330219775e451d10696fc3..496b49e298837ed5b371bbadfb544edaee9196f9 100644 --- a/commands/select/select.go +++ b/commands/select/select.go @@ -38,10 +38,10 @@ type Resolver[CacheT cache.CacheEntity] interface { // line. If it fails, it falls back to the select mechanism. // // Returns: -// - the entity if any -// - the new list of command line arguments with the entity prefix removed if it -// has been used -// - an error if the process failed +// +// Contrary to golang convention, the list of args returned is still correct even in +// case of error, which allows to keep going and decide to handle the failure case more +// naturally. func Resolve[CacheT cache.CacheEntity](repo *cache.RepoCache, typename string, namespace string, resolver Resolver[CacheT], args []string) (CacheT, []string, error) { @@ -54,7 +54,7 @@ func Resolve[CacheT cache.CacheEntity](repo *cache.RepoCache, } if !entity.IsErrNotFound(err) { - return *new(CacheT), nil, err + return *new(CacheT), args, err } } @@ -67,14 +67,14 @@ func Resolve[CacheT cache.CacheEntity](repo *cache.RepoCache, // we clear the selected bug err = Clear(repo, namespace) if err != nil { - return *new(CacheT), nil, err + return *new(CacheT), args, err } - return *new(CacheT), nil, NewErrNoValidId(typename) + return *new(CacheT), args, NewErrNoValidId(typename) } // another error when reading the entity if err != nil { - return *new(CacheT), nil, err + return *new(CacheT), args, err } // entity is successfully retrieved @@ -83,7 +83,7 @@ func Resolve[CacheT cache.CacheEntity](repo *cache.RepoCache, } // no selected bug and no valid first argument - return *new(CacheT), nil, NewErrNoValidId(typename) + return *new(CacheT), args, NewErrNoValidId(typename) } func selectFileName(namespace string) string { diff --git a/doc/man/git-bug-board.1 b/doc/man/git-bug-board.1 index 8d68804fd3e568a89ae646e5ecd5be7828fbf439..423eeab733b9b63dcdbc49439d402fb198a6c9d6 100644 --- a/doc/man/git-bug-board.1 +++ b/doc/man/git-bug-board.1 @@ -35,4 +35,4 @@ List boards .SH SEE ALSO -\fBgit-bug(1)\fP, \fBgit-bug-board-add-draft(1)\fP, \fBgit-bug-board-description(1)\fP, \fBgit-bug-board-deselect(1)\fP, \fBgit-bug-board-new(1)\fP, \fBgit-bug-board-rm(1)\fP, \fBgit-bug-board-select(1)\fP, \fBgit-bug-board-show(1)\fP, \fBgit-bug-board-title(1)\fP +\fBgit-bug(1)\fP, \fBgit-bug-board-add-bug(1)\fP, \fBgit-bug-board-add-draft(1)\fP, \fBgit-bug-board-description(1)\fP, \fBgit-bug-board-deselect(1)\fP, \fBgit-bug-board-new(1)\fP, \fBgit-bug-board-rm(1)\fP, \fBgit-bug-board-select(1)\fP, \fBgit-bug-board-show(1)\fP, \fBgit-bug-board-title(1)\fP diff --git a/doc/md/git-bug_board.md b/doc/md/git-bug_board.md index d7d836e8b045d011e7c41a928e37345484cce63d..b77bd7cca3df0e0108bf4b8d7e3dc90a8e85f83e 100644 --- a/doc/md/git-bug_board.md +++ b/doc/md/git-bug_board.md @@ -19,6 +19,7 @@ git-bug board [flags] ### SEE ALSO * [git-bug](git-bug.md) - A bug tracker embedded in Git +* [git-bug board add-bug](git-bug_board_add-bug.md) - Add a bug to a board * [git-bug board add-draft](git-bug_board_add-draft.md) - Add a draft item to a board * [git-bug board description](git-bug_board_description.md) - Display the description of a board * [git-bug board deselect](git-bug_board_deselect.md) - Clear the implicitly selected board diff --git a/entities/board/op_add_item_draft.go b/entities/board/op_add_item_draft.go index fe51c689e797d7e74be6532ac13a5c3a092ab05b..253dfc4b0d8096489b6ae595895226c554d1d29a 100644 --- a/entities/board/op_add_item_draft.go +++ b/entities/board/op_add_item_draft.go @@ -59,10 +59,11 @@ func (op *AddItemDraftOperation) Validate() error { } func (op *AddItemDraftOperation) Apply(snapshot *Snapshot) { - snapshot.addParticipant(op.Author()) + // Recreate the combined Id to match on + combinedId := entity.CombineIds(snapshot.Id(), op.ColumnId) for _, column := range snapshot.Columns { - if column.Id == op.ColumnId { + if column.CombinedId == combinedId { column.Items = append(column.Items, &Draft{ combinedId: entity.CombineIds(snapshot.id, op.Id()), Author: op.Author(), @@ -70,6 +71,8 @@ func (op *AddItemDraftOperation) Apply(snapshot *Snapshot) { Message: op.Message, unixTime: timestamp.Timestamp(op.UnixTime), }) + + snapshot.addParticipant(op.Author()) return } } diff --git a/entities/board/op_add_item_entity.go b/entities/board/op_add_item_entity.go index fa4f54595c1e21f1ef7cccc634e61fd827afa69f..91df207cf7424ef0c6ff2feea94c7f7204468fc4 100644 --- a/entities/board/op_add_item_entity.go +++ b/entities/board/op_add_item_entity.go @@ -9,11 +9,11 @@ import ( "github.com/git-bug/git-bug/entity/dag" ) -// itemEntityType indicate the type of entity board item -type itemEntityType string +// ItemEntityType indicate the type of entity board item +type ItemEntityType string const ( - entityTypeBug itemEntityType = "bug" + EntityTypeBug ItemEntityType = "bug" ) var _ Operation = &AddItemEntityOperation{} @@ -21,7 +21,7 @@ var _ Operation = &AddItemEntityOperation{} type AddItemEntityOperation struct { dag.OpBase ColumnId entity.Id `json:"column"` - EntityType itemEntityType `json:"entity_type"` + EntityType ItemEntityType `json:"entity_type"` EntityId entity.Id `json:"entity_id"` entity entity.Interface // not serialized } @@ -40,7 +40,7 @@ func (op *AddItemEntityOperation) Validate() error { } switch op.EntityType { - case entityTypeBug: + case EntityTypeBug: default: return fmt.Errorf("unknown entity type") } @@ -57,40 +57,39 @@ func (op *AddItemEntityOperation) Apply(snapshot *Snapshot) { return } - snapshot.addParticipant(op.Author()) + // Recreate the combined Id to match on + combinedId := entity.CombineIds(snapshot.Id(), op.ColumnId) for _, column := range snapshot.Columns { - if column.Id == op.ColumnId { - switch e := op.entity.(type) { - case bug.Interface: + if column.CombinedId == combinedId { + switch op.EntityType { + case EntityTypeBug: column.Items = append(column.Items, &BugItem{ - combinedId: entity.CombineIds(snapshot.Id(), e.Id()), - Bug: e, + combinedId: entity.CombineIds(snapshot.Id(), op.entity.Id()), + Bug: op.entity.(bug.Interface), }) } + snapshot.addParticipant(op.Author()) return } } } -func NewAddItemEntityOp(author identity.Interface, unixTime int64, columnId entity.Id, e entity.Interface) *AddItemEntityOperation { - switch e := e.(type) { - case bug.Interface: - return &AddItemEntityOperation{ - OpBase: dag.NewOpBase(AddItemEntityOp, author, unixTime), - ColumnId: columnId, - EntityType: entityTypeBug, - EntityId: e.Id(), - entity: e, - } - default: - panic("invalid entity type") +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; + // proceed with caution! + return &AddItemEntityOperation{ + OpBase: dag.NewOpBase(AddItemEntityOp, author, unixTime), + ColumnId: columnId, + EntityType: entityType, + EntityId: e.Id(), + entity: e, } } // 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, e entity.Interface, metadata map[string]string) (entity.CombinedId, *AddItemEntityOperation, error) { - op := NewAddItemEntityOp(author, unixTime, columnId, e) +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) { + op := NewAddItemEntityOp(author, unixTime, columnId, entityType, e) for key, val := range metadata { op.SetMetadata(key, val) } diff --git a/entities/board/op_create.go b/entities/board/op_create.go index e401ed0bb20a3a5ce8bf4c9e0bcbd9ad67731a6a..673bd742350e83e8d15462a78e106ba11af18e30 100644 --- a/entities/board/op_create.go +++ b/entities/board/op_create.go @@ -86,13 +86,20 @@ func (op *CreateOperation) Apply(snap *Snapshot) { snap.CreateTime = op.Time() for _, name := range op.Columns { - // we derive a unique Id from the original column name - id := entity.DeriveId([]byte(name)) + // we derive a unique ID from: + // - the ID of the operation that created the column + // - the original column name + id := entity.DeriveId(append([]byte(op.Id()), []byte(name)...)) + + // we derived the combined ID by interleaving the board ID (the same in + // this case). + combinedID := entity.CombineIds(snap.id, id) snap.Columns = append(snap.Columns, &Column{ - Id: id, - Name: name, - Items: nil, + Id: id, + CombinedId: combinedID, + Name: name, + Items: nil, }) } diff --git a/entities/board/operation.go b/entities/board/operation.go index aa7ffc4629a338e5bc96384753dcc109615bbad1..0c0e5c35b776351f179d3c446aa899671fbe49e1 100644 --- a/entities/board/operation.go +++ b/entities/board/operation.go @@ -65,7 +65,7 @@ func operationUnmarshaler(raw json.RawMessage, resolvers entity.Resolvers) (dag. switch op := op.(type) { case *AddItemEntityOperation: switch op.EntityType { - case entityTypeBug: + case EntityTypeBug: op.entity, err = entity.Resolve[bug.Interface](resolvers, op.EntityId) default: return nil, fmt.Errorf("unknown entity type") diff --git a/entities/board/snapshot.go b/entities/board/snapshot.go index d895fca36c77036b6de32dc56c206aff2880915a..3500c47a5f628f7bdbe72b1e98bb77883294a09c 100644 --- a/entities/board/snapshot.go +++ b/entities/board/snapshot.go @@ -1,6 +1,7 @@ package board import ( + "fmt" "time" "github.com/git-bug/git-bug/entities/identity" @@ -9,9 +10,12 @@ import ( ) type Column struct { - Id entity.Id - Name string - Items []Item + // id is the identifier of the column within the board context + Id entity.Id + // CombinedId is the global identifier of the column + CombinedId entity.CombinedId + Name string + Items []Item } type Item interface { @@ -60,6 +64,17 @@ func (snap *Snapshot) EditTime() time.Time { return snap.Operations[len(snap.Operations)-1].Time() } +// SearchColumn will search for a column matching the given id +func (snap *Snapshot) SearchColumn(id entity.CombinedId) (*Column, error) { + for _, column := range snap.Columns { + if column.CombinedId == id { + return column, nil + } + } + + return nil, fmt.Errorf("column not found") +} + // append the operation author to the participants list func (snap *Snapshot) addParticipant(participant identity.Interface) { for _, p := range snap.Participants { diff --git a/entity/resolver.go b/entity/resolver.go index bd16b901cfc8d8d5519e6244173db87b18ff5e67..c2d3b787688b9c1dfb6a2c259c4a5e3aa892fca3 100644 --- a/entity/resolver.go +++ b/entity/resolver.go @@ -24,8 +24,7 @@ type Resolvers map[Resolved]Resolver func Resolve[T Resolved](rs Resolvers, id Id) (T, error) { var zero T for t, resolver := range rs { - switch t.(type) { - case T: + if _, ok := t.(T); ok { val, err := resolver.(Resolver).Resolve(id) if err != nil { return zero, err