board: CLI tooling

Michael Muré created

Change summary

cache/board_cache.go                      |   8 
cache/board_excerpt.go                    |  20 +-
cache/board_subcache.go                   |   8 
commands/board/board.go                   | 145 +++++++++++++++++++++++++
commands/board/board_adddraft.go          |  96 ++++++++++++++++
commands/board/board_description.go       |  38 ++++++
commands/board/board_description_edit.go  |  70 ++++++++++++
commands/board/board_deselect.go          |  34 +++++
commands/board/board_new.go               |  83 ++++++++++++++
commands/board/board_rm.go                |  45 +++++++
commands/board/board_select.go            |  58 ++++++++++
commands/board/board_show.go              | 136 +++++++++++++++++++++++
commands/board/board_title.go             |  38 ++++++
commands/board/board_title_edit.go        |  70 ++++++++++++
commands/board/completion.go              |  56 +++++++++
commands/cmdjson/board.go                 | 123 +++++++++++++++++++++
commands/root.go                          |   6 
doc/man/git-bug-board-add-draft.1         |  42 +++++++
doc/man/git-bug-board-description-edit.1  |  30 +++++
doc/man/git-bug-board-description.1       |  22 +++
doc/man/git-bug-board-deselect.1          |  22 +++
doc/man/git-bug-board-new.1               |  38 ++++++
doc/man/git-bug-board-rm.1                |  22 +++
doc/man/git-bug-board-select.1            |  25 ++++
doc/man/git-bug-board-show.1              |  26 ++++
doc/man/git-bug-board-title-edit.1        |  30 +++++
doc/man/git-bug-board-title.1             |  22 +++
doc/man/git-bug-board.1                   |  38 ++++++
doc/man/git-bug.1                         |   2 
doc/md/git-bug.md                         |   1 
doc/md/git-bug_board.md                   |  30 +++++
doc/md/git-bug_board_add-draft.md         |  23 +++
doc/md/git-bug_board_description.md       |  19 +++
doc/md/git-bug_board_description_edit.md  |  20 +++
doc/md/git-bug_board_deselect.md          |  18 +++
doc/md/git-bug_board_new.md               |  22 +++
doc/md/git-bug_board_rm.md                |  22 +++
doc/md/git-bug_board_select.md            |  25 ++++
doc/md/git-bug_board_show.md              |  19 +++
doc/md/git-bug_board_title.md             |  19 +++
doc/md/git-bug_board_title_edit.md        |  20 +++
entities/board/board.go                   |  10 
entities/board/board_actions.go           |   8 
entities/board/item_draft.go              |  20 +--
entities/board/item_entity.go             |   6 
entities/board/op_add_item_draft.go       |  22 +--
entities/board/op_add_item_draft_test.go  |   8 
entities/board/op_add_item_entity.go      |  12 +-
entities/board/op_add_item_entity_test.go |   8 
entities/board/op_create.go               |  10 
entities/board/op_create_test.go          |   8 
entities/board/op_set_description.go      |  10 
entities/board/op_set_description_test.go |   6 
entities/board/op_set_title.go            |  10 
entities/board/op_set_title_test.go       |   6 
entities/board/operation.go               |   6 
entities/board/snapshot.go                |  37 +++---
57 files changed, 1,660 insertions(+), 118 deletions(-)

Detailed changes

cache/board_cache.go 🔗

@@ -3,10 +3,10 @@ package cache
 import (
 	"time"
 
-	"github.com/MichaelMure/git-bug/entities/board"
-	"github.com/MichaelMure/git-bug/entities/identity"
-	"github.com/MichaelMure/git-bug/entity"
-	"github.com/MichaelMure/git-bug/repository"
+	"github.com/git-bug/git-bug/entities/board"
+	"github.com/git-bug/git-bug/entities/identity"
+	"github.com/git-bug/git-bug/entity"
+	"github.com/git-bug/git-bug/repository"
 )
 
 // BoardCache is a wrapper around a Board. It provides multiple functions:

cache/board_excerpt.go 🔗

@@ -4,8 +4,8 @@ import (
 	"encoding/gob"
 	"time"
 
-	"github.com/MichaelMure/git-bug/entity"
-	"github.com/MichaelMure/git-bug/util/lamport"
+	"github.com/git-bug/git-bug/entity"
+	"github.com/git-bug/git-bug/util/lamport"
 )
 
 // Package initialisation used to register the type for (de)serialization
@@ -25,10 +25,10 @@ type BoardExcerpt struct {
 	CreateUnixTime    int64
 	EditUnixTime      int64
 
-	Title       string
-	Description string
-	ItemCount   int
-	Actors      []entity.Id
+	Title        string
+	Description  string
+	ItemCount    int
+	Participants []entity.Id
 
 	CreateMetadata map[string]string
 }
@@ -36,9 +36,9 @@ type BoardExcerpt struct {
 func NewBoardExcerpt(b *BoardCache) *BoardExcerpt {
 	snap := b.Snapshot()
 
-	actorsIds := make([]entity.Id, 0, len(snap.Actors))
-	for _, actor := range snap.Actors {
-		actorsIds = append(actorsIds, actor.Id())
+	participantsIds := make([]entity.Id, 0, len(snap.Participants))
+	for _, participant := range snap.Participants {
+		participantsIds = append(participantsIds, participant.Id())
 	}
 
 	return &BoardExcerpt{
@@ -50,7 +50,7 @@ func NewBoardExcerpt(b *BoardCache) *BoardExcerpt {
 		Title:             snap.Title,
 		Description:       snap.Description,
 		ItemCount:         snap.ItemCount(),
-		Actors:            actorsIds,
+		Participants:      participantsIds,
 		CreateMetadata:    b.FirstOp().AllMetadata(),
 	}
 }

cache/board_subcache.go 🔗

@@ -3,10 +3,10 @@ package cache
 import (
 	"time"
 
-	"github.com/MichaelMure/git-bug/entities/board"
-	"github.com/MichaelMure/git-bug/entities/identity"
-	"github.com/MichaelMure/git-bug/entity"
-	"github.com/MichaelMure/git-bug/repository"
+	"github.com/git-bug/git-bug/entities/board"
+	"github.com/git-bug/git-bug/entities/identity"
+	"github.com/git-bug/git-bug/entity"
+	"github.com/git-bug/git-bug/repository"
 )
 
 type RepoCacheBoard struct {

commands/board/board.go 🔗

@@ -0,0 +1,145 @@
+package boardcmd
+
+import (
+	"fmt"
+	"strings"
+
+	text "github.com/MichaelMure/go-term-text"
+	"github.com/spf13/cobra"
+
+	"github.com/git-bug/git-bug/cache"
+	"github.com/git-bug/git-bug/commands/cmdjson"
+	"github.com/git-bug/git-bug/commands/completion"
+	"github.com/git-bug/git-bug/commands/execenv"
+	"github.com/git-bug/git-bug/util/colors"
+)
+
+type boardOptions struct {
+	metadataQuery []string
+	actorQuery    []string
+	titleQuery    []string
+	outputFormat  string
+}
+
+func NewBoardCommand() *cobra.Command {
+	env := execenv.NewEnv()
+	options := boardOptions{}
+
+	cmd := &cobra.Command{
+		Use:     "board",
+		Short:   "List boards",
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBoard(env, options, args)
+		}),
+	}
+
+	flags := cmd.Flags()
+	flags.SortFlags = false
+
+	flags.StringSliceVarP(&options.metadataQuery, "metadata", "m", nil,
+		"Filter by metadata. Example: github-url=URL")
+	cmd.RegisterFlagCompletionFunc("author", completion.UserForQuery(env))
+	flags.StringSliceVarP(&options.actorQuery, "actor", "A", nil,
+		"Filter by actor")
+	cmd.RegisterFlagCompletionFunc("actor", completion.UserForQuery(env))
+	flags.StringSliceVarP(&options.titleQuery, "title", "t", nil,
+		"Filter by title")
+	flags.StringVarP(&options.outputFormat, "format", "f", "default",
+		"Select the output formatting style. Valid values are [default,plain,compact,id,json,org-mode]")
+	cmd.RegisterFlagCompletionFunc("format",
+		completion.From([]string{"default", "id", "json"}))
+
+	const selectGroup = "select"
+	cmd.AddGroup(&cobra.Group{ID: selectGroup, Title: "Implicit selection"})
+
+	addCmdWithGroup := func(child *cobra.Command, groupID string) {
+		cmd.AddCommand(child)
+		child.GroupID = groupID
+	}
+
+	addCmdWithGroup(newBoardDeselectCommand(), selectGroup)
+	addCmdWithGroup(newBoardSelectCommand(), selectGroup)
+
+	cmd.AddCommand(newBoardNewCommand())
+	cmd.AddCommand(newBoardRmCommand())
+	cmd.AddCommand(newBoardShowCommand())
+	cmd.AddCommand(newBoardDescriptionCommand())
+	cmd.AddCommand(newBoardTitleCommand())
+	cmd.AddCommand(newBoardAddDraftCommand())
+
+	return cmd
+}
+
+func runBoard(env *execenv.Env, opts boardOptions, args []string) error {
+	// TODO: query
+
+	allIds := env.Backend.Boards().AllIds()
+
+	excerpts := make([]*cache.BoardExcerpt, len(allIds))
+	for i, id := range allIds {
+		b, err := env.Backend.Boards().ResolveExcerpt(id)
+		if err != nil {
+			return err
+		}
+		excerpts[i] = b
+	}
+
+	switch opts.outputFormat {
+	case "json":
+		return boardJsonFormatter(env, excerpts)
+	case "id":
+		return boardIDFormatter(env, excerpts)
+	case "default":
+		return boardDefaultFormatter(env, excerpts)
+	default:
+		return fmt.Errorf("unknown format %s", opts.outputFormat)
+	}
+}
+
+func boardIDFormatter(env *execenv.Env, excerpts []*cache.BoardExcerpt) error {
+	for _, b := range excerpts {
+		env.Out.Println(b.Id().String())
+	}
+
+	return nil
+}
+
+func boardDefaultFormatter(env *execenv.Env, excerpts []*cache.BoardExcerpt) error {
+	for _, b := range excerpts {
+		// truncate + pad if needed
+		titleFmt := text.LeftPadMaxLine(strings.TrimSpace(b.Title), 50, 0)
+		descFmt := text.LeftPadMaxLine(strings.TrimSpace(b.Description), 50, 0)
+
+		var itemFmt string
+		switch {
+		case b.ItemCount < 1:
+			itemFmt = "empty"
+		case b.ItemCount < 1000:
+			itemFmt = fmt.Sprintf("%3d 📝", b.ItemCount)
+		default:
+			itemFmt = "  ∞ 📝"
+
+		}
+
+		env.Out.Printf("%s\t%s\t%s\t%s\n",
+			colors.Cyan(b.Id().Human()),
+			titleFmt,
+			descFmt,
+			itemFmt,
+		)
+	}
+	return nil
+}
+
+func boardJsonFormatter(env *execenv.Env, excerpts []*cache.BoardExcerpt) error {
+	res := make([]cmdjson.BoardExcerpt, len(excerpts))
+	for i, b := range excerpts {
+		jsonBoard, err := cmdjson.NewBoardExcerpt(env.Backend, b)
+		if err != nil {
+			return err
+		}
+		res[i] = jsonBoard
+	}
+	return env.Out.PrintJSON(res)
+}

commands/board/board_adddraft.go 🔗

@@ -0,0 +1,96 @@
+package boardcmd
+
+import (
+	"strconv"
+
+	"github.com/spf13/cobra"
+
+	buginput "github.com/git-bug/git-bug/commands/bug/input"
+	"github.com/git-bug/git-bug/commands/execenv"
+	"github.com/git-bug/git-bug/entity"
+)
+
+type boardAddDraftOptions struct {
+	title          string
+	messageFile    string
+	message        string
+	column         string
+	nonInteractive bool
+}
+
+func newBoardAddDraftCommand() *cobra.Command {
+	env := execenv.NewEnv()
+	options := boardAddDraftOptions{}
+
+	cmd := &cobra.Command{
+		Use:     "add-draft [BOARD_ID]",
+		Short:   "Add a draft item to a board",
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBoardAddDraft(env, options, args)
+		}),
+		ValidArgsFunction: BoardCompletion(env),
+	}
+
+	flags := cmd.Flags()
+	flags.SortFlags = false
+
+	flags.StringVarP(&options.title, "title", "t", "",
+		"Provide the title to describe the draft item")
+	flags.StringVarP(&options.message, "message", "m", "",
+		"Provide the message of the draft item")
+	flags.StringVarP(&options.messageFile, "file", "F", "",
+		"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")
+
+	return cmd
+}
+
+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
+	}
+
+	if opts.messageFile != "" && opts.message == "" {
+		// Note: reuse the bug inputs
+		opts.title, opts.message, err = buginput.BugCreateFileInput(opts.messageFile)
+		if err != nil {
+			return err
+		}
+	}
+
+	if !opts.nonInteractive && opts.messageFile == "" && (opts.message == "" || opts.title == "") {
+		opts.title, opts.message, err = buginput.BugCreateEditorInput(env.Backend, opts.title, opts.message)
+		if err == buginput.ErrEmptyTitle {
+			env.Out.Println("Empty title, aborting.")
+			return nil
+		}
+		if err != nil {
+			return err
+		}
+	}
+
+	id, _, err := b.AddItemDraft(columnId, opts.title, opts.message, nil)
+	if err != nil {
+		return err
+	}
+
+	env.Out.Printf("%s created\n", id.Human())
+
+	return b.Commit()
+}

commands/board/board_description.go 🔗

@@ -0,0 +1,38 @@
+package boardcmd
+
+import (
+	"github.com/spf13/cobra"
+
+	"github.com/git-bug/git-bug/commands/execenv"
+)
+
+func newBoardDescriptionCommand() *cobra.Command {
+	env := execenv.NewEnv()
+
+	cmd := &cobra.Command{
+		Use:     "description [BOARD_ID]",
+		Short:   "Display the description of a board",
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBoardDescription(env, args)
+		}),
+		ValidArgsFunction: BoardCompletion(env),
+	}
+
+	cmd.AddCommand(newBoardDescriptionEditCommand())
+
+	return cmd
+}
+
+func runBoardDescription(env *execenv.Env, args []string) error {
+	b, args, err := ResolveSelected(env.Backend, args)
+	if err != nil {
+		return err
+	}
+
+	snap := b.Snapshot()
+
+	env.Out.Println(snap.Description)
+
+	return nil
+}

commands/board/board_description_edit.go 🔗

@@ -0,0 +1,70 @@
+package boardcmd
+
+import (
+	"github.com/spf13/cobra"
+
+	"github.com/git-bug/git-bug/commands/execenv"
+	"github.com/git-bug/git-bug/commands/input"
+	"github.com/git-bug/git-bug/util/text"
+)
+
+type boardDescriptionEditOptions struct {
+	description    string
+	nonInteractive bool
+}
+
+func newBoardDescriptionEditCommand() *cobra.Command {
+	env := execenv.NewEnv()
+	options := boardDescriptionEditOptions{}
+
+	cmd := &cobra.Command{
+		Use:     "edit [BUG_ID]",
+		Short:   "Edit a description of a board",
+		PreRunE: execenv.LoadBackendEnsureUser(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBugDescriptionEdit(env, options, args)
+		}),
+		ValidArgsFunction: BoardCompletion(env),
+	}
+
+	flags := cmd.Flags()
+	flags.SortFlags = false
+
+	flags.StringVarP(&options.description, "description", "t", "",
+		"Provide a description for the board",
+	)
+	flags.BoolVar(&options.nonInteractive, "non-interactive", false, "Do not ask for user input")
+
+	return cmd
+}
+
+func runBugDescriptionEdit(env *execenv.Env, opts boardDescriptionEditOptions, args []string) error {
+	b, args, err := ResolveSelected(env.Backend, args)
+	if err != nil {
+		return err
+	}
+
+	snap := b.Snapshot()
+
+	if opts.description == "" {
+		if opts.nonInteractive {
+			env.Err.Println("No description given. Aborting.")
+			return nil
+		}
+		opts.description, err = input.PromptDefault("Board description", "description", snap.Description, input.Required)
+		if err != nil {
+			return err
+		}
+	}
+
+	if opts.description == snap.Description {
+		env.Err.Println("No change, aborting.")
+	}
+
+	_, err = b.SetDescription(text.CleanupOneLine(opts.description))
+	if err != nil {
+		return err
+	}
+
+	return b.Commit()
+}

commands/board/board_deselect.go 🔗

@@ -0,0 +1,34 @@
+package boardcmd
+
+import (
+	"github.com/spf13/cobra"
+
+	"github.com/git-bug/git-bug/commands/execenv"
+	_select "github.com/git-bug/git-bug/commands/select"
+	"github.com/git-bug/git-bug/entities/board"
+)
+
+func newBoardDeselectCommand() *cobra.Command {
+	env := execenv.NewEnv()
+
+	cmd := &cobra.Command{
+		Use:   "deselect",
+		Short: "Clear the implicitly selected board",
+
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBoardDeselect(env)
+		}),
+	}
+
+	return cmd
+}
+
+func runBoardDeselect(env *execenv.Env) error {
+	err := _select.Clear(env.Backend, board.Namespace)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}

commands/board/board_new.go 🔗

@@ -0,0 +1,83 @@
+package boardcmd
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/spf13/cobra"
+
+	"github.com/git-bug/git-bug/commands/execenv"
+	"github.com/git-bug/git-bug/commands/input"
+	"github.com/git-bug/git-bug/entities/board"
+	"github.com/git-bug/git-bug/util/text"
+)
+
+type boardNewOptions struct {
+	title          string
+	description    string
+	columns        []string
+	nonInteractive bool
+}
+
+func newBoardNewCommand() *cobra.Command {
+	env := execenv.NewEnv()
+	options := boardNewOptions{}
+
+	cmd := &cobra.Command{
+		Use:     "new",
+		Short:   "Create a new board",
+		PreRunE: execenv.LoadBackendEnsureUser(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBugNew(env, options)
+		}),
+	}
+
+	flags := cmd.Flags()
+	flags.SortFlags = false
+
+	flags.StringVarP(&options.title, "title", "t", "",
+		"Provide a title to describe the issue")
+	flags.StringVarP(&options.description, "description", "d", "",
+		"Provide a message to describe the board")
+	flags.StringArrayVarP(&options.columns, "columns", "c", board.DefaultColumns,
+		fmt.Sprintf("Define the columns of the board (default to %s)",
+			strings.Join(board.DefaultColumns, ",")))
+	flags.BoolVar(&options.nonInteractive, "non-interactive", false, "Do not ask for user input")
+
+	return cmd
+}
+
+func runBugNew(env *execenv.Env, opts boardNewOptions) error {
+	var err error
+
+	if !opts.nonInteractive && opts.title == "" {
+		opts.title, err = input.Prompt("Board title", "title", input.Required)
+		if err != nil {
+			return err
+		}
+	}
+
+	if !opts.nonInteractive && opts.description == "" {
+		opts.description, err = input.Prompt("Board description", "description")
+		if err != nil {
+			return err
+		}
+	}
+
+	for i, column := range opts.columns {
+		opts.columns[i] = text.Cleanup(column)
+	}
+
+	b, _, err := env.Backend.Boards().New(
+		text.CleanupOneLine(opts.title),
+		text.CleanupOneLine(opts.description),
+		opts.columns,
+	)
+	if err != nil {
+		return err
+	}
+
+	env.Out.Printf("%s created\n", b.Id().Human())
+
+	return nil
+}

commands/board/board_rm.go 🔗

@@ -0,0 +1,45 @@
+package boardcmd
+
+import (
+	"errors"
+
+	"github.com/spf13/cobra"
+
+	"github.com/git-bug/git-bug/commands/execenv"
+)
+
+func newBoardRmCommand() *cobra.Command {
+	env := execenv.NewEnv()
+
+	cmd := &cobra.Command{
+		Use:     "rm BOARD_ID",
+		Short:   "Remove an existing board",
+		Long:    "Remove an existing board in the local repository.",
+		PreRunE: execenv.LoadBackendEnsureUser(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBoardRm(env, args)
+		}),
+		ValidArgsFunction: BoardCompletion(env),
+	}
+
+	flags := cmd.Flags()
+	flags.SortFlags = false
+
+	return cmd
+}
+
+func runBoardRm(env *execenv.Env, args []string) (err error) {
+	if len(args) == 0 {
+		return errors.New("you must provide a board prefix to remove")
+	}
+
+	err = env.Backend.Boards().Remove(args[0])
+
+	if err != nil {
+		return
+	}
+
+	env.Out.Printf("board %s removed\n", args[0])
+
+	return
+}

commands/board/board_select.go 🔗

@@ -0,0 +1,58 @@
+package boardcmd
+
+import (
+	"errors"
+
+	"github.com/spf13/cobra"
+
+	"github.com/git-bug/git-bug/cache"
+	"github.com/git-bug/git-bug/commands/execenv"
+	_select "github.com/git-bug/git-bug/commands/select"
+	"github.com/git-bug/git-bug/entities/board"
+)
+
+func ResolveSelected(repo *cache.RepoCache, args []string) (*cache.BoardCache, []string, error) {
+	return _select.Resolve[*cache.BoardCache](repo, board.Typename, board.Namespace, repo.Boards(), args)
+}
+
+func newBoardSelectCommand() *cobra.Command {
+	env := execenv.NewEnv()
+
+	cmd := &cobra.Command{
+		Use:   "select BOARD_ID",
+		Short: "Select a board for implicit use in future commands",
+		Long: `Select a board for implicit use in future commands.
+
+The complementary command is "git board deselect" performing the opposite operation.
+`,
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBoardSelect(env, args)
+		}),
+		ValidArgsFunction: BoardCompletion(env),
+	}
+
+	return cmd
+}
+
+func runBoardSelect(env *execenv.Env, args []string) error {
+	if len(args) == 0 {
+		return errors.New("You must provide a board id")
+	}
+
+	prefix := args[0]
+
+	b, err := env.Backend.Boards().ResolvePrefix(prefix)
+	if err != nil {
+		return err
+	}
+
+	err = _select.Select(env.Backend, board.Namespace, b.Id())
+	if err != nil {
+		return err
+	}
+
+	env.Out.Printf("selected board %s: %s\n", b.Id().Human(), b.Snapshot().Title)
+
+	return nil
+}

commands/board/board_show.go 🔗

@@ -0,0 +1,136 @@
+package boardcmd
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+
+	"github.com/git-bug/git-bug/commands/cmdjson"
+	"github.com/git-bug/git-bug/commands/execenv"
+	"github.com/git-bug/git-bug/entities/board"
+)
+
+type boardShowOptions struct {
+	format string
+}
+
+func newBoardShowCommand() *cobra.Command {
+	env := execenv.NewEnv()
+	options := boardShowOptions{}
+
+	cmd := &cobra.Command{
+		Use:     "show [BOARD_ID]",
+		Short:   "Display a board",
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBoardShow(env, options, args)
+		}),
+		ValidArgsFunction: BoardCompletion(env),
+	}
+
+	flags := cmd.Flags()
+	flags.SortFlags = false
+
+	flags.StringVarP(&options.format, "format", "f", "default",
+		"Select the output formatting style. Valid values are [default,json,org-mode]")
+
+	return cmd
+}
+
+func runBoardShow(env *execenv.Env, opts boardShowOptions, args []string) error {
+	b, args, err := ResolveSelected(env.Backend, args)
+	if err != nil {
+		return err
+	}
+
+	snap := b.Snapshot()
+
+	switch opts.format {
+	case "json":
+		return showJsonFormatter(env, snap)
+	case "default":
+		return showDefaultFormatter(env, snap)
+	default:
+		return fmt.Errorf("unknown format %s", opts.format)
+	}
+}
+
+func showDefaultFormatter(env *execenv.Env, snapshot *board.Snapshot) error {
+	// // Header
+	// env.Out.Printf("%s [%s] %s\n\n",
+	// 	colors.Cyan(snapshot.Id().Human()),
+	// 	colors.Yellow(snapshot.Status),
+	// 	snapshot.Title,
+	// )
+	//
+	// env.Out.Printf("%s opened this issue %s\n",
+	// 	colors.Magenta(snapshot.Author.DisplayName()),
+	// 	snapshot.CreateTime.String(),
+	// )
+	//
+	// env.Out.Printf("This was last edited at %s\n\n",
+	// 	snapshot.EditTime().String(),
+	// )
+	//
+	// // Labels
+	// var labels = make([]string, len(snapshot.Labels))
+	// for i := range snapshot.Labels {
+	// 	labels[i] = string(snapshot.Labels[i])
+	// }
+	//
+	// env.Out.Printf("labels: %s\n",
+	// 	strings.Join(labels, ", "),
+	// )
+	//
+	// // Actors
+	// var actors = make([]string, len(snapshot.Actors))
+	// for i := range snapshot.Actors {
+	// 	actors[i] = snapshot.Actors[i].DisplayName()
+	// }
+	//
+	// env.Out.Printf("actors: %s\n",
+	// 	strings.Join(actors, ", "),
+	// )
+	//
+	// // Participants
+	// var participants = make([]string, len(snapshot.Participants))
+	// for i := range snapshot.Participants {
+	// 	participants[i] = snapshot.Participants[i].DisplayName()
+	// }
+	//
+	// env.Out.Printf("participants: %s\n\n",
+	// 	strings.Join(participants, ", "),
+	// )
+	//
+	// // Comments
+	// indent := "  "
+	//
+	// for i, comment := range snapshot.Comments {
+	// 	var message string
+	// 	env.Out.Printf("%s%s #%d %s <%s>\n\n",
+	// 		indent,
+	// 		comment.CombinedId().Human(),
+	// 		i,
+	// 		comment.Author.DisplayName(),
+	// 		comment.Author.Email(),
+	// 	)
+	//
+	// 	if comment.Message == "" {
+	// 		message = colors.BlackBold(colors.WhiteBg("No description provided."))
+	// 	} else {
+	// 		message = comment.Message
+	// 	}
+	//
+	// 	env.Out.Printf("%s%s\n\n\n",
+	// 		indent,
+	// 		message,
+	// 	)
+	// }
+
+	return nil
+}
+
+func showJsonFormatter(env *execenv.Env, snap *board.Snapshot) error {
+	jsonBoard := cmdjson.NewBoardSnapshot(snap)
+	return env.Out.PrintJSON(jsonBoard)
+}

commands/board/board_title.go 🔗

@@ -0,0 +1,38 @@
+package boardcmd
+
+import (
+	"github.com/spf13/cobra"
+
+	"github.com/git-bug/git-bug/commands/execenv"
+)
+
+func newBoardTitleCommand() *cobra.Command {
+	env := execenv.NewEnv()
+
+	cmd := &cobra.Command{
+		Use:     "title [BOARD_ID]",
+		Short:   "Display the title of a board",
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBoardTitle(env, args)
+		}),
+		ValidArgsFunction: BoardCompletion(env),
+	}
+
+	cmd.AddCommand(newBoardTitleEditCommand())
+
+	return cmd
+}
+
+func runBoardTitle(env *execenv.Env, args []string) error {
+	b, args, err := ResolveSelected(env.Backend, args)
+	if err != nil {
+		return err
+	}
+
+	snap := b.Snapshot()
+
+	env.Out.Println(snap.Title)
+
+	return nil
+}

commands/board/board_title_edit.go 🔗

@@ -0,0 +1,70 @@
+package boardcmd
+
+import (
+	"github.com/spf13/cobra"
+
+	"github.com/git-bug/git-bug/commands/execenv"
+	"github.com/git-bug/git-bug/commands/input"
+	"github.com/git-bug/git-bug/util/text"
+)
+
+type boardTitleEditOptions struct {
+	title          string
+	nonInteractive bool
+}
+
+func newBoardTitleEditCommand() *cobra.Command {
+	env := execenv.NewEnv()
+	options := boardTitleEditOptions{}
+
+	cmd := &cobra.Command{
+		Use:     "edit [BUG_ID]",
+		Short:   "Edit a title of a board",
+		PreRunE: execenv.LoadBackendEnsureUser(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBugTitleEdit(env, options, args)
+		}),
+		ValidArgsFunction: BoardCompletion(env),
+	}
+
+	flags := cmd.Flags()
+	flags.SortFlags = false
+
+	flags.StringVarP(&options.title, "title", "t", "",
+		"Provide a title to describe the board",
+	)
+	flags.BoolVar(&options.nonInteractive, "non-interactive", false, "Do not ask for user input")
+
+	return cmd
+}
+
+func runBugTitleEdit(env *execenv.Env, opts boardTitleEditOptions, args []string) error {
+	b, args, err := ResolveSelected(env.Backend, args)
+	if err != nil {
+		return err
+	}
+
+	snap := b.Snapshot()
+
+	if opts.title == "" {
+		if opts.nonInteractive {
+			env.Err.Println("No title given. Aborting.")
+			return nil
+		}
+		opts.title, err = input.PromptDefault("Board title", "title", snap.Title, input.Required)
+		if err != nil {
+			return err
+		}
+	}
+
+	if opts.title == snap.Title {
+		env.Err.Println("No change, aborting.")
+	}
+
+	_, err = b.SetTitle(text.CleanupOneLine(opts.title))
+	if err != nil {
+		return err
+	}
+
+	return b.Commit()
+}

commands/board/completion.go 🔗

@@ -0,0 +1,56 @@
+package boardcmd
+
+import (
+	"strings"
+
+	"github.com/spf13/cobra"
+
+	"github.com/git-bug/git-bug/commands/completion"
+	"github.com/git-bug/git-bug/commands/execenv"
+)
+
+// BoardCompletion complete a board id
+func BoardCompletion(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()
+		}()
+
+		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 completions, cobra.ShellCompDirectiveNoFileComp
+	}
+}
+
+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 {
+			return completion.HandleError(err)
+		}
+		defer func() {
+			_ = env.Backend.Close()
+		}()
+
+		b, _, err := ResolveSelected(env.Backend, args)
+		if err != nil {
+			return completion.HandleError(err)
+		}
+
+		for _, column := range b.Snapshot().Columns {
+			completions = append(completions, column.Id.Human()+"\t"+column.Name)
+		}
+
+		return completions, cobra.ShellCompDirectiveNoFileComp
+	}
+}

commands/cmdjson/board.go 🔗

@@ -0,0 +1,123 @@
+package cmdjson
+
+import (
+	"github.com/git-bug/git-bug/cache"
+	"github.com/git-bug/git-bug/entities/board"
+)
+
+type BoardSnapshot struct {
+	Id         string `json:"id"`
+	HumanId    string `json:"human_id"`
+	CreateTime Time   `json:"create_time"`
+	EditTime   Time   `json:"edit_time"`
+
+	Title        string        `json:"title"`
+	Description  string        `json:"description"`
+	Participants []Identity    `json:"participants"`
+	Columns      []BoardColumn `json:"columns"`
+}
+
+func NewBoardSnapshot(snapshot *board.Snapshot) BoardSnapshot {
+	jsonBoard := BoardSnapshot{
+		Id:          snapshot.Id().String(),
+		HumanId:     snapshot.Id().Human(),
+		CreateTime:  NewTime(snapshot.CreateTime, 0),
+		EditTime:    NewTime(snapshot.EditTime(), 0),
+		Title:       snapshot.Title,
+		Description: snapshot.Description,
+	}
+
+	jsonBoard.Participants = make([]Identity, len(snapshot.Participants))
+	for i, element := range snapshot.Participants {
+		jsonBoard.Participants[i] = NewIdentity(element)
+	}
+
+	jsonBoard.Columns = make([]BoardColumn, len(snapshot.Columns))
+	for i, column := range snapshot.Columns {
+		jsonBoard.Columns[i] = NewBoardColumn(column)
+	}
+
+	return jsonBoard
+}
+
+type BoardColumn struct {
+	Id      string `json:"id"`
+	HumanId string `json:"human_id"`
+	Name    string `json:"name"`
+	Items   []any  `json:"items"`
+}
+
+func NewBoardColumn(column *board.Column) BoardColumn {
+	jsonColumn := BoardColumn{
+		Id:      column.Id.String(),
+		HumanId: column.Id.Human(),
+		Name:    column.Name,
+	}
+	jsonColumn.Items = make([]any, len(column.Items))
+	for j, item := range column.Items {
+		switch item := item.(type) {
+		case *board.Draft:
+			jsonColumn.Items[j] = NewBoardDraftItem(item)
+		case *board.BugItem:
+			jsonColumn.Items[j] = NewBugSnapshot(item.Bug.Compile())
+		default:
+			panic("unknown item type")
+		}
+	}
+	return jsonColumn
+}
+
+type BoardDraftItem struct {
+	Id      string   `json:"id"`
+	HumanId string   `json:"human_id"`
+	Author  Identity `json:"author"`
+	Title   string   `json:"title"`
+	Message string   `json:"message"`
+}
+
+func NewBoardDraftItem(item *board.Draft) BoardDraftItem {
+	return BoardDraftItem{
+		Id:      item.CombinedId().String(),
+		HumanId: item.CombinedId().Human(),
+		Author:  NewIdentity(item.Author),
+		Title:   item.Title,
+		Message: item.Message,
+	}
+}
+
+type BoardExcerpt struct {
+	Id         string `json:"id"`
+	HumanId    string `json:"human_id"`
+	CreateTime Time   `json:"create_time"`
+	EditTime   Time   `json:"edit_time"`
+
+	Title        string     `json:"title"`
+	Description  string     `json:"description"`
+	Participants []Identity `json:"participants"`
+
+	Items    int               `json:"items"`
+	Metadata map[string]string `json:"metadata"`
+}
+
+func NewBoardExcerpt(backend *cache.RepoCache, b *cache.BoardExcerpt) (BoardExcerpt, error) {
+	jsonBoard := BoardExcerpt{
+		Id:          b.Id().String(),
+		HumanId:     b.Id().Human(),
+		CreateTime:  NewTime(b.CreateTime(), b.CreateLamportTime),
+		EditTime:    NewTime(b.EditTime(), b.EditLamportTime),
+		Title:       b.Title,
+		Description: b.Description,
+		Items:       b.ItemCount,
+		Metadata:    b.CreateMetadata,
+	}
+
+	jsonBoard.Participants = make([]Identity, len(b.Participants))
+	for i, element := range b.Participants {
+		participant, err := backend.Identities().ResolveExcerpt(element)
+		if err != nil {
+			return BoardExcerpt{}, err
+		}
+		jsonBoard.Participants[i] = NewIdentityFromExcerpt(participant)
+	}
+	return jsonBoard, nil
+}

commands/root.go 🔗

@@ -5,8 +5,9 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"github.com/git-bug/git-bug/commands/bridge"
-	"github.com/git-bug/git-bug/commands/bug"
+	boardcmd "github.com/git-bug/git-bug/commands/board"
+	bridgecmd "github.com/git-bug/git-bug/commands/bridge"
+	bugcmd "github.com/git-bug/git-bug/commands/bug"
 	"github.com/git-bug/git-bug/commands/execenv"
 	"github.com/git-bug/git-bug/commands/user"
 )
@@ -56,6 +57,7 @@ the same git remote you are already using to collaborate with other people.
 
 	env := execenv.NewEnv()
 
+	addCmdWithGroup(boardcmd.NewBoardCommand(), entityGroup)
 	addCmdWithGroup(bugcmd.NewBugCommand(env), entityGroup)
 	addCmdWithGroup(usercmd.NewUserCommand(env), entityGroup)
 	addCmdWithGroup(newLabelCommand(env), entityGroup)

doc/man/git-bug-board-add-draft.1 🔗

@@ -0,0 +1,42 @@
+.nh
+.TH "GIT-BUG" "1" "Apr 2019" "Generated from git-bug's source code" ""
+
+.SH NAME
+git-bug-board-add-draft - Add a draft item to a board
+
+
+.SH SYNOPSIS
+\fBgit-bug board add-draft [BOARD_ID] [flags]\fP
+
+
+.SH DESCRIPTION
+Add a draft item to a board
+
+
+.SH OPTIONS
+\fB-t\fP, \fB--title\fP=""
+	Provide the title to describe the draft item
+
+.PP
+\fB-m\fP, \fB--message\fP=""
+	Provide the message of the draft item
+
+.PP
+\fB-F\fP, \fB--file\fP=""
+	Take the message from the given file. Use - to read the message from the standard input
+
+.PP
+\fB-c\fP, \fB--column\fP="1"
+	The column to add to. Either a column Id or prefix, or the column number starting from 1.
+
+.PP
+\fB--non-interactive\fP[=false]
+	Do not ask for user input
+
+.PP
+\fB-h\fP, \fB--help\fP[=false]
+	help for add-draft
+
+
+.SH SEE ALSO
+\fBgit-bug-board(1)\fP

doc/man/git-bug-board-description-edit.1 🔗

@@ -0,0 +1,30 @@
+.nh
+.TH "GIT-BUG" "1" "Apr 2019" "Generated from git-bug's source code" ""
+
+.SH NAME
+git-bug-board-description-edit - Edit a description of a board
+
+
+.SH SYNOPSIS
+\fBgit-bug board description edit [BUG_ID] [flags]\fP
+
+
+.SH DESCRIPTION
+Edit a description of a board
+
+
+.SH OPTIONS
+\fB-t\fP, \fB--description\fP=""
+	Provide a description for the board
+
+.PP
+\fB--non-interactive\fP[=false]
+	Do not ask for user input
+
+.PP
+\fB-h\fP, \fB--help\fP[=false]
+	help for edit
+
+
+.SH SEE ALSO
+\fBgit-bug-board-description(1)\fP

doc/man/git-bug-board-description.1 🔗

@@ -0,0 +1,22 @@
+.nh
+.TH "GIT-BUG" "1" "Apr 2019" "Generated from git-bug's source code" ""
+
+.SH NAME
+git-bug-board-description - Display the description of a board
+
+
+.SH SYNOPSIS
+\fBgit-bug board description [BOARD_ID] [flags]\fP
+
+
+.SH DESCRIPTION
+Display the description of a board
+
+
+.SH OPTIONS
+\fB-h\fP, \fB--help\fP[=false]
+	help for description
+
+
+.SH SEE ALSO
+\fBgit-bug-board(1)\fP, \fBgit-bug-board-description-edit(1)\fP

doc/man/git-bug-board-deselect.1 🔗

@@ -0,0 +1,22 @@
+.nh
+.TH "GIT-BUG" "1" "Apr 2019" "Generated from git-bug's source code" ""
+
+.SH NAME
+git-bug-board-deselect - Clear the implicitly selected board
+
+
+.SH SYNOPSIS
+\fBgit-bug board deselect [flags]\fP
+
+
+.SH DESCRIPTION
+Clear the implicitly selected board
+
+
+.SH OPTIONS
+\fB-h\fP, \fB--help\fP[=false]
+	help for deselect
+
+
+.SH SEE ALSO
+\fBgit-bug-board(1)\fP

doc/man/git-bug-board-new.1 🔗

@@ -0,0 +1,38 @@
+.nh
+.TH "GIT-BUG" "1" "Apr 2019" "Generated from git-bug's source code" ""
+
+.SH NAME
+git-bug-board-new - Create a new board
+
+
+.SH SYNOPSIS
+\fBgit-bug board new [flags]\fP
+
+
+.SH DESCRIPTION
+Create a new board
+
+
+.SH OPTIONS
+\fB-t\fP, \fB--title\fP=""
+	Provide a title to describe the issue
+
+.PP
+\fB-d\fP, \fB--description\fP=""
+	Provide a message to describe the board
+
+.PP
+\fB-c\fP, \fB--columns\fP=[To Do,In Progress,Done]
+	Define the columns of the board (default to To Do,In Progress,Done)
+
+.PP
+\fB--non-interactive\fP[=false]
+	Do not ask for user input
+
+.PP
+\fB-h\fP, \fB--help\fP[=false]
+	help for new
+
+
+.SH SEE ALSO
+\fBgit-bug-board(1)\fP

doc/man/git-bug-board-rm.1 🔗

@@ -0,0 +1,22 @@
+.nh
+.TH "GIT-BUG" "1" "Apr 2019" "Generated from git-bug's source code" ""
+
+.SH NAME
+git-bug-board-rm - Remove an existing board
+
+
+.SH SYNOPSIS
+\fBgit-bug board rm BOARD_ID [flags]\fP
+
+
+.SH DESCRIPTION
+Remove an existing board in the local repository.
+
+
+.SH OPTIONS
+\fB-h\fP, \fB--help\fP[=false]
+	help for rm
+
+
+.SH SEE ALSO
+\fBgit-bug-board(1)\fP

doc/man/git-bug-board-select.1 🔗

@@ -0,0 +1,25 @@
+.nh
+.TH "GIT-BUG" "1" "Apr 2019" "Generated from git-bug's source code" ""
+
+.SH NAME
+git-bug-board-select - Select a board for implicit use in future commands
+
+
+.SH SYNOPSIS
+\fBgit-bug board select BOARD_ID [flags]\fP
+
+
+.SH DESCRIPTION
+Select a board for implicit use in future commands.
+
+.PP
+The complementary command is "git board deselect" performing the opposite operation.
+
+
+.SH OPTIONS
+\fB-h\fP, \fB--help\fP[=false]
+	help for select
+
+
+.SH SEE ALSO
+\fBgit-bug-board(1)\fP

doc/man/git-bug-board-show.1 🔗

@@ -0,0 +1,26 @@
+.nh
+.TH "GIT-BUG" "1" "Apr 2019" "Generated from git-bug's source code" ""
+
+.SH NAME
+git-bug-board-show - Display a board
+
+
+.SH SYNOPSIS
+\fBgit-bug board show [BOARD_ID] [flags]\fP
+
+
+.SH DESCRIPTION
+Display a board
+
+
+.SH OPTIONS
+\fB-f\fP, \fB--format\fP="default"
+	Select the output formatting style. Valid values are [default,json,org-mode]
+
+.PP
+\fB-h\fP, \fB--help\fP[=false]
+	help for show
+
+
+.SH SEE ALSO
+\fBgit-bug-board(1)\fP

doc/man/git-bug-board-title-edit.1 🔗

@@ -0,0 +1,30 @@
+.nh
+.TH "GIT-BUG" "1" "Apr 2019" "Generated from git-bug's source code" ""
+
+.SH NAME
+git-bug-board-title-edit - Edit a title of a board
+
+
+.SH SYNOPSIS
+\fBgit-bug board title edit [BUG_ID] [flags]\fP
+
+
+.SH DESCRIPTION
+Edit a title of a board
+
+
+.SH OPTIONS
+\fB-t\fP, \fB--title\fP=""
+	Provide a title to describe the board
+
+.PP
+\fB--non-interactive\fP[=false]
+	Do not ask for user input
+
+.PP
+\fB-h\fP, \fB--help\fP[=false]
+	help for edit
+
+
+.SH SEE ALSO
+\fBgit-bug-board-title(1)\fP

doc/man/git-bug-board-title.1 🔗

@@ -0,0 +1,22 @@
+.nh
+.TH "GIT-BUG" "1" "Apr 2019" "Generated from git-bug's source code" ""
+
+.SH NAME
+git-bug-board-title - Display the title of a board
+
+
+.SH SYNOPSIS
+\fBgit-bug board title [BOARD_ID] [flags]\fP
+
+
+.SH DESCRIPTION
+Display the title of a board
+
+
+.SH OPTIONS
+\fB-h\fP, \fB--help\fP[=false]
+	help for title
+
+
+.SH SEE ALSO
+\fBgit-bug-board(1)\fP, \fBgit-bug-board-title-edit(1)\fP

doc/man/git-bug-board.1 🔗

@@ -0,0 +1,38 @@
+.nh
+.TH "GIT-BUG" "1" "Apr 2019" "Generated from git-bug's source code" ""
+
+.SH NAME
+git-bug-board - List boards
+
+
+.SH SYNOPSIS
+\fBgit-bug board [flags]\fP
+
+
+.SH DESCRIPTION
+List boards
+
+
+.SH OPTIONS
+\fB-m\fP, \fB--metadata\fP=[]
+	Filter by metadata. Example: github-url=URL
+
+.PP
+\fB-A\fP, \fB--actor\fP=[]
+	Filter by actor
+
+.PP
+\fB-t\fP, \fB--title\fP=[]
+	Filter by title
+
+.PP
+\fB-f\fP, \fB--format\fP="default"
+	Select the output formatting style. Valid values are [default,plain,compact,id,json,org-mode]
+
+.PP
+\fB-h\fP, \fB--help\fP[=false]
+	help for board
+
+
+.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

doc/man/git-bug.1 🔗

@@ -24,4 +24,4 @@ the same git remote you are already using to collaborate with other people.
 
 
 .SH SEE ALSO
-\fBgit-bug-bridge(1)\fP, \fBgit-bug-bug(1)\fP, \fBgit-bug-label(1)\fP, \fBgit-bug-pull(1)\fP, \fBgit-bug-push(1)\fP, \fBgit-bug-termui(1)\fP, \fBgit-bug-user(1)\fP, \fBgit-bug-version(1)\fP, \fBgit-bug-webui(1)\fP, \fBgit-bug-wipe(1)\fP
+\fBgit-bug-board(1)\fP, \fBgit-bug-bridge(1)\fP, \fBgit-bug-bug(1)\fP, \fBgit-bug-label(1)\fP, \fBgit-bug-pull(1)\fP, \fBgit-bug-push(1)\fP, \fBgit-bug-termui(1)\fP, \fBgit-bug-user(1)\fP, \fBgit-bug-version(1)\fP, \fBgit-bug-webui(1)\fP, \fBgit-bug-wipe(1)\fP

doc/md/git-bug.md 🔗

@@ -24,6 +24,7 @@ git-bug [flags]
 
 ### SEE ALSO
 
+* [git-bug board](git-bug_board.md)	 - List boards
 * [git-bug bridge](git-bug_bridge.md)	 - List bridges to other bug trackers
 * [git-bug bug](git-bug_bug.md)	 - List bugs
 * [git-bug label](git-bug_label.md)	 - List valid labels

doc/md/git-bug_board.md 🔗

@@ -0,0 +1,30 @@
+## git-bug board
+
+List boards
+
+```
+git-bug board [flags]
+```
+
+### Options
+
+```
+  -m, --metadata strings   Filter by metadata. Example: github-url=URL
+  -A, --actor strings      Filter by actor
+  -t, --title strings      Filter by title
+  -f, --format string      Select the output formatting style. Valid values are [default,plain,compact,id,json,org-mode] (default "default")
+  -h, --help               help for board
+```
+
+### SEE ALSO
+
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
+* [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
+* [git-bug board new](git-bug_board_new.md)	 - Create a new board
+* [git-bug board rm](git-bug_board_rm.md)	 - Remove an existing board
+* [git-bug board select](git-bug_board_select.md)	 - Select a board for implicit use in future commands
+* [git-bug board show](git-bug_board_show.md)	 - Display a board
+* [git-bug board title](git-bug_board_title.md)	 - Display the title of a board
+

doc/md/git-bug_board_add-draft.md 🔗

@@ -0,0 +1,23 @@
+## git-bug board add-draft
+
+Add a draft item to a board
+
+```
+git-bug board add-draft [BOARD_ID] [flags]
+```
+
+### Options
+
+```
+  -t, --title string      Provide the title to describe the draft item
+  -m, --message string    Provide the message of the draft item
+  -F, --file string       Take the message from the given file. Use - to read the message from the standard input
+  -c, --column string     The column to add to. Either a column Id or prefix, or the column number starting from 1. (default "1")
+      --non-interactive   Do not ask for user input
+  -h, --help              help for add-draft
+```
+
+### SEE ALSO
+
+* [git-bug board](git-bug_board.md)	 - List boards
+

doc/md/git-bug_board_description.md 🔗

@@ -0,0 +1,19 @@
+## git-bug board description
+
+Display the description of a board
+
+```
+git-bug board description [BOARD_ID] [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for description
+```
+
+### SEE ALSO
+
+* [git-bug board](git-bug_board.md)	 - List boards
+* [git-bug board description edit](git-bug_board_description_edit.md)	 - Edit a description of a board
+

doc/md/git-bug_board_description_edit.md 🔗

@@ -0,0 +1,20 @@
+## git-bug board description edit
+
+Edit a description of a board
+
+```
+git-bug board description edit [BUG_ID] [flags]
+```
+
+### Options
+
+```
+  -t, --description string   Provide a description for the board
+      --non-interactive      Do not ask for user input
+  -h, --help                 help for edit
+```
+
+### SEE ALSO
+
+* [git-bug board description](git-bug_board_description.md)	 - Display the description of a board
+

doc/md/git-bug_board_deselect.md 🔗

@@ -0,0 +1,18 @@
+## git-bug board deselect
+
+Clear the implicitly selected board
+
+```
+git-bug board deselect [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for deselect
+```
+
+### SEE ALSO
+
+* [git-bug board](git-bug_board.md)	 - List boards
+

doc/md/git-bug_board_new.md 🔗

@@ -0,0 +1,22 @@
+## git-bug board new
+
+Create a new board
+
+```
+git-bug board new [flags]
+```
+
+### Options
+
+```
+  -t, --title string          Provide a title to describe the issue
+  -d, --description string    Provide a message to describe the board
+  -c, --columns stringArray   Define the columns of the board (default to To Do,In Progress,Done) (default [To Do,In Progress,Done])
+      --non-interactive       Do not ask for user input
+  -h, --help                  help for new
+```
+
+### SEE ALSO
+
+* [git-bug board](git-bug_board.md)	 - List boards
+

doc/md/git-bug_board_rm.md 🔗

@@ -0,0 +1,22 @@
+## git-bug board rm
+
+Remove an existing board
+
+### Synopsis
+
+Remove an existing board in the local repository.
+
+```
+git-bug board rm BOARD_ID [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for rm
+```
+
+### SEE ALSO
+
+* [git-bug board](git-bug_board.md)	 - List boards
+

doc/md/git-bug_board_select.md 🔗

@@ -0,0 +1,25 @@
+## git-bug board select
+
+Select a board for implicit use in future commands
+
+### Synopsis
+
+Select a board for implicit use in future commands.
+
+The complementary command is "git board deselect" performing the opposite operation.
+
+
+```
+git-bug board select BOARD_ID [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for select
+```
+
+### SEE ALSO
+
+* [git-bug board](git-bug_board.md)	 - List boards
+

doc/md/git-bug_board_show.md 🔗

@@ -0,0 +1,19 @@
+## git-bug board show
+
+Display a board
+
+```
+git-bug board show [BOARD_ID] [flags]
+```
+
+### Options
+
+```
+  -f, --format string   Select the output formatting style. Valid values are [default,json,org-mode] (default "default")
+  -h, --help            help for show
+```
+
+### SEE ALSO
+
+* [git-bug board](git-bug_board.md)	 - List boards
+

doc/md/git-bug_board_title.md 🔗

@@ -0,0 +1,19 @@
+## git-bug board title
+
+Display the title of a board
+
+```
+git-bug board title [BOARD_ID] [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for title
+```
+
+### SEE ALSO
+
+* [git-bug board](git-bug_board.md)	 - List boards
+* [git-bug board title edit](git-bug_board_title_edit.md)	 - Edit a title of a board
+

doc/md/git-bug_board_title_edit.md 🔗

@@ -0,0 +1,20 @@
+## git-bug board title edit
+
+Edit a title of a board
+
+```
+git-bug board title edit [BUG_ID] [flags]
+```
+
+### Options
+
+```
+  -t, --title string      Provide a title to describe the board
+      --non-interactive   Do not ask for user input
+  -h, --help              help for edit
+```
+
+### SEE ALSO
+
+* [git-bug board title](git-bug_board_title.md)	 - Display the title of a board
+

entities/board/board.go 🔗

@@ -3,12 +3,12 @@ package board
 import (
 	"fmt"
 
-	"github.com/MichaelMure/git-bug/entities/bug"
-	"github.com/MichaelMure/git-bug/entities/identity"
+	"github.com/git-bug/git-bug/entities/bug"
+	"github.com/git-bug/git-bug/entities/identity"
 
-	"github.com/MichaelMure/git-bug/entity"
-	"github.com/MichaelMure/git-bug/entity/dag"
-	"github.com/MichaelMure/git-bug/repository"
+	"github.com/git-bug/git-bug/entity"
+	"github.com/git-bug/git-bug/entity/dag"
+	"github.com/git-bug/git-bug/repository"
 )
 
 var _ Interface = &Board{}

entities/board/board_actions.go 🔗

@@ -1,10 +1,10 @@
 package board
 
 import (
-	"github.com/MichaelMure/git-bug/entities/identity"
-	"github.com/MichaelMure/git-bug/entity"
-	"github.com/MichaelMure/git-bug/entity/dag"
-	"github.com/MichaelMure/git-bug/repository"
+	"github.com/git-bug/git-bug/entities/identity"
+	"github.com/git-bug/git-bug/entity"
+	"github.com/git-bug/git-bug/entity/dag"
+	"github.com/git-bug/git-bug/repository"
 )
 
 // Fetch retrieve updates from a remote

entities/board/item_draft.go 🔗

@@ -1,12 +1,12 @@
 package board
 
 import (
-	"github.com/MichaelMure/git-bug/entities/identity"
-	"github.com/MichaelMure/git-bug/entity"
 	"github.com/dustin/go-humanize"
 
-	"github.com/MichaelMure/git-bug/entities/common"
-	"github.com/MichaelMure/git-bug/util/timestamp"
+	"github.com/git-bug/git-bug/entities/identity"
+	"github.com/git-bug/git-bug/entity"
+
+	"github.com/git-bug/git-bug/util/timestamp"
 )
 
 var _ Item = &Draft{}
@@ -16,10 +16,9 @@ type Draft struct {
 	// of the Operation that created the Draft
 	combinedId entity.CombinedId
 
-	author  identity.Interface
-	status  common.Status
-	title   string
-	message string
+	Author  identity.Interface
+	Title   string
+	Message string
 
 	// Creation time of the comment.
 	// Should be used only for human display, never for ordering as we can't rely on it in a distributed system.
@@ -34,11 +33,6 @@ func (d *Draft) CombinedId() entity.CombinedId {
 	return d.combinedId
 }
 
-func (d *Draft) Status() common.Status {
-	// TODO implement me
-	panic("implement me")
-}
-
 // FormatTimeRel format the UnixTime of the comment for human consumption
 func (d *Draft) FormatTimeRel() string {
 	return humanize.Time(d.unixTime.Time())

entities/board/item_entity.go 🔗

@@ -1,15 +1,15 @@
 package board
 
 import (
-	"github.com/MichaelMure/git-bug/entities/bug"
-	"github.com/MichaelMure/git-bug/entity"
+	"github.com/git-bug/git-bug/entities/bug"
+	"github.com/git-bug/git-bug/entity"
 )
 
 var _ Item = &BugItem{}
 
 type BugItem struct {
 	combinedId entity.CombinedId
-	bug        bug.Interface
+	Bug        bug.Interface
 }
 
 func (e *BugItem) CombinedId() entity.CombinedId {

entities/board/op_add_item_draft.go 🔗

@@ -3,13 +3,12 @@ package board
 import (
 	"fmt"
 
-	"github.com/MichaelMure/git-bug/entities/common"
-	"github.com/MichaelMure/git-bug/entities/identity"
-	"github.com/MichaelMure/git-bug/entity"
-	"github.com/MichaelMure/git-bug/entity/dag"
-	"github.com/MichaelMure/git-bug/repository"
-	"github.com/MichaelMure/git-bug/util/text"
-	"github.com/MichaelMure/git-bug/util/timestamp"
+	"github.com/git-bug/git-bug/entities/identity"
+	"github.com/git-bug/git-bug/entity"
+	"github.com/git-bug/git-bug/entity/dag"
+	"github.com/git-bug/git-bug/repository"
+	"github.com/git-bug/git-bug/util/text"
+	"github.com/git-bug/git-bug/util/timestamp"
 )
 
 var _ Operation = &AddItemDraftOperation{}
@@ -60,16 +59,15 @@ func (op *AddItemDraftOperation) Validate() error {
 }
 
 func (op *AddItemDraftOperation) Apply(snapshot *Snapshot) {
-	snapshot.addActor(op.Author())
+	snapshot.addParticipant(op.Author())
 
 	for _, column := range snapshot.Columns {
 		if column.Id == op.ColumnId {
 			column.Items = append(column.Items, &Draft{
 				combinedId: entity.CombineIds(snapshot.id, op.Id()),
-				author:     op.Author(),
-				status:     common.OpenStatus,
-				title:      op.Title,
-				message:    op.Message,
+				Author:     op.Author(),
+				Title:      op.Title,
+				Message:    op.Message,
 				unixTime:   timestamp.Timestamp(op.UnixTime),
 			})
 			return

entities/board/op_add_item_draft_test.go 🔗

@@ -3,10 +3,10 @@ package board
 import (
 	"testing"
 
-	"github.com/MichaelMure/git-bug/entities/identity"
-	"github.com/MichaelMure/git-bug/entity"
-	"github.com/MichaelMure/git-bug/entity/dag"
-	"github.com/MichaelMure/git-bug/repository"
+	"github.com/git-bug/git-bug/entities/identity"
+	"github.com/git-bug/git-bug/entity"
+	"github.com/git-bug/git-bug/entity/dag"
+	"github.com/git-bug/git-bug/repository"
 )
 
 func TestAddItemDraftOpSerialize(t *testing.T) {

entities/board/op_add_item_entity.go 🔗

@@ -3,10 +3,10 @@ package board
 import (
 	"fmt"
 
-	"github.com/MichaelMure/git-bug/entities/bug"
-	"github.com/MichaelMure/git-bug/entities/identity"
-	"github.com/MichaelMure/git-bug/entity"
-	"github.com/MichaelMure/git-bug/entity/dag"
+	"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"
 )
 
 // itemEntityType indicate the type of entity board item
@@ -57,7 +57,7 @@ func (op *AddItemEntityOperation) Apply(snapshot *Snapshot) {
 		return
 	}
 
-	snapshot.addActor(op.Author())
+	snapshot.addParticipant(op.Author())
 
 	for _, column := range snapshot.Columns {
 		if column.Id == op.ColumnId {
@@ -65,7 +65,7 @@ func (op *AddItemEntityOperation) Apply(snapshot *Snapshot) {
 			case bug.Interface:
 				column.Items = append(column.Items, &BugItem{
 					combinedId: entity.CombineIds(snapshot.Id(), e.Id()),
-					bug:        e,
+					Bug:        e,
 				})
 			}
 			return

entities/board/op_add_item_entity_test.go 🔗

@@ -5,10 +5,10 @@ import (
 
 	"github.com/stretchr/testify/require"
 
-	"github.com/MichaelMure/git-bug/entities/bug"
-	"github.com/MichaelMure/git-bug/entities/identity"
-	"github.com/MichaelMure/git-bug/entity"
-	"github.com/MichaelMure/git-bug/entity/dag"
+	"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"
 )
 
 func TestAddItemEntityOpSerialize(t *testing.T) {

entities/board/op_create.go 🔗

@@ -3,11 +3,11 @@ package board
 import (
 	"fmt"
 
-	"github.com/MichaelMure/git-bug/entities/identity"
+	"github.com/git-bug/git-bug/entities/identity"
 
-	"github.com/MichaelMure/git-bug/entity"
-	"github.com/MichaelMure/git-bug/entity/dag"
-	"github.com/MichaelMure/git-bug/util/text"
+	"github.com/git-bug/git-bug/entity"
+	"github.com/git-bug/git-bug/entity/dag"
+	"github.com/git-bug/git-bug/util/text"
 )
 
 var DefaultColumns = []string{"To Do", "In Progress", "Done"}
@@ -96,7 +96,7 @@ func (op *CreateOperation) Apply(snap *Snapshot) {
 		})
 	}
 
-	snap.addActor(op.Author())
+	snap.addParticipant(op.Author())
 }
 
 // CreateDefaultColumns is a convenience function to create a board with the default columns

entities/board/op_create_test.go 🔗

@@ -6,10 +6,10 @@ import (
 
 	"github.com/stretchr/testify/require"
 
-	"github.com/MichaelMure/git-bug/entities/identity"
-	"github.com/MichaelMure/git-bug/entity"
-	"github.com/MichaelMure/git-bug/entity/dag"
-	"github.com/MichaelMure/git-bug/repository"
+	"github.com/git-bug/git-bug/entities/identity"
+	"github.com/git-bug/git-bug/entity"
+	"github.com/git-bug/git-bug/entity/dag"
+	"github.com/git-bug/git-bug/repository"
 )
 
 func TestCreate(t *testing.T) {

entities/board/op_set_description.go 🔗

@@ -3,10 +3,10 @@ package board
 import (
 	"fmt"
 
-	"github.com/MichaelMure/git-bug/entities/identity"
-	"github.com/MichaelMure/git-bug/entity"
-	"github.com/MichaelMure/git-bug/entity/dag"
-	"github.com/MichaelMure/git-bug/util/text"
+	"github.com/git-bug/git-bug/entities/identity"
+	"github.com/git-bug/git-bug/entity"
+	"github.com/git-bug/git-bug/entity/dag"
+	"github.com/git-bug/git-bug/util/text"
 )
 
 var _ Operation = &SetDescriptionOperation{}
@@ -44,7 +44,7 @@ func (op *SetDescriptionOperation) Validate() error {
 
 func (op *SetDescriptionOperation) Apply(snapshot *Snapshot) {
 	snapshot.Description = op.Description
-	snapshot.addActor(op.Author())
+	snapshot.addParticipant(op.Author())
 }
 
 func NewSetDescriptionOp(author identity.Interface, unixTime int64, description string, was string) *SetDescriptionOperation {

entities/board/op_set_description_test.go 🔗

@@ -3,9 +3,9 @@ package board
 import (
 	"testing"
 
-	"github.com/MichaelMure/git-bug/entities/identity"
-	"github.com/MichaelMure/git-bug/entity"
-	"github.com/MichaelMure/git-bug/entity/dag"
+	"github.com/git-bug/git-bug/entities/identity"
+	"github.com/git-bug/git-bug/entity"
+	"github.com/git-bug/git-bug/entity/dag"
 )
 
 func TestSetDescriptionSerialize(t *testing.T) {

entities/board/op_set_title.go 🔗

@@ -3,10 +3,10 @@ package board
 import (
 	"fmt"
 
-	"github.com/MichaelMure/git-bug/entities/identity"
-	"github.com/MichaelMure/git-bug/entity"
-	"github.com/MichaelMure/git-bug/entity/dag"
-	"github.com/MichaelMure/git-bug/util/text"
+	"github.com/git-bug/git-bug/entities/identity"
+	"github.com/git-bug/git-bug/entity"
+	"github.com/git-bug/git-bug/entity/dag"
+	"github.com/git-bug/git-bug/util/text"
 )
 
 var _ Operation = &SetTitleOperation{}
@@ -44,7 +44,7 @@ func (op *SetTitleOperation) Validate() error {
 
 func (op *SetTitleOperation) Apply(snapshot *Snapshot) {
 	snapshot.Title = op.Title
-	snapshot.addActor(op.Author())
+	snapshot.addParticipant(op.Author())
 }
 
 func NewSetTitleOp(author identity.Interface, unixTime int64, title string, was string) *SetTitleOperation {

entities/board/op_set_title_test.go 🔗

@@ -3,9 +3,9 @@ package board
 import (
 	"testing"
 
-	"github.com/MichaelMure/git-bug/entities/identity"
-	"github.com/MichaelMure/git-bug/entity"
-	"github.com/MichaelMure/git-bug/entity/dag"
+	"github.com/git-bug/git-bug/entities/identity"
+	"github.com/git-bug/git-bug/entity"
+	"github.com/git-bug/git-bug/entity/dag"
 )
 
 func TestSetTitleSerialize(t *testing.T) {

entities/board/operation.go 🔗

@@ -4,9 +4,9 @@ import (
 	"encoding/json"
 	"fmt"
 
-	"github.com/MichaelMure/git-bug/entities/bug"
-	"github.com/MichaelMure/git-bug/entity"
-	"github.com/MichaelMure/git-bug/entity/dag"
+	"github.com/git-bug/git-bug/entities/bug"
+	"github.com/git-bug/git-bug/entity"
+	"github.com/git-bug/git-bug/entity/dag"
 )
 
 // OperationType is an operation type identifier

entities/board/snapshot.go 🔗

@@ -3,9 +3,9 @@ package board
 import (
 	"time"
 
-	"github.com/MichaelMure/git-bug/entities/identity"
-	"github.com/MichaelMure/git-bug/entity"
-	"github.com/MichaelMure/git-bug/entity/dag"
+	"github.com/git-bug/git-bug/entities/identity"
+	"github.com/git-bug/git-bug/entity"
+	"github.com/git-bug/git-bug/entity/dag"
 )
 
 type Column struct {
@@ -16,6 +16,7 @@ type Column struct {
 
 type Item interface {
 	CombinedId() entity.CombinedId
+	// TODO: all items have status?
 	// Status() common.Status
 }
 
@@ -24,10 +25,10 @@ var _ dag.Snapshot = &Snapshot{}
 type Snapshot struct {
 	id entity.Id
 
-	Title       string
-	Description string
-	Columns     []*Column
-	Actors      []identity.Interface
+	Title        string
+	Description  string
+	Columns      []*Column
+	Participants []identity.Interface
 
 	CreateTime time.Time
 	Operations []dag.Operation
@@ -59,20 +60,20 @@ func (snap *Snapshot) EditTime() time.Time {
 	return snap.Operations[len(snap.Operations)-1].Time()
 }
 
-// append the operation author to the actors list
-func (snap *Snapshot) addActor(actor identity.Interface) {
-	for _, a := range snap.Actors {
-		if actor.Id() == a.Id() {
+// append the operation author to the participants list
+func (snap *Snapshot) addParticipant(participant identity.Interface) {
+	for _, p := range snap.Participants {
+		if participant.Id() == p.Id() {
 			return
 		}
 	}
 
-	snap.Actors = append(snap.Actors, actor)
+	snap.Participants = append(snap.Participants, participant)
 }
 
-// HasActor return true if the id is a actor
-func (snap *Snapshot) HasActor(id entity.Id) bool {
-	for _, p := range snap.Actors {
+// HasParticipant return true if the id is a participant
+func (snap *Snapshot) HasParticipant(id entity.Id) bool {
+	for _, p := range snap.Participants {
 		if p.Id() == id {
 			return true
 		}
@@ -80,10 +81,10 @@ func (snap *Snapshot) HasActor(id entity.Id) bool {
 	return false
 }
 
-// HasAnyActor return true if one of the ids is a actor
-func (snap *Snapshot) HasAnyActor(ids ...entity.Id) bool {
+// HasAnyParticipant return true if one of the ids is a participant
+func (snap *Snapshot) HasAnyParticipant(ids ...entity.Id) bool {
 	for _, id := range ids {
-		if snap.HasActor(id) {
+		if snap.HasParticipant(id) {
 			return true
 		}
 	}