Merge pull request #870 from MichaelMure/cli-reorg

Michael Muré created

commands: reorg into different packages

Change summary

cache/repo_cache_identity.go                            |   2 
commands/add_test.go                                    |  32 -
commands/bridge.go                                      |  42 --
commands/bridge/bridge.go                               |  43 ++
commands/bridge/bridge_auth.go                          |  20 
commands/bridge/bridge_auth_addtoken.go                 |  26 
commands/bridge/bridge_auth_rm.go                       |  41 ++
commands/bridge/bridge_auth_show.go                     |  60 +++
commands/bridge/bridge_new.go                           |  40 +-
commands/bridge/bridge_pull.go                          |  34 +
commands/bridge/bridge_push.go                          |  28 
commands/bridge/bridge_rm.go                            |  36 ++
commands/bridge_auth_rm.go                              |  39 --
commands/bridge_auth_show.go                            |  58 ---
commands/bridge_rm.go                                   |  34 --
commands/bug/bug.go                                     | 178 ++++++----
commands/bug/bug_comment.go                             |  52 +++
commands/bug/bug_comment_add.go                         |  36 +-
commands/bug/bug_comment_add_test.go                    |  18 +
commands/bug/bug_comment_edit.go                        |  29 
commands/bug/bug_comment_edit_test.go                   |  23 +
commands/bug/bug_comment_test.go                        |  28 
commands/bug/bug_deselect.go                            |  37 ++
commands/bug/bug_label.go                               |  43 ++
commands/bug/bug_label_new.go                           |  47 ++
commands/bug/bug_label_rm.go                            |  47 ++
commands/bug/bug_new.go                                 |  31 
commands/bug/bug_new_test.go                            |  21 +
commands/bug/bug_rm.go                                  |  46 ++
commands/bug/bug_rm_test.go                             |  19 +
commands/bug/bug_select.go                              |  62 +++
commands/bug/bug_show.go                                | 157 ++++----
commands/bug/bug_status.go                              |  41 ++
commands/bug/bug_status_close.go                        |  39 ++
commands/bug/bug_status_open.go                         |  39 ++
commands/bug/bug_test.go                                |  24 
commands/bug/bug_title.go                               |  40 ++
commands/bug/bug_title_edit.go                          |  76 ++++
commands/bug/select/select.go                           |   0 
commands/bug/select/select_test.go                      |   0 
commands/bug/testdata/comment/add-0-golden.txt          |   0 
commands/bug/testdata/comment/add-1-golden.txt          |   0 
commands/bug/testdata/comment/edit-0-golden.txt         |   0 
commands/bug/testdata/comment/edit-1-golden.txt         |   0 
commands/bug/testdata/comment/message-only-0-golden.txt |   0 
commands/bug/testenv/testenv.go                         |  63 +++
commands/cmdjson/json_common.go                         |  25 
commands/cmdtest/golden.go                              |   5 
commands/commands.go                                    |  16 
commands/comment.go                                     |  50 ---
commands/comment_add_test.go                            |  33 --
commands/comment_edit_test.go                           |  21 -
commands/completion/helper_completion.go                | 131 ++++----
commands/deselect.go                                    |  36 --
commands/env_testing.go                                 |  40 --
commands/execenv/env.go                                 |  99 ++++--
commands/execenv/env_testing.go                         |  48 ++
commands/golden_test.go                                 |   5 
commands/input/input.go                                 |   2 
commands/label.go                                       |  35 -
commands/label_add.go                                   |  45 --
commands/label_ls.go                                    |  33 --
commands/label_rm.go                                    |  45 --
commands/ls-id.go                                       |  42 --
commands/ls-labels.go                                   |  29 -
commands/pull.go                                        |  28 
commands/push.go                                        |  19 
commands/rm.go                                          |  43 --
commands/rm_test.go                                     |  17 -
commands/root.go                                        |  54 +-
commands/select.go                                      |  60 ---
commands/status.go                                      |  38 --
commands/status_close.go                                |  35 --
commands/status_open.go                                 |  35 --
commands/termui.go                                      |  13 
commands/title.go                                       |  37 --
commands/title_edit.go                                  |  74 ----
commands/user.go                                        | 110 ------
commands/user/user.go                                   |  89 +++++
commands/user/user_adopt.go                             |  43 ++
commands/user/user_new.go                               |  37 +-
commands/user/user_new_test.go                          |  14 
commands/user/user_show.go                              | 108 ++++++
commands/user_adopt.go                                  |  40 --
commands/user_create_test.go                            |  38 --
commands/user_ls.go                                     |  81 -----
commands/version.go                                     |  20 
commands/webui.go                                       |  33 +
doc/cli-convention.md                                   |  13 
doc/man/git-bug-bridge-auth-rm.1                        |   6 
doc/man/git-bug-bridge-auth-show.1                      |   4 
doc/man/git-bug-bridge-auth.1                           |   4 
doc/man/git-bug-bridge-new.1                            |  12 
doc/man/git-bug-bridge-pull.1                           |   4 
doc/man/git-bug-bridge-push.1                           |   4 
doc/man/git-bug-bridge-rm.1                             |   4 
doc/man/git-bug-bridge.1                                |   6 
doc/man/git-bug-bug-comment-edit.1                      |   8 
doc/man/git-bug-bug-comment-new.1                       |  10 
doc/man/git-bug-bug-comment.1                           |   8 
doc/man/git-bug-bug-deselect.1                          |   8 
doc/man/git-bug-bug-label-new.1                         |  10 
doc/man/git-bug-bug-label-rm.1                          |   8 
doc/man/git-bug-bug-label.1                             |  27 +
doc/man/git-bug-bug-new.1                               |  10 
doc/man/git-bug-bug-rm.1                                |   6 
doc/man/git-bug-bug-select.1                            |   6 
doc/man/git-bug-bug-show.1                              |   8 
doc/man/git-bug-bug-status-close.1                      |   8 
doc/man/git-bug-bug-status-open.1                       |   8 
doc/man/git-bug-bug-status.1                            |   8 
doc/man/git-bug-bug-title-edit.1                        |   8 
doc/man/git-bug-bug-title.1                             |   8 
doc/man/git-bug-bug.1                                   |  16 
doc/man/git-bug-label-ls.1                              |  30 -
doc/man/git-bug-label.1                                 |  11 
doc/man/git-bug-pull.1                                  |   4 
doc/man/git-bug-push.1                                  |   4 
doc/man/git-bug-termui.1                                |   4 
doc/man/git-bug-user-adopt.1                            |   6 
doc/man/git-bug-user-ls.1                               |  31 -
doc/man/git-bug-user-new.1                              |   8 
doc/man/git-bug-user-user.1                             |  31 +
doc/man/git-bug-user.1                                  |  12 
doc/man/git-bug-version.1                               |   4 
doc/man/git-bug-webui.1                                 |   2 
doc/man/git-bug.1                                       |   4 
doc/md/git-bug.md                                       |  28 -
doc/md/git-bug_bridge.md                                |  14 
doc/md/git-bug_bridge_auth.md                           |   8 
doc/md/git-bug_bridge_auth_add-token.md                 |   2 
doc/md/git-bug_bridge_auth_rm.md                        |   6 
doc/md/git-bug_bridge_auth_show.md                      |   4 
doc/md/git-bug_bridge_new.md                            |  16 
doc/md/git-bug_bridge_pull.md                           |   4 
doc/md/git-bug_bridge_push.md                           |   4 
doc/md/git-bug_bridge_rm.md                             |   4 
doc/md/git-bug_bug.md                                   |  27 +
doc/md/git-bug_bug_comment.md                           |  20 +
doc/md/git-bug_bug_comment_edit.md                      |   8 
doc/md/git-bug_bug_comment_new.md                       |  10 
doc/md/git-bug_bug_deselect.md                          |   8 
doc/md/git-bug_bug_label.md                             |  20 +
doc/md/git-bug_bug_label_new.md                         |  18 +
doc/md/git-bug_bug_label_rm.md                          |  18 +
doc/md/git-bug_bug_new.md                               |  10 
doc/md/git-bug_bug_rm.md                                |   8 
doc/md/git-bug_bug_select.md                            |   8 
doc/md/git-bug_bug_show.md                              |   8 
doc/md/git-bug_bug_status.md                            |  20 +
doc/md/git-bug_bug_status_close.md                      |  18 +
doc/md/git-bug_bug_status_open.md                       |  18 +
doc/md/git-bug_bug_title.md                             |  19 +
doc/md/git-bug_bug_title_edit.md                        |   8 
doc/md/git-bug_commands.md                              |   2 
doc/md/git-bug_comment.md                               |  20 -
doc/md/git-bug_label.md                                 |  15 
doc/md/git-bug_label_add.md                             |  18 -
doc/md/git-bug_label_ls.md                              |  24 -
doc/md/git-bug_label_rm.md                              |  18 -
doc/md/git-bug_pull.md                                  |   4 
doc/md/git-bug_push.md                                  |   4 
doc/md/git-bug_status.md                                |  20 -
doc/md/git-bug_status_close.md                          |  18 -
doc/md/git-bug_status_open.md                           |  18 -
doc/md/git-bug_termui.md                                |   4 
doc/md/git-bug_title.md                                 |  19 -
doc/md/git-bug_user.md                                  |  16 
doc/md/git-bug_user_adopt.md                            |   6 
doc/md/git-bug_user_ls.md                               |  19 -
doc/md/git-bug_user_new.md                              |  10 
doc/md/git-bug_user_user.md                             |  19 +
doc/md/git-bug_version.md                               |   4 
doc/md/git-bug_webui.md                                 |   4 
entities/identity/identity.go                           |   2 
go.mod                                                  |   5 
go.sum                                                  |   7 
misc/completion/bash/git-bug                            |   2 
misc/completion/powershell/git-bug                      |   4 
repository/config.go                                    |   8 
180 files changed, 2,321 insertions(+), 2,215 deletions(-)

Detailed changes

cache/repo_cache_identity.go 🔗

@@ -147,7 +147,7 @@ func (c *RepoCache) ResolveIdentityPrefix(prefix string) (*IdentityCache, error)
 }
 
 // ResolveIdentityImmutableMetadata retrieve an Identity that has the exact given metadata on
-// one of it's version. If multiple version have the same key, the first defined take precedence.
+// one of its version. If multiple version have the same key, the first defined take precedence.
 func (c *RepoCache) ResolveIdentityImmutableMetadata(key string, value string) (*IdentityCache, error) {
 	return c.ResolveIdentityMatcher(func(excerpt *IdentityExcerpt) bool {
 		return excerpt.ImmutableMetadata[key] == value

commands/add_test.go 🔗

@@ -1,32 +0,0 @@
-package commands
-
-import (
-	"strings"
-	"testing"
-
-	"github.com/stretchr/testify/require"
-)
-
-func newTestEnvAndBug(t *testing.T) (*testEnv, string) {
-	t.Helper()
-
-	testEnv, _ := newTestEnvAndUser(t)
-	opts := addOptions{
-		title:          "this is a bug title",
-		message:        "this is a bug message",
-		messageFile:    "",
-		nonInteractive: true,
-	}
-
-	require.NoError(t, runAdd(testEnv.env, opts))
-	require.Regexp(t, "^[0-9A-Fa-f]{7} created\n$", testEnv.out)
-	bugID := strings.Split(testEnv.out.String(), " ")[0]
-	testEnv.out.Reset()
-
-	return testEnv, bugID
-}
-
-func TestAdd(t *testing.T) {
-	_, bugID := newTestEnvAndBug(t)
-	require.Regexp(t, "^[0-9A-Fa-f]{7}$", bugID)
-}

commands/bridge.go 🔗

@@ -1,42 +0,0 @@
-package commands
-
-import (
-	"github.com/spf13/cobra"
-
-	"github.com/MichaelMure/git-bug/bridge"
-)
-
-func newBridgeCommand() *cobra.Command {
-	env := newEnv()
-
-	cmd := &cobra.Command{
-		Use:     "bridge",
-		Short:   "Configure and use bridges to other bug trackers.",
-		PreRunE: loadBackend(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runBridge(env)
-		}),
-		Args: cobra.NoArgs,
-	}
-
-	cmd.AddCommand(newBridgeAuthCommand())
-	cmd.AddCommand(newBridgeConfigureCommand())
-	cmd.AddCommand(newBridgePullCommand())
-	cmd.AddCommand(newBridgePushCommand())
-	cmd.AddCommand(newBridgeRm())
-
-	return cmd
-}
-
-func runBridge(env *Env) error {
-	configured, err := bridge.ConfiguredBridges(env.backend)
-	if err != nil {
-		return err
-	}
-
-	for _, c := range configured {
-		env.out.Println(c)
-	}
-
-	return nil
-}

commands/bridge/bridge.go 🔗

@@ -0,0 +1,43 @@
+package bridgecmd
+
+import (
+	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/bridge"
+	"github.com/MichaelMure/git-bug/commands/execenv"
+)
+
+func NewBridgeCommand() *cobra.Command {
+	env := execenv.NewEnv()
+
+	cmd := &cobra.Command{
+		Use:     "bridge",
+		Short:   "List bridges to other bug trackers",
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBridge(env)
+		}),
+		Args: cobra.NoArgs,
+	}
+
+	cmd.AddCommand(newBridgeAuthCommand())
+	cmd.AddCommand(newBridgeNewCommand())
+	cmd.AddCommand(newBridgePullCommand())
+	cmd.AddCommand(newBridgePushCommand())
+	cmd.AddCommand(newBridgeRm())
+
+	return cmd
+}
+
+func runBridge(env *execenv.Env) error {
+	configured, err := bridge.ConfiguredBridges(env.Backend)
+	if err != nil {
+		return err
+	}
+
+	for _, c := range configured {
+		env.Out.Println(c)
+	}
+
+	return nil
+}

commands/bridge_auth.go → commands/bridge/bridge_auth.go 🔗

@@ -1,25 +1,25 @@
-package commands
+package bridgecmd
 
 import (
 	"sort"
 	"strings"
 
-	"github.com/spf13/cobra"
-
 	text "github.com/MichaelMure/go-term-text"
+	"github.com/spf13/cobra"
 
 	"github.com/MichaelMure/git-bug/bridge/core/auth"
+	"github.com/MichaelMure/git-bug/commands/execenv"
 	"github.com/MichaelMure/git-bug/util/colors"
 )
 
 func newBridgeAuthCommand() *cobra.Command {
-	env := newEnv()
+	env := execenv.NewEnv()
 
 	cmd := &cobra.Command{
 		Use:     "auth",
-		Short:   "List all known bridge authentication credentials.",
-		PreRunE: loadBackend(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
+		Short:   "List all known bridge authentication credentials",
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
 			return runBridgeAuth(env)
 		}),
 		Args: cobra.NoArgs,
@@ -32,8 +32,8 @@ func newBridgeAuthCommand() *cobra.Command {
 	return cmd
 }
 
-func runBridgeAuth(env *Env) error {
-	creds, err := auth.List(env.backend)
+func runBridgeAuth(env *execenv.Env) error {
+	creds, err := auth.List(env.Backend)
 	if err != nil {
 		return err
 	}
@@ -54,7 +54,7 @@ func runBridgeAuth(env *Env) error {
 		sort.Strings(meta)
 		metaFmt := strings.Join(meta, ",")
 
-		env.out.Printf("%s %s %s %s %s\n",
+		env.Out.Printf("%s %s %s %s %s\n",
 			colors.Cyan(cred.ID().Human()),
 			colors.Yellow(targetFmt),
 			colors.Magenta(cred.Kind()),

commands/bridge_auth_addtoken.go → commands/bridge/bridge_auth_addtoken.go 🔗

@@ -1,4 +1,4 @@
-package commands
+package bridgecmd
 
 import (
 	"bufio"
@@ -14,6 +14,8 @@ import (
 	"github.com/MichaelMure/git-bug/bridge/core"
 	"github.com/MichaelMure/git-bug/bridge/core/auth"
 	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/commands/completion"
+	"github.com/MichaelMure/git-bug/commands/execenv"
 )
 
 type bridgeAuthAddTokenOptions struct {
@@ -23,14 +25,14 @@ type bridgeAuthAddTokenOptions struct {
 }
 
 func newBridgeAuthAddTokenCommand() *cobra.Command {
-	env := newEnv()
+	env := execenv.NewEnv()
 	options := bridgeAuthAddTokenOptions{}
 
 	cmd := &cobra.Command{
 		Use:     "add-token [TOKEN]",
 		Short:   "Store a new token",
-		PreRunE: loadBackendEnsureUser(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
+		PreRunE: execenv.LoadBackendEnsureUser(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
 			return runBridgeAuthAddToken(env, options, args)
 		}),
 		Args: cobra.MaximumNArgs(1),
@@ -41,17 +43,17 @@ func newBridgeAuthAddTokenCommand() *cobra.Command {
 
 	flags.StringVarP(&options.target, "target", "t", "",
 		fmt.Sprintf("The target of the bridge. Valid values are [%s]", strings.Join(bridge.Targets(), ",")))
-	cmd.RegisterFlagCompletionFunc("target", completeFrom(bridge.Targets()))
+	cmd.RegisterFlagCompletionFunc("target", completion.From(bridge.Targets()))
 	flags.StringVarP(&options.login,
 		"login", "l", "", "The login in the remote bug-tracker")
 	flags.StringVarP(&options.user,
 		"user", "u", "", "The user to add the token to. Default is the current user")
-	cmd.RegisterFlagCompletionFunc("user", completeUser(env))
+	cmd.RegisterFlagCompletionFunc("user", completion.User(env))
 
 	return cmd
 }
 
-func runBridgeAuthAddToken(env *Env, opts bridgeAuthAddTokenOptions, args []string) error {
+func runBridgeAuthAddToken(env *execenv.Env, opts bridgeAuthAddTokenOptions, args []string) error {
 	// Note: as bridgeAuthAddTokenLogin is not checked against the remote bug-tracker,
 	// it's possible to register a credential with an incorrect login (including bad case).
 	// The consequence is that it will not get picked later by the bridge. I find that
@@ -76,7 +78,7 @@ func runBridgeAuthAddToken(env *Env, opts bridgeAuthAddTokenOptions, args []stri
 	} else {
 		// Read from Stdin
 		if isatty.IsTerminal(os.Stdin.Fd()) {
-			env.err.Println("Enter the token:")
+			env.Err.Println("Enter the token:")
 		}
 		reader := bufio.NewReader(os.Stdin)
 		raw, err := reader.ReadString('\n')
@@ -90,9 +92,9 @@ func runBridgeAuthAddToken(env *Env, opts bridgeAuthAddTokenOptions, args []stri
 	var err error
 
 	if opts.user == "" {
-		user, err = env.backend.GetUserIdentity()
+		user, err = env.Backend.GetUserIdentity()
 	} else {
-		user, err = env.backend.ResolveIdentityPrefix(opts.user)
+		user, err = env.Backend.ResolveIdentityPrefix(opts.user)
 	}
 	if err != nil {
 		return err
@@ -121,11 +123,11 @@ func runBridgeAuthAddToken(env *Env, opts bridgeAuthAddTokenOptions, args []stri
 		return errors.Wrap(err, "invalid token")
 	}
 
-	err = auth.Store(env.repo, token)
+	err = auth.Store(env.Repo, token)
 	if err != nil {
 		return err
 	}
 
-	env.out.Printf("token %s added\n", token.ID())
+	env.Out.Printf("token %s added\n", token.ID())
 	return nil
 }

commands/bridge/bridge_auth_rm.go 🔗

@@ -0,0 +1,41 @@
+package bridgecmd
+
+import (
+	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/bridge/core/auth"
+	"github.com/MichaelMure/git-bug/commands/completion"
+	"github.com/MichaelMure/git-bug/commands/execenv"
+)
+
+func newBridgeAuthRm() *cobra.Command {
+	env := execenv.NewEnv()
+
+	cmd := &cobra.Command{
+		Use:     "rm BRIDGE_ID",
+		Short:   "Remove a credential",
+		PreRunE: execenv.LoadRepo(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runBridgeAuthRm(env, args)
+		},
+		Args:              cobra.ExactArgs(1),
+		ValidArgsFunction: completion.BridgeAuth(env),
+	}
+
+	return cmd
+}
+
+func runBridgeAuthRm(env *execenv.Env, args []string) error {
+	cred, err := auth.LoadWithPrefix(env.Repo, args[0])
+	if err != nil {
+		return err
+	}
+
+	err = auth.Remove(env.Repo, cred.ID())
+	if err != nil {
+		return err
+	}
+
+	env.Out.Printf("credential %s removed\n", cred.ID())
+	return nil
+}

commands/bridge/bridge_auth_show.go 🔗

@@ -0,0 +1,60 @@
+package bridgecmd
+
+import (
+	"fmt"
+	"sort"
+	"strings"
+	"time"
+
+	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/bridge/core/auth"
+	"github.com/MichaelMure/git-bug/commands/completion"
+	"github.com/MichaelMure/git-bug/commands/execenv"
+)
+
+func newBridgeAuthShow() *cobra.Command {
+	env := execenv.NewEnv()
+
+	cmd := &cobra.Command{
+		Use:     "show",
+		Short:   "Display an authentication credential",
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBridgeAuthShow(env, args)
+		}),
+		Args:              cobra.ExactArgs(1),
+		ValidArgsFunction: completion.BridgeAuth(env),
+	}
+
+	return cmd
+}
+
+func runBridgeAuthShow(env *execenv.Env, args []string) error {
+	cred, err := auth.LoadWithPrefix(env.Repo, args[0])
+	if err != nil {
+		return err
+	}
+
+	env.Out.Printf("Id: %s\n", cred.ID())
+	env.Out.Printf("Target: %s\n", cred.Target())
+	env.Out.Printf("Kind: %s\n", cred.Kind())
+	env.Out.Printf("Creation: %s\n", cred.CreateTime().Format(time.RFC822))
+
+	switch cred := cred.(type) {
+	case *auth.Token:
+		env.Out.Printf("Value: %s\n", cred.Value)
+	}
+
+	env.Out.Println("Metadata:")
+
+	meta := make([]string, 0, len(cred.Metadata()))
+	for key, value := range cred.Metadata() {
+		meta = append(meta, fmt.Sprintf("    %s --> %s\n", key, value))
+	}
+	sort.Strings(meta)
+
+	env.Out.Print(strings.Join(meta, ""))
+
+	return nil
+}

commands/bridge_configure.go → commands/bridge/bridge_new.go 🔗

@@ -1,4 +1,4 @@
-package commands
+package bridgecmd
 
 import (
 	"bufio"
@@ -12,10 +12,12 @@ import (
 	"github.com/MichaelMure/git-bug/bridge"
 	"github.com/MichaelMure/git-bug/bridge/core"
 	"github.com/MichaelMure/git-bug/bridge/core/auth"
+	"github.com/MichaelMure/git-bug/commands/completion"
+	"github.com/MichaelMure/git-bug/commands/execenv"
 	"github.com/MichaelMure/git-bug/repository"
 )
 
-type bridgeConfigureOptions struct {
+type bridgeNewOptions struct {
 	name           string
 	target         string
 	params         core.BridgeParams
@@ -24,13 +26,13 @@ type bridgeConfigureOptions struct {
 	nonInteractive bool
 }
 
-func newBridgeConfigureCommand() *cobra.Command {
-	env := newEnv()
-	options := bridgeConfigureOptions{}
+func newBridgeNewCommand() *cobra.Command {
+	env := execenv.NewEnv()
+	options := bridgeNewOptions{}
 
 	cmd := &cobra.Command{
-		Use:   "configure",
-		Short: "Configure a new bridge.",
+		Use:   "new",
+		Short: "Configure a new bridge",
 		Long: `	Configure a new bridge by passing flags or/and using interactive terminal prompts. You can avoid all the terminal prompts by passing all the necessary flags to configure your bridge.`,
 		Example: `# Interactive example
 [1]: github
@@ -66,7 +68,7 @@ Enter token: 87cf5c03b64029f18ea5f9ca5679daa08ccbd700
 Successfully configured bridge: default
 
 # For GitHub
-git bug bridge configure \
+git bug bridge new \
     --name=default \
     --target=github \
     --owner=$(OWNER) \
@@ -74,20 +76,20 @@ git bug bridge configure \
     --token=$(TOKEN)
 
 # For Launchpad
-git bug bridge configure \
+git bug bridge new \
     --name=default \
     --target=launchpad-preview \
     --url=https://bugs.launchpad.net/ubuntu/
 
 # For Gitlab
-git bug bridge configure \
+git bug bridge new \
     --name=default \
     --target=github \
     --url=https://github.com/michaelmure/git-bug \
     --token=$(TOKEN)`,
-		PreRunE: loadBackend(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runBridgeConfigure(env, options)
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBridgeNew(env, options)
 		}),
 	}
 
@@ -97,7 +99,7 @@ git bug bridge configure \
 	flags.StringVarP(&options.name, "name", "n", "", "A distinctive name to identify the bridge")
 	flags.StringVarP(&options.target, "target", "t", "",
 		fmt.Sprintf("The target of the bridge. Valid values are [%s]", strings.Join(bridge.Targets(), ",")))
-	cmd.RegisterFlagCompletionFunc("target", completeFrom(bridge.Targets()))
+	cmd.RegisterFlagCompletionFunc("target", completion.From(bridge.Targets()))
 	flags.StringVarP(&options.params.URL, "url", "u", "", "The URL of the remote repository")
 	flags.StringVarP(&options.params.BaseURL, "base-url", "b", "", "The base URL of your remote issue tracker")
 	flags.StringVarP(&options.params.Login, "login", "l", "", "The login on your remote issue tracker")
@@ -111,7 +113,7 @@ git bug bridge configure \
 	return cmd
 }
 
-func runBridgeConfigure(env *Env, opts bridgeConfigureOptions) error {
+func runBridgeNew(env *execenv.Env, opts bridgeNewOptions) error {
 	var err error
 
 	if (opts.tokenStdin || opts.token != "" || opts.params.CredPrefix != "") &&
@@ -121,7 +123,7 @@ func runBridgeConfigure(env *Env, opts bridgeConfigureOptions) error {
 
 	// early fail
 	if opts.params.CredPrefix != "" {
-		if _, err := auth.LoadWithPrefix(env.repo, opts.params.CredPrefix); err != nil {
+		if _, err := auth.LoadWithPrefix(env.Repo, opts.params.CredPrefix); err != nil {
 			return err
 		}
 	}
@@ -146,13 +148,13 @@ func runBridgeConfigure(env *Env, opts bridgeConfigureOptions) error {
 	}
 
 	if !opts.nonInteractive && opts.name == "" {
-		opts.name, err = promptName(env.repo)
+		opts.name, err = promptName(env.Repo)
 		if err != nil {
 			return err
 		}
 	}
 
-	b, err := bridge.NewBridge(env.backend, opts.target, opts.name)
+	b, err := bridge.NewBridge(env.Backend, opts.target, opts.name)
 	if err != nil {
 		return err
 	}
@@ -162,7 +164,7 @@ func runBridgeConfigure(env *Env, opts bridgeConfigureOptions) error {
 		return err
 	}
 
-	env.out.Printf("Successfully configured bridge: %s\n", opts.name)
+	env.Out.Printf("Successfully configured bridge: %s\n", opts.name)
 	return nil
 }
 

commands/bridge_pull.go → commands/bridge/bridge_pull.go 🔗

@@ -1,4 +1,4 @@
-package commands
+package bridgecmd
 
 import (
 	"context"
@@ -13,6 +13,8 @@ import (
 
 	"github.com/MichaelMure/git-bug/bridge"
 	"github.com/MichaelMure/git-bug/bridge/core"
+	"github.com/MichaelMure/git-bug/commands/completion"
+	"github.com/MichaelMure/git-bug/commands/execenv"
 	"github.com/MichaelMure/git-bug/util/interrupt"
 )
 
@@ -22,18 +24,18 @@ type bridgePullOptions struct {
 }
 
 func newBridgePullCommand() *cobra.Command {
-	env := newEnv()
+	env := execenv.NewEnv()
 	options := bridgePullOptions{}
 
 	cmd := &cobra.Command{
 		Use:     "pull [NAME]",
-		Short:   "Pull updates.",
-		PreRunE: loadBackend(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
+		Short:   "Pull updates from a remote bug tracker",
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
 			return runBridgePull(env, options, args)
 		}),
 		Args:              cobra.MaximumNArgs(1),
-		ValidArgsFunction: completeBridge(env),
+		ValidArgsFunction: completion.Bridge(env),
 	}
 
 	flags := cmd.Flags()
@@ -45,7 +47,7 @@ func newBridgePullCommand() *cobra.Command {
 	return cmd
 }
 
-func runBridgePull(env *Env, opts bridgePullOptions, args []string) error {
+func runBridgePull(env *execenv.Env, opts bridgePullOptions, args []string) error {
 	if opts.noResume && opts.importSince != "" {
 		return fmt.Errorf("only one of --no-resume and --since flags should be used")
 	}
@@ -54,9 +56,9 @@ func runBridgePull(env *Env, opts bridgePullOptions, args []string) error {
 	var err error
 
 	if len(args) == 0 {
-		b, err = bridge.DefaultBridge(env.backend)
+		b, err = bridge.DefaultBridge(env.Backend)
 	} else {
-		b, err = bridge.LoadBridge(env.backend, args[0])
+		b, err = bridge.LoadBridge(env.Backend, args[0])
 	}
 
 	if err != nil {
@@ -75,14 +77,14 @@ func runBridgePull(env *Env, opts bridgePullOptions, args []string) error {
 	interrupt.RegisterCleaner(func() error {
 		mu.Lock()
 		if interruptCount > 0 {
-			env.err.Println("Received another interrupt before graceful stop, terminating...")
+			env.Err.Println("Received another interrupt before graceful stop, terminating...")
 			os.Exit(0)
 		}
 
 		interruptCount++
 		mu.Unlock()
 
-		env.err.Println("Received interrupt signal, stopping the import...\n(Hit ctrl-c again to kill the process.)")
+		env.Err.Println("Received interrupt signal, stopping the import...\n(Hit ctrl-c again to kill the process.)")
 
 		// send signal to stop the importer
 		cancel()
@@ -119,23 +121,23 @@ func runBridgePull(env *Env, opts bridgePullOptions, args []string) error {
 
 		case core.ImportEventBug:
 			importedIssues++
-			env.out.Println(result.String())
+			env.Out.Println(result.String())
 
 		case core.ImportEventIdentity:
 			importedIdentities++
-			env.out.Println(result.String())
+			env.Out.Println(result.String())
 
 		case core.ImportEventError:
 			if result.Err != context.Canceled {
-				env.out.Println(result.String())
+				env.Out.Println(result.String())
 			}
 
 		default:
-			env.out.Println(result.String())
+			env.Out.Println(result.String())
 		}
 	}
 
-	env.out.Printf("imported %d issues and %d identities with %s bridge\n", importedIssues, importedIdentities, b.Name)
+	env.Out.Printf("imported %d issues and %d identities with %s bridge\n", importedIssues, importedIdentities, b.Name)
 
 	// send done signal
 	close(done)

commands/bridge_push.go → commands/bridge/bridge_push.go 🔗

@@ -1,4 +1,4 @@
-package commands
+package bridgecmd
 
 import (
 	"context"
@@ -10,34 +10,36 @@ import (
 
 	"github.com/MichaelMure/git-bug/bridge"
 	"github.com/MichaelMure/git-bug/bridge/core"
+	"github.com/MichaelMure/git-bug/commands/completion"
+	"github.com/MichaelMure/git-bug/commands/execenv"
 	"github.com/MichaelMure/git-bug/util/interrupt"
 )
 
 func newBridgePushCommand() *cobra.Command {
-	env := newEnv()
+	env := execenv.NewEnv()
 
 	cmd := &cobra.Command{
 		Use:     "push [NAME]",
-		Short:   "Push updates.",
-		PreRunE: loadBackendEnsureUser(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
+		Short:   "Push updates to remote bug tracker",
+		PreRunE: execenv.LoadBackendEnsureUser(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
 			return runBridgePush(env, args)
 		}),
 		Args:              cobra.MaximumNArgs(1),
-		ValidArgsFunction: completeBridge(env),
+		ValidArgsFunction: completion.Bridge(env),
 	}
 
 	return cmd
 }
 
-func runBridgePush(env *Env, args []string) error {
+func runBridgePush(env *execenv.Env, args []string) error {
 	var b *core.Bridge
 	var err error
 
 	if len(args) == 0 {
-		b, err = bridge.DefaultBridge(env.backend)
+		b, err = bridge.DefaultBridge(env.Backend)
 	} else {
-		b, err = bridge.LoadBridge(env.backend, args[0])
+		b, err = bridge.LoadBridge(env.Backend, args[0])
 	}
 
 	if err != nil {
@@ -55,14 +57,14 @@ func runBridgePush(env *Env, args []string) error {
 	interrupt.RegisterCleaner(func() error {
 		mu.Lock()
 		if interruptCount > 0 {
-			env.err.Println("Received another interrupt before graceful stop, terminating...")
+			env.Err.Println("Received another interrupt before graceful stop, terminating...")
 			os.Exit(0)
 		}
 
 		interruptCount++
 		mu.Unlock()
 
-		env.err.Println("Received interrupt signal, stopping the import...\n(Hit ctrl-c again to kill the process.)")
+		env.Err.Println("Received interrupt signal, stopping the import...\n(Hit ctrl-c again to kill the process.)")
 
 		// send signal to stop the importer
 		cancel()
@@ -80,7 +82,7 @@ func runBridgePush(env *Env, args []string) error {
 	exportedIssues := 0
 	for result := range events {
 		if result.Event != core.ExportEventNothing {
-			env.out.Println(result.String())
+			env.Out.Println(result.String())
 		}
 
 		switch result.Event {
@@ -89,7 +91,7 @@ func runBridgePush(env *Env, args []string) error {
 		}
 	}
 
-	env.out.Printf("exported %d issues with %s bridge\n", exportedIssues, b.Name)
+	env.Out.Printf("exported %d issues with %s bridge\n", exportedIssues, b.Name)
 
 	// send done signal
 	close(done)

commands/bridge/bridge_rm.go 🔗

@@ -0,0 +1,36 @@
+package bridgecmd
+
+import (
+	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/bridge"
+	"github.com/MichaelMure/git-bug/commands/completion"
+	"github.com/MichaelMure/git-bug/commands/execenv"
+)
+
+func newBridgeRm() *cobra.Command {
+	env := execenv.NewEnv()
+
+	cmd := &cobra.Command{
+		Use:     "rm NAME",
+		Short:   "Delete a configured bridge",
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBridgeRm(env, args)
+		}),
+		Args:              cobra.ExactArgs(1),
+		ValidArgsFunction: completion.Bridge(env),
+	}
+
+	return cmd
+}
+
+func runBridgeRm(env *execenv.Env, args []string) error {
+	err := bridge.RemoveBridge(env.Backend, args[0])
+	if err != nil {
+		return err
+	}
+
+	env.Out.Printf("Successfully removed bridge configuration %v\n", args[0])
+	return nil
+}

commands/bridge_auth_rm.go 🔗

@@ -1,39 +0,0 @@
-package commands
-
-import (
-	"github.com/spf13/cobra"
-
-	"github.com/MichaelMure/git-bug/bridge/core/auth"
-)
-
-func newBridgeAuthRm() *cobra.Command {
-	env := newEnv()
-
-	cmd := &cobra.Command{
-		Use:     "rm ID",
-		Short:   "Remove a credential.",
-		PreRunE: loadRepo(env),
-		RunE: func(cmd *cobra.Command, args []string) error {
-			return runBridgeAuthRm(env, args)
-		},
-		Args:              cobra.ExactArgs(1),
-		ValidArgsFunction: completeBridgeAuth(env),
-	}
-
-	return cmd
-}
-
-func runBridgeAuthRm(env *Env, args []string) error {
-	cred, err := auth.LoadWithPrefix(env.repo, args[0])
-	if err != nil {
-		return err
-	}
-
-	err = auth.Remove(env.repo, cred.ID())
-	if err != nil {
-		return err
-	}
-
-	env.out.Printf("credential %s removed\n", cred.ID())
-	return nil
-}

commands/bridge_auth_show.go 🔗

@@ -1,58 +0,0 @@
-package commands
-
-import (
-	"fmt"
-	"sort"
-	"strings"
-	"time"
-
-	"github.com/spf13/cobra"
-
-	"github.com/MichaelMure/git-bug/bridge/core/auth"
-)
-
-func newBridgeAuthShow() *cobra.Command {
-	env := newEnv()
-
-	cmd := &cobra.Command{
-		Use:     "show",
-		Short:   "Display an authentication credential.",
-		PreRunE: loadBackend(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runBridgeAuthShow(env, args)
-		}),
-		Args:              cobra.ExactArgs(1),
-		ValidArgsFunction: completeBridgeAuth(env),
-	}
-
-	return cmd
-}
-
-func runBridgeAuthShow(env *Env, args []string) error {
-	cred, err := auth.LoadWithPrefix(env.repo, args[0])
-	if err != nil {
-		return err
-	}
-
-	env.out.Printf("Id: %s\n", cred.ID())
-	env.out.Printf("Target: %s\n", cred.Target())
-	env.out.Printf("Kind: %s\n", cred.Kind())
-	env.out.Printf("Creation: %s\n", cred.CreateTime().Format(time.RFC822))
-
-	switch cred := cred.(type) {
-	case *auth.Token:
-		env.out.Printf("Value: %s\n", cred.Value)
-	}
-
-	env.out.Println("Metadata:")
-
-	meta := make([]string, 0, len(cred.Metadata()))
-	for key, value := range cred.Metadata() {
-		meta = append(meta, fmt.Sprintf("    %s --> %s\n", key, value))
-	}
-	sort.Strings(meta)
-
-	env.out.Print(strings.Join(meta, ""))
-
-	return nil
-}

commands/bridge_rm.go 🔗

@@ -1,34 +0,0 @@
-package commands
-
-import (
-	"github.com/spf13/cobra"
-
-	"github.com/MichaelMure/git-bug/bridge"
-)
-
-func newBridgeRm() *cobra.Command {
-	env := newEnv()
-
-	cmd := &cobra.Command{
-		Use:     "rm NAME",
-		Short:   "Delete a configured bridge.",
-		PreRunE: loadBackend(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runBridgeRm(env, args)
-		}),
-		Args:              cobra.ExactArgs(1),
-		ValidArgsFunction: completeBridge(env),
-	}
-
-	return cmd
-}
-
-func runBridgeRm(env *Env, args []string) error {
-	err := bridge.RemoveBridge(env.backend, args[0])
-	if err != nil {
-		return err
-	}
-
-	env.out.Printf("Successfully removed bridge configuration %v\n", args[0])
-	return nil
-}

commands/ls.go → commands/bug/bug.go 🔗

@@ -1,4 +1,4 @@
-package commands
+package bugcmd
 
 import (
 	"encoding/json"
@@ -11,13 +11,16 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/commands/cmdjson"
+	"github.com/MichaelMure/git-bug/commands/completion"
+	"github.com/MichaelMure/git-bug/commands/execenv"
 	"github.com/MichaelMure/git-bug/entities/bug"
 	"github.com/MichaelMure/git-bug/entities/common"
 	"github.com/MichaelMure/git-bug/query"
 	"github.com/MichaelMure/git-bug/util/colors"
 )
 
-type lsOptions struct {
+type bugOptions struct {
 	statusQuery      []string
 	authorQuery      []string
 	metadataQuery    []string
@@ -31,33 +34,33 @@ type lsOptions struct {
 	outputFormat     string
 }
 
-func newLsCommand() *cobra.Command {
-	env := newEnv()
-	options := lsOptions{}
+func NewBugCommand() *cobra.Command {
+	env := execenv.NewEnv()
+	options := bugOptions{}
 
 	cmd := &cobra.Command{
-		Use:   "ls [QUERY]",
-		Short: "List bugs.",
+		Use:   "bug [QUERY]",
+		Short: "List bugs",
 		Long: `Display a summary of each bugs.
 
 You can pass an additional query to filter and order the list. This query can be expressed either with a simple query language, flags, a natural language full text search, or a combination of the aforementioned.`,
 		Example: `List open bugs sorted by last edition with a query:
-git bug ls status:open sort:edit-desc
+git bug status:open sort:edit-desc
 
 List closed bugs sorted by creation with flags:
-git bug ls --status closed --by creation
+git bug --status closed --by creation
 
 Do a full text search of all bugs:
-git bug ls "foo bar" baz
+git bug "foo bar" baz
 
 Use queries, flags, and full text search:
-git bug ls status:open --by creation "foo bar" baz
+git bug status:open --by creation "foo bar" baz
 `,
-		PreRunE: loadBackend(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runLs(env, options, args)
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBug(env, options, args)
 		}),
-		ValidArgsFunction: completeLs(env),
+		ValidArgsFunction: completion.Ls(env),
 	}
 
 	flags := cmd.Flags()
@@ -65,41 +68,60 @@ git bug ls status:open --by creation "foo bar" baz
 
 	flags.StringSliceVarP(&options.statusQuery, "status", "s", nil,
 		"Filter by status. Valid values are [open,closed]")
-	cmd.RegisterFlagCompletionFunc("status", completeFrom([]string{"open", "closed"}))
+	cmd.RegisterFlagCompletionFunc("status", completion.From([]string{"open", "closed"}))
 	flags.StringSliceVarP(&options.authorQuery, "author", "a", nil,
 		"Filter by author")
 	flags.StringSliceVarP(&options.metadataQuery, "metadata", "m", nil,
 		"Filter by metadata. Example: github-url=URL")
-	cmd.RegisterFlagCompletionFunc("author", completeUserForQuery(env))
+	cmd.RegisterFlagCompletionFunc("author", completion.UserForQuery(env))
 	flags.StringSliceVarP(&options.participantQuery, "participant", "p", nil,
 		"Filter by participant")
-	cmd.RegisterFlagCompletionFunc("participant", completeUserForQuery(env))
+	cmd.RegisterFlagCompletionFunc("participant", completion.UserForQuery(env))
 	flags.StringSliceVarP(&options.actorQuery, "actor", "A", nil,
 		"Filter by actor")
-	cmd.RegisterFlagCompletionFunc("actor", completeUserForQuery(env))
+	cmd.RegisterFlagCompletionFunc("actor", completion.UserForQuery(env))
 	flags.StringSliceVarP(&options.labelQuery, "label", "l", nil,
 		"Filter by label")
-	cmd.RegisterFlagCompletionFunc("label", completeLabel(env))
+	cmd.RegisterFlagCompletionFunc("label", completion.Label(env))
 	flags.StringSliceVarP(&options.titleQuery, "title", "t", nil,
 		"Filter by title")
 	flags.StringSliceVarP(&options.noQuery, "no", "n", nil,
 		"Filter by absence of something. Valid values are [label]")
-	cmd.RegisterFlagCompletionFunc("no", completeLabel(env))
+	cmd.RegisterFlagCompletionFunc("no", completion.Label(env))
 	flags.StringVarP(&options.sortBy, "by", "b", "creation",
 		"Sort the results by a characteristic. Valid values are [id,creation,edit]")
-	cmd.RegisterFlagCompletionFunc("by", completeFrom([]string{"id", "creation", "edit"}))
+	cmd.RegisterFlagCompletionFunc("by", completion.From([]string{"id", "creation", "edit"}))
 	flags.StringVarP(&options.sortDirection, "direction", "d", "asc",
 		"Select the sorting direction. Valid values are [asc,desc]")
-	cmd.RegisterFlagCompletionFunc("direction", completeFrom([]string{"asc", "desc"}))
+	cmd.RegisterFlagCompletionFunc("direction", completion.From([]string{"asc", "desc"}))
 	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",
-		completeFrom([]string{"default", "plain", "compact", "id", "json", "org-mode"}))
+		completion.From([]string{"default", "plain", "compact", "id", "json", "org-mode"}))
+
+	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(newBugDeselectCommand(), selectGroup)
+	addCmdWithGroup(newBugSelectCommand(), selectGroup)
+
+	cmd.AddCommand(newBugCommentCommand())
+	cmd.AddCommand(newBugLabelCommand())
+	cmd.AddCommand(newBugNewCommand())
+	cmd.AddCommand(newBugRmCommand())
+	cmd.AddCommand(newBugShowCommand())
+	cmd.AddCommand(newBugStatusCommand())
+	cmd.AddCommand(newBugTitleCommand())
 
 	return cmd
 }
 
-func runLs(env *Env, opts lsOptions, args []string) error {
+func runBug(env *execenv.Env, opts bugOptions, args []string) error {
 	var q *query.Query
 	var err error
 
@@ -120,14 +142,14 @@ func runLs(env *Env, opts lsOptions, args []string) error {
 		return err
 	}
 
-	allIds, err := env.backend.QueryBugs(q)
+	allIds, err := env.Backend.QueryBugs(q)
 	if err != nil {
 		return err
 	}
 
 	bugExcerpt := make([]*cache.BugExcerpt, len(allIds))
 	for i, id := range allIds {
-		b, err := env.backend.ResolveBugExcerpt(id)
+		b, err := env.Backend.ResolveBugExcerpt(id)
 		if err != nil {
 			return err
 		}
@@ -136,17 +158,17 @@ func runLs(env *Env, opts lsOptions, args []string) error {
 
 	switch opts.outputFormat {
 	case "org-mode":
-		return lsOrgmodeFormatter(env, bugExcerpt)
+		return bugsOrgmodeFormatter(env, bugExcerpt)
 	case "plain":
-		return lsPlainFormatter(env, bugExcerpt)
+		return bugsPlainFormatter(env, bugExcerpt)
 	case "json":
-		return lsJsonFormatter(env, bugExcerpt)
+		return bugsJsonFormatter(env, bugExcerpt)
 	case "compact":
-		return lsCompactFormatter(env, bugExcerpt)
+		return bugsCompactFormatter(env, bugExcerpt)
 	case "id":
-		return lsIDFormatter(env, bugExcerpt)
+		return bugsIDFormatter(env, bugExcerpt)
 	case "default":
-		return lsDefaultFormatter(env, bugExcerpt)
+		return bugsDefaultFormatter(env, bugExcerpt)
 	default:
 		return fmt.Errorf("unknown format %s", opts.outputFormat)
 	}
@@ -166,30 +188,30 @@ func repairQuery(args []string) string {
 }
 
 type JSONBugExcerpt struct {
-	Id         string   `json:"id"`
-	HumanId    string   `json:"human_id"`
-	CreateTime JSONTime `json:"create_time"`
-	EditTime   JSONTime `json:"edit_time"`
-
-	Status       string         `json:"status"`
-	Labels       []bug.Label    `json:"labels"`
-	Title        string         `json:"title"`
-	Actors       []JSONIdentity `json:"actors"`
-	Participants []JSONIdentity `json:"participants"`
-	Author       JSONIdentity   `json:"author"`
+	Id         string       `json:"id"`
+	HumanId    string       `json:"human_id"`
+	CreateTime cmdjson.Time `json:"create_time"`
+	EditTime   cmdjson.Time `json:"edit_time"`
+
+	Status       string             `json:"status"`
+	Labels       []bug.Label        `json:"labels"`
+	Title        string             `json:"title"`
+	Actors       []cmdjson.Identity `json:"actors"`
+	Participants []cmdjson.Identity `json:"participants"`
+	Author       cmdjson.Identity   `json:"author"`
 
 	Comments int               `json:"comments"`
 	Metadata map[string]string `json:"metadata"`
 }
 
-func lsJsonFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
+func bugsJsonFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error {
 	jsonBugs := make([]JSONBugExcerpt, len(bugExcerpts))
 	for i, b := range bugExcerpts {
 		jsonBug := JSONBugExcerpt{
 			Id:         b.Id.String(),
 			HumanId:    b.Id.Human(),
-			CreateTime: NewJSONTime(b.CreateTime(), b.CreateLamportTime),
-			EditTime:   NewJSONTime(b.EditTime(), b.EditLamportTime),
+			CreateTime: cmdjson.NewTime(b.CreateTime(), b.CreateLamportTime),
+			EditTime:   cmdjson.NewTime(b.EditTime(), b.EditLamportTime),
 			Status:     b.Status.String(),
 			Labels:     b.Labels,
 			Title:      b.Title,
@@ -197,40 +219,40 @@ func lsJsonFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
 			Metadata:   b.CreateMetadata,
 		}
 
-		author, err := env.backend.ResolveIdentityExcerpt(b.AuthorId)
+		author, err := env.Backend.ResolveIdentityExcerpt(b.AuthorId)
 		if err != nil {
 			return err
 		}
-		jsonBug.Author = NewJSONIdentityFromExcerpt(author)
+		jsonBug.Author = cmdjson.NewIdentityFromExcerpt(author)
 
-		jsonBug.Actors = make([]JSONIdentity, len(b.Actors))
+		jsonBug.Actors = make([]cmdjson.Identity, len(b.Actors))
 		for i, element := range b.Actors {
-			actor, err := env.backend.ResolveIdentityExcerpt(element)
+			actor, err := env.Backend.ResolveIdentityExcerpt(element)
 			if err != nil {
 				return err
 			}
-			jsonBug.Actors[i] = NewJSONIdentityFromExcerpt(actor)
+			jsonBug.Actors[i] = cmdjson.NewIdentityFromExcerpt(actor)
 		}
 
-		jsonBug.Participants = make([]JSONIdentity, len(b.Participants))
+		jsonBug.Participants = make([]cmdjson.Identity, len(b.Participants))
 		for i, element := range b.Participants {
-			participant, err := env.backend.ResolveIdentityExcerpt(element)
+			participant, err := env.Backend.ResolveIdentityExcerpt(element)
 			if err != nil {
 				return err
 			}
-			jsonBug.Participants[i] = NewJSONIdentityFromExcerpt(participant)
+			jsonBug.Participants[i] = cmdjson.NewIdentityFromExcerpt(participant)
 		}
 
 		jsonBugs[i] = jsonBug
 	}
 	jsonObject, _ := json.MarshalIndent(jsonBugs, "", "    ")
-	env.out.Printf("%s\n", jsonObject)
+	env.Out.Printf("%s\n", jsonObject)
 	return nil
 }
 
-func lsCompactFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
+func bugsCompactFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error {
 	for _, b := range bugExcerpts {
-		author, err := env.backend.ResolveIdentityExcerpt(b.AuthorId)
+		author, err := env.Backend.ResolveIdentityExcerpt(b.AuthorId)
 		if err != nil {
 			return err
 		}
@@ -243,7 +265,7 @@ func lsCompactFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
 			labelsTxt.WriteString(lc256.Unescape())
 		}
 
-		env.out.Printf("%s %s %s %s %s\n",
+		env.Out.Printf("%s %s %s %s %s\n",
 			colors.Cyan(b.Id.Human()),
 			colors.Yellow(b.Status),
 			text.LeftPadMaxLine(strings.TrimSpace(b.Title), 40, 0),
@@ -254,17 +276,17 @@ func lsCompactFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
 	return nil
 }
 
-func lsIDFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
+func bugsIDFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error {
 	for _, b := range bugExcerpts {
-		env.out.Println(b.Id.String())
+		env.Out.Println(b.Id.String())
 	}
 
 	return nil
 }
 
-func lsDefaultFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
+func bugsDefaultFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error {
 	for _, b := range bugExcerpts {
-		author, err := env.backend.ResolveIdentityExcerpt(b.AuthorId)
+		author, err := env.Backend.ResolveIdentityExcerpt(b.AuthorId)
 		if err != nil {
 			return err
 		}
@@ -290,7 +312,7 @@ func lsDefaultFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
 			comments = "  ∞ 💬"
 		}
 
-		env.out.Printf("%s\t%s\t%s\t%s\t%s\n",
+		env.Out.Printf("%s\t%s\t%s\t%s\t%s\n",
 			colors.Cyan(b.Id.Human()),
 			colors.Yellow(b.Status),
 			titleFmt+labelsFmt,
@@ -301,14 +323,14 @@ func lsDefaultFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
 	return nil
 }
 
-func lsPlainFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
+func bugsPlainFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error {
 	for _, b := range bugExcerpts {
-		env.out.Printf("%s [%s] %s\n", b.Id.Human(), b.Status, strings.TrimSpace(b.Title))
+		env.Out.Printf("%s [%s] %s\n", b.Id.Human(), b.Status, strings.TrimSpace(b.Title))
 	}
 	return nil
 }
 
-func lsOrgmodeFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
+func bugsOrgmodeFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error {
 	// see https://orgmode.org/manual/Tags.html
 	orgTagRe := regexp.MustCompile("[^[:alpha:]_@]")
 	formatTag := func(l bug.Label) string {
@@ -319,7 +341,7 @@ func lsOrgmodeFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
 		return time.Format("[2006-01-02 Mon 15:05]")
 	}
 
-	env.out.Println("#+TODO: OPEN | CLOSED")
+	env.Out.Println("#+TODO: OPEN | CLOSED")
 
 	for _, b := range bugExcerpts {
 		status := strings.ToUpper(b.Status.String())
@@ -331,7 +353,7 @@ func lsOrgmodeFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
 			title = b.Title
 		}
 
-		author, err := env.backend.ResolveIdentityExcerpt(b.AuthorId)
+		author, err := env.Backend.ResolveIdentityExcerpt(b.AuthorId)
 		if err != nil {
 			return err
 		}
@@ -346,7 +368,7 @@ func lsOrgmodeFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
 		}
 		labels.WriteString(":")
 
-		env.out.Printf("* %-6s %s %s %s: %s %s\n",
+		env.Out.Printf("* %-6s %s %s %s: %s %s\n",
 			status,
 			b.Id.Human(),
 			formatTime(b.CreateTime()),
@@ -355,29 +377,29 @@ func lsOrgmodeFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
 			labels.String(),
 		)
 
-		env.out.Printf("** Last Edited: %s\n", formatTime(b.EditTime()))
+		env.Out.Printf("** Last Edited: %s\n", formatTime(b.EditTime()))
 
-		env.out.Printf("** Actors:\n")
+		env.Out.Printf("** Actors:\n")
 		for _, element := range b.Actors {
-			actor, err := env.backend.ResolveIdentityExcerpt(element)
+			actor, err := env.Backend.ResolveIdentityExcerpt(element)
 			if err != nil {
 				return err
 			}
 
-			env.out.Printf(": %s %s\n",
+			env.Out.Printf(": %s %s\n",
 				actor.Id.Human(),
 				actor.DisplayName(),
 			)
 		}
 
-		env.out.Printf("** Participants:\n")
+		env.Out.Printf("** Participants:\n")
 		for _, element := range b.Participants {
-			participant, err := env.backend.ResolveIdentityExcerpt(element)
+			participant, err := env.Backend.ResolveIdentityExcerpt(element)
 			if err != nil {
 				return err
 			}
 
-			env.out.Printf(": %s %s\n",
+			env.Out.Printf(": %s %s\n",
 				participant.Id.Human(),
 				participant.DisplayName(),
 			)
@@ -388,7 +410,7 @@ func lsOrgmodeFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
 }
 
 // Finish the command flags transformation into the query.Query
-func completeQuery(q *query.Query, opts lsOptions) error {
+func completeQuery(q *query.Query, opts bugOptions) error {
 	for _, str := range opts.statusQuery {
 		status, err := common.StatusFromString(str)
 		if err != nil {

commands/bug/bug_comment.go 🔗

@@ -0,0 +1,52 @@
+package bugcmd
+
+import (
+	text "github.com/MichaelMure/go-term-text"
+	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/commands/bug/select"
+	"github.com/MichaelMure/git-bug/commands/completion"
+	"github.com/MichaelMure/git-bug/commands/execenv"
+	"github.com/MichaelMure/git-bug/util/colors"
+)
+
+func newBugCommentCommand() *cobra.Command {
+	env := execenv.NewEnv()
+
+	cmd := &cobra.Command{
+		Use:     "comment [BUG_ID]",
+		Short:   "List a bug's comments",
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBugComment(env, args)
+		}),
+		ValidArgsFunction: completion.Bug(env),
+	}
+
+	cmd.AddCommand(newBugCommentNewCommand())
+	cmd.AddCommand(newBugCommentEditCommand())
+
+	return cmd
+}
+
+func runBugComment(env *execenv.Env, args []string) error {
+	b, args, err := _select.ResolveBug(env.Backend, args)
+	if err != nil {
+		return err
+	}
+
+	snap := b.Snapshot()
+
+	for i, comment := range snap.Comments {
+		if i != 0 {
+			env.Out.Println()
+		}
+
+		env.Out.Printf("Author: %s\n", colors.Magenta(comment.Author.DisplayName()))
+		env.Out.Printf("Id: %s\n", colors.Cyan(comment.CombinedId().Human()))
+		env.Out.Printf("Date: %s\n\n", comment.FormatTime())
+		env.Out.Println(text.LeftPadLines(comment.Message, 4))
+	}
+
+	return nil
+}

commands/comment_add.go → commands/bug/bug_comment_add.go 🔗

@@ -1,31 +1,33 @@
-package commands
+package bugcmd
 
 import (
 	"github.com/spf13/cobra"
 
+	"github.com/MichaelMure/git-bug/commands/bug/select"
+	"github.com/MichaelMure/git-bug/commands/completion"
+	"github.com/MichaelMure/git-bug/commands/execenv"
 	"github.com/MichaelMure/git-bug/commands/input"
-	_select "github.com/MichaelMure/git-bug/commands/select"
 	"github.com/MichaelMure/git-bug/util/text"
 )
 
-type commentAddOptions struct {
+type bugCommentNewOptions struct {
 	messageFile    string
 	message        string
 	nonInteractive bool
 }
 
-func newCommentAddCommand() *cobra.Command {
-	env := newEnv()
-	options := commentAddOptions{}
+func newBugCommentNewCommand() *cobra.Command {
+	env := execenv.NewEnv()
+	options := bugCommentNewOptions{}
 
 	cmd := &cobra.Command{
-		Use:     "add [ID]",
-		Short:   "Add a new comment to a bug.",
-		PreRunE: loadBackendEnsureUser(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runCommentAdd(env, options, args)
+		Use:     "new [BUG_ID]",
+		Short:   "Add a new comment to a bug",
+		PreRunE: execenv.LoadBackendEnsureUser(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBugCommentNew(env, options, args)
 		}),
-		ValidArgsFunction: completeBug(env),
+		ValidArgsFunction: completion.Bug(env),
 	}
 
 	flags := cmd.Flags()
@@ -41,8 +43,8 @@ func newCommentAddCommand() *cobra.Command {
 	return cmd
 }
 
-func runCommentAdd(env *Env, opts commentAddOptions, args []string) error {
-	b, args, err := _select.ResolveBug(env.backend, args)
+func runBugCommentNew(env *execenv.Env, opts bugCommentNewOptions, args []string) error {
+	b, args, err := _select.ResolveBug(env.Backend, args)
 	if err != nil {
 		return err
 	}
@@ -56,12 +58,12 @@ func runCommentAdd(env *Env, opts commentAddOptions, args []string) error {
 
 	if opts.messageFile == "" && opts.message == "" {
 		if opts.nonInteractive {
-			env.err.Println("No message given. Use -m or -F option to specify a message. Aborting.")
+			env.Err.Println("No message given. Use -m or -F option to specify a message. Aborting.")
 			return nil
 		}
-		opts.message, err = input.BugCommentEditorInput(env.backend, "")
+		opts.message, err = input.BugCommentEditorInput(env.Backend, "")
 		if err == input.ErrEmptyMessage {
-			env.err.Println("Empty message, aborting.")
+			env.Err.Println("Empty message, aborting.")
 			return nil
 		}
 		if err != nil {

commands/bug/bug_comment_add_test.go 🔗

@@ -0,0 +1,18 @@
+package bugcmd
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/MichaelMure/git-bug/commands/bug/testenv"
+)
+
+func TestBugCommentNew(t *testing.T) {
+	const golden = "testdata/comment/add"
+
+	env, bugID, _ := testenv.NewTestEnvAndBugWithComment(t)
+
+	require.NoError(t, runBugComment(env, []string{bugID.String()}))
+	requireCommentsEqual(t, golden, env)
+}

commands/comment_edit.go → commands/bug/bug_comment_edit.go 🔗

@@ -1,28 +1,29 @@
-package commands
+package bugcmd
 
 import (
 	"github.com/spf13/cobra"
 
+	"github.com/MichaelMure/git-bug/commands/execenv"
 	"github.com/MichaelMure/git-bug/commands/input"
 )
 
-type commentEditOptions struct {
+type bugCommentEditOptions struct {
 	messageFile    string
 	message        string
 	nonInteractive bool
 }
 
-func newCommentEditCommand() *cobra.Command {
-	env := newEnv()
-	options := commentEditOptions{}
+func newBugCommentEditCommand() *cobra.Command {
+	env := execenv.NewEnv()
+	options := bugCommentEditOptions{}
 
 	cmd := &cobra.Command{
 		Use:     "edit [COMMENT_ID]",
-		Short:   "Edit an existing comment on a bug.",
+		Short:   "Edit an existing comment on a bug",
 		Args:    cobra.ExactArgs(1),
-		PreRunE: loadBackendEnsureUser(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runCommentEdit(env, options, args)
+		PreRunE: execenv.LoadBackendEnsureUser(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBugCommentEdit(env, options, args)
 		}),
 	}
 
@@ -39,8 +40,8 @@ func newCommentEditCommand() *cobra.Command {
 	return cmd
 }
 
-func runCommentEdit(env *Env, opts commentEditOptions, args []string) error {
-	b, commentId, err := env.backend.ResolveComment(args[0])
+func runBugCommentEdit(env *execenv.Env, opts bugCommentEditOptions, args []string) error {
+	b, commentId, err := env.Backend.ResolveComment(args[0])
 	if err != nil {
 		return err
 	}
@@ -54,12 +55,12 @@ func runCommentEdit(env *Env, opts commentEditOptions, args []string) error {
 
 	if opts.messageFile == "" && opts.message == "" {
 		if opts.nonInteractive {
-			env.err.Println("No message given. Use -m or -F option to specify a message. Aborting.")
+			env.Err.Println("No message given. Use -m or -F option to specify a message. Aborting.")
 			return nil
 		}
-		opts.message, err = input.BugCommentEditorInput(env.backend, "")
+		opts.message, err = input.BugCommentEditorInput(env.Backend, "")
 		if err == input.ErrEmptyMessage {
-			env.err.Println("Empty message, aborting.")
+			env.Err.Println("Empty message, aborting.")
 			return nil
 		}
 		if err != nil {

commands/bug/bug_comment_edit_test.go 🔗

@@ -0,0 +1,23 @@
+package bugcmd
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/MichaelMure/git-bug/commands/bug/testenv"
+)
+
+func TestBugCommentEdit(t *testing.T) {
+	const golden = "testdata/comment/edit"
+
+	env, bugID, commentID := testenv.NewTestEnvAndBugWithComment(t)
+
+	opts := bugCommentEditOptions{
+		message: "this is an altered bug comment",
+	}
+	require.NoError(t, runBugCommentEdit(env, opts, []string{commentID.Human()}))
+
+	require.NoError(t, runBugComment(env, []string{bugID.Human()}))
+	requireCommentsEqual(t, golden, env)
+}

commands/comment_test.go → commands/bug/bug_comment_test.go 🔗

@@ -1,4 +1,4 @@
-package commands
+package bugcmd
 
 import (
 	"fmt"
@@ -8,14 +8,18 @@ import (
 	"time"
 
 	"github.com/stretchr/testify/require"
+
+	"github.com/MichaelMure/git-bug/commands/bug/testenv"
+	"github.com/MichaelMure/git-bug/commands/cmdtest"
+	"github.com/MichaelMure/git-bug/commands/execenv"
 )
 
-func TestComment(t *testing.T) {
+func TestBugComment(t *testing.T) {
 	const golden = "testdata/comment/message-only"
 
-	env, bug := newTestEnvAndBug(t)
+	env, bug := testenv.NewTestEnvAndBug(t)
 
-	require.NoError(t, runComment(env.env, []string{bug}))
+	require.NoError(t, runBugComment(env, []string{bug.Human()}))
 
 	requireCommentsEqual(t, golden, env)
 }
@@ -37,7 +41,7 @@ type commentParser struct {
 	comments []parsedComment
 }
 
-func parseComments(t *testing.T, env *testEnv) []parsedComment {
+func parseComments(t *testing.T, env *execenv.Env) []parsedComment {
 	t.Helper()
 
 	parser := &commentParser{
@@ -48,7 +52,7 @@ func parseComments(t *testing.T, env *testEnv) []parsedComment {
 	comment := &parsedComment{}
 	parser.fn = parser.parseAuthor
 
-	for _, line := range strings.Split(env.out.String(), "\n") {
+	for _, line := range strings.Split(env.Out.String(), "\n") {
 		parser.fn(comment, line)
 	}
 
@@ -116,7 +120,7 @@ func normalizeParsedComments(t *testing.T, comments []parsedComment) []parsedCom
 	date, err := time.Parse(gitDateFormat, "Fri Aug 19 07:00:00 2022 +1900")
 	require.NoError(t, err)
 
-	out := []parsedComment{}
+	var out []parsedComment
 
 	for i, comment := range comments {
 		comment.id = fmt.Sprintf("%7x", prefix+i)
@@ -127,18 +131,18 @@ func normalizeParsedComments(t *testing.T, comments []parsedComment) []parsedCom
 	return out
 }
 
-func requireCommentsEqual(t *testing.T, golden string, env *testEnv) {
+func requireCommentsEqual(t *testing.T, golden string, env *execenv.Env) {
 	t.Helper()
 
-	const goldenFilePatter = "%s-%d-golden.txt"
+	const goldenFilePattern = "%s-%d-golden.txt"
 
 	comments := parseComments(t, env)
 	comments = normalizeParsedComments(t, comments)
 
-	if *update {
+	if *cmdtest.Update {
 		t.Log("Got here")
 		for i, comment := range comments {
-			fileName := fmt.Sprintf(goldenFilePatter, golden, i)
+			fileName := fmt.Sprintf(goldenFilePattern, golden, i)
 			require.NoError(t, ioutil.WriteFile(fileName, []byte(comment.message), 0644))
 		}
 	}
@@ -152,7 +156,7 @@ func requireCommentsEqual(t *testing.T, golden string, env *testEnv) {
 		require.Equal(t, fmt.Sprintf("%7x", prefix+i), comment.id)
 		require.Equal(t, date.Add(time.Duration(i)*time.Minute), comment.date)
 
-		fileName := fmt.Sprintf(goldenFilePatter, golden, i)
+		fileName := fmt.Sprintf(goldenFilePattern, golden, i)
 		exp, err := ioutil.ReadFile(fileName)
 		require.NoError(t, err)
 		require.Equal(t, strings.ReplaceAll(string(exp), "\r", ""), strings.ReplaceAll(comment.message, "\r", ""))

commands/bug/bug_deselect.go 🔗

@@ -0,0 +1,37 @@
+package bugcmd
+
+import (
+	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/commands/bug/select"
+	"github.com/MichaelMure/git-bug/commands/execenv"
+)
+
+func newBugDeselectCommand() *cobra.Command {
+	env := execenv.NewEnv()
+
+	cmd := &cobra.Command{
+		Use:   "deselect",
+		Short: "Clear the implicitly selected bug",
+		Example: `git bug select 2f15
+git bug comment
+git bug status
+git bug deselect
+`,
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBugDeselect(env)
+		}),
+	}
+
+	return cmd
+}
+
+func runBugDeselect(env *execenv.Env) error {
+	err := _select.Clear(env.Backend)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}

commands/bug/bug_label.go 🔗

@@ -0,0 +1,43 @@
+package bugcmd
+
+import (
+	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/commands/bug/select"
+	"github.com/MichaelMure/git-bug/commands/completion"
+	"github.com/MichaelMure/git-bug/commands/execenv"
+)
+
+func newBugLabelCommand() *cobra.Command {
+	env := execenv.NewEnv()
+
+	cmd := &cobra.Command{
+		Use:     "label [BUG_ID]",
+		Short:   "Display labels of a bug",
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBugLabel(env, args)
+		}),
+		ValidArgsFunction: completion.Bug(env),
+	}
+
+	cmd.AddCommand(newBugLabelNewCommand())
+	cmd.AddCommand(newBugLabelRmCommand())
+
+	return cmd
+}
+
+func runBugLabel(env *execenv.Env, args []string) error {
+	b, args, err := _select.ResolveBug(env.Backend, args)
+	if err != nil {
+		return err
+	}
+
+	snap := b.Snapshot()
+
+	for _, l := range snap.Labels {
+		env.Out.Println(l)
+	}
+
+	return nil
+}

commands/bug/bug_label_new.go 🔗

@@ -0,0 +1,47 @@
+package bugcmd
+
+import (
+	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/commands/bug/select"
+	"github.com/MichaelMure/git-bug/commands/completion"
+	"github.com/MichaelMure/git-bug/commands/execenv"
+	"github.com/MichaelMure/git-bug/util/text"
+)
+
+func newBugLabelNewCommand() *cobra.Command {
+	env := execenv.NewEnv()
+
+	cmd := &cobra.Command{
+		Use:     "new [BUG_ID] LABEL...",
+		Short:   "Add a label to a bug",
+		PreRunE: execenv.LoadBackendEnsureUser(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBugLabelNew(env, args)
+		}),
+		ValidArgsFunction: completion.BugAndLabels(env, true),
+	}
+
+	return cmd
+}
+
+func runBugLabelNew(env *execenv.Env, args []string) error {
+	b, args, err := _select.ResolveBug(env.Backend, args)
+	if err != nil {
+		return err
+	}
+
+	added := args
+
+	changes, _, err := b.ChangeLabels(text.CleanupOneLineArray(added), nil)
+
+	for _, change := range changes {
+		env.Out.Println(change)
+	}
+
+	if err != nil {
+		return err
+	}
+
+	return b.Commit()
+}

commands/bug/bug_label_rm.go 🔗

@@ -0,0 +1,47 @@
+package bugcmd
+
+import (
+	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/commands/bug/select"
+	"github.com/MichaelMure/git-bug/commands/completion"
+	"github.com/MichaelMure/git-bug/commands/execenv"
+	"github.com/MichaelMure/git-bug/util/text"
+)
+
+func newBugLabelRmCommand() *cobra.Command {
+	env := execenv.NewEnv()
+
+	cmd := &cobra.Command{
+		Use:     "rm [BUG_ID] LABEL...",
+		Short:   "Remove a label from a bug",
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBugLabelRm(env, args)
+		}),
+		ValidArgsFunction: completion.BugAndLabels(env, false),
+	}
+
+	return cmd
+}
+
+func runBugLabelRm(env *execenv.Env, args []string) error {
+	b, args, err := _select.ResolveBug(env.Backend, args)
+	if err != nil {
+		return err
+	}
+
+	removed := args
+
+	changes, _, err := b.ChangeLabels(nil, text.CleanupOneLineArray(removed))
+
+	for _, change := range changes {
+		env.Out.Println(change)
+	}
+
+	if err != nil {
+		return err
+	}
+
+	return b.Commit()
+}

commands/add.go → commands/bug/bug_new.go 🔗

@@ -1,29 +1,30 @@
-package commands
+package bugcmd
 
 import (
 	"github.com/spf13/cobra"
 
+	"github.com/MichaelMure/git-bug/commands/execenv"
 	"github.com/MichaelMure/git-bug/commands/input"
 	"github.com/MichaelMure/git-bug/util/text"
 )
 
-type addOptions struct {
+type bugNewOptions struct {
 	title          string
 	message        string
 	messageFile    string
 	nonInteractive bool
 }
 
-func newAddCommand() *cobra.Command {
-	env := newEnv()
-	options := addOptions{}
+func newBugNewCommand() *cobra.Command {
+	env := execenv.NewEnv()
+	options := bugNewOptions{}
 
 	cmd := &cobra.Command{
-		Use:     "add",
-		Short:   "Create a new bug.",
-		PreRunE: loadBackendEnsureUser(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runAdd(env, options)
+		Use:     "new",
+		Short:   "Create a new bug",
+		PreRunE: execenv.LoadBackendEnsureUser(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBugNew(env, options)
 		}),
 	}
 
@@ -41,7 +42,7 @@ func newAddCommand() *cobra.Command {
 	return cmd
 }
 
-func runAdd(env *Env, opts addOptions) error {
+func runBugNew(env *execenv.Env, opts bugNewOptions) error {
 	var err error
 	if opts.messageFile != "" && opts.message == "" {
 		opts.title, opts.message, err = input.BugCreateFileInput(opts.messageFile)
@@ -51,10 +52,10 @@ func runAdd(env *Env, opts addOptions) error {
 	}
 
 	if !opts.nonInteractive && opts.messageFile == "" && (opts.message == "" || opts.title == "") {
-		opts.title, opts.message, err = input.BugCreateEditorInput(env.backend, opts.title, opts.message)
+		opts.title, opts.message, err = input.BugCreateEditorInput(env.Backend, opts.title, opts.message)
 
 		if err == input.ErrEmptyTitle {
-			env.out.Println("Empty title, aborting.")
+			env.Out.Println("Empty title, aborting.")
 			return nil
 		}
 		if err != nil {
@@ -62,7 +63,7 @@ func runAdd(env *Env, opts addOptions) error {
 		}
 	}
 
-	b, _, err := env.backend.NewBug(
+	b, _, err := env.Backend.NewBug(
 		text.CleanupOneLine(opts.title),
 		text.Cleanup(opts.message),
 	)
@@ -70,7 +71,7 @@ func runAdd(env *Env, opts addOptions) error {
 		return err
 	}
 
-	env.out.Printf("%s created\n", b.Id().Human())
+	env.Out.Printf("%s created\n", b.Id().Human())
 
 	return nil
 }

commands/bug/bug_new_test.go 🔗

@@ -0,0 +1,21 @@
+package bugcmd
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/MichaelMure/git-bug/commands/bug/testenv"
+)
+
+func TestBugNew(t *testing.T) {
+	env, _ := testenv.NewTestEnvAndUser(t)
+
+	err := runBugNew(env, bugNewOptions{
+		nonInteractive: true,
+		message:        "message",
+		title:          "title",
+	})
+	require.NoError(t, err)
+	require.Regexp(t, "^[0-9A-Fa-f]{7} created\n$", env.Out.String())
+}

commands/bug/bug_rm.go 🔗

@@ -0,0 +1,46 @@
+package bugcmd
+
+import (
+	"errors"
+
+	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/commands/completion"
+	"github.com/MichaelMure/git-bug/commands/execenv"
+)
+
+func newBugRmCommand() *cobra.Command {
+	env := execenv.NewEnv()
+
+	cmd := &cobra.Command{
+		Use:     "rm BUG_ID",
+		Short:   "Remove an existing bug",
+		Long:    "Remove an existing bug in the local repository. Note removing bugs that were imported from bridges will not remove the bug on the remote, and will only remove the local copy of the bug.",
+		PreRunE: execenv.LoadBackendEnsureUser(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBugRm(env, args)
+		}),
+		ValidArgsFunction: completion.Bug(env),
+	}
+
+	flags := cmd.Flags()
+	flags.SortFlags = false
+
+	return cmd
+}
+
+func runBugRm(env *execenv.Env, args []string) (err error) {
+	if len(args) == 0 {
+		return errors.New("you must provide a bug prefix to remove")
+	}
+
+	err = env.Backend.RemoveBug(args[0])
+
+	if err != nil {
+		return
+	}
+
+	env.Out.Printf("bug %s removed\n", args[0])
+
+	return
+}

commands/bug/bug_rm_test.go 🔗

@@ -0,0 +1,19 @@
+package bugcmd
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/MichaelMure/git-bug/commands/bug/testenv"
+)
+
+func TestBugRm(t *testing.T) {
+	env, bugID := testenv.NewTestEnvAndBug(t)
+
+	exp := "bug " + bugID.Human() + " removed\n"
+
+	require.NoError(t, runBugRm(env, []string{bugID.Human()}))
+	require.Equal(t, exp, env.Out.String())
+	env.Out.Reset()
+}

commands/bug/bug_select.go 🔗

@@ -0,0 +1,62 @@
+package bugcmd
+
+import (
+	"errors"
+
+	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/commands/bug/select"
+	"github.com/MichaelMure/git-bug/commands/completion"
+	"github.com/MichaelMure/git-bug/commands/execenv"
+)
+
+func newBugSelectCommand() *cobra.Command {
+	env := execenv.NewEnv()
+
+	cmd := &cobra.Command{
+		Use:   "select BUG_ID",
+		Short: "Select a bug for implicit use in future commands",
+		Example: `git bug select 2f15
+git bug comment
+git bug status
+`,
+		Long: `Select a bug for implicit use in future commands.
+
+This command allows you to omit any bug ID argument, for example:
+  git bug show
+instead of
+  git bug show 2f153ca
+
+The complementary command is "git bug deselect" performing the opposite operation.
+`,
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBugSelect(env, args)
+		}),
+		ValidArgsFunction: completion.Bug(env),
+	}
+
+	return cmd
+}
+
+func runBugSelect(env *execenv.Env, args []string) error {
+	if len(args) == 0 {
+		return errors.New("You must provide a bug id")
+	}
+
+	prefix := args[0]
+
+	b, err := env.Backend.ResolveBugPrefix(prefix)
+	if err != nil {
+		return err
+	}
+
+	err = _select.Select(env.Backend, b.Id())
+	if err != nil {
+		return err
+	}
+
+	env.Out.Printf("selected bug %s: %s\n", b.Id().Human(), b.Snapshot().Title)
+
+	return nil
+}

commands/show.go → commands/bug/bug_show.go 🔗

@@ -1,4 +1,4 @@
-package commands
+package bugcmd
 
 import (
 	"encoding/json"
@@ -8,28 +8,31 @@ import (
 
 	"github.com/spf13/cobra"
 
-	_select "github.com/MichaelMure/git-bug/commands/select"
+	"github.com/MichaelMure/git-bug/commands/bug/select"
+	"github.com/MichaelMure/git-bug/commands/cmdjson"
+	"github.com/MichaelMure/git-bug/commands/completion"
+	"github.com/MichaelMure/git-bug/commands/execenv"
 	"github.com/MichaelMure/git-bug/entities/bug"
 	"github.com/MichaelMure/git-bug/util/colors"
 )
 
-type showOptions struct {
+type bugShowOptions struct {
 	fields string
 	format string
 }
 
-func newShowCommand() *cobra.Command {
-	env := newEnv()
-	options := showOptions{}
+func newBugShowCommand() *cobra.Command {
+	env := execenv.NewEnv()
+	options := bugShowOptions{}
 
 	cmd := &cobra.Command{
-		Use:     "show [ID]",
-		Short:   "Display the details of a bug.",
-		PreRunE: loadBackend(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runShow(env, options, args)
+		Use:     "show [BUG_ID]",
+		Short:   "Display the details of a bug",
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBugShow(env, options, args)
 		}),
-		ValidArgsFunction: completeBug(env),
+		ValidArgsFunction: completion.Bug(env),
 	}
 
 	flags := cmd.Flags()
@@ -39,15 +42,15 @@ func newShowCommand() *cobra.Command {
 		"id", "labels", "shortId", "status", "title", "actors", "participants"}
 	flags.StringVarP(&options.fields, "field", "", "",
 		"Select field to display. Valid values are ["+strings.Join(fields, ",")+"]")
-	cmd.RegisterFlagCompletionFunc("by", completeFrom(fields))
+	cmd.RegisterFlagCompletionFunc("by", completion.From(fields))
 	flags.StringVarP(&options.format, "format", "f", "default",
 		"Select the output formatting style. Valid values are [default,json,org-mode]")
 
 	return cmd
 }
 
-func runShow(env *Env, opts showOptions, args []string) error {
-	b, args, err := _select.ResolveBug(env.backend, args)
+func runBugShow(env *execenv.Env, opts bugShowOptions, args []string) error {
+	b, args, err := _select.ResolveBug(env.Backend, args)
 	if err != nil {
 		return err
 	}
@@ -61,35 +64,35 @@ func runShow(env *Env, opts showOptions, args []string) error {
 	if opts.fields != "" {
 		switch opts.fields {
 		case "author":
-			env.out.Printf("%s\n", snap.Author.DisplayName())
+			env.Out.Printf("%s\n", snap.Author.DisplayName())
 		case "authorEmail":
-			env.out.Printf("%s\n", snap.Author.Email())
+			env.Out.Printf("%s\n", snap.Author.Email())
 		case "createTime":
-			env.out.Printf("%s\n", snap.CreateTime.String())
+			env.Out.Printf("%s\n", snap.CreateTime.String())
 		case "lastEdit":
-			env.out.Printf("%s\n", snap.EditTime().String())
+			env.Out.Printf("%s\n", snap.EditTime().String())
 		case "humanId":
-			env.out.Printf("%s\n", snap.Id().Human())
+			env.Out.Printf("%s\n", snap.Id().Human())
 		case "id":
-			env.out.Printf("%s\n", snap.Id())
+			env.Out.Printf("%s\n", snap.Id())
 		case "labels":
 			for _, l := range snap.Labels {
-				env.out.Printf("%s\n", l.String())
+				env.Out.Printf("%s\n", l.String())
 			}
 		case "actors":
 			for _, a := range snap.Actors {
-				env.out.Printf("%s\n", a.DisplayName())
+				env.Out.Printf("%s\n", a.DisplayName())
 			}
 		case "participants":
 			for _, p := range snap.Participants {
-				env.out.Printf("%s\n", p.DisplayName())
+				env.Out.Printf("%s\n", p.DisplayName())
 			}
 		case "shortId":
-			env.out.Printf("%s\n", snap.Id().Human())
+			env.Out.Printf("%s\n", snap.Id().Human())
 		case "status":
-			env.out.Printf("%s\n", snap.Status)
+			env.Out.Printf("%s\n", snap.Status)
 		case "title":
-			env.out.Printf("%s\n", snap.Title)
+			env.Out.Printf("%s\n", snap.Title)
 		default:
 			return fmt.Errorf("\nUnsupported field: %s\n", opts.fields)
 		}
@@ -109,20 +112,20 @@ func runShow(env *Env, opts showOptions, args []string) error {
 	}
 }
 
-func showDefaultFormatter(env *Env, snapshot *bug.Snapshot) error {
+func showDefaultFormatter(env *execenv.Env, snapshot *bug.Snapshot) error {
 	// Header
-	env.out.Printf("%s [%s] %s\n\n",
+	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",
+	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",
+	env.Out.Printf("This was last edited at %s\n\n",
 		snapshot.EditTime().String(),
 	)
 
@@ -132,7 +135,7 @@ func showDefaultFormatter(env *Env, snapshot *bug.Snapshot) error {
 		labels[i] = string(snapshot.Labels[i])
 	}
 
-	env.out.Printf("labels: %s\n",
+	env.Out.Printf("labels: %s\n",
 		strings.Join(labels, ", "),
 	)
 
@@ -142,7 +145,7 @@ func showDefaultFormatter(env *Env, snapshot *bug.Snapshot) error {
 		actors[i] = snapshot.Actors[i].DisplayName()
 	}
 
-	env.out.Printf("actors: %s\n",
+	env.Out.Printf("actors: %s\n",
 		strings.Join(actors, ", "),
 	)
 
@@ -152,7 +155,7 @@ func showDefaultFormatter(env *Env, snapshot *bug.Snapshot) error {
 		participants[i] = snapshot.Participants[i].DisplayName()
 	}
 
-	env.out.Printf("participants: %s\n\n",
+	env.Out.Printf("participants: %s\n\n",
 		strings.Join(participants, ", "),
 	)
 
@@ -161,7 +164,7 @@ func showDefaultFormatter(env *Env, snapshot *bug.Snapshot) error {
 
 	for i, comment := range snapshot.Comments {
 		var message string
-		env.out.Printf("%s%s #%d %s <%s>\n\n",
+		env.Out.Printf("%s%s #%d %s <%s>\n\n",
 			indent,
 			comment.CombinedId().Human(),
 			i,
@@ -175,7 +178,7 @@ func showDefaultFormatter(env *Env, snapshot *bug.Snapshot) error {
 			message = comment.Message
 		}
 
-		env.out.Printf("%s%s\n\n\n",
+		env.Out.Printf("%s%s\n\n\n",
 			indent,
 			message,
 		)
@@ -185,85 +188,85 @@ func showDefaultFormatter(env *Env, snapshot *bug.Snapshot) error {
 }
 
 type JSONBugSnapshot struct {
-	Id           string         `json:"id"`
-	HumanId      string         `json:"human_id"`
-	CreateTime   JSONTime       `json:"create_time"`
-	EditTime     JSONTime       `json:"edit_time"`
-	Status       string         `json:"status"`
-	Labels       []bug.Label    `json:"labels"`
-	Title        string         `json:"title"`
-	Author       JSONIdentity   `json:"author"`
-	Actors       []JSONIdentity `json:"actors"`
-	Participants []JSONIdentity `json:"participants"`
-	Comments     []JSONComment  `json:"comments"`
+	Id           string             `json:"id"`
+	HumanId      string             `json:"human_id"`
+	CreateTime   cmdjson.Time       `json:"create_time"`
+	EditTime     cmdjson.Time       `json:"edit_time"`
+	Status       string             `json:"status"`
+	Labels       []bug.Label        `json:"labels"`
+	Title        string             `json:"title"`
+	Author       cmdjson.Identity   `json:"author"`
+	Actors       []cmdjson.Identity `json:"actors"`
+	Participants []cmdjson.Identity `json:"participants"`
+	Comments     []JSONBugComment   `json:"comments"`
 }
 
-type JSONComment struct {
-	Id      string       `json:"id"`
-	HumanId string       `json:"human_id"`
-	Author  JSONIdentity `json:"author"`
-	Message string       `json:"message"`
+type JSONBugComment struct {
+	Id      string           `json:"id"`
+	HumanId string           `json:"human_id"`
+	Author  cmdjson.Identity `json:"author"`
+	Message string           `json:"message"`
 }
 
-func NewJSONComment(comment bug.Comment) JSONComment {
-	return JSONComment{
+func NewJSONComment(comment bug.Comment) JSONBugComment {
+	return JSONBugComment{
 		Id:      comment.CombinedId().String(),
 		HumanId: comment.CombinedId().Human(),
-		Author:  NewJSONIdentity(comment.Author),
+		Author:  cmdjson.NewIdentity(comment.Author),
 		Message: comment.Message,
 	}
 }
 
-func showJsonFormatter(env *Env, snapshot *bug.Snapshot) error {
+func showJsonFormatter(env *execenv.Env, snapshot *bug.Snapshot) error {
 	jsonBug := JSONBugSnapshot{
 		Id:         snapshot.Id().String(),
 		HumanId:    snapshot.Id().Human(),
-		CreateTime: NewJSONTime(snapshot.CreateTime, 0),
-		EditTime:   NewJSONTime(snapshot.EditTime(), 0),
+		CreateTime: cmdjson.NewTime(snapshot.CreateTime, 0),
+		EditTime:   cmdjson.NewTime(snapshot.EditTime(), 0),
 		Status:     snapshot.Status.String(),
 		Labels:     snapshot.Labels,
 		Title:      snapshot.Title,
-		Author:     NewJSONIdentity(snapshot.Author),
+		Author:     cmdjson.NewIdentity(snapshot.Author),
 	}
 
-	jsonBug.Actors = make([]JSONIdentity, len(snapshot.Actors))
+	jsonBug.Actors = make([]cmdjson.Identity, len(snapshot.Actors))
 	for i, element := range snapshot.Actors {
-		jsonBug.Actors[i] = NewJSONIdentity(element)
+		jsonBug.Actors[i] = cmdjson.NewIdentity(element)
 	}
 
-	jsonBug.Participants = make([]JSONIdentity, len(snapshot.Participants))
+	jsonBug.Participants = make([]cmdjson.Identity, len(snapshot.Participants))
 	for i, element := range snapshot.Participants {
-		jsonBug.Participants[i] = NewJSONIdentity(element)
+		jsonBug.Participants[i] = cmdjson.NewIdentity(element)
 	}
 
-	jsonBug.Comments = make([]JSONComment, len(snapshot.Comments))
+	jsonBug.Comments = make([]JSONBugComment, len(snapshot.Comments))
 	for i, comment := range snapshot.Comments {
 		jsonBug.Comments[i] = NewJSONComment(comment)
 	}
 
 	jsonObject, _ := json.MarshalIndent(jsonBug, "", "    ")
-	env.out.Printf("%s\n", jsonObject)
+	env.Out.Printf("%s\n", jsonObject)
 
 	return nil
 }
 
-func showOrgModeFormatter(env *Env, snapshot *bug.Snapshot) error {
+func showOrgModeFormatter(env *execenv.Env, snapshot *bug.Snapshot) error {
 	// Header
-	env.out.Printf("%s [%s] %s\n",
+	env.Out.Printf("%s [%s] %s\n",
 		snapshot.Id().Human(),
 		snapshot.Status,
 		snapshot.Title,
 	)
 
-	env.out.Printf("* Author: %s\n",
+	env.Out.Printf("* Author: %s\n",
 		snapshot.Author.DisplayName(),
 	)
 
-	env.out.Printf("* Creation Time: %s\n",
+	env.Out.Printf("* Creation Time: %s\n",
 		snapshot.CreateTime.String(),
 	)
 
-	env.out.Printf("* Last Edit: %s\n",
+	env.Out.Printf("* Last Edit: %s\n",
 		snapshot.EditTime().String(),
 	)
 
@@ -273,9 +276,9 @@ func showOrgModeFormatter(env *Env, snapshot *bug.Snapshot) error {
 		labels[i] = string(label)
 	}
 
-	env.out.Printf("* Labels:\n")
+	env.Out.Printf("* Labels:\n")
 	if len(labels) > 0 {
-		env.out.Printf("** %s\n",
+		env.Out.Printf("** %s\n",
 			strings.Join(labels, "\n** "),
 		)
 	}
@@ -289,7 +292,7 @@ func showOrgModeFormatter(env *Env, snapshot *bug.Snapshot) error {
 		)
 	}
 
-	env.out.Printf("* Actors:\n** %s\n",
+	env.Out.Printf("* Actors:\n** %s\n",
 		strings.Join(actors, "\n** "),
 	)
 
@@ -302,15 +305,15 @@ func showOrgModeFormatter(env *Env, snapshot *bug.Snapshot) error {
 		)
 	}
 
-	env.out.Printf("* Participants:\n** %s\n",
+	env.Out.Printf("* Participants:\n** %s\n",
 		strings.Join(participants, "\n** "),
 	)
 
-	env.out.Printf("* Comments:\n")
+	env.Out.Printf("* Comments:\n")
 
 	for i, comment := range snapshot.Comments {
 		var message string
-		env.out.Printf("** #%d %s\n",
+		env.Out.Printf("** #%d %s\n",
 			i, comment.Author.DisplayName())
 
 		if comment.Message == "" {
@@ -319,7 +322,7 @@ func showOrgModeFormatter(env *Env, snapshot *bug.Snapshot) error {
 			message = strings.ReplaceAll(comment.Message, "\n", "\n: ")
 		}
 
-		env.out.Printf(": %s\n", message)
+		env.Out.Printf(": %s\n", message)
 	}
 
 	return nil

commands/bug/bug_status.go 🔗

@@ -0,0 +1,41 @@
+package bugcmd
+
+import (
+	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/commands/bug/select"
+	"github.com/MichaelMure/git-bug/commands/completion"
+	"github.com/MichaelMure/git-bug/commands/execenv"
+)
+
+func newBugStatusCommand() *cobra.Command {
+	env := execenv.NewEnv()
+
+	cmd := &cobra.Command{
+		Use:     "status [BUG_ID]",
+		Short:   "Display the status of a bug",
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBugStatus(env, args)
+		}),
+		ValidArgsFunction: completion.Bug(env),
+	}
+
+	cmd.AddCommand(newBugStatusCloseCommand())
+	cmd.AddCommand(newBugStatusOpenCommand())
+
+	return cmd
+}
+
+func runBugStatus(env *execenv.Env, args []string) error {
+	b, args, err := _select.ResolveBug(env.Backend, args)
+	if err != nil {
+		return err
+	}
+
+	snap := b.Snapshot()
+
+	env.Out.Println(snap.Status)
+
+	return nil
+}

commands/bug/bug_status_close.go 🔗

@@ -0,0 +1,39 @@
+package bugcmd
+
+import (
+	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/commands/bug/select"
+	"github.com/MichaelMure/git-bug/commands/completion"
+	"github.com/MichaelMure/git-bug/commands/execenv"
+)
+
+func newBugStatusCloseCommand() *cobra.Command {
+	env := execenv.NewEnv()
+
+	cmd := &cobra.Command{
+		Use:     "close [BUG_ID]",
+		Short:   "Mark a bug as closed",
+		PreRunE: execenv.LoadBackendEnsureUser(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBugStatusClose(env, args)
+		}),
+		ValidArgsFunction: completion.Bug(env),
+	}
+
+	return cmd
+}
+
+func runBugStatusClose(env *execenv.Env, args []string) error {
+	b, args, err := _select.ResolveBug(env.Backend, args)
+	if err != nil {
+		return err
+	}
+
+	_, err = b.Close()
+	if err != nil {
+		return err
+	}
+
+	return b.Commit()
+}

commands/bug/bug_status_open.go 🔗

@@ -0,0 +1,39 @@
+package bugcmd
+
+import (
+	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/commands/bug/select"
+	"github.com/MichaelMure/git-bug/commands/completion"
+	"github.com/MichaelMure/git-bug/commands/execenv"
+)
+
+func newBugStatusOpenCommand() *cobra.Command {
+	env := execenv.NewEnv()
+
+	cmd := &cobra.Command{
+		Use:     "open [BUG_ID]",
+		Short:   "Mark a bug as open",
+		PreRunE: execenv.LoadBackendEnsureUser(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBugStatusOpen(env, args)
+		}),
+		ValidArgsFunction: completion.Bug(env),
+	}
+
+	return cmd
+}
+
+func runBugStatusOpen(env *execenv.Env, args []string) error {
+	b, args, err := _select.ResolveBug(env.Backend, args)
+	if err != nil {
+		return err
+	}
+
+	_, err = b.Open()
+	if err != nil {
+		return err
+	}
+
+	return b.Commit()
+}

commands/ls_test.go → commands/bug/bug_test.go 🔗

@@ -1,4 +1,4 @@
-package commands
+package bugcmd
 
 import (
 	"encoding/json"
@@ -6,6 +6,8 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/require"
+
+	"github.com/MichaelMure/git-bug/commands/bug/testenv"
 )
 
 func Test_repairQuery(t *testing.T) {
@@ -44,7 +46,7 @@ func Test_repairQuery(t *testing.T) {
 	}
 }
 
-func TestLs_Format(t *testing.T) {
+func TestBug_Format(t *testing.T) {
 	const expOrgMode = `^#+TODO: OPEN | CLOSED
 [*] OPEN   [0-9a-f]{7} \[\d\d\d\d-\d\d-\d\d [[:alpha:]]{3} \d\d:\d\d\] John Doe: this is a bug title ::
 [*]{2} Last Edited: \[\d\d\d\d-\d\d-\d\d [[:alpha:]]{3} \d\d:\d\d\]
@@ -66,7 +68,7 @@ $`
 	}
 
 	for _, testcase := range cases {
-		opts := lsOptions{
+		opts := bugOptions{
 			sortDirection: "asc",
 			sortBy:        "creation",
 			outputFormat:  testcase.format,
@@ -75,26 +77,26 @@ $`
 		name := fmt.Sprintf("with %s format", testcase.format)
 
 		t.Run(name, func(t *testing.T) {
-			env, _ := newTestEnvAndBug(t)
+			env, _ := testenv.NewTestEnvAndBug(t)
 
-			require.NoError(t, runLs(env.env, opts, []string{}))
-			require.Regexp(t, testcase.exp, env.out.String())
+			require.NoError(t, runBug(env, opts, []string{}))
+			require.Regexp(t, testcase.exp, env.Out.String())
 		})
 	}
 
 	t.Run("with JSON format", func(t *testing.T) {
-		opts := lsOptions{
+		opts := bugOptions{
 			sortDirection: "asc",
 			sortBy:        "creation",
 			outputFormat:  "json",
 		}
 
-		env, _ := newTestEnvAndBug(t)
+		env, _ := testenv.NewTestEnvAndBug(t)
 
-		require.NoError(t, runLs(env.env, opts, []string{}))
+		require.NoError(t, runBug(env, opts, []string{}))
 
-		bugs := []JSONBugExcerpt{}
-		require.NoError(t, json.Unmarshal(env.out.Bytes(), &bugs))
+		var bugs []JSONBugExcerpt
+		require.NoError(t, json.Unmarshal(env.Out.Bytes(), &bugs))
 
 		require.Len(t, bugs, 1)
 	})

commands/bug/bug_title.go 🔗

@@ -0,0 +1,40 @@
+package bugcmd
+
+import (
+	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/commands/bug/select"
+	"github.com/MichaelMure/git-bug/commands/completion"
+	"github.com/MichaelMure/git-bug/commands/execenv"
+)
+
+func newBugTitleCommand() *cobra.Command {
+	env := execenv.NewEnv()
+
+	cmd := &cobra.Command{
+		Use:     "title [BUG_ID]",
+		Short:   "Display the title of a bug",
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBugTitle(env, args)
+		}),
+		ValidArgsFunction: completion.Bug(env),
+	}
+
+	cmd.AddCommand(newBugTitleEditCommand())
+
+	return cmd
+}
+
+func runBugTitle(env *execenv.Env, args []string) error {
+	b, args, err := _select.ResolveBug(env.Backend, args)
+	if err != nil {
+		return err
+	}
+
+	snap := b.Snapshot()
+
+	env.Out.Println(snap.Title)
+
+	return nil
+}

commands/bug/bug_title_edit.go 🔗

@@ -0,0 +1,76 @@
+package bugcmd
+
+import (
+	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/commands/bug/select"
+	"github.com/MichaelMure/git-bug/commands/completion"
+	"github.com/MichaelMure/git-bug/commands/execenv"
+	"github.com/MichaelMure/git-bug/commands/input"
+	"github.com/MichaelMure/git-bug/util/text"
+)
+
+type bugTitleEditOptions struct {
+	title          string
+	nonInteractive bool
+}
+
+func newBugTitleEditCommand() *cobra.Command {
+	env := execenv.NewEnv()
+	options := bugTitleEditOptions{}
+
+	cmd := &cobra.Command{
+		Use:     "edit [BUG_ID]",
+		Short:   "Edit a title of a bug",
+		PreRunE: execenv.LoadBackendEnsureUser(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runBugTitleEdit(env, options, args)
+		}),
+		ValidArgsFunction: completion.Bug(env),
+	}
+
+	flags := cmd.Flags()
+	flags.SortFlags = false
+
+	flags.StringVarP(&options.title, "title", "t", "",
+		"Provide a title to describe the issue",
+	)
+	flags.BoolVar(&options.nonInteractive, "non-interactive", false, "Do not ask for user input")
+
+	return cmd
+}
+
+func runBugTitleEdit(env *execenv.Env, opts bugTitleEditOptions, args []string) error {
+	b, args, err := _select.ResolveBug(env.Backend, args)
+	if err != nil {
+		return err
+	}
+
+	snap := b.Snapshot()
+
+	if opts.title == "" {
+		if opts.nonInteractive {
+			env.Err.Println("No title given. Use -m or -F option to specify a title. Aborting.")
+			return nil
+		}
+		opts.title, err = input.BugTitleEditorInput(env.Repo, snap.Title)
+		if err == input.ErrEmptyTitle {
+			env.Out.Println("Empty title, aborting.")
+			return nil
+		}
+		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/bug/testenv/testenv.go 🔗

@@ -0,0 +1,63 @@
+package testenv
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/MichaelMure/git-bug/commands/execenv"
+	"github.com/MichaelMure/git-bug/entity"
+)
+
+const (
+	testUserName  = "John Doe"
+	testUserEmail = "jdoe@example.com"
+)
+
+func NewTestEnvAndUser(t *testing.T) (*execenv.Env, entity.Id) {
+	t.Helper()
+
+	testEnv := execenv.NewTestEnv(t)
+
+	i, err := testEnv.Backend.NewIdentity(testUserName, testUserEmail)
+	require.NoError(t, err)
+
+	err = testEnv.Backend.SetUserIdentity(i)
+	require.NoError(t, err)
+
+	return testEnv, i.Id()
+}
+
+const (
+	testBugTitle   = "this is a bug title"
+	testBugMessage = "this is a bug message"
+)
+
+func NewTestEnvAndBug(t *testing.T) (*execenv.Env, entity.Id) {
+	t.Helper()
+
+	testEnv, _ := NewTestEnvAndUser(t)
+
+	b, _, err := testEnv.Backend.NewBug(testBugTitle, testBugMessage)
+	require.NoError(t, err)
+
+	return testEnv, b.Id()
+}
+
+const (
+	testCommentMessage = "this is a bug comment"
+)
+
+func NewTestEnvAndBugWithComment(t *testing.T) (*execenv.Env, entity.Id, entity.CombinedId) {
+	t.Helper()
+
+	env, bugID := NewTestEnvAndBug(t)
+
+	b, err := env.Backend.ResolveBug(bugID)
+	require.NoError(t, err)
+
+	commentId, _, err := b.AddComment(testCommentMessage)
+	require.NoError(t, err)
+
+	return env, bugID, commentId
+}

commands/json_common.go → commands/cmdjson/json_common.go 🔗

@@ -1,4 +1,4 @@
-package commands
+package cmdjson
 
 import (
 	"time"
@@ -8,15 +8,15 @@ import (
 	"github.com/MichaelMure/git-bug/util/lamport"
 )
 
-type JSONIdentity struct {
+type Identity struct {
 	Id      string `json:"id"`
 	HumanId string `json:"human_id"`
 	Name    string `json:"name"`
 	Login   string `json:"login"`
 }
 
-func NewJSONIdentity(i identity.Interface) JSONIdentity {
-	return JSONIdentity{
+func NewIdentity(i identity.Interface) Identity {
+	return Identity{
 		Id:      i.Id().String(),
 		HumanId: i.Id().Human(),
 		Name:    i.Name(),
@@ -24,8 +24,8 @@ func NewJSONIdentity(i identity.Interface) JSONIdentity {
 	}
 }
 
-func NewJSONIdentityFromExcerpt(excerpt *cache.IdentityExcerpt) JSONIdentity {
-	return JSONIdentity{
+func NewIdentityFromExcerpt(excerpt *cache.IdentityExcerpt) Identity {
+	return Identity{
 		Id:      excerpt.Id.String(),
 		HumanId: excerpt.Id.Human(),
 		Name:    excerpt.Name,
@@ -33,21 +33,14 @@ func NewJSONIdentityFromExcerpt(excerpt *cache.IdentityExcerpt) JSONIdentity {
 	}
 }
 
-func NewJSONIdentityFromLegacyExcerpt(excerpt *cache.LegacyAuthorExcerpt) JSONIdentity {
-	return JSONIdentity{
-		Name:  excerpt.Name,
-		Login: excerpt.Login,
-	}
-}
-
-type JSONTime struct {
+type Time struct {
 	Timestamp int64        `json:"timestamp"`
 	Time      time.Time    `json:"time"`
 	Lamport   lamport.Time `json:"lamport,omitempty"`
 }
 
-func NewJSONTime(t time.Time, l lamport.Time) JSONTime {
-	return JSONTime{
+func NewTime(t time.Time, l lamport.Time) Time {
+	return Time{
 		Timestamp: t.Unix(),
 		Time:      t,
 		Lamport:   l,

commands/commands.go 🔗

@@ -4,6 +4,8 @@ import (
 	"sort"
 
 	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/commands/execenv"
 )
 
 type commandOptions struct {
@@ -11,7 +13,7 @@ type commandOptions struct {
 }
 
 func newCommandsCommand() *cobra.Command {
-	env := newEnv()
+	env := execenv.NewEnv()
 	options := commandOptions{}
 
 	cmd := &cobra.Command{
@@ -32,7 +34,7 @@ func newCommandsCommand() *cobra.Command {
 	return cmd
 }
 
-func runCommands(env *Env, opts commandOptions) error {
+func runCommands(env *execenv.Env, opts commandOptions) error {
 	first := true
 
 	var allCmds []*cobra.Command
@@ -49,24 +51,24 @@ func runCommands(env *Env, opts commandOptions) error {
 
 	for _, cmd := range allCmds {
 		if !first {
-			env.out.Println()
+			env.Out.Println()
 		}
 
 		first = false
 
 		if opts.desc {
-			env.out.Printf("# %s\n", cmd.Short)
+			env.Out.Printf("# %s\n", cmd.Short)
 		}
 
-		env.out.Print(cmd.UseLine())
+		env.Out.Print(cmd.UseLine())
 
 		if opts.desc {
-			env.out.Println()
+			env.Out.Println()
 		}
 	}
 
 	if !opts.desc {
-		env.out.Println()
+		env.Out.Println()
 	}
 
 	return nil

commands/comment.go 🔗

@@ -1,50 +0,0 @@
-package commands
-
-import (
-	text "github.com/MichaelMure/go-term-text"
-	"github.com/spf13/cobra"
-
-	_select "github.com/MichaelMure/git-bug/commands/select"
-	"github.com/MichaelMure/git-bug/util/colors"
-)
-
-func newCommentCommand() *cobra.Command {
-	env := newEnv()
-
-	cmd := &cobra.Command{
-		Use:     "comment [ID]",
-		Short:   "Display or add comments to a bug.",
-		PreRunE: loadBackend(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runComment(env, args)
-		}),
-		ValidArgsFunction: completeBug(env),
-	}
-
-	cmd.AddCommand(newCommentAddCommand())
-	cmd.AddCommand(newCommentEditCommand())
-
-	return cmd
-}
-
-func runComment(env *Env, args []string) error {
-	b, args, err := _select.ResolveBug(env.backend, args)
-	if err != nil {
-		return err
-	}
-
-	snap := b.Snapshot()
-
-	for i, comment := range snap.Comments {
-		if i != 0 {
-			env.out.Println()
-		}
-
-		env.out.Printf("Author: %s\n", colors.Magenta(comment.Author.DisplayName()))
-		env.out.Printf("Id: %s\n", colors.Cyan(comment.CombinedId().Human()))
-		env.out.Printf("Date: %s\n\n", comment.FormatTime())
-		env.out.Println(text.LeftPadLines(comment.Message, 4))
-	}
-
-	return nil
-}

commands/comment_add_test.go 🔗

@@ -1,33 +0,0 @@
-package commands
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/require"
-)
-
-func newTestEnvAndBugWithComment(t *testing.T) (*testEnv, string, string) {
-	t.Helper()
-
-	env, bugID := newTestEnvAndBug(t)
-
-	opts := commentAddOptions{
-		message: "this is a bug comment",
-	}
-	require.NoError(t, runCommentAdd(env.env, opts, []string{bugID}))
-	require.NoError(t, runComment(env.env, []string{bugID}))
-	comments := parseComments(t, env)
-	require.Len(t, comments, 2)
-
-	env.out.Reset()
-
-	return env, bugID, comments[1].id
-}
-
-func TestCommentAdd(t *testing.T) {
-	const golden = "testdata/comment/add"
-
-	env, bugID, _ := newTestEnvAndBugWithComment(t)
-	require.NoError(t, runComment(env.env, []string{bugID}))
-	requireCommentsEqual(t, golden, env)
-}

commands/comment_edit_test.go 🔗

@@ -1,21 +0,0 @@
-package commands
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/require"
-)
-
-func TestCommentEdit(t *testing.T) {
-	const golden = "testdata/comment/edit"
-
-	env, bugID, commentID := newTestEnvAndBugWithComment(t)
-
-	opts := commentEditOptions{
-		message: "this is an altered bug comment",
-	}
-	require.NoError(t, runCommentEdit(env.env, opts, []string{commentID}))
-
-	require.NoError(t, runComment(env.env, []string{bugID}))
-	requireCommentsEqual(t, golden, env)
-}

commands/helper_completion.go → commands/completion/helper_completion.go 🔗

@@ -1,4 +1,4 @@
-package commands
+package completion
 
 import (
 	"fmt"
@@ -10,28 +10,29 @@ import (
 	"github.com/MichaelMure/git-bug/bridge"
 	"github.com/MichaelMure/git-bug/bridge/core/auth"
 	"github.com/MichaelMure/git-bug/cache"
-	_select "github.com/MichaelMure/git-bug/commands/select"
+	"github.com/MichaelMure/git-bug/commands/bug/select"
+	"github.com/MichaelMure/git-bug/commands/execenv"
 	"github.com/MichaelMure/git-bug/entities/bug"
 )
 
-type validArgsFunction func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective)
+type ValidArgsFunction func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective)
 
-func completionHandlerError(err error) (completions []string, directives cobra.ShellCompDirective) {
+func handleError(err error) (completions []string, directives cobra.ShellCompDirective) {
 	return nil, cobra.ShellCompDirectiveError
 }
 
-func completeBridge(env *Env) validArgsFunction {
+func Bridge(env *execenv.Env) ValidArgsFunction {
 	return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
-		if err := loadBackend(env)(cmd, args); err != nil {
-			return completionHandlerError(err)
+		if err := execenv.LoadBackend(env)(cmd, args); err != nil {
+			return handleError(err)
 		}
 		defer func() {
-			_ = env.backend.Close()
+			_ = env.Backend.Close()
 		}()
 
-		bridges, err := bridge.ConfiguredBridges(env.backend)
+		bridges, err := bridge.ConfiguredBridges(env.Backend)
 		if err != nil {
-			return completionHandlerError(err)
+			return handleError(err)
 		}
 
 		completions = make([]string, len(bridges))
@@ -43,18 +44,18 @@ func completeBridge(env *Env) validArgsFunction {
 	}
 }
 
-func completeBridgeAuth(env *Env) validArgsFunction {
+func BridgeAuth(env *execenv.Env) ValidArgsFunction {
 	return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
-		if err := loadBackend(env)(cmd, args); err != nil {
-			return completionHandlerError(err)
+		if err := execenv.LoadBackend(env)(cmd, args); err != nil {
+			return handleError(err)
 		}
 		defer func() {
-			_ = env.backend.Close()
+			_ = env.Backend.Close()
 		}()
 
-		creds, err := auth.List(env.backend)
+		creds, err := auth.List(env.Backend)
 		if err != nil {
-			return completionHandlerError(err)
+			return handleError(err)
 		}
 
 		completions = make([]string, len(creds))
@@ -73,27 +74,27 @@ func completeBridgeAuth(env *Env) validArgsFunction {
 	}
 }
 
-func completeBug(env *Env) validArgsFunction {
+func Bug(env *execenv.Env) ValidArgsFunction {
 	return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
-		if err := loadBackend(env)(cmd, args); err != nil {
-			return completionHandlerError(err)
+		if err := execenv.LoadBackend(env)(cmd, args); err != nil {
+			return handleError(err)
 		}
 		defer func() {
-			_ = env.backend.Close()
+			_ = env.Backend.Close()
 		}()
 
-		return completeBugWithBackend(env.backend, toComplete)
+		return bugWithBackend(env.Backend, toComplete)
 	}
 }
 
-func completeBugWithBackend(backend *cache.RepoCache, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
+func bugWithBackend(backend *cache.RepoCache, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
 	allIds := backend.AllBugsIds()
 	bugExcerpt := make([]*cache.BugExcerpt, len(allIds))
 	for i, id := range allIds {
 		var err error
 		bugExcerpt[i], err = backend.ResolveBugExcerpt(id)
 		if err != nil {
-			return completionHandlerError(err)
+			return handleError(err)
 		}
 	}
 
@@ -106,22 +107,22 @@ func completeBugWithBackend(backend *cache.RepoCache, toComplete string) (comple
 	return completions, cobra.ShellCompDirectiveNoFileComp
 }
 
-func completeBugAndLabels(env *Env, addOrRemove bool) validArgsFunction {
+func BugAndLabels(env *execenv.Env, addOrRemove bool) ValidArgsFunction {
 	return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
-		if err := loadBackend(env)(cmd, args); err != nil {
-			return completionHandlerError(err)
+		if err := execenv.LoadBackend(env)(cmd, args); err != nil {
+			return handleError(err)
 		}
 		defer func() {
-			_ = env.backend.Close()
+			_ = env.Backend.Close()
 		}()
 
-		b, args, err := _select.ResolveBug(env.backend, args)
+		b, args, err := _select.ResolveBug(env.Backend, args)
 		if err == _select.ErrNoValidId {
 			// we need a bug first to complete labels
-			return completeBugWithBackend(env.backend, toComplete)
+			return bugWithBackend(env.Backend, toComplete)
 		}
 		if err != nil {
-			return completionHandlerError(err)
+			return handleError(err)
 		}
 
 		snap := b.Snapshot()
@@ -137,7 +138,7 @@ func completeBugAndLabels(env *Env, addOrRemove bool) validArgsFunction {
 				seenLabels[label] = true
 			}
 
-			allLabels := env.backend.ValidLabels()
+			allLabels := env.Backend.ValidLabels()
 			labels = make([]bug.Label, 0, len(allLabels))
 			for _, label := range allLabels {
 				if !seenLabels[label] {
@@ -162,24 +163,24 @@ func completeBugAndLabels(env *Env, addOrRemove bool) validArgsFunction {
 	}
 }
 
-func completeFrom(choices []string) validArgsFunction {
+func From(choices []string) ValidArgsFunction {
 	return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 		return choices, cobra.ShellCompDirectiveNoFileComp
 	}
 }
 
-func completeGitRemote(env *Env) validArgsFunction {
+func GitRemote(env *execenv.Env) ValidArgsFunction {
 	return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
-		if err := loadBackend(env)(cmd, args); err != nil {
-			return completionHandlerError(err)
+		if err := execenv.LoadBackend(env)(cmd, args); err != nil {
+			return handleError(err)
 		}
 		defer func() {
-			_ = env.backend.Close()
+			_ = env.Backend.Close()
 		}()
 
-		remoteMap, err := env.backend.GetRemotes()
+		remoteMap, err := env.Backend.GetRemotes()
 		if err != nil {
-			return completionHandlerError(err)
+			return handleError(err)
 		}
 		completions = make([]string, 0, len(remoteMap))
 		for remote, url := range remoteMap {
@@ -190,16 +191,16 @@ func completeGitRemote(env *Env) validArgsFunction {
 	}
 }
 
-func completeLabel(env *Env) validArgsFunction {
+func Label(env *execenv.Env) ValidArgsFunction {
 	return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
-		if err := loadBackend(env)(cmd, args); err != nil {
-			return completionHandlerError(err)
+		if err := execenv.LoadBackend(env)(cmd, args); err != nil {
+			return handleError(err)
 		}
 		defer func() {
-			_ = env.backend.Close()
+			_ = env.Backend.Close()
 		}()
 
-		labels := env.backend.ValidLabels()
+		labels := env.Backend.ValidLabels()
 		completions = make([]string, len(labels))
 		for i, label := range labels {
 			if strings.Contains(label.String(), " ") {
@@ -212,7 +213,7 @@ func completeLabel(env *Env) validArgsFunction {
 	}
 }
 
-func completeLs(env *Env) validArgsFunction {
+func Ls(env *execenv.Env) ValidArgsFunction {
 	return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
 		if strings.HasPrefix(toComplete, "status:") {
 			completions = append(completions, "status:open\tOpen bugs")
@@ -230,11 +231,11 @@ func completeLs(env *Env) validArgsFunction {
 		}
 
 		if needBackend {
-			if err := loadBackend(env)(cmd, args); err != nil {
-				return completionHandlerError(err)
+			if err := execenv.LoadBackend(env)(cmd, args); err != nil {
+				return handleError(err)
 			}
 			defer func() {
-				_ = env.backend.Close()
+				_ = env.Backend.Close()
 			}()
 		}
 
@@ -242,12 +243,12 @@ func completeLs(env *Env) validArgsFunction {
 			if !strings.HasPrefix(toComplete, key) {
 				continue
 			}
-			ids := env.backend.AllIdentityIds()
+			ids := env.Backend.AllIdentityIds()
 			completions = make([]string, len(ids))
 			for i, id := range ids {
-				user, err := env.backend.ResolveIdentityExcerpt(id)
+				user, err := env.Backend.ResolveIdentityExcerpt(id)
 				if err != nil {
-					return completionHandlerError(err)
+					return handleError(err)
 				}
 				var handle string
 				if user.Login != "" {
@@ -265,7 +266,7 @@ func completeLs(env *Env) validArgsFunction {
 			if !strings.HasPrefix(toComplete, key) {
 				continue
 			}
-			labels := env.backend.ValidLabels()
+			labels := env.Backend.ValidLabels()
 			completions = make([]string, len(labels))
 			for i, label := range labels {
 				if strings.Contains(label.String(), " ") {
@@ -290,21 +291,21 @@ func completeLs(env *Env) validArgsFunction {
 	}
 }
 
-func completeUser(env *Env) validArgsFunction {
+func User(env *execenv.Env) ValidArgsFunction {
 	return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
-		if err := loadBackend(env)(cmd, args); err != nil {
-			return completionHandlerError(err)
+		if err := execenv.LoadBackend(env)(cmd, args); err != nil {
+			return handleError(err)
 		}
 		defer func() {
-			_ = env.backend.Close()
+			_ = env.Backend.Close()
 		}()
 
-		ids := env.backend.AllIdentityIds()
+		ids := env.Backend.AllIdentityIds()
 		completions = make([]string, len(ids))
 		for i, id := range ids {
-			user, err := env.backend.ResolveIdentityExcerpt(id)
+			user, err := env.Backend.ResolveIdentityExcerpt(id)
 			if err != nil {
-				return completionHandlerError(err)
+				return handleError(err)
 			}
 			completions[i] = user.Id.Human() + "\t" + user.DisplayName()
 		}
@@ -312,21 +313,21 @@ func completeUser(env *Env) validArgsFunction {
 	}
 }
 
-func completeUserForQuery(env *Env) validArgsFunction {
+func UserForQuery(env *execenv.Env) ValidArgsFunction {
 	return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
-		if err := loadBackend(env)(cmd, args); err != nil {
-			return completionHandlerError(err)
+		if err := execenv.LoadBackend(env)(cmd, args); err != nil {
+			return handleError(err)
 		}
 		defer func() {
-			_ = env.backend.Close()
+			_ = env.Backend.Close()
 		}()
 
-		ids := env.backend.AllIdentityIds()
+		ids := env.Backend.AllIdentityIds()
 		completions = make([]string, len(ids))
 		for i, id := range ids {
-			user, err := env.backend.ResolveIdentityExcerpt(id)
+			user, err := env.Backend.ResolveIdentityExcerpt(id)
 			if err != nil {
-				return completionHandlerError(err)
+				return handleError(err)
 			}
 			var handle string
 			if user.Login != "" {

commands/deselect.go 🔗

@@ -1,36 +0,0 @@
-package commands
-
-import (
-	"github.com/spf13/cobra"
-
-	"github.com/MichaelMure/git-bug/commands/select"
-)
-
-func newDeselectCommand() *cobra.Command {
-	env := newEnv()
-
-	cmd := &cobra.Command{
-		Use:   "deselect",
-		Short: "Clear the implicitly selected bug.",
-		Example: `git bug select 2f15
-git bug comment
-git bug status
-git bug deselect
-`,
-		PreRunE: loadBackend(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runDeselect(env)
-		}),
-	}
-
-	return cmd
-}
-
-func runDeselect(env *Env) error {
-	err := _select.Clear(env.backend)
-	if err != nil {
-		return err
-	}
-
-	return nil
-}

commands/env_testing.go 🔗

@@ -1,40 +0,0 @@
-package commands
-
-import (
-	"bytes"
-	"testing"
-
-	"github.com/stretchr/testify/require"
-
-	"github.com/MichaelMure/git-bug/cache"
-	"github.com/MichaelMure/git-bug/repository"
-)
-
-type testEnv struct {
-	env *Env
-	out *bytes.Buffer
-}
-
-func newTestEnv(t *testing.T) *testEnv {
-	t.Helper()
-
-	repo := repository.CreateGoGitTestRepo(t, false)
-
-	buf := new(bytes.Buffer)
-
-	backend, err := cache.NewRepoCache(repo)
-	require.NoError(t, err)
-	t.Cleanup(func() {
-		backend.Close()
-	})
-
-	return &testEnv{
-		env: &Env{
-			repo:    repo,
-			backend: backend,
-			out:     out{Writer: buf},
-			err:     out{Writer: buf},
-		},
-		out: buf,
-	}
-}

commands/env.go → commands/execenv/env.go 🔗

@@ -1,4 +1,4 @@
-package commands
+package execenv
 
 import (
 	"fmt"
@@ -14,24 +14,43 @@ import (
 	"github.com/MichaelMure/git-bug/util/interrupt"
 )
 
+const RootCommandName = "git-bug"
+
 const gitBugNamespace = "git-bug"
 
 // Env is the environment of a command
 type Env struct {
-	repo    repository.ClockedRepo
-	backend *cache.RepoCache
-	out     out
-	err     out
+	Repo    repository.ClockedRepo
+	Backend *cache.RepoCache
+	Out     Out
+	Err     Out
 }
 
-func newEnv() *Env {
+func NewEnv() *Env {
 	return &Env{
-		repo: nil,
-		out:  out{Writer: os.Stdout},
-		err:  out{Writer: os.Stderr},
+		Repo: nil,
+		Out:  out{Writer: os.Stdout},
+		Err:  out{Writer: os.Stderr},
 	}
 }
 
+type Out interface {
+	io.Writer
+	Printf(format string, a ...interface{})
+	Print(a ...interface{})
+	Println(a ...interface{})
+
+	// String returns what have been written in the output before, as a string.
+	// This only works in test scenario.
+	String() string
+	// Bytes returns what have been written in the output before, as []byte.
+	// This only works in test scenario.
+	Bytes() []byte
+	// Reset clear what has been recorded as written in the output before.
+	// This only works in test scenario.
+	Reset()
+}
+
 type out struct {
 	io.Writer
 }
@@ -48,17 +67,29 @@ func (o out) Println(a ...interface{}) {
 	_, _ = fmt.Fprintln(o, a...)
 }
 
-// loadRepo is a pre-run function that load the repository for use in a command
-func loadRepo(env *Env) func(*cobra.Command, []string) error {
+func (o out) String() string {
+	panic("only work with a test env")
+}
+
+func (o out) Bytes() []byte {
+	panic("only work with a test env")
+}
+
+func (o out) Reset() {
+	panic("only work with a test env")
+}
+
+// LoadRepo is a pre-run function that load the repository for use in a command
+func LoadRepo(env *Env) func(*cobra.Command, []string) error {
 	return func(cmd *cobra.Command, args []string) error {
 		cwd, err := os.Getwd()
 		if err != nil {
 			return fmt.Errorf("unable to get the current working directory: %q", err)
 		}
 
-		env.repo, err = repository.OpenGoGitRepo(cwd, gitBugNamespace, []repository.ClockLoader{bug.ClockLoader})
+		env.Repo, err = repository.OpenGoGitRepo(cwd, gitBugNamespace, []repository.ClockLoader{bug.ClockLoader})
 		if err == repository.ErrNotARepo {
-			return fmt.Errorf("%s must be run from within a git repo", rootCommandName)
+			return fmt.Errorf("%s must be run from within a git Repo", RootCommandName)
 		}
 
 		if err != nil {
@@ -69,17 +100,17 @@ func loadRepo(env *Env) func(*cobra.Command, []string) error {
 	}
 }
 
-// loadRepoEnsureUser is the same as loadRepo, but also ensure that the user has configured
+// LoadRepoEnsureUser is the same as LoadRepo, but also ensure that the user has configured
 // an identity. Use this pre-run function when an error after using the configured user won't
 // do.
-func loadRepoEnsureUser(env *Env) func(*cobra.Command, []string) error {
+func LoadRepoEnsureUser(env *Env) func(*cobra.Command, []string) error {
 	return func(cmd *cobra.Command, args []string) error {
-		err := loadRepo(env)(cmd, args)
+		err := LoadRepo(env)(cmd, args)
 		if err != nil {
 			return err
 		}
 
-		_, err = identity.GetUserIdentity(env.repo)
+		_, err = identity.GetUserIdentity(env.Repo)
 		if err != nil {
 			return err
 		}
@@ -88,25 +119,25 @@ func loadRepoEnsureUser(env *Env) func(*cobra.Command, []string) error {
 	}
 }
 
-// loadBackend is a pre-run function that load the repository and the backend for use in a command
-// When using this function you also need to use closeBackend as a post-run
-func loadBackend(env *Env) func(*cobra.Command, []string) error {
+// LoadBackend is a pre-run function that load the repository and the Backend for use in a command
+// When using this function you also need to use CloseBackend as a post-run
+func LoadBackend(env *Env) func(*cobra.Command, []string) error {
 	return func(cmd *cobra.Command, args []string) error {
-		err := loadRepo(env)(cmd, args)
+		err := LoadRepo(env)(cmd, args)
 		if err != nil {
 			return err
 		}
 
-		env.backend, err = cache.NewRepoCache(env.repo)
+		env.Backend, err = cache.NewRepoCache(env.Repo)
 		if err != nil {
 			return err
 		}
 
 		cleaner := func(env *Env) interrupt.CleanerFunc {
 			return func() error {
-				if env.backend != nil {
-					err := env.backend.Close()
-					env.backend = nil
+				if env.Backend != nil {
+					err := env.Backend.Close()
+					env.Backend = nil
 					return err
 				}
 				return nil
@@ -119,17 +150,17 @@ func loadBackend(env *Env) func(*cobra.Command, []string) error {
 	}
 }
 
-// loadBackendEnsureUser is the same as loadBackend, but also ensure that the user has configured
+// LoadBackendEnsureUser is the same as LoadBackend, but also ensure that the user has configured
 // an identity. Use this pre-run function when an error after using the configured user won't
 // do.
-func loadBackendEnsureUser(env *Env) func(*cobra.Command, []string) error {
+func LoadBackendEnsureUser(env *Env) func(*cobra.Command, []string) error {
 	return func(cmd *cobra.Command, args []string) error {
-		err := loadBackend(env)(cmd, args)
+		err := LoadBackend(env)(cmd, args)
 		if err != nil {
 			return err
 		}
 
-		_, err = identity.GetUserIdentity(env.repo)
+		_, err = identity.GetUserIdentity(env.Repo)
 		if err != nil {
 			return err
 		}
@@ -138,18 +169,18 @@ func loadBackendEnsureUser(env *Env) func(*cobra.Command, []string) error {
 	}
 }
 
-// closeBackend is a wrapper for a RunE function that will close the backend properly
+// CloseBackend is a wrapper for a RunE function that will close the Backend properly
 // if it has been opened.
 // This wrapper style is necessary because a Cobra PostE function does not run if RunE return an error.
-func closeBackend(env *Env, runE func(cmd *cobra.Command, args []string) error) func(*cobra.Command, []string) error {
+func CloseBackend(env *Env, runE func(cmd *cobra.Command, args []string) error) func(*cobra.Command, []string) error {
 	return func(cmd *cobra.Command, args []string) error {
 		errRun := runE(cmd, args)
 
-		if env.backend == nil {
+		if env.Backend == nil {
 			return nil
 		}
-		err := env.backend.Close()
-		env.backend = nil
+		err := env.Backend.Close()
+		env.Backend = nil
 
 		// prioritize the RunE error
 		if errRun != nil {

commands/execenv/env_testing.go 🔗

@@ -0,0 +1,48 @@
+package execenv
+
+import (
+	"bytes"
+	"fmt"
+	"testing"
+
+	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/repository"
+	"github.com/stretchr/testify/require"
+)
+
+type TestOut struct {
+	*bytes.Buffer
+}
+
+func (te *TestOut) Printf(format string, a ...interface{}) {
+	_, _ = fmt.Fprintf(te.Buffer, format, a...)
+}
+
+func (te *TestOut) Print(a ...interface{}) {
+	_, _ = fmt.Fprint(te.Buffer, a...)
+}
+
+func (te *TestOut) Println(a ...interface{}) {
+	_, _ = fmt.Fprintln(te.Buffer, a...)
+}
+
+func NewTestEnv(t *testing.T) *Env {
+	t.Helper()
+
+	repo := repository.CreateGoGitTestRepo(t, false)
+
+	buf := new(bytes.Buffer)
+
+	backend, err := cache.NewRepoCache(repo)
+	require.NoError(t, err)
+	t.Cleanup(func() {
+		backend.Close()
+	})
+
+	return &Env{
+		Repo:    repo,
+		Backend: backend,
+		Out:     &TestOut{buf},
+		Err:     &TestOut{buf},
+	}
+}

commands/golden_test.go 🔗

@@ -1,5 +0,0 @@
-package commands
-
-import "flag"
-
-var update = flag.Bool("update", false, "update golden files")

commands/input/input.go 🔗

@@ -25,7 +25,7 @@ const messageFilename = "BUG_MESSAGE_EDITMSG"
 // ErrEmptyMessage is returned when the required message has not been entered
 var ErrEmptyMessage = errors.New("empty message")
 
-// ErrEmptyMessage is returned when the required title has not been entered
+// ErrEmptyTitle is returned when the required title has not been entered
 var ErrEmptyTitle = errors.New("empty title")
 
 const bugTitleCommentTemplate = `%s%s

commands/label.go 🔗

@@ -3,39 +3,32 @@ package commands
 import (
 	"github.com/spf13/cobra"
 
-	_select "github.com/MichaelMure/git-bug/commands/select"
+	"github.com/MichaelMure/git-bug/commands/execenv"
 )
 
 func newLabelCommand() *cobra.Command {
-	env := newEnv()
+	env := execenv.NewEnv()
 
 	cmd := &cobra.Command{
-		Use:     "label [ID]",
-		Short:   "Display, add or remove labels to/from a bug.",
-		PreRunE: loadBackend(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runLabel(env, args)
+		Use:   "label",
+		Short: "List valid labels",
+		Long: `List valid labels.
+
+Note: in the future, a proper label policy could be implemented where valid labels are defined in a configuration file. Until that, the default behavior is to return the list of labels already used.`,
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runLabel(env)
 		}),
-		ValidArgsFunction: completeBug(env),
 	}
 
-	cmd.AddCommand(newLabelAddCommand())
-	cmd.AddCommand(newLabelLsCommand())
-	cmd.AddCommand(newLabelRmCommand())
-
 	return cmd
 }
 
-func runLabel(env *Env, args []string) error {
-	b, _, err := _select.ResolveBug(env.backend, args)
-	if err != nil {
-		return err
-	}
-
-	snap := b.Snapshot()
+func runLabel(env *execenv.Env) error {
+	labels := env.Backend.ValidLabels()
 
-	for _, l := range snap.Labels {
-		env.out.Println(l)
+	for _, l := range labels {
+		env.Out.Println(l)
 	}
 
 	return nil

commands/label_add.go 🔗

@@ -1,45 +0,0 @@
-package commands
-
-import (
-	"github.com/spf13/cobra"
-
-	_select "github.com/MichaelMure/git-bug/commands/select"
-	"github.com/MichaelMure/git-bug/util/text"
-)
-
-func newLabelAddCommand() *cobra.Command {
-	env := newEnv()
-
-	cmd := &cobra.Command{
-		Use:     "add [ID] LABEL...",
-		Short:   "Add a label to a bug.",
-		PreRunE: loadBackendEnsureUser(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runLabelAdd(env, args)
-		}),
-		ValidArgsFunction: completeBugAndLabels(env, true),
-	}
-
-	return cmd
-}
-
-func runLabelAdd(env *Env, args []string) error {
-	b, args, err := _select.ResolveBug(env.backend, args)
-	if err != nil {
-		return err
-	}
-
-	added := args
-
-	changes, _, err := b.ChangeLabels(text.CleanupOneLineArray(added), nil)
-
-	for _, change := range changes {
-		env.out.Println(change)
-	}
-
-	if err != nil {
-		return err
-	}
-
-	return b.Commit()
-}

commands/label_ls.go 🔗

@@ -1,33 +0,0 @@
-package commands
-
-import (
-	"github.com/spf13/cobra"
-)
-
-func newLabelLsCommand() *cobra.Command {
-	env := newEnv()
-
-	cmd := &cobra.Command{
-		Use:   "ls",
-		Short: "List valid labels.",
-		Long: `List valid labels.
-
-Note: in the future, a proper label policy could be implemented where valid labels are defined in a configuration file. Until that, the default behavior is to return the list of labels already used.`,
-		PreRunE: loadBackend(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runLabelLs(env)
-		}),
-	}
-
-	return cmd
-}
-
-func runLabelLs(env *Env) error {
-	labels := env.backend.ValidLabels()
-
-	for _, l := range labels {
-		env.out.Println(l)
-	}
-
-	return nil
-}

commands/label_rm.go 🔗

@@ -1,45 +0,0 @@
-package commands
-
-import (
-	"github.com/spf13/cobra"
-
-	_select "github.com/MichaelMure/git-bug/commands/select"
-	"github.com/MichaelMure/git-bug/util/text"
-)
-
-func newLabelRmCommand() *cobra.Command {
-	env := newEnv()
-
-	cmd := &cobra.Command{
-		Use:     "rm [ID] LABEL...",
-		Short:   "Remove a label from a bug.",
-		PreRunE: loadBackend(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runLabelRm(env, args)
-		}),
-		ValidArgsFunction: completeBugAndLabels(env, false),
-	}
-
-	return cmd
-}
-
-func runLabelRm(env *Env, args []string) error {
-	b, args, err := _select.ResolveBug(env.backend, args)
-	if err != nil {
-		return err
-	}
-
-	removed := args
-
-	changes, _, err := b.ChangeLabels(nil, text.CleanupOneLineArray(removed))
-
-	for _, change := range changes {
-		env.out.Println(change)
-	}
-
-	if err != nil {
-		return err
-	}
-
-	return b.Commit()
-}

commands/ls-id.go 🔗

@@ -1,42 +0,0 @@
-package commands
-
-import (
-	"github.com/spf13/cobra"
-)
-
-func newLsIdCommand() *cobra.Command {
-	env := newEnv()
-
-	cmd := &cobra.Command{
-		Use:     "ls-id [PREFIX]",
-		Short:   "List bug identifiers.",
-		PreRunE: loadBackend(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runLsId(env, args)
-		}),
-		Deprecated: `and will be removed in v1.0.  
-
-Please use the "ls" command which allows filtering and sorting of the resulting
-list of ids.  The following example would print a new-line separated list containing
-the ids of all open bugs:
-git-bug ls --format id --status open
-`,
-	}
-
-	return cmd
-}
-
-func runLsId(env *Env, args []string) error {
-	var prefix = ""
-	if len(args) != 0 {
-		prefix = args[0]
-	}
-
-	for _, id := range env.backend.AllBugsIds() {
-		if prefix == "" || id.HasPrefix(prefix) {
-			env.out.Println(id)
-		}
-	}
-
-	return nil
-}

commands/ls-labels.go 🔗

@@ -1,29 +0,0 @@
-package commands
-
-import (
-	"github.com/spf13/cobra"
-)
-
-func newLsLabelCommand() *cobra.Command {
-	env := newEnv()
-
-	cmd := &cobra.Command{
-		Use:   "ls-label",
-		Short: "List valid labels.",
-		Long: `List valid labels.
-
-Note: in the future, a proper label policy could be implemented where valid labels are defined in a configuration file. Until that, the default behavior is to return the list of labels already used.`,
-		PreRunE: loadBackend(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runLabelLs(env)
-		}),
-		Deprecated: ` and will be removed in v1.0.
-
-The functionality provided by this command is now provided by
-the following (equivalent) command:
-git-bug label ls
-`,
-	}
-
-	return cmd
-}

commands/pull.go 🔗

@@ -5,26 +5,28 @@ import (
 
 	"github.com/spf13/cobra"
 
+	"github.com/MichaelMure/git-bug/commands/completion"
+	"github.com/MichaelMure/git-bug/commands/execenv"
 	"github.com/MichaelMure/git-bug/entity"
 )
 
 func newPullCommand() *cobra.Command {
-	env := newEnv()
+	env := execenv.NewEnv()
 
 	cmd := &cobra.Command{
 		Use:     "pull [REMOTE]",
-		Short:   "Pull bugs update from a git remote.",
-		PreRunE: loadBackend(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
+		Short:   "Pull updates from a git remote",
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
 			return runPull(env, args)
 		}),
-		ValidArgsFunction: completeGitRemote(env),
+		ValidArgsFunction: completion.GitRemote(env),
 	}
 
 	return cmd
 }
 
-func runPull(env *Env, args []string) error {
+func runPull(env *execenv.Env, args []string) error {
 	if len(args) > 1 {
 		return errors.New("Only pulling from one remote at a time is supported")
 	}
@@ -34,24 +36,24 @@ func runPull(env *Env, args []string) error {
 		remote = args[0]
 	}
 
-	env.out.Println("Fetching remote ...")
+	env.Out.Println("Fetching remote ...")
 
-	stdout, err := env.backend.Fetch(remote)
+	stdout, err := env.Backend.Fetch(remote)
 	if err != nil {
 		return err
 	}
 
-	env.out.Println(stdout)
+	env.Out.Println(stdout)
 
-	env.out.Println("Merging data ...")
+	env.Out.Println("Merging data ...")
 
-	for result := range env.backend.MergeAll(remote) {
+	for result := range env.Backend.MergeAll(remote) {
 		if result.Err != nil {
-			env.err.Println(result.Err)
+			env.Err.Println(result.Err)
 		}
 
 		if result.Status != entity.MergeStatusNothing {
-			env.out.Printf("%s: %s\n", result.Id.Human(), result)
+			env.Out.Printf("%s: %s\n", result.Id.Human(), result)
 		}
 	}
 

commands/push.go 🔗

@@ -4,25 +4,28 @@ import (
 	"errors"
 
 	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/commands/completion"
+	"github.com/MichaelMure/git-bug/commands/execenv"
 )
 
 func newPushCommand() *cobra.Command {
-	env := newEnv()
+	env := execenv.NewEnv()
 
 	cmd := &cobra.Command{
 		Use:     "push [REMOTE]",
-		Short:   "Push bugs update to a git remote.",
-		PreRunE: loadBackend(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
+		Short:   "Push updates to a git remote",
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
 			return runPush(env, args)
 		}),
-		ValidArgsFunction: completeGitRemote(env),
+		ValidArgsFunction: completion.GitRemote(env),
 	}
 
 	return cmd
 }
 
-func runPush(env *Env, args []string) error {
+func runPush(env *execenv.Env, args []string) error {
 	if len(args) > 1 {
 		return errors.New("Only pushing to one remote at a time is supported")
 	}
@@ -32,12 +35,12 @@ func runPush(env *Env, args []string) error {
 		remote = args[0]
 	}
 
-	stdout, err := env.backend.Push(remote)
+	stdout, err := env.Backend.Push(remote)
 	if err != nil {
 		return err
 	}
 
-	env.out.Println(stdout)
+	env.Out.Println(stdout)
 
 	return nil
 }

commands/rm.go 🔗

@@ -1,43 +0,0 @@
-package commands
-
-import (
-	"errors"
-
-	"github.com/spf13/cobra"
-)
-
-func newRmCommand() *cobra.Command {
-	env := newEnv()
-
-	cmd := &cobra.Command{
-		Use:     "rm ID",
-		Short:   "Remove an existing bug.",
-		Long:    "Remove an existing bug in the local repository. Note removing bugs that were imported from bridges will not remove the bug on the remote, and will only remove the local copy of the bug.",
-		PreRunE: loadBackendEnsureUser(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runRm(env, args)
-		}),
-		ValidArgsFunction: completeBug(env),
-	}
-
-	flags := cmd.Flags()
-	flags.SortFlags = false
-
-	return cmd
-}
-
-func runRm(env *Env, args []string) (err error) {
-	if len(args) == 0 {
-		return errors.New("you must provide a bug prefix to remove")
-	}
-
-	err = env.backend.RemoveBug(args[0])
-
-	if err != nil {
-		return
-	}
-
-	env.out.Printf("bug %s removed\n", args[0])
-
-	return
-}

commands/rm_test.go 🔗

@@ -1,17 +0,0 @@
-package commands
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/require"
-)
-
-func TestRm(t *testing.T) {
-	testEnv, bugID := newTestEnvAndBug(t)
-
-	exp := "bug " + bugID + " removed\n"
-
-	require.NoError(t, runRm(testEnv.env, []string{bugID}))
-	require.Equal(t, exp, testEnv.out.String())
-	testEnv.out.Reset()
-}

commands/root.go 🔗

@@ -6,9 +6,13 @@ import (
 	"os"
 
 	"github.com/spf13/cobra"
-)
 
-const rootCommandName = "git-bug"
+	"github.com/MichaelMure/git-bug/commands/bridge"
+	usercmd "github.com/MichaelMure/git-bug/commands/user"
+
+	"github.com/MichaelMure/git-bug/commands/bug"
+	"github.com/MichaelMure/git-bug/commands/execenv"
+)
 
 // These variables are initialized externally during the build. See the Makefile.
 var GitCommit string
@@ -17,8 +21,8 @@ var GitExactTag string
 
 func NewRootCommand() *cobra.Command {
 	cmd := &cobra.Command{
-		Use:   rootCommandName,
-		Short: "A bug tracker embedded in Git.",
+		Use:   execenv.RootCommandName,
+		Short: "A bug tracker embedded in Git",
 		Long: `git-bug is a bug tracker embedded in git.
 
 git-bug use git objects to store the bug tracking separated from the files
@@ -52,26 +56,32 @@ the same git remote you are already using to collaborate with other people.
 		DisableAutoGenTag: true,
 	}
 
-	cmd.AddCommand(newAddCommand())
-	cmd.AddCommand(newBridgeCommand())
+	const entityGroup = "entity"
+	const uiGroup = "ui"
+	const remoteGroup = "remote"
+
+	cmd.AddGroup(&cobra.Group{ID: entityGroup, Title: "Entities"})
+	cmd.AddGroup(&cobra.Group{ID: uiGroup, Title: "User interfaces"})
+	cmd.AddGroup(&cobra.Group{ID: remoteGroup, Title: "Interaction with the outside world"})
+
+	addCmdWithGroup := func(child *cobra.Command, groupID string) {
+		cmd.AddCommand(child)
+		child.GroupID = groupID
+	}
+
+	addCmdWithGroup(bugcmd.NewBugCommand(), entityGroup)
+	addCmdWithGroup(usercmd.NewUserCommand(), entityGroup)
+	addCmdWithGroup(newLabelCommand(), entityGroup)
+
+	addCmdWithGroup(newTermUICommand(), uiGroup)
+	addCmdWithGroup(newWebUICommand(), uiGroup)
+
+	addCmdWithGroup(newPullCommand(), remoteGroup)
+	addCmdWithGroup(newPushCommand(), remoteGroup)
+	addCmdWithGroup(bridgecmd.NewBridgeCommand(), remoteGroup)
+
 	cmd.AddCommand(newCommandsCommand())
-	cmd.AddCommand(newCommentCommand())
-	cmd.AddCommand(newDeselectCommand())
-	cmd.AddCommand(newLabelCommand())
-	cmd.AddCommand(newLsCommand())
-	cmd.AddCommand(newLsIdCommand())
-	cmd.AddCommand(newLsLabelCommand())
-	cmd.AddCommand(newPullCommand())
-	cmd.AddCommand(newPushCommand())
-	cmd.AddCommand(newRmCommand())
-	cmd.AddCommand(newSelectCommand())
-	cmd.AddCommand(newShowCommand())
-	cmd.AddCommand(newStatusCommand())
-	cmd.AddCommand(newTermUICommand())
-	cmd.AddCommand(newTitleCommand())
-	cmd.AddCommand(newUserCommand())
 	cmd.AddCommand(newVersionCommand())
-	cmd.AddCommand(newWebUICommand())
 
 	return cmd
 }

commands/select.go 🔗

@@ -1,60 +0,0 @@
-package commands
-
-import (
-	"errors"
-
-	"github.com/spf13/cobra"
-
-	_select "github.com/MichaelMure/git-bug/commands/select"
-)
-
-func newSelectCommand() *cobra.Command {
-	env := newEnv()
-
-	cmd := &cobra.Command{
-		Use:   "select ID",
-		Short: "Select a bug for implicit use in future commands.",
-		Example: `git bug select 2f15
-git bug comment
-git bug status
-`,
-		Long: `Select a bug for implicit use in future commands.
-
-This command allows you to omit any bug ID argument, for example:
-  git bug show
-instead of
-  git bug show 2f153ca
-
-The complementary command is "git bug deselect" performing the opposite operation.
-`,
-		PreRunE: loadBackend(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runSelect(env, args)
-		}),
-		ValidArgsFunction: completeBug(env),
-	}
-
-	return cmd
-}
-
-func runSelect(env *Env, args []string) error {
-	if len(args) == 0 {
-		return errors.New("You must provide a bug id")
-	}
-
-	prefix := args[0]
-
-	b, err := env.backend.ResolveBugPrefix(prefix)
-	if err != nil {
-		return err
-	}
-
-	err = _select.Select(env.backend, b.Id())
-	if err != nil {
-		return err
-	}
-
-	env.out.Printf("selected bug %s: %s\n", b.Id().Human(), b.Snapshot().Title)
-
-	return nil
-}

commands/status.go 🔗

@@ -1,38 +0,0 @@
-package commands
-
-import (
-	_select "github.com/MichaelMure/git-bug/commands/select"
-	"github.com/spf13/cobra"
-)
-
-func newStatusCommand() *cobra.Command {
-	env := newEnv()
-
-	cmd := &cobra.Command{
-		Use:     "status [ID]",
-		Short:   "Display or change a bug status.",
-		PreRunE: loadBackend(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runStatus(env, args)
-		}),
-		ValidArgsFunction: completeBug(env),
-	}
-
-	cmd.AddCommand(newStatusCloseCommand())
-	cmd.AddCommand(newStatusOpenCommand())
-
-	return cmd
-}
-
-func runStatus(env *Env, args []string) error {
-	b, args, err := _select.ResolveBug(env.backend, args)
-	if err != nil {
-		return err
-	}
-
-	snap := b.Snapshot()
-
-	env.out.Println(snap.Status)
-
-	return nil
-}

commands/status_close.go 🔗

@@ -1,35 +0,0 @@
-package commands
-
-import (
-	_select "github.com/MichaelMure/git-bug/commands/select"
-	"github.com/spf13/cobra"
-)
-
-func newStatusCloseCommand() *cobra.Command {
-	env := newEnv()
-
-	cmd := &cobra.Command{
-		Use:     "close [ID]",
-		Short:   "Mark a bug as closed.",
-		PreRunE: loadBackendEnsureUser(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runStatusClose(env, args)
-		}),
-	}
-
-	return cmd
-}
-
-func runStatusClose(env *Env, args []string) error {
-	b, args, err := _select.ResolveBug(env.backend, args)
-	if err != nil {
-		return err
-	}
-
-	_, err = b.Close()
-	if err != nil {
-		return err
-	}
-
-	return b.Commit()
-}

commands/status_open.go 🔗

@@ -1,35 +0,0 @@
-package commands
-
-import (
-	_select "github.com/MichaelMure/git-bug/commands/select"
-	"github.com/spf13/cobra"
-)
-
-func newStatusOpenCommand() *cobra.Command {
-	env := newEnv()
-
-	cmd := &cobra.Command{
-		Use:     "open [ID]",
-		Short:   "Mark a bug as open.",
-		PreRunE: loadBackendEnsureUser(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runStatusOpen(env, args)
-		}),
-	}
-
-	return cmd
-}
-
-func runStatusOpen(env *Env, args []string) error {
-	b, args, err := _select.ResolveBug(env.backend, args)
-	if err != nil {
-		return err
-	}
-
-	_, err = b.Open()
-	if err != nil {
-		return err
-	}
-
-	return b.Commit()
-}

commands/termui.go 🔗

@@ -3,18 +3,19 @@ package commands
 import (
 	"github.com/spf13/cobra"
 
+	"github.com/MichaelMure/git-bug/commands/execenv"
 	"github.com/MichaelMure/git-bug/termui"
 )
 
 func newTermUICommand() *cobra.Command {
-	env := newEnv()
+	env := execenv.NewEnv()
 
 	cmd := &cobra.Command{
 		Use:     "termui",
 		Aliases: []string{"tui"},
-		Short:   "Launch the terminal UI.",
-		PreRunE: loadBackendEnsureUser(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
+		Short:   "Launch the terminal UI",
+		PreRunE: execenv.LoadBackendEnsureUser(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
 			return runTermUI(env)
 		}),
 	}
@@ -22,6 +23,6 @@ func newTermUICommand() *cobra.Command {
 	return cmd
 }
 
-func runTermUI(env *Env) error {
-	return termui.Run(env.backend)
+func runTermUI(env *execenv.Env) error {
+	return termui.Run(env.Backend)
 }

commands/title.go 🔗

@@ -1,37 +0,0 @@
-package commands
-
-import (
-	_select "github.com/MichaelMure/git-bug/commands/select"
-	"github.com/spf13/cobra"
-)
-
-func newTitleCommand() *cobra.Command {
-	env := newEnv()
-
-	cmd := &cobra.Command{
-		Use:     "title [ID]",
-		Short:   "Display or change a title of a bug.",
-		PreRunE: loadBackend(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runTitle(env, args)
-		}),
-		ValidArgsFunction: completeBug(env),
-	}
-
-	cmd.AddCommand(newTitleEditCommand())
-
-	return cmd
-}
-
-func runTitle(env *Env, args []string) error {
-	b, args, err := _select.ResolveBug(env.backend, args)
-	if err != nil {
-		return err
-	}
-
-	snap := b.Snapshot()
-
-	env.out.Println(snap.Title)
-
-	return nil
-}

commands/title_edit.go 🔗

@@ -1,74 +0,0 @@
-package commands
-
-import (
-	"github.com/spf13/cobra"
-
-	"github.com/MichaelMure/git-bug/commands/input"
-	_select "github.com/MichaelMure/git-bug/commands/select"
-	"github.com/MichaelMure/git-bug/util/text"
-)
-
-type titleEditOptions struct {
-	title          string
-	nonInteractive bool
-}
-
-func newTitleEditCommand() *cobra.Command {
-	env := newEnv()
-	options := titleEditOptions{}
-
-	cmd := &cobra.Command{
-		Use:     "edit [ID]",
-		Short:   "Edit a title of a bug.",
-		PreRunE: loadBackendEnsureUser(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runTitleEdit(env, options, args)
-		}),
-		ValidArgsFunction: completeBug(env),
-	}
-
-	flags := cmd.Flags()
-	flags.SortFlags = false
-
-	flags.StringVarP(&options.title, "title", "t", "",
-		"Provide a title to describe the issue",
-	)
-	flags.BoolVar(&options.nonInteractive, "non-interactive", false, "Do not ask for user input")
-
-	return cmd
-}
-
-func runTitleEdit(env *Env, opts titleEditOptions, args []string) error {
-	b, args, err := _select.ResolveBug(env.backend, args)
-	if err != nil {
-		return err
-	}
-
-	snap := b.Snapshot()
-
-	if opts.title == "" {
-		if opts.nonInteractive {
-			env.err.Println("No title given. Use -m or -F option to specify a title. Aborting.")
-			return nil
-		}
-		opts.title, err = input.BugTitleEditorInput(env.repo, snap.Title)
-		if err == input.ErrEmptyTitle {
-			env.out.Println("Empty title, aborting.")
-			return nil
-		}
-		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/user.go 🔗

@@ -1,110 +0,0 @@
-package commands
-
-import (
-	"errors"
-	"fmt"
-	"strings"
-
-	"github.com/spf13/cobra"
-
-	"github.com/MichaelMure/git-bug/cache"
-)
-
-type userOptions struct {
-	fields string
-}
-
-func newUserCommand() *cobra.Command {
-	env := newEnv()
-	options := userOptions{}
-
-	cmd := &cobra.Command{
-		Use:     "user [USER-ID]",
-		Short:   "Display or change the user identity.",
-		PreRunE: loadBackendEnsureUser(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runUser(env, options, args)
-		}),
-		ValidArgsFunction: completeUser(env),
-	}
-
-	cmd.AddCommand(newUserAdoptCommand())
-	cmd.AddCommand(newUserCreateCommand())
-	cmd.AddCommand(newUserLsCommand())
-
-	flags := cmd.Flags()
-	flags.SortFlags = false
-
-	fields := []string{"email", "humanId", "id", "lastModification", "lastModificationLamports", "login", "metadata", "name"}
-	flags.StringVarP(&options.fields, "field", "f", "",
-		"Select field to display. Valid values are ["+strings.Join(fields, ",")+"]")
-	cmd.RegisterFlagCompletionFunc("field", completeFrom(fields))
-
-	return cmd
-}
-
-func runUser(env *Env, opts userOptions, args []string) error {
-	if len(args) > 1 {
-		return errors.New("only one identity can be displayed at a time")
-	}
-
-	var id *cache.IdentityCache
-	var err error
-	if len(args) == 1 {
-		id, err = env.backend.ResolveIdentityPrefix(args[0])
-	} else {
-		id, err = env.backend.GetUserIdentity()
-	}
-
-	if err != nil {
-		return err
-	}
-
-	if opts.fields != "" {
-		switch opts.fields {
-		case "email":
-			env.out.Printf("%s\n", id.Email())
-		case "login":
-			env.out.Printf("%s\n", id.Login())
-		case "humanId":
-			env.out.Printf("%s\n", id.Id().Human())
-		case "id":
-			env.out.Printf("%s\n", id.Id())
-		case "lastModification":
-			env.out.Printf("%s\n", id.LastModification().
-				Time().Format("Mon Jan 2 15:04:05 2006 +0200"))
-		case "lastModificationLamport":
-			for name, t := range id.LastModificationLamports() {
-				env.out.Printf("%s\n%d\n", name, t)
-			}
-		case "metadata":
-			for key, value := range id.ImmutableMetadata() {
-				env.out.Printf("%s\n%s\n", key, value)
-			}
-		case "name":
-			env.out.Printf("%s\n", id.Name())
-
-		default:
-			return fmt.Errorf("\nUnsupported field: %s\n", opts.fields)
-		}
-
-		return nil
-	}
-
-	env.out.Printf("Id: %s\n", id.Id())
-	env.out.Printf("Name: %s\n", id.Name())
-	env.out.Printf("Email: %s\n", id.Email())
-	env.out.Printf("Login: %s\n", id.Login())
-	env.out.Printf("Last modification: %s\n", id.LastModification().Time().Format("Mon Jan 2 15:04:05 2006 +0200"))
-	env.out.Printf("Last moditication (lamport):\n")
-	for name, t := range id.LastModificationLamports() {
-		env.out.Printf("\t%s: %d", name, t)
-	}
-	env.out.Println("Metadata:")
-	for key, value := range id.ImmutableMetadata() {
-		env.out.Printf("    %s --> %s\n", key, value)
-	}
-	// env.out.Printf("Protected: %v\n", id.IsProtected())
-
-	return nil
-}

commands/user/user.go 🔗

@@ -0,0 +1,89 @@
+package usercmd
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/spf13/cobra"
+
+	json2 "github.com/MichaelMure/git-bug/commands/cmdjson"
+
+	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/commands/completion"
+	"github.com/MichaelMure/git-bug/commands/execenv"
+	"github.com/MichaelMure/git-bug/util/colors"
+)
+
+type userOptions struct {
+	format string
+}
+
+func NewUserCommand() *cobra.Command {
+	env := execenv.NewEnv()
+	options := userOptions{}
+
+	cmd := &cobra.Command{
+		Use:     "user",
+		Short:   "List identities",
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runUser(env, options)
+		}),
+	}
+
+	cmd.AddCommand(newUserNewCommand())
+	cmd.AddCommand(newUserShowCommand())
+	cmd.AddCommand(newUserAdoptCommand())
+
+	flags := cmd.Flags()
+	flags.SortFlags = false
+
+	flags.StringVarP(&options.format, "format", "f", "default",
+		"Select the output formatting style. Valid values are [default,json]")
+	cmd.RegisterFlagCompletionFunc("format", completion.From([]string{"default", "json"}))
+
+	return cmd
+}
+
+func runUser(env *execenv.Env, opts userOptions) error {
+	ids := env.Backend.AllIdentityIds()
+	var users []*cache.IdentityExcerpt
+	for _, id := range ids {
+		user, err := env.Backend.ResolveIdentityExcerpt(id)
+		if err != nil {
+			return err
+		}
+		users = append(users, user)
+	}
+
+	switch opts.format {
+	case "json":
+		return userJsonFormatter(env, users)
+	case "default":
+		return userDefaultFormatter(env, users)
+	default:
+		return fmt.Errorf("unknown format %s", opts.format)
+	}
+}
+
+func userDefaultFormatter(env *execenv.Env, users []*cache.IdentityExcerpt) error {
+	for _, user := range users {
+		env.Out.Printf("%s %s\n",
+			colors.Cyan(user.Id.Human()),
+			user.DisplayName(),
+		)
+	}
+
+	return nil
+}
+
+func userJsonFormatter(env *execenv.Env, users []*cache.IdentityExcerpt) error {
+	jsonUsers := make([]json2.Identity, len(users))
+	for i, user := range users {
+		jsonUsers[i] = json2.NewIdentityFromExcerpt(user)
+	}
+
+	jsonObject, _ := json.MarshalIndent(jsonUsers, "", "    ")
+	env.Out.Printf("%s\n", jsonObject)
+	return nil
+}

commands/user/user_adopt.go 🔗

@@ -0,0 +1,43 @@
+package usercmd
+
+import (
+	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/commands/completion"
+	"github.com/MichaelMure/git-bug/commands/execenv"
+)
+
+func newUserAdoptCommand() *cobra.Command {
+	env := execenv.NewEnv()
+
+	cmd := &cobra.Command{
+		Use:     "adopt USER_ID",
+		Short:   "Adopt an existing identity as your own",
+		Args:    cobra.ExactArgs(1),
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runUserAdopt(env, args)
+		}),
+		ValidArgsFunction: completion.User(env),
+	}
+
+	return cmd
+}
+
+func runUserAdopt(env *execenv.Env, args []string) error {
+	prefix := args[0]
+
+	i, err := env.Backend.ResolveIdentityPrefix(prefix)
+	if err != nil {
+		return err
+	}
+
+	err = env.Backend.SetUserIdentity(i)
+	if err != nil {
+		return err
+	}
+
+	env.Out.Printf("Your identity is now: %s\n", i.DisplayName())
+
+	return nil
+}

commands/user_create.go → commands/user/user_new.go 🔗

@@ -1,28 +1,29 @@
-package commands
+package usercmd
 
 import (
 	"github.com/spf13/cobra"
 
+	"github.com/MichaelMure/git-bug/commands/execenv"
 	"github.com/MichaelMure/git-bug/commands/input"
 )
 
-type createUserOptions struct {
+type userNewOptions struct {
 	name           string
 	email          string
 	avatarURL      string
 	nonInteractive bool
 }
 
-func newUserCreateCommand() *cobra.Command {
-	env := newEnv()
+func newUserNewCommand() *cobra.Command {
+	env := execenv.NewEnv()
 
-	options := createUserOptions{}
+	options := userNewOptions{}
 	cmd := &cobra.Command{
-		Use:     "create",
-		Short:   "Create a new identity.",
-		PreRunE: loadBackend(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runUserCreate(env, options)
+		Use:     "new",
+		Short:   "Create a new identity",
+		PreRunE: execenv.LoadBackend(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runUserNew(env, options)
 		}),
 	}
 
@@ -35,10 +36,10 @@ func newUserCreateCommand() *cobra.Command {
 	return cmd
 }
 
-func runUserCreate(env *Env, opts createUserOptions) error {
+func runUserNew(env *execenv.Env, opts userNewOptions) error {
 
 	if !opts.nonInteractive && opts.name == "" {
-		preName, err := env.backend.GetUserName()
+		preName, err := env.Backend.GetUserName()
 		if err != nil {
 			return err
 		}
@@ -49,7 +50,7 @@ func runUserCreate(env *Env, opts createUserOptions) error {
 	}
 
 	if !opts.nonInteractive && opts.email == "" {
-		preEmail, err := env.backend.GetUserEmail()
+		preEmail, err := env.Backend.GetUserEmail()
 		if err != nil {
 			return err
 		}
@@ -68,7 +69,7 @@ func runUserCreate(env *Env, opts createUserOptions) error {
 		}
 	}
 
-	id, err := env.backend.NewIdentityRaw(opts.name, opts.email, "", opts.avatarURL, nil, nil)
+	id, err := env.Backend.NewIdentityRaw(opts.name, opts.email, "", opts.avatarURL, nil, nil)
 	if err != nil {
 		return err
 	}
@@ -78,20 +79,20 @@ func runUserCreate(env *Env, opts createUserOptions) error {
 		return err
 	}
 
-	set, err := env.backend.IsUserIdentitySet()
+	set, err := env.Backend.IsUserIdentitySet()
 	if err != nil {
 		return err
 	}
 
 	if !set {
-		err = env.backend.SetUserIdentity(id)
+		err = env.Backend.SetUserIdentity(id)
 		if err != nil {
 			return err
 		}
 	}
 
-	env.err.Println()
-	env.out.Println(id.Id())
+	env.Err.Println()
+	env.Out.Println(id.Id())
 
 	return nil
 }

commands/user/user_new_test.go 🔗

@@ -0,0 +1,14 @@
+package usercmd
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/MichaelMure/git-bug/commands/bug/testenv"
+)
+
+func TestUserNewCommand(t *testing.T) {
+	_, userID := testenv.NewTestEnvAndUser(t)
+	require.Regexp(t, "[0-9a-f]{64}", userID)
+}

commands/user/user_show.go 🔗

@@ -0,0 +1,108 @@
+package usercmd
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+
+	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/commands/completion"
+	"github.com/MichaelMure/git-bug/commands/execenv"
+)
+
+type userShowOptions struct {
+	fields string
+}
+
+func newUserShowCommand() *cobra.Command {
+	env := execenv.NewEnv()
+	options := userShowOptions{}
+
+	cmd := &cobra.Command{
+		Use:     "user show [USER_ID]",
+		Short:   "Display a user identity",
+		PreRunE: execenv.LoadBackendEnsureUser(env),
+		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
+			return runUserShow(env, options, args)
+		}),
+		ValidArgsFunction: completion.User(env),
+	}
+
+	flags := cmd.Flags()
+	flags.SortFlags = false
+
+	fields := []string{"email", "humanId", "id", "lastModification", "lastModificationLamports", "login", "metadata", "name"}
+	flags.StringVarP(&options.fields, "field", "f", "",
+		"Select field to display. Valid values are ["+strings.Join(fields, ",")+"]")
+	cmd.RegisterFlagCompletionFunc("field", completion.From(fields))
+
+	return cmd
+}
+
+func runUserShow(env *execenv.Env, opts userShowOptions, args []string) error {
+	if len(args) > 1 {
+		return errors.New("only one identity can be displayed at a time")
+	}
+
+	var id *cache.IdentityCache
+	var err error
+	if len(args) == 1 {
+		id, err = env.Backend.ResolveIdentityPrefix(args[0])
+	} else {
+		id, err = env.Backend.GetUserIdentity()
+	}
+
+	if err != nil {
+		return err
+	}
+
+	if opts.fields != "" {
+		switch opts.fields {
+		case "email":
+			env.Out.Printf("%s\n", id.Email())
+		case "login":
+			env.Out.Printf("%s\n", id.Login())
+		case "humanId":
+			env.Out.Printf("%s\n", id.Id().Human())
+		case "id":
+			env.Out.Printf("%s\n", id.Id())
+		case "lastModification":
+			env.Out.Printf("%s\n", id.LastModification().
+				Time().Format("Mon Jan 2 15:04:05 2006 +0200"))
+		case "lastModificationLamport":
+			for name, t := range id.LastModificationLamports() {
+				env.Out.Printf("%s\n%d\n", name, t)
+			}
+		case "metadata":
+			for key, value := range id.ImmutableMetadata() {
+				env.Out.Printf("%s\n%s\n", key, value)
+			}
+		case "name":
+			env.Out.Printf("%s\n", id.Name())
+
+		default:
+			return fmt.Errorf("\nUnsupported field: %s\n", opts.fields)
+		}
+
+		return nil
+	}
+
+	env.Out.Printf("Id: %s\n", id.Id())
+	env.Out.Printf("Name: %s\n", id.Name())
+	env.Out.Printf("Email: %s\n", id.Email())
+	env.Out.Printf("Login: %s\n", id.Login())
+	env.Out.Printf("Last modification: %s\n", id.LastModification().Time().Format("Mon Jan 2 15:04:05 2006 +0200"))
+	env.Out.Printf("Last moditication (lamport):\n")
+	for name, t := range id.LastModificationLamports() {
+		env.Out.Printf("\t%s: %d", name, t)
+	}
+	env.Out.Println("Metadata:")
+	for key, value := range id.ImmutableMetadata() {
+		env.Out.Printf("    %s --> %s\n", key, value)
+	}
+	// env.Out.Printf("Protected: %v\n", id.IsProtected())
+
+	return nil
+}

commands/user_adopt.go 🔗

@@ -1,40 +0,0 @@
-package commands
-
-import (
-	"github.com/spf13/cobra"
-)
-
-func newUserAdoptCommand() *cobra.Command {
-	env := newEnv()
-
-	cmd := &cobra.Command{
-		Use:     "adopt USER-ID",
-		Short:   "Adopt an existing identity as your own.",
-		Args:    cobra.ExactArgs(1),
-		PreRunE: loadBackend(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runUserAdopt(env, args)
-		}),
-		ValidArgsFunction: completeUser(env),
-	}
-
-	return cmd
-}
-
-func runUserAdopt(env *Env, args []string) error {
-	prefix := args[0]
-
-	i, err := env.backend.ResolveIdentityPrefix(prefix)
-	if err != nil {
-		return err
-	}
-
-	err = env.backend.SetUserIdentity(i)
-	if err != nil {
-		return err
-	}
-
-	env.out.Printf("Your identity is now: %s\n", i.DisplayName())
-
-	return nil
-}

commands/user_create_test.go 🔗

@@ -1,38 +0,0 @@
-package commands
-
-import (
-	"strings"
-	"testing"
-
-	"github.com/stretchr/testify/require"
-)
-
-const (
-	testUserName  = "John Doe"
-	testUserEmail = "jdoe@example.com"
-)
-
-func newTestEnvAndUser(t *testing.T) (*testEnv, string) {
-	t.Helper()
-
-	testEnv := newTestEnv(t)
-
-	opts := createUserOptions{
-		name:           testUserName,
-		email:          testUserEmail,
-		avatarURL:      "",
-		nonInteractive: true,
-	}
-
-	require.NoError(t, runUserCreate(testEnv.env, opts))
-
-	userID := strings.TrimSpace(testEnv.out.String())
-	testEnv.out.Reset()
-
-	return testEnv, userID
-}
-
-func TestUserCreateCommand(t *testing.T) {
-	_, userID := newTestEnvAndUser(t)
-	require.Regexp(t, "[0-9a-f]{64}", userID)
-}

commands/user_ls.go 🔗

@@ -1,81 +0,0 @@
-package commands
-
-import (
-	"encoding/json"
-	"fmt"
-
-	"github.com/spf13/cobra"
-
-	"github.com/MichaelMure/git-bug/cache"
-	"github.com/MichaelMure/git-bug/util/colors"
-)
-
-type userLsOptions struct {
-	format string
-}
-
-func newUserLsCommand() *cobra.Command {
-	env := newEnv()
-	options := userLsOptions{}
-
-	cmd := &cobra.Command{
-		Use:     "ls",
-		Short:   "List identities.",
-		PreRunE: loadBackend(env),
-		RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
-			return runUserLs(env, options)
-		}),
-	}
-
-	flags := cmd.Flags()
-	flags.SortFlags = false
-
-	flags.StringVarP(&options.format, "format", "f", "default",
-		"Select the output formatting style. Valid values are [default,json]")
-	cmd.RegisterFlagCompletionFunc("format", completeFrom([]string{"default", "json"}))
-
-	return cmd
-}
-
-func runUserLs(env *Env, opts userLsOptions) error {
-	ids := env.backend.AllIdentityIds()
-	var users []*cache.IdentityExcerpt
-	for _, id := range ids {
-		user, err := env.backend.ResolveIdentityExcerpt(id)
-		if err != nil {
-			return err
-		}
-		users = append(users, user)
-	}
-
-	switch opts.format {
-	case "json":
-		return userLsJsonFormatter(env, users)
-	case "default":
-		return userLsDefaultFormatter(env, users)
-	default:
-		return fmt.Errorf("unknown format %s", opts.format)
-	}
-}
-
-func userLsDefaultFormatter(env *Env, users []*cache.IdentityExcerpt) error {
-	for _, user := range users {
-		env.out.Printf("%s %s\n",
-			colors.Cyan(user.Id.Human()),
-			user.DisplayName(),
-		)
-	}
-
-	return nil
-}
-
-func userLsJsonFormatter(env *Env, users []*cache.IdentityExcerpt) error {
-	jsonUsers := make([]JSONIdentity, len(users))
-	for i, user := range users {
-		jsonUsers[i] = NewJSONIdentityFromExcerpt(user)
-	}
-
-	jsonObject, _ := json.MarshalIndent(jsonUsers, "", "    ")
-	env.out.Printf("%s\n", jsonObject)
-	return nil
-}

commands/version.go 🔗

@@ -4,6 +4,8 @@ import (
 	"runtime"
 
 	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/commands/execenv"
 )
 
 type versionOptions struct {
@@ -13,12 +15,12 @@ type versionOptions struct {
 }
 
 func newVersionCommand() *cobra.Command {
-	env := newEnv()
+	env := execenv.NewEnv()
 	options := versionOptions{}
 
 	cmd := &cobra.Command{
 		Use:   "version",
-		Short: "Show git-bug version information.",
+		Short: "Show git-bug version information",
 		Run: func(cmd *cobra.Command, args []string) {
 			runVersion(env, options, cmd.Root())
 		},
@@ -40,23 +42,23 @@ func newVersionCommand() *cobra.Command {
 	return cmd
 }
 
-func runVersion(env *Env, opts versionOptions, root *cobra.Command) {
+func runVersion(env *execenv.Env, opts versionOptions, root *cobra.Command) {
 	if opts.all {
-		env.out.Printf("%s version: %s\n", rootCommandName, root.Version)
-		env.out.Printf("System version: %s/%s\n", runtime.GOARCH, runtime.GOOS)
-		env.out.Printf("Golang version: %s\n", runtime.Version())
+		env.Out.Printf("%s version: %s\n", execenv.RootCommandName, root.Version)
+		env.Out.Printf("System version: %s/%s\n", runtime.GOARCH, runtime.GOOS)
+		env.Out.Printf("Golang version: %s\n", runtime.Version())
 		return
 	}
 
 	if opts.number {
-		env.out.Println(root.Version)
+		env.Out.Println(root.Version)
 		return
 	}
 
 	if opts.commit {
-		env.out.Println(GitCommit)
+		env.Out.Println(GitCommit)
 		return
 	}
 
-	env.out.Printf("%s version: %s\n", rootCommandName, root.Version)
+	env.Out.Printf("%s version: %s\n", execenv.RootCommandName, root.Version)
 }

commands/webui.go 🔗

@@ -23,6 +23,7 @@ import (
 	"github.com/MichaelMure/git-bug/api/graphql"
 	httpapi "github.com/MichaelMure/git-bug/api/http"
 	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/commands/execenv"
 	"github.com/MichaelMure/git-bug/entities/identity"
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/MichaelMure/git-bug/webui"
@@ -41,18 +42,18 @@ type webUIOptions struct {
 }
 
 func newWebUICommand() *cobra.Command {
-	env := newEnv()
+	env := execenv.NewEnv()
 	options := webUIOptions{}
 
 	cmd := &cobra.Command{
 		Use:   "webui",
-		Short: "Launch the web UI.",
+		Short: "Launch the web UI",
 		Long: `Launch the web UI.
 
 Available git config:
   git-bug.webui.open [bool]: control the automatic opening of the web UI in the default browser
 `,
-		PreRunE: loadRepo(env),
+		PreRunE: execenv.LoadRepo(env),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			return runWebUI(env, options)
 		},
@@ -72,7 +73,7 @@ Available git config:
 	return cmd
 }
 
-func runWebUI(env *Env, opts webUIOptions) error {
+func runWebUI(env *execenv.Env, opts webUIOptions) error {
 	if opts.port == 0 {
 		var err error
 		opts.port, err = freeport.GetFreePort()
@@ -96,7 +97,7 @@ func runWebUI(env *Env, opts webUIOptions) error {
 	// fixed identity: the default user of the repo
 	// TODO: support dynamic authentication with OAuth
 	if !opts.readOnly {
-		author, err := identity.GetUserIdentity(env.repo)
+		author, err := identity.GetUserIdentity(env.Repo)
 		if err != nil {
 			return err
 		}
@@ -104,14 +105,14 @@ func runWebUI(env *Env, opts webUIOptions) error {
 	}
 
 	mrc := cache.NewMultiRepoCache()
-	_, err := mrc.RegisterDefaultRepository(env.repo)
+	_, err := mrc.RegisterDefaultRepository(env.Repo)
 	if err != nil {
 		return err
 	}
 
 	var errOut io.Writer
 	if opts.logErrors {
-		errOut = env.err
+		errOut = env.Err
 	}
 
 	graphqlHandler := graphql.NewHandler(mrc, errOut)
@@ -136,7 +137,7 @@ func runWebUI(env *Env, opts webUIOptions) error {
 
 	go func() {
 		<-quit
-		env.out.Println("WebUI is shutting down...")
+		env.Out.Println("WebUI is shutting down...")
 
 		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
 		defer cancel()
@@ -149,18 +150,18 @@ func runWebUI(env *Env, opts webUIOptions) error {
 		// Teardown
 		err := graphqlHandler.Close()
 		if err != nil {
-			env.out.Println(err)
+			env.Out.Println(err)
 		}
 
 		close(done)
 	}()
 
-	env.out.Printf("Web UI: %s\n", webUiAddr)
-	env.out.Printf("Graphql API: http://%s/graphql\n", addr)
-	env.out.Printf("Graphql Playground: http://%s/playground\n", addr)
-	env.out.Println("Press Ctrl+c to quit")
+	env.Out.Printf("Web UI: %s\n", webUiAddr)
+	env.Out.Printf("Graphql API: http://%s/graphql\n", addr)
+	env.Out.Printf("Graphql Playground: http://%s/playground\n", addr)
+	env.Out.Println("Press Ctrl+c to quit")
 
-	configOpen, err := env.repo.AnyConfig().ReadBool(webUIOpenConfigKey)
+	configOpen, err := env.Repo.AnyConfig().ReadBool(webUIOpenConfigKey)
 	if err == repository.ErrNoConfigEntry {
 		// default to true
 		configOpen = true
@@ -173,7 +174,7 @@ func runWebUI(env *Env, opts webUIOptions) error {
 	if shouldOpen {
 		err = open.Run(toOpen)
 		if err != nil {
-			env.out.Println(err)
+			env.Out.Println(err)
 		}
 	}
 
@@ -184,6 +185,6 @@ func runWebUI(env *Env, opts webUIOptions) error {
 
 	<-done
 
-	env.out.Println("WebUI stopped")
+	env.Out.Println("WebUI stopped")
 	return nil
 }

doc/cli-convention.md 🔗

@@ -0,0 +1,13 @@
+## Pattern
+
+CLI commands should consistently follow this pattern: 
+
+```
+xxx                 --> list xxx things if list, otherwise show one
+xxx new             --> create thing
+xxx rm              --> delete thing
+xxx show ID         --> show one
+xxx show            --> show one with "select" implied ID
+xxx yyy             --> action commands for that thing, or subcommand
+xxx select|deselect --> select/deselect implied ID
+```

doc/man/git-bug-bridge-auth-rm.1 🔗

@@ -3,17 +3,17 @@
 
 .SH NAME
 .PP
-git-bug-bridge-auth-rm - Remove a credential.
+git-bug-bridge-auth-rm - Remove a credential
 
 
 .SH SYNOPSIS
 .PP
-\fBgit-bug bridge auth rm ID [flags]\fP
+\fBgit-bug bridge auth rm BRIDGE_ID [flags]\fP
 
 
 .SH DESCRIPTION
 .PP
-Remove a credential.
+Remove a credential
 
 
 .SH OPTIONS

doc/man/git-bug-bridge-auth-show.1 🔗

@@ -3,7 +3,7 @@
 
 .SH NAME
 .PP
-git-bug-bridge-auth-show - Display an authentication credential.
+git-bug-bridge-auth-show - Display an authentication credential
 
 
 .SH SYNOPSIS
@@ -13,7 +13,7 @@ git-bug-bridge-auth-show - Display an authentication credential.
 
 .SH DESCRIPTION
 .PP
-Display an authentication credential.
+Display an authentication credential
 
 
 .SH OPTIONS

doc/man/git-bug-bridge-auth.1 🔗

@@ -3,7 +3,7 @@
 
 .SH NAME
 .PP
-git-bug-bridge-auth - List all known bridge authentication credentials.
+git-bug-bridge-auth - List all known bridge authentication credentials
 
 
 .SH SYNOPSIS
@@ -13,7 +13,7 @@ git-bug-bridge-auth - List all known bridge authentication credentials.
 
 .SH DESCRIPTION
 .PP
-List all known bridge authentication credentials.
+List all known bridge authentication credentials
 
 
 .SH OPTIONS

doc/man/git-bug-bridge-configure.1 → doc/man/git-bug-bridge-new.1 🔗

@@ -3,12 +3,12 @@
 
 .SH NAME
 .PP
-git-bug-bridge-configure - Configure a new bridge.
+git-bug-bridge-new - Configure a new bridge
 
 
 .SH SYNOPSIS
 .PP
-\fBgit-bug bridge configure [flags]\fP
+\fBgit-bug bridge new [flags]\fP
 
 
 .SH DESCRIPTION
@@ -69,7 +69,7 @@ Configure a new bridge by passing flags or/and using interactive terminal prompt
 
 .PP
 \fB-h\fP, \fB--help\fP[=false]
-	help for configure
+	help for new
 
 
 .SH EXAMPLE
@@ -111,7 +111,7 @@ Enter token: 87cf5c03b64029f18ea5f9ca5679daa08ccbd700
 Successfully configured bridge: default
 
 # For GitHub
-git bug bridge configure \\
+git bug bridge new \\
     --name=default \\
     --target=github \\
     --owner=$(OWNER) \\
@@ -119,13 +119,13 @@ git bug bridge configure \\
     --token=$(TOKEN)
 
 # For Launchpad
-git bug bridge configure \\
+git bug bridge new \\
     --name=default \\
     --target=launchpad-preview \\
     --url=https://bugs.launchpad.net/ubuntu/
 
 # For Gitlab
-git bug bridge configure \\
+git bug bridge new \\
     --name=default \\
     --target=github \\
     --url=https://github.com/michaelmure/git-bug \\

doc/man/git-bug-bridge-pull.1 🔗

@@ -3,7 +3,7 @@
 
 .SH NAME
 .PP
-git-bug-bridge-pull - Pull updates.
+git-bug-bridge-pull - Pull updates from a remote bug tracker
 
 
 .SH SYNOPSIS
@@ -13,7 +13,7 @@ git-bug-bridge-pull - Pull updates.
 
 .SH DESCRIPTION
 .PP
-Pull updates.
+Pull updates from a remote bug tracker
 
 
 .SH OPTIONS

doc/man/git-bug-bridge-push.1 🔗

@@ -3,7 +3,7 @@
 
 .SH NAME
 .PP
-git-bug-bridge-push - Push updates.
+git-bug-bridge-push - Push updates to remote bug tracker
 
 
 .SH SYNOPSIS
@@ -13,7 +13,7 @@ git-bug-bridge-push - Push updates.
 
 .SH DESCRIPTION
 .PP
-Push updates.
+Push updates to remote bug tracker
 
 
 .SH OPTIONS

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

@@ -3,7 +3,7 @@
 
 .SH NAME
 .PP
-git-bug-bridge-rm - Delete a configured bridge.
+git-bug-bridge-rm - Delete a configured bridge
 
 
 .SH SYNOPSIS
@@ -13,7 +13,7 @@ git-bug-bridge-rm - Delete a configured bridge.
 
 .SH DESCRIPTION
 .PP
-Delete a configured bridge.
+Delete a configured bridge
 
 
 .SH OPTIONS

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

@@ -3,7 +3,7 @@
 
 .SH NAME
 .PP
-git-bug-bridge - Configure and use bridges to other bug trackers.
+git-bug-bridge - List bridges to other bug trackers
 
 
 .SH SYNOPSIS
@@ -13,7 +13,7 @@ git-bug-bridge - Configure and use bridges to other bug trackers.
 
 .SH DESCRIPTION
 .PP
-Configure and use bridges to other bug trackers.
+List bridges to other bug trackers
 
 
 .SH OPTIONS
@@ -24,4 +24,4 @@ Configure and use bridges to other bug trackers.
 
 .SH SEE ALSO
 .PP
-\fBgit-bug(1)\fP, \fBgit-bug-bridge-auth(1)\fP, \fBgit-bug-bridge-configure(1)\fP, \fBgit-bug-bridge-pull(1)\fP, \fBgit-bug-bridge-push(1)\fP, \fBgit-bug-bridge-rm(1)\fP
+\fBgit-bug(1)\fP, \fBgit-bug-bridge-auth(1)\fP, \fBgit-bug-bridge-new(1)\fP, \fBgit-bug-bridge-pull(1)\fP, \fBgit-bug-bridge-push(1)\fP, \fBgit-bug-bridge-rm(1)\fP

doc/man/git-bug-comment-edit.1 → doc/man/git-bug-bug-comment-edit.1 🔗

@@ -3,17 +3,17 @@
 
 .SH NAME
 .PP
-git-bug-comment-edit - Edit an existing comment on a bug.
+git-bug-bug-comment-edit - Edit an existing comment on a bug
 
 
 .SH SYNOPSIS
 .PP
-\fBgit-bug comment edit [COMMENT_ID] [flags]\fP
+\fBgit-bug bug comment edit [COMMENT_ID] [flags]\fP
 
 
 .SH DESCRIPTION
 .PP
-Edit an existing comment on a bug.
+Edit an existing comment on a bug
 
 
 .SH OPTIONS
@@ -36,4 +36,4 @@ Edit an existing comment on a bug.
 
 .SH SEE ALSO
 .PP
-\fBgit-bug-comment(1)\fP
+\fBgit-bug-bug-comment(1)\fP

doc/man/git-bug-comment-add.1 → doc/man/git-bug-bug-comment-new.1 🔗

@@ -3,17 +3,17 @@
 
 .SH NAME
 .PP
-git-bug-comment-add - Add a new comment to a bug.
+git-bug-bug-comment-new - Add a new comment to a bug
 
 
 .SH SYNOPSIS
 .PP
-\fBgit-bug comment add [ID] [flags]\fP
+\fBgit-bug bug comment new [BUG_ID] [flags]\fP
 
 
 .SH DESCRIPTION
 .PP
-Add a new comment to a bug.
+Add a new comment to a bug
 
 
 .SH OPTIONS
@@ -31,9 +31,9 @@ Add a new comment to a bug.
 
 .PP
 \fB-h\fP, \fB--help\fP[=false]
-	help for add
+	help for new
 
 
 .SH SEE ALSO
 .PP
-\fBgit-bug-comment(1)\fP
+\fBgit-bug-bug-comment(1)\fP

doc/man/git-bug-comment.1 → doc/man/git-bug-bug-comment.1 🔗

@@ -3,17 +3,17 @@
 
 .SH NAME
 .PP
-git-bug-comment - Display or add comments to a bug.
+git-bug-bug-comment - List a bug's comments
 
 
 .SH SYNOPSIS
 .PP
-\fBgit-bug comment [ID] [flags]\fP
+\fBgit-bug bug comment [BUG_ID] [flags]\fP
 
 
 .SH DESCRIPTION
 .PP
-Display or add comments to a bug.
+List a bug's comments
 
 
 .SH OPTIONS
@@ -24,4 +24,4 @@ Display or add comments to a bug.
 
 .SH SEE ALSO
 .PP
-\fBgit-bug(1)\fP, \fBgit-bug-comment-add(1)\fP, \fBgit-bug-comment-edit(1)\fP
+\fBgit-bug-bug(1)\fP, \fBgit-bug-bug-comment-edit(1)\fP, \fBgit-bug-bug-comment-new(1)\fP

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

@@ -3,17 +3,17 @@
 
 .SH NAME
 .PP
-git-bug-deselect - Clear the implicitly selected bug.
+git-bug-bug-deselect - Clear the implicitly selected bug
 
 
 .SH SYNOPSIS
 .PP
-\fBgit-bug deselect [flags]\fP
+\fBgit-bug bug deselect [flags]\fP
 
 
 .SH DESCRIPTION
 .PP
-Clear the implicitly selected bug.
+Clear the implicitly selected bug
 
 
 .SH OPTIONS
@@ -39,4 +39,4 @@ git bug deselect
 
 .SH SEE ALSO
 .PP
-\fBgit-bug(1)\fP
+\fBgit-bug-bug(1)\fP

doc/man/git-bug-label-add.1 → doc/man/git-bug-bug-label-new.1 🔗

@@ -3,25 +3,25 @@
 
 .SH NAME
 .PP
-git-bug-label-add - Add a label to a bug.
+git-bug-bug-label-new - Add a label to a bug
 
 
 .SH SYNOPSIS
 .PP
-\fBgit-bug label add [ID] LABEL... [flags]\fP
+\fBgit-bug bug label new [BUG_ID] LABEL... [flags]\fP
 
 
 .SH DESCRIPTION
 .PP
-Add a label to a bug.
+Add a label to a bug
 
 
 .SH OPTIONS
 .PP
 \fB-h\fP, \fB--help\fP[=false]
-	help for add
+	help for new
 
 
 .SH SEE ALSO
 .PP
-\fBgit-bug-label(1)\fP
+\fBgit-bug-bug-label(1)\fP

doc/man/git-bug-label-rm.1 → doc/man/git-bug-bug-label-rm.1 🔗

@@ -3,17 +3,17 @@
 
 .SH NAME
 .PP
-git-bug-label-rm - Remove a label from a bug.
+git-bug-bug-label-rm - Remove a label from a bug
 
 
 .SH SYNOPSIS
 .PP
-\fBgit-bug label rm [ID] LABEL... [flags]\fP
+\fBgit-bug bug label rm [BUG_ID] LABEL... [flags]\fP
 
 
 .SH DESCRIPTION
 .PP
-Remove a label from a bug.
+Remove a label from a bug
 
 
 .SH OPTIONS
@@ -24,4 +24,4 @@ Remove a label from a bug.
 
 .SH SEE ALSO
 .PP
-\fBgit-bug-label(1)\fP
+\fBgit-bug-bug-label(1)\fP

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

@@ -0,0 +1,27 @@
+.nh
+.TH "GIT-BUG" "1" "Apr 2019" "Generated from git-bug's source code" ""
+
+.SH NAME
+.PP
+git-bug-bug-label - Display labels of a bug
+
+
+.SH SYNOPSIS
+.PP
+\fBgit-bug bug label [BUG_ID] [flags]\fP
+
+
+.SH DESCRIPTION
+.PP
+Display labels of a bug
+
+
+.SH OPTIONS
+.PP
+\fB-h\fP, \fB--help\fP[=false]
+	help for label
+
+
+.SH SEE ALSO
+.PP
+\fBgit-bug-bug(1)\fP, \fBgit-bug-bug-label-new(1)\fP, \fBgit-bug-bug-label-rm(1)\fP

doc/man/git-bug-add.1 → doc/man/git-bug-bug-new.1 🔗

@@ -3,17 +3,17 @@
 
 .SH NAME
 .PP
-git-bug-add - Create a new bug.
+git-bug-bug-new - Create a new bug
 
 
 .SH SYNOPSIS
 .PP
-\fBgit-bug add [flags]\fP
+\fBgit-bug bug new [flags]\fP
 
 
 .SH DESCRIPTION
 .PP
-Create a new bug.
+Create a new bug
 
 
 .SH OPTIONS
@@ -35,9 +35,9 @@ Create a new bug.
 
 .PP
 \fB-h\fP, \fB--help\fP[=false]
-	help for add
+	help for new
 
 
 .SH SEE ALSO
 .PP
-\fBgit-bug(1)\fP
+\fBgit-bug-bug(1)\fP

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

@@ -3,12 +3,12 @@
 
 .SH NAME
 .PP
-git-bug-rm - Remove an existing bug.
+git-bug-bug-rm - Remove an existing bug
 
 
 .SH SYNOPSIS
 .PP
-\fBgit-bug rm ID [flags]\fP
+\fBgit-bug bug rm BUG_ID [flags]\fP
 
 
 .SH DESCRIPTION
@@ -24,4 +24,4 @@ Remove an existing bug in the local repository. Note removing bugs that were imp
 
 .SH SEE ALSO
 .PP
-\fBgit-bug(1)\fP
+\fBgit-bug-bug(1)\fP

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

@@ -3,12 +3,12 @@
 
 .SH NAME
 .PP
-git-bug-select - Select a bug for implicit use in future commands.
+git-bug-bug-select - Select a bug for implicit use in future commands
 
 
 .SH SYNOPSIS
 .PP
-\fBgit-bug select ID [flags]\fP
+\fBgit-bug bug select BUG_ID [flags]\fP
 
 
 .SH DESCRIPTION
@@ -47,4 +47,4 @@ git bug status
 
 .SH SEE ALSO
 .PP
-\fBgit-bug(1)\fP
+\fBgit-bug-bug(1)\fP

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

@@ -3,17 +3,17 @@
 
 .SH NAME
 .PP
-git-bug-show - Display the details of a bug.
+git-bug-bug-show - Display the details of a bug
 
 
 .SH SYNOPSIS
 .PP
-\fBgit-bug show [ID] [flags]\fP
+\fBgit-bug bug show [BUG_ID] [flags]\fP
 
 
 .SH DESCRIPTION
 .PP
-Display the details of a bug.
+Display the details of a bug
 
 
 .SH OPTIONS
@@ -32,4 +32,4 @@ Display the details of a bug.
 
 .SH SEE ALSO
 .PP
-\fBgit-bug(1)\fP
+\fBgit-bug-bug(1)\fP

doc/man/git-bug-status-close.1 → doc/man/git-bug-bug-status-close.1 🔗

@@ -3,17 +3,17 @@
 
 .SH NAME
 .PP
-git-bug-status-close - Mark a bug as closed.
+git-bug-bug-status-close - Mark a bug as closed
 
 
 .SH SYNOPSIS
 .PP
-\fBgit-bug status close [ID] [flags]\fP
+\fBgit-bug bug status close [BUG_ID] [flags]\fP
 
 
 .SH DESCRIPTION
 .PP
-Mark a bug as closed.
+Mark a bug as closed
 
 
 .SH OPTIONS
@@ -24,4 +24,4 @@ Mark a bug as closed.
 
 .SH SEE ALSO
 .PP
-\fBgit-bug-status(1)\fP
+\fBgit-bug-bug-status(1)\fP

doc/man/git-bug-status-open.1 → doc/man/git-bug-bug-status-open.1 🔗

@@ -3,17 +3,17 @@
 
 .SH NAME
 .PP
-git-bug-status-open - Mark a bug as open.
+git-bug-bug-status-open - Mark a bug as open
 
 
 .SH SYNOPSIS
 .PP
-\fBgit-bug status open [ID] [flags]\fP
+\fBgit-bug bug status open [BUG_ID] [flags]\fP
 
 
 .SH DESCRIPTION
 .PP
-Mark a bug as open.
+Mark a bug as open
 
 
 .SH OPTIONS
@@ -24,4 +24,4 @@ Mark a bug as open.
 
 .SH SEE ALSO
 .PP
-\fBgit-bug-status(1)\fP
+\fBgit-bug-bug-status(1)\fP

doc/man/git-bug-status.1 → doc/man/git-bug-bug-status.1 🔗

@@ -3,17 +3,17 @@
 
 .SH NAME
 .PP
-git-bug-status - Display or change a bug status.
+git-bug-bug-status - Display the status of a bug
 
 
 .SH SYNOPSIS
 .PP
-\fBgit-bug status [ID] [flags]\fP
+\fBgit-bug bug status [BUG_ID] [flags]\fP
 
 
 .SH DESCRIPTION
 .PP
-Display or change a bug status.
+Display the status of a bug
 
 
 .SH OPTIONS
@@ -24,4 +24,4 @@ Display or change a bug status.
 
 .SH SEE ALSO
 .PP
-\fBgit-bug(1)\fP, \fBgit-bug-status-close(1)\fP, \fBgit-bug-status-open(1)\fP
+\fBgit-bug-bug(1)\fP, \fBgit-bug-bug-status-close(1)\fP, \fBgit-bug-bug-status-open(1)\fP

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

@@ -3,17 +3,17 @@
 
 .SH NAME
 .PP
-git-bug-title-edit - Edit a title of a bug.
+git-bug-bug-title-edit - Edit a title of a bug
 
 
 .SH SYNOPSIS
 .PP
-\fBgit-bug title edit [ID] [flags]\fP
+\fBgit-bug bug title edit [BUG_ID] [flags]\fP
 
 
 .SH DESCRIPTION
 .PP
-Edit a title of a bug.
+Edit a title of a bug
 
 
 .SH OPTIONS
@@ -32,4 +32,4 @@ Edit a title of a bug.
 
 .SH SEE ALSO
 .PP
-\fBgit-bug-title(1)\fP
+\fBgit-bug-bug-title(1)\fP

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

@@ -3,17 +3,17 @@
 
 .SH NAME
 .PP
-git-bug-title - Display or change a title of a bug.
+git-bug-bug-title - Display the title of a bug
 
 
 .SH SYNOPSIS
 .PP
-\fBgit-bug title [ID] [flags]\fP
+\fBgit-bug bug title [BUG_ID] [flags]\fP
 
 
 .SH DESCRIPTION
 .PP
-Display or change a title of a bug.
+Display the title of a bug
 
 
 .SH OPTIONS
@@ -24,4 +24,4 @@ Display or change a title of a bug.
 
 .SH SEE ALSO
 .PP
-\fBgit-bug(1)\fP, \fBgit-bug-title-edit(1)\fP
+\fBgit-bug-bug(1)\fP, \fBgit-bug-bug-title-edit(1)\fP

doc/man/git-bug-ls.1 → doc/man/git-bug-bug.1 🔗

@@ -3,12 +3,12 @@
 
 .SH NAME
 .PP
-git-bug-ls - List bugs.
+git-bug-bug - List bugs
 
 
 .SH SYNOPSIS
 .PP
-\fBgit-bug ls [QUERY] [flags]\fP
+\fBgit-bug bug [QUERY] [flags]\fP
 
 
 .SH DESCRIPTION
@@ -66,7 +66,7 @@ You can pass an additional query to filter and order the list. This query can be
 
 .PP
 \fB-h\fP, \fB--help\fP[=false]
-	help for ls
+	help for bug
 
 
 .SH EXAMPLE
@@ -75,16 +75,16 @@ You can pass an additional query to filter and order the list. This query can be
 
 .nf
 List open bugs sorted by last edition with a query:
-git bug ls status:open sort:edit-desc
+git bug status:open sort:edit-desc
 
 List closed bugs sorted by creation with flags:
-git bug ls --status closed --by creation
+git bug --status closed --by creation
 
 Do a full text search of all bugs:
-git bug ls "foo bar" baz
+git bug "foo bar" baz
 
 Use queries, flags, and full text search:
-git bug ls status:open --by creation "foo bar" baz
+git bug status:open --by creation "foo bar" baz
 
 
 .fi
@@ -93,4 +93,4 @@ git bug ls status:open --by creation "foo bar" baz
 
 .SH SEE ALSO
 .PP
-\fBgit-bug(1)\fP
+\fBgit-bug(1)\fP, \fBgit-bug-bug-comment(1)\fP, \fBgit-bug-bug-deselect(1)\fP, \fBgit-bug-bug-label(1)\fP, \fBgit-bug-bug-new(1)\fP, \fBgit-bug-bug-rm(1)\fP, \fBgit-bug-bug-select(1)\fP, \fBgit-bug-bug-show(1)\fP, \fBgit-bug-bug-status(1)\fP, \fBgit-bug-bug-title(1)\fP

doc/man/git-bug-label-ls.1 🔗

@@ -1,30 +0,0 @@
-.nh
-.TH "GIT-BUG" "1" "Apr 2019" "Generated from git-bug's source code" ""
-
-.SH NAME
-.PP
-git-bug-label-ls - List valid labels.
-
-
-.SH SYNOPSIS
-.PP
-\fBgit-bug label ls [flags]\fP
-
-
-.SH DESCRIPTION
-.PP
-List valid labels.
-
-.PP
-Note: in the future, a proper label policy could be implemented where valid labels are defined in a configuration file. Until that, the default behavior is to return the list of labels already used.
-
-
-.SH OPTIONS
-.PP
-\fB-h\fP, \fB--help\fP[=false]
-	help for ls
-
-
-.SH SEE ALSO
-.PP
-\fBgit-bug-label(1)\fP

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

@@ -3,17 +3,20 @@
 
 .SH NAME
 .PP
-git-bug-label - Display, add or remove labels to/from a bug.
+git-bug-label - List valid labels
 
 
 .SH SYNOPSIS
 .PP
-\fBgit-bug label [ID] [flags]\fP
+\fBgit-bug label [flags]\fP
 
 
 .SH DESCRIPTION
 .PP
-Display, add or remove labels to/from a bug.
+List valid labels.
+
+.PP
+Note: in the future, a proper label policy could be implemented where valid labels are defined in a configuration file. Until that, the default behavior is to return the list of labels already used.
 
 
 .SH OPTIONS
@@ -24,4 +27,4 @@ Display, add or remove labels to/from a bug.
 
 .SH SEE ALSO
 .PP
-\fBgit-bug(1)\fP, \fBgit-bug-label-add(1)\fP, \fBgit-bug-label-ls(1)\fP, \fBgit-bug-label-rm(1)\fP
+\fBgit-bug(1)\fP

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

@@ -3,7 +3,7 @@
 
 .SH NAME
 .PP
-git-bug-pull - Pull bugs update from a git remote.
+git-bug-pull - Pull updates from a git remote
 
 
 .SH SYNOPSIS
@@ -13,7 +13,7 @@ git-bug-pull - Pull bugs update from a git remote.
 
 .SH DESCRIPTION
 .PP
-Pull bugs update from a git remote.
+Pull updates from a git remote
 
 
 .SH OPTIONS

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

@@ -3,7 +3,7 @@
 
 .SH NAME
 .PP
-git-bug-push - Push bugs update to a git remote.
+git-bug-push - Push updates to a git remote
 
 
 .SH SYNOPSIS
@@ -13,7 +13,7 @@ git-bug-push - Push bugs update to a git remote.
 
 .SH DESCRIPTION
 .PP
-Push bugs update to a git remote.
+Push updates to a git remote
 
 
 .SH OPTIONS

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

@@ -3,7 +3,7 @@
 
 .SH NAME
 .PP
-git-bug-termui - Launch the terminal UI.
+git-bug-termui - Launch the terminal UI
 
 
 .SH SYNOPSIS
@@ -13,7 +13,7 @@ git-bug-termui - Launch the terminal UI.
 
 .SH DESCRIPTION
 .PP
-Launch the terminal UI.
+Launch the terminal UI
 
 
 .SH OPTIONS

doc/man/git-bug-user-adopt.1 🔗

@@ -3,17 +3,17 @@
 
 .SH NAME
 .PP
-git-bug-user-adopt - Adopt an existing identity as your own.
+git-bug-user-adopt - Adopt an existing identity as your own
 
 
 .SH SYNOPSIS
 .PP
-\fBgit-bug user adopt USER-ID [flags]\fP
+\fBgit-bug user adopt USER_ID [flags]\fP
 
 
 .SH DESCRIPTION
 .PP
-Adopt an existing identity as your own.
+Adopt an existing identity as your own
 
 
 .SH OPTIONS

doc/man/git-bug-user-ls.1 🔗

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

doc/man/git-bug-user-create.1 → doc/man/git-bug-user-new.1 🔗

@@ -3,17 +3,17 @@
 
 .SH NAME
 .PP
-git-bug-user-create - Create a new identity.
+git-bug-user-new - Create a new identity
 
 
 .SH SYNOPSIS
 .PP
-\fBgit-bug user create [flags]\fP
+\fBgit-bug user new [flags]\fP
 
 
 .SH DESCRIPTION
 .PP
-Create a new identity.
+Create a new identity
 
 
 .SH OPTIONS
@@ -27,7 +27,7 @@ Create a new identity.
 
 .PP
 \fB-h\fP, \fB--help\fP[=false]
-	help for create
+	help for new
 
 .PP
 \fB-n\fP, \fB--name\fP=""

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

@@ -0,0 +1,31 @@
+.nh
+.TH "GIT-BUG" "1" "Apr 2019" "Generated from git-bug's source code" ""
+
+.SH NAME
+.PP
+git-bug-user-user - Display a user identity
+
+
+.SH SYNOPSIS
+.PP
+\fBgit-bug user user show [USER_ID] [flags]\fP
+
+
+.SH DESCRIPTION
+.PP
+Display a user identity
+
+
+.SH OPTIONS
+.PP
+\fB-f\fP, \fB--field\fP=""
+	Select field to display. Valid values are [email,humanId,id,lastModification,lastModificationLamports,login,metadata,name]
+
+.PP
+\fB-h\fP, \fB--help\fP[=false]
+	help for user
+
+
+.SH SEE ALSO
+.PP
+\fBgit-bug-user(1)\fP

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

@@ -3,23 +3,23 @@
 
 .SH NAME
 .PP
-git-bug-user - Display or change the user identity.
+git-bug-user - List identities
 
 
 .SH SYNOPSIS
 .PP
-\fBgit-bug user [USER-ID] [flags]\fP
+\fBgit-bug user [flags]\fP
 
 
 .SH DESCRIPTION
 .PP
-Display or change the user identity.
+List identities
 
 
 .SH OPTIONS
 .PP
-\fB-f\fP, \fB--field\fP=""
-	Select field to display. Valid values are [email,humanId,id,lastModification,lastModificationLamports,login,metadata,name]
+\fB-f\fP, \fB--format\fP="default"
+	Select the output formatting style. Valid values are [default,json]
 
 .PP
 \fB-h\fP, \fB--help\fP[=false]
@@ -28,4 +28,4 @@ Display or change the user identity.
 
 .SH SEE ALSO
 .PP
-\fBgit-bug(1)\fP, \fBgit-bug-user-adopt(1)\fP, \fBgit-bug-user-create(1)\fP, \fBgit-bug-user-ls(1)\fP
+\fBgit-bug(1)\fP, \fBgit-bug-user-adopt(1)\fP, \fBgit-bug-user-new(1)\fP, \fBgit-bug-user-user(1)\fP

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

@@ -3,7 +3,7 @@
 
 .SH NAME
 .PP
-git-bug-version - Show git-bug version information.
+git-bug-version - Show git-bug version information
 
 
 .SH SYNOPSIS
@@ -13,7 +13,7 @@ git-bug-version - Show git-bug version information.
 
 .SH DESCRIPTION
 .PP
-Show git-bug version information.
+Show git-bug version information
 
 
 .SH OPTIONS

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

@@ -3,7 +3,7 @@
 
 .SH NAME
 .PP
-git-bug-webui - Launch the web UI.
+git-bug-webui - Launch the web UI
 
 
 .SH SYNOPSIS

doc/man/git-bug.1 🔗

@@ -3,7 +3,7 @@
 
 .SH NAME
 .PP
-git-bug - A bug tracker embedded in Git.
+git-bug - A bug tracker embedded in Git
 
 
 .SH SYNOPSIS
@@ -29,4 +29,4 @@ the same git remote you are already using to collaborate with other people.
 
 .SH SEE ALSO
 .PP
-\fBgit-bug-add(1)\fP, \fBgit-bug-bridge(1)\fP, \fBgit-bug-commands(1)\fP, \fBgit-bug-comment(1)\fP, \fBgit-bug-deselect(1)\fP, \fBgit-bug-label(1)\fP, \fBgit-bug-ls(1)\fP, \fBgit-bug-pull(1)\fP, \fBgit-bug-push(1)\fP, \fBgit-bug-rm(1)\fP, \fBgit-bug-select(1)\fP, \fBgit-bug-show(1)\fP, \fBgit-bug-status(1)\fP, \fBgit-bug-termui(1)\fP, \fBgit-bug-title(1)\fP, \fBgit-bug-user(1)\fP, \fBgit-bug-version(1)\fP, \fBgit-bug-webui(1)\fP
+\fBgit-bug-bridge(1)\fP, \fBgit-bug-bug(1)\fP, \fBgit-bug-commands(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

doc/md/git-bug.md 🔗

@@ -1,6 +1,6 @@
 ## git-bug
 
-A bug tracker embedded in Git.
+A bug tracker embedded in Git
 
 ### Synopsis
 
@@ -24,22 +24,14 @@ git-bug [flags]
 
 ### SEE ALSO
 
-* [git-bug add](git-bug_add.md)	 - Create a new bug.
-* [git-bug bridge](git-bug_bridge.md)	 - Configure and use bridges to other bug trackers.
+* [git-bug bridge](git-bug_bridge.md)	 - List bridges to other bug trackers
+* [git-bug bug](git-bug_bug.md)	 - List bugs
 * [git-bug commands](git-bug_commands.md)	 - Display available commands.
-* [git-bug comment](git-bug_comment.md)	 - Display or add comments to a bug.
-* [git-bug deselect](git-bug_deselect.md)	 - Clear the implicitly selected bug.
-* [git-bug label](git-bug_label.md)	 - Display, add or remove labels to/from a bug.
-* [git-bug ls](git-bug_ls.md)	 - List bugs.
-* [git-bug pull](git-bug_pull.md)	 - Pull bugs update from a git remote.
-* [git-bug push](git-bug_push.md)	 - Push bugs update to a git remote.
-* [git-bug rm](git-bug_rm.md)	 - Remove an existing bug.
-* [git-bug select](git-bug_select.md)	 - Select a bug for implicit use in future commands.
-* [git-bug show](git-bug_show.md)	 - Display the details of a bug.
-* [git-bug status](git-bug_status.md)	 - Display or change a bug status.
-* [git-bug termui](git-bug_termui.md)	 - Launch the terminal UI.
-* [git-bug title](git-bug_title.md)	 - Display or change a title of a bug.
-* [git-bug user](git-bug_user.md)	 - Display or change the user identity.
-* [git-bug version](git-bug_version.md)	 - Show git-bug version information.
-* [git-bug webui](git-bug_webui.md)	 - Launch the web UI.
+* [git-bug label](git-bug_label.md)	 - List valid labels
+* [git-bug pull](git-bug_pull.md)	 - Pull updates from a git remote
+* [git-bug push](git-bug_push.md)	 - Push updates to a git remote
+* [git-bug termui](git-bug_termui.md)	 - Launch the terminal UI
+* [git-bug user](git-bug_user.md)	 - List identities
+* [git-bug version](git-bug_version.md)	 - Show git-bug version information
+* [git-bug webui](git-bug_webui.md)	 - Launch the web UI
 

doc/md/git-bug_bridge.md 🔗

@@ -1,6 +1,6 @@
 ## git-bug bridge
 
-Configure and use bridges to other bug trackers.
+List bridges to other bug trackers
 
 ```
 git-bug bridge [flags]
@@ -14,10 +14,10 @@ git-bug bridge [flags]
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
-* [git-bug bridge auth](git-bug_bridge_auth.md)	 - List all known bridge authentication credentials.
-* [git-bug bridge configure](git-bug_bridge_configure.md)	 - Configure a new bridge.
-* [git-bug bridge pull](git-bug_bridge_pull.md)	 - Pull updates.
-* [git-bug bridge push](git-bug_bridge_push.md)	 - Push updates.
-* [git-bug bridge rm](git-bug_bridge_rm.md)	 - Delete a configured bridge.
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
+* [git-bug bridge auth](git-bug_bridge_auth.md)	 - List all known bridge authentication credentials
+* [git-bug bridge new](git-bug_bridge_new.md)	 - Configure a new bridge
+* [git-bug bridge pull](git-bug_bridge_pull.md)	 - Pull updates from a remote bug tracker
+* [git-bug bridge push](git-bug_bridge_push.md)	 - Push updates to remote bug tracker
+* [git-bug bridge rm](git-bug_bridge_rm.md)	 - Delete a configured bridge
 

doc/md/git-bug_bridge_auth.md 🔗

@@ -1,6 +1,6 @@
 ## git-bug bridge auth
 
-List all known bridge authentication credentials.
+List all known bridge authentication credentials
 
 ```
 git-bug bridge auth [flags]
@@ -14,8 +14,8 @@ git-bug bridge auth [flags]
 
 ### SEE ALSO
 
-* [git-bug bridge](git-bug_bridge.md)	 - Configure and use bridges to other bug trackers.
+* [git-bug bridge](git-bug_bridge.md)	 - List bridges to other bug trackers
 * [git-bug bridge auth add-token](git-bug_bridge_auth_add-token.md)	 - Store a new token
-* [git-bug bridge auth rm](git-bug_bridge_auth_rm.md)	 - Remove a credential.
-* [git-bug bridge auth show](git-bug_bridge_auth_show.md)	 - Display an authentication credential.
+* [git-bug bridge auth rm](git-bug_bridge_auth_rm.md)	 - Remove a credential
+* [git-bug bridge auth show](git-bug_bridge_auth_show.md)	 - Display an authentication credential
 

doc/md/git-bug_bridge_auth_add-token.md 🔗

@@ -17,5 +17,5 @@ git-bug bridge auth add-token [TOKEN] [flags]
 
 ### SEE ALSO
 
-* [git-bug bridge auth](git-bug_bridge_auth.md)	 - List all known bridge authentication credentials.
+* [git-bug bridge auth](git-bug_bridge_auth.md)	 - List all known bridge authentication credentials
 

doc/md/git-bug_bridge_auth_rm.md 🔗

@@ -1,9 +1,9 @@
 ## git-bug bridge auth rm
 
-Remove a credential.
+Remove a credential
 
 ```
-git-bug bridge auth rm ID [flags]
+git-bug bridge auth rm BRIDGE_ID [flags]
 ```
 
 ### Options
@@ -14,5 +14,5 @@ git-bug bridge auth rm ID [flags]
 
 ### SEE ALSO
 
-* [git-bug bridge auth](git-bug_bridge_auth.md)	 - List all known bridge authentication credentials.
+* [git-bug bridge auth](git-bug_bridge_auth.md)	 - List all known bridge authentication credentials
 

doc/md/git-bug_bridge_auth_show.md 🔗

@@ -1,6 +1,6 @@
 ## git-bug bridge auth show
 
-Display an authentication credential.
+Display an authentication credential
 
 ```
 git-bug bridge auth show [flags]
@@ -14,5 +14,5 @@ git-bug bridge auth show [flags]
 
 ### SEE ALSO
 
-* [git-bug bridge auth](git-bug_bridge_auth.md)	 - List all known bridge authentication credentials.
+* [git-bug bridge auth](git-bug_bridge_auth.md)	 - List all known bridge authentication credentials
 

doc/md/git-bug_bridge_configure.md → doc/md/git-bug_bridge_new.md 🔗

@@ -1,13 +1,13 @@
-## git-bug bridge configure
+## git-bug bridge new
 
-Configure a new bridge.
+Configure a new bridge
 
 ### Synopsis
 
 	Configure a new bridge by passing flags or/and using interactive terminal prompts. You can avoid all the terminal prompts by passing all the necessary flags to configure your bridge.
 
 ```
-git-bug bridge configure [flags]
+git-bug bridge new [flags]
 ```
 
 ### Examples
@@ -47,7 +47,7 @@ Enter token: 87cf5c03b64029f18ea5f9ca5679daa08ccbd700
 Successfully configured bridge: default
 
 # For GitHub
-git bug bridge configure \
+git bug bridge new \
     --name=default \
     --target=github \
     --owner=$(OWNER) \
@@ -55,13 +55,13 @@ git bug bridge configure \
     --token=$(TOKEN)
 
 # For Launchpad
-git bug bridge configure \
+git bug bridge new \
     --name=default \
     --target=launchpad-preview \
     --url=https://bugs.launchpad.net/ubuntu/
 
 # For Gitlab
-git bug bridge configure \
+git bug bridge new \
     --name=default \
     --target=github \
     --url=https://github.com/michaelmure/git-bug \
@@ -82,10 +82,10 @@ git bug bridge configure \
   -o, --owner string        The owner of the remote repository
   -p, --project string      The name of the remote repository
       --non-interactive     Do not ask for user input
-  -h, --help                help for configure
+  -h, --help                help for new
 ```
 
 ### SEE ALSO
 
-* [git-bug bridge](git-bug_bridge.md)	 - Configure and use bridges to other bug trackers.
+* [git-bug bridge](git-bug_bridge.md)	 - List bridges to other bug trackers
 

doc/md/git-bug_bridge_pull.md 🔗

@@ -1,6 +1,6 @@
 ## git-bug bridge pull
 
-Pull updates.
+Pull updates from a remote bug tracker
 
 ```
 git-bug bridge pull [NAME] [flags]
@@ -16,5 +16,5 @@ git-bug bridge pull [NAME] [flags]
 
 ### SEE ALSO
 
-* [git-bug bridge](git-bug_bridge.md)	 - Configure and use bridges to other bug trackers.
+* [git-bug bridge](git-bug_bridge.md)	 - List bridges to other bug trackers
 

doc/md/git-bug_bridge_push.md 🔗

@@ -1,6 +1,6 @@
 ## git-bug bridge push
 
-Push updates.
+Push updates to remote bug tracker
 
 ```
 git-bug bridge push [NAME] [flags]
@@ -14,5 +14,5 @@ git-bug bridge push [NAME] [flags]
 
 ### SEE ALSO
 
-* [git-bug bridge](git-bug_bridge.md)	 - Configure and use bridges to other bug trackers.
+* [git-bug bridge](git-bug_bridge.md)	 - List bridges to other bug trackers
 

doc/md/git-bug_bridge_rm.md 🔗

@@ -1,6 +1,6 @@
 ## git-bug bridge rm
 
-Delete a configured bridge.
+Delete a configured bridge
 
 ```
 git-bug bridge rm NAME [flags]
@@ -14,5 +14,5 @@ git-bug bridge rm NAME [flags]
 
 ### SEE ALSO
 
-* [git-bug bridge](git-bug_bridge.md)	 - Configure and use bridges to other bug trackers.
+* [git-bug bridge](git-bug_bridge.md)	 - List bridges to other bug trackers
 

doc/md/git-bug_ls.md → doc/md/git-bug_bug.md 🔗

@@ -1,6 +1,6 @@
-## git-bug ls
+## git-bug bug
 
-List bugs.
+List bugs
 
 ### Synopsis
 
@@ -9,23 +9,23 @@ Display a summary of each bugs.
 You can pass an additional query to filter and order the list. This query can be expressed either with a simple query language, flags, a natural language full text search, or a combination of the aforementioned.
 
 ```
-git-bug ls [QUERY] [flags]
+git-bug bug [QUERY] [flags]
 ```
 
 ### Examples
 
 ```
 List open bugs sorted by last edition with a query:
-git bug ls status:open sort:edit-desc
+git bug status:open sort:edit-desc
 
 List closed bugs sorted by creation with flags:
-git bug ls --status closed --by creation
+git bug --status closed --by creation
 
 Do a full text search of all bugs:
-git bug ls "foo bar" baz
+git bug "foo bar" baz
 
 Use queries, flags, and full text search:
-git bug ls status:open --by creation "foo bar" baz
+git bug status:open --by creation "foo bar" baz
 
 ```
 
@@ -43,10 +43,19 @@ git bug ls status:open --by creation "foo bar" baz
   -b, --by string             Sort the results by a characteristic. Valid values are [id,creation,edit] (default "creation")
   -d, --direction string      Select the sorting direction. Valid values are [asc,desc] (default "asc")
   -f, --format string         Select the output formatting style. Valid values are [default,plain,compact,id,json,org-mode] (default "default")
-  -h, --help                  help for ls
+  -h, --help                  help for bug
 ```
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
+* [git-bug bug comment](git-bug_bug_comment.md)	 - List a bug's comments
+* [git-bug bug deselect](git-bug_bug_deselect.md)	 - Clear the implicitly selected bug
+* [git-bug bug label](git-bug_bug_label.md)	 - Display labels of a bug
+* [git-bug bug new](git-bug_bug_new.md)	 - Create a new bug
+* [git-bug bug rm](git-bug_bug_rm.md)	 - Remove an existing bug
+* [git-bug bug select](git-bug_bug_select.md)	 - Select a bug for implicit use in future commands
+* [git-bug bug show](git-bug_bug_show.md)	 - Display the details of a bug
+* [git-bug bug status](git-bug_bug_status.md)	 - Display the status of a bug
+* [git-bug bug title](git-bug_bug_title.md)	 - Display the title of a bug
 

doc/md/git-bug_bug_comment.md 🔗

@@ -0,0 +1,20 @@
+## git-bug bug comment
+
+List a bug's comments
+
+```
+git-bug bug comment [BUG_ID] [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for comment
+```
+
+### SEE ALSO
+
+* [git-bug bug](git-bug_bug.md)	 - List bugs
+* [git-bug bug comment edit](git-bug_bug_comment_edit.md)	 - Edit an existing comment on a bug
+* [git-bug bug comment new](git-bug_bug_comment_new.md)	 - Add a new comment to a bug
+

doc/md/git-bug_comment_edit.md → doc/md/git-bug_bug_comment_edit.md 🔗

@@ -1,9 +1,9 @@
-## git-bug comment edit
+## git-bug bug comment edit
 
-Edit an existing comment on a bug.
+Edit an existing comment on a bug
 
 ```
-git-bug comment edit [COMMENT_ID] [flags]
+git-bug bug comment edit [COMMENT_ID] [flags]
 ```
 
 ### Options
@@ -17,5 +17,5 @@ git-bug comment edit [COMMENT_ID] [flags]
 
 ### SEE ALSO
 
-* [git-bug comment](git-bug_comment.md)	 - Display or add comments to a bug.
+* [git-bug bug comment](git-bug_bug_comment.md)	 - List a bug's comments
 

doc/md/git-bug_comment_add.md → doc/md/git-bug_bug_comment_new.md 🔗

@@ -1,9 +1,9 @@
-## git-bug comment add
+## git-bug bug comment new
 
-Add a new comment to a bug.
+Add a new comment to a bug
 
 ```
-git-bug comment add [ID] [flags]
+git-bug bug comment new [BUG_ID] [flags]
 ```
 
 ### Options
@@ -12,10 +12,10 @@ git-bug comment add [ID] [flags]
   -F, --file string       Take the message from the given file. Use - to read the message from the standard input
   -m, --message string    Provide the new message from the command line
       --non-interactive   Do not ask for user input
-  -h, --help              help for add
+  -h, --help              help for new
 ```
 
 ### SEE ALSO
 
-* [git-bug comment](git-bug_comment.md)	 - Display or add comments to a bug.
+* [git-bug bug comment](git-bug_bug_comment.md)	 - List a bug's comments
 

doc/md/git-bug_deselect.md → doc/md/git-bug_bug_deselect.md 🔗

@@ -1,9 +1,9 @@
-## git-bug deselect
+## git-bug bug deselect
 
-Clear the implicitly selected bug.
+Clear the implicitly selected bug
 
 ```
-git-bug deselect [flags]
+git-bug bug deselect [flags]
 ```
 
 ### Examples
@@ -24,5 +24,5 @@ git bug deselect
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
+* [git-bug bug](git-bug_bug.md)	 - List bugs
 

doc/md/git-bug_bug_label.md 🔗

@@ -0,0 +1,20 @@
+## git-bug bug label
+
+Display labels of a bug
+
+```
+git-bug bug label [BUG_ID] [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for label
+```
+
+### SEE ALSO
+
+* [git-bug bug](git-bug_bug.md)	 - List bugs
+* [git-bug bug label new](git-bug_bug_label_new.md)	 - Add a label to a bug
+* [git-bug bug label rm](git-bug_bug_label_rm.md)	 - Remove a label from a bug
+

doc/md/git-bug_bug_label_new.md 🔗

@@ -0,0 +1,18 @@
+## git-bug bug label new
+
+Add a label to a bug
+
+```
+git-bug bug label new [BUG_ID] LABEL... [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for new
+```
+
+### SEE ALSO
+
+* [git-bug bug label](git-bug_bug_label.md)	 - Display labels of a bug
+

doc/md/git-bug_bug_label_rm.md 🔗

@@ -0,0 +1,18 @@
+## git-bug bug label rm
+
+Remove a label from a bug
+
+```
+git-bug bug label rm [BUG_ID] LABEL... [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for rm
+```
+
+### SEE ALSO
+
+* [git-bug bug label](git-bug_bug_label.md)	 - Display labels of a bug
+

doc/md/git-bug_add.md → doc/md/git-bug_bug_new.md 🔗

@@ -1,9 +1,9 @@
-## git-bug add
+## git-bug bug new
 
-Create a new bug.
+Create a new bug
 
 ```
-git-bug add [flags]
+git-bug bug new [flags]
 ```
 
 ### Options
@@ -13,10 +13,10 @@ git-bug add [flags]
   -m, --message string    Provide a message to describe the issue
   -F, --file string       Take the message from the given file. Use - to read the message from the standard input
       --non-interactive   Do not ask for user input
-  -h, --help              help for add
+  -h, --help              help for new
 ```
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
+* [git-bug bug](git-bug_bug.md)	 - List bugs
 

doc/md/git-bug_rm.md → doc/md/git-bug_bug_rm.md 🔗

@@ -1,13 +1,13 @@
-## git-bug rm
+## git-bug bug rm
 
-Remove an existing bug.
+Remove an existing bug
 
 ### Synopsis
 
 Remove an existing bug in the local repository. Note removing bugs that were imported from bridges will not remove the bug on the remote, and will only remove the local copy of the bug.
 
 ```
-git-bug rm ID [flags]
+git-bug bug rm BUG_ID [flags]
 ```
 
 ### Options
@@ -18,5 +18,5 @@ git-bug rm ID [flags]
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
+* [git-bug bug](git-bug_bug.md)	 - List bugs
 

doc/md/git-bug_select.md → doc/md/git-bug_bug_select.md 🔗

@@ -1,6 +1,6 @@
-## git-bug select
+## git-bug bug select
 
-Select a bug for implicit use in future commands.
+Select a bug for implicit use in future commands
 
 ### Synopsis
 
@@ -15,7 +15,7 @@ The complementary command is "git bug deselect" performing the opposite operatio
 
 
 ```
-git-bug select ID [flags]
+git-bug bug select BUG_ID [flags]
 ```
 
 ### Examples
@@ -35,5 +35,5 @@ git bug status
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
+* [git-bug bug](git-bug_bug.md)	 - List bugs
 

doc/md/git-bug_show.md → doc/md/git-bug_bug_show.md 🔗

@@ -1,9 +1,9 @@
-## git-bug show
+## git-bug bug show
 
-Display the details of a bug.
+Display the details of a bug
 
 ```
-git-bug show [ID] [flags]
+git-bug bug show [BUG_ID] [flags]
 ```
 
 ### Options
@@ -16,5 +16,5 @@ git-bug show [ID] [flags]
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
+* [git-bug bug](git-bug_bug.md)	 - List bugs
 

doc/md/git-bug_bug_status.md 🔗

@@ -0,0 +1,20 @@
+## git-bug bug status
+
+Display the status of a bug
+
+```
+git-bug bug status [BUG_ID] [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for status
+```
+
+### SEE ALSO
+
+* [git-bug bug](git-bug_bug.md)	 - List bugs
+* [git-bug bug status close](git-bug_bug_status_close.md)	 - Mark a bug as closed
+* [git-bug bug status open](git-bug_bug_status_open.md)	 - Mark a bug as open
+

doc/md/git-bug_bug_status_close.md 🔗

@@ -0,0 +1,18 @@
+## git-bug bug status close
+
+Mark a bug as closed
+
+```
+git-bug bug status close [BUG_ID] [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for close
+```
+
+### SEE ALSO
+
+* [git-bug bug status](git-bug_bug_status.md)	 - Display the status of a bug
+

doc/md/git-bug_bug_status_open.md 🔗

@@ -0,0 +1,18 @@
+## git-bug bug status open
+
+Mark a bug as open
+
+```
+git-bug bug status open [BUG_ID] [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for open
+```
+
+### SEE ALSO
+
+* [git-bug bug status](git-bug_bug_status.md)	 - Display the status of a bug
+

doc/md/git-bug_bug_title.md 🔗

@@ -0,0 +1,19 @@
+## git-bug bug title
+
+Display the title of a bug
+
+```
+git-bug bug title [BUG_ID] [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for title
+```
+
+### SEE ALSO
+
+* [git-bug bug](git-bug_bug.md)	 - List bugs
+* [git-bug bug title edit](git-bug_bug_title_edit.md)	 - Edit a title of a bug
+

doc/md/git-bug_title_edit.md → doc/md/git-bug_bug_title_edit.md 🔗

@@ -1,9 +1,9 @@
-## git-bug title edit
+## git-bug bug title edit
 
-Edit a title of a bug.
+Edit a title of a bug
 
 ```
-git-bug title edit [ID] [flags]
+git-bug bug title edit [BUG_ID] [flags]
 ```
 
 ### Options
@@ -16,5 +16,5 @@ git-bug title edit [ID] [flags]
 
 ### SEE ALSO
 
-* [git-bug title](git-bug_title.md)	 - Display or change a title of a bug.
+* [git-bug bug title](git-bug_bug_title.md)	 - Display the title of a bug
 

doc/md/git-bug_commands.md 🔗

@@ -15,5 +15,5 @@ git-bug commands [flags]
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
 

doc/md/git-bug_comment.md 🔗

@@ -1,20 +0,0 @@
-## git-bug comment
-
-Display or add comments to a bug.
-
-```
-git-bug comment [ID] [flags]
-```
-
-### Options
-
-```
-  -h, --help   help for comment
-```
-
-### SEE ALSO
-
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
-* [git-bug comment add](git-bug_comment_add.md)	 - Add a new comment to a bug.
-* [git-bug comment edit](git-bug_comment_edit.md)	 - Edit an existing comment on a bug.
-

doc/md/git-bug_label.md 🔗

@@ -1,9 +1,15 @@
 ## git-bug label
 
-Display, add or remove labels to/from a bug.
+List valid labels
+
+### Synopsis
+
+List valid labels.
+
+Note: in the future, a proper label policy could be implemented where valid labels are defined in a configuration file. Until that, the default behavior is to return the list of labels already used.
 
 ```
-git-bug label [ID] [flags]
+git-bug label [flags]
 ```
 
 ### Options
@@ -14,8 +20,5 @@ git-bug label [ID] [flags]
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
-* [git-bug label add](git-bug_label_add.md)	 - Add a label to a bug.
-* [git-bug label ls](git-bug_label_ls.md)	 - List valid labels.
-* [git-bug label rm](git-bug_label_rm.md)	 - Remove a label from a bug.
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
 

doc/md/git-bug_label_add.md 🔗

@@ -1,18 +0,0 @@
-## git-bug label add
-
-Add a label to a bug.
-
-```
-git-bug label add [ID] LABEL... [flags]
-```
-
-### Options
-
-```
-  -h, --help   help for add
-```
-
-### SEE ALSO
-
-* [git-bug label](git-bug_label.md)	 - Display, add or remove labels to/from a bug.
-

doc/md/git-bug_label_ls.md 🔗

@@ -1,24 +0,0 @@
-## git-bug label ls
-
-List valid labels.
-
-### Synopsis
-
-List valid labels.
-
-Note: in the future, a proper label policy could be implemented where valid labels are defined in a configuration file. Until that, the default behavior is to return the list of labels already used.
-
-```
-git-bug label ls [flags]
-```
-
-### Options
-
-```
-  -h, --help   help for ls
-```
-
-### SEE ALSO
-
-* [git-bug label](git-bug_label.md)	 - Display, add or remove labels to/from a bug.
-

doc/md/git-bug_label_rm.md 🔗

@@ -1,18 +0,0 @@
-## git-bug label rm
-
-Remove a label from a bug.
-
-```
-git-bug label rm [ID] LABEL... [flags]
-```
-
-### Options
-
-```
-  -h, --help   help for rm
-```
-
-### SEE ALSO
-
-* [git-bug label](git-bug_label.md)	 - Display, add or remove labels to/from a bug.
-

doc/md/git-bug_pull.md 🔗

@@ -1,6 +1,6 @@
 ## git-bug pull
 
-Pull bugs update from a git remote.
+Pull updates from a git remote
 
 ```
 git-bug pull [REMOTE] [flags]
@@ -14,5 +14,5 @@ git-bug pull [REMOTE] [flags]
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
 

doc/md/git-bug_push.md 🔗

@@ -1,6 +1,6 @@
 ## git-bug push
 
-Push bugs update to a git remote.
+Push updates to a git remote
 
 ```
 git-bug push [REMOTE] [flags]
@@ -14,5 +14,5 @@ git-bug push [REMOTE] [flags]
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
 

doc/md/git-bug_status.md 🔗

@@ -1,20 +0,0 @@
-## git-bug status
-
-Display or change a bug status.
-
-```
-git-bug status [ID] [flags]
-```
-
-### Options
-
-```
-  -h, --help   help for status
-```
-
-### SEE ALSO
-
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
-* [git-bug status close](git-bug_status_close.md)	 - Mark a bug as closed.
-* [git-bug status open](git-bug_status_open.md)	 - Mark a bug as open.
-

doc/md/git-bug_status_close.md 🔗

@@ -1,18 +0,0 @@
-## git-bug status close
-
-Mark a bug as closed.
-
-```
-git-bug status close [ID] [flags]
-```
-
-### Options
-
-```
-  -h, --help   help for close
-```
-
-### SEE ALSO
-
-* [git-bug status](git-bug_status.md)	 - Display or change a bug status.
-

doc/md/git-bug_status_open.md 🔗

@@ -1,18 +0,0 @@
-## git-bug status open
-
-Mark a bug as open.
-
-```
-git-bug status open [ID] [flags]
-```
-
-### Options
-
-```
-  -h, --help   help for open
-```
-
-### SEE ALSO
-
-* [git-bug status](git-bug_status.md)	 - Display or change a bug status.
-

doc/md/git-bug_termui.md 🔗

@@ -1,6 +1,6 @@
 ## git-bug termui
 
-Launch the terminal UI.
+Launch the terminal UI
 
 ```
 git-bug termui [flags]
@@ -14,5 +14,5 @@ git-bug termui [flags]
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
 

doc/md/git-bug_title.md 🔗

@@ -1,19 +0,0 @@
-## git-bug title
-
-Display or change a title of a bug.
-
-```
-git-bug title [ID] [flags]
-```
-
-### Options
-
-```
-  -h, --help   help for title
-```
-
-### SEE ALSO
-
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
-* [git-bug title edit](git-bug_title_edit.md)	 - Edit a title of a bug.
-

doc/md/git-bug_user.md 🔗

@@ -1,22 +1,22 @@
 ## git-bug user
 
-Display or change the user identity.
+List identities
 
 ```
-git-bug user [USER-ID] [flags]
+git-bug user [flags]
 ```
 
 ### Options
 
 ```
-  -f, --field string   Select field to display. Valid values are [email,humanId,id,lastModification,lastModificationLamports,login,metadata,name]
-  -h, --help           help for user
+  -f, --format string   Select the output formatting style. Valid values are [default,json] (default "default")
+  -h, --help            help for user
 ```
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
-* [git-bug user adopt](git-bug_user_adopt.md)	 - Adopt an existing identity as your own.
-* [git-bug user create](git-bug_user_create.md)	 - Create a new identity.
-* [git-bug user ls](git-bug_user_ls.md)	 - List identities.
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
+* [git-bug user adopt](git-bug_user_adopt.md)	 - Adopt an existing identity as your own
+* [git-bug user new](git-bug_user_new.md)	 - Create a new identity
+* [git-bug user user](git-bug_user_user.md)	 - Display a user identity
 

doc/md/git-bug_user_adopt.md 🔗

@@ -1,9 +1,9 @@
 ## git-bug user adopt
 
-Adopt an existing identity as your own.
+Adopt an existing identity as your own
 
 ```
-git-bug user adopt USER-ID [flags]
+git-bug user adopt USER_ID [flags]
 ```
 
 ### Options
@@ -14,5 +14,5 @@ git-bug user adopt USER-ID [flags]
 
 ### SEE ALSO
 
-* [git-bug user](git-bug_user.md)	 - Display or change the user identity.
+* [git-bug user](git-bug_user.md)	 - List identities
 

doc/md/git-bug_user_ls.md 🔗

@@ -1,19 +0,0 @@
-## git-bug user ls
-
-List identities.
-
-```
-git-bug user ls [flags]
-```
-
-### Options
-
-```
-  -f, --format string   Select the output formatting style. Valid values are [default,json] (default "default")
-  -h, --help            help for ls
-```
-
-### SEE ALSO
-
-* [git-bug user](git-bug_user.md)	 - Display or change the user identity.
-

doc/md/git-bug_user_create.md → doc/md/git-bug_user_new.md 🔗

@@ -1,9 +1,9 @@
-## git-bug user create
+## git-bug user new
 
-Create a new identity.
+Create a new identity
 
 ```
-git-bug user create [flags]
+git-bug user new [flags]
 ```
 
 ### Options
@@ -11,12 +11,12 @@ git-bug user create [flags]
 ```
   -a, --avatar string     Avatar URL
   -e, --email string      Email of the user
-  -h, --help              help for create
+  -h, --help              help for new
   -n, --name string       Name to identify the user
       --non-interactive   Do not ask for user input
 ```
 
 ### SEE ALSO
 
-* [git-bug user](git-bug_user.md)	 - Display or change the user identity.
+* [git-bug user](git-bug_user.md)	 - List identities
 

doc/md/git-bug_user_user.md 🔗

@@ -0,0 +1,19 @@
+## git-bug user user
+
+Display a user identity
+
+```
+git-bug user user show [USER_ID] [flags]
+```
+
+### Options
+
+```
+  -f, --field string   Select field to display. Valid values are [email,humanId,id,lastModification,lastModificationLamports,login,metadata,name]
+  -h, --help           help for user
+```
+
+### SEE ALSO
+
+* [git-bug user](git-bug_user.md)	 - List identities
+

doc/md/git-bug_version.md 🔗

@@ -1,6 +1,6 @@
 ## git-bug version
 
-Show git-bug version information.
+Show git-bug version information
 
 ```
 git-bug version [flags]
@@ -17,5 +17,5 @@ git-bug version [flags]
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
 

doc/md/git-bug_webui.md 🔗

@@ -1,6 +1,6 @@
 ## git-bug webui
 
-Launch the web UI.
+Launch the web UI
 
 ### Synopsis
 
@@ -29,5 +29,5 @@ git-bug webui [flags]
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
 

entities/identity/identity.go 🔗

@@ -22,7 +22,7 @@ const identityConfigKey = "git-bug.identity"
 var ErrNonFastForwardMerge = errors.New("non fast-forward identity merge")
 var ErrNoIdentitySet = errors.New("No identity is set.\n" +
 	"To interact with bugs, an identity first needs to be created using " +
-	"\"git bug user create\" or adopted with \"git bug user adopt\"")
+	"\"git bug user new\" or adopted with \"git bug user adopt\"")
 var ErrMultipleIdentitiesSet = errors.New("multiple user identities set")
 
 func NewErrMultipleMatchIdentity(matching []entity.Id) *entity.ErrMultipleMatch {

go.mod 🔗

@@ -23,7 +23,7 @@ require (
 	github.com/pkg/errors v0.9.1
 	github.com/shurcooL/githubv4 v0.0.0-20190601194912-068505affed7
 	github.com/skratchdot/open-golang v0.0.0-20190402232053-79abb63cd66e
-	github.com/spf13/cobra v1.5.0
+	github.com/spf13/cobra v1.6.1
 	github.com/stretchr/testify v1.8.1
 	github.com/vektah/gqlparser/v2 v2.5.1
 	github.com/xanzy/go-gitlab v0.74.0
@@ -79,7 +79,7 @@ require (
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
 	github.com/imdario/mergo v0.3.12 // indirect
-	github.com/inconshreveable/mousetrap v1.0.0 // indirect
+	github.com/inconshreveable/mousetrap v1.0.1 // indirect
 	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
 	github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
 	github.com/lucasb-eyer/go-colorful v1.0.3 // indirect
@@ -114,6 +114,5 @@ require (
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/protobuf v1.28.1 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
-	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )

go.sum 🔗

@@ -167,8 +167,9 @@ github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428/go.mod h1:uhpZMVGzny
 github.com/ikawaha/kagome.ipadic v1.1.2/go.mod h1:DPSBbU0czaJhAb/5uKQZHMc9MTVRpDugJfX+HddPHHg=
 github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
 github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
-github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
+github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
 github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
@@ -262,8 +263,8 @@ github.com/skratchdot/open-golang v0.0.0-20190402232053-79abb63cd66e/go.mod h1:s
 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
-github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
-github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
+github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
+github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
 github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=

misc/completion/bash/git-bug 🔗

@@ -128,7 +128,7 @@ __git-bug_process_completion_results() {
     __git-bug_handle_special_char "$cur" =
 
     # Print the activeHelp statements before we finish
-    if [ ${#activeHelp} -ne 0 ]; then
+    if [ ${#activeHelp[*]} -ne 0 ]; then
         printf "\n";
         printf "%s\n" "${activeHelp[@]}"
         printf "\n"

misc/completion/powershell/git-bug 🔗

@@ -10,7 +10,7 @@ filter __git-bug_escapeStringWithSpecialChars {
     $_ -replace '\s|#|@|\$|;|,|''|\{|\}|\(|\)|"|`|\||<|>|&','`$&'
 }
 
-Register-ArgumentCompleter -CommandName 'git-bug' -ScriptBlock {
+[scriptblock]$__git_bugCompleterBlock = {
     param(
             $WordToComplete,
             $CommandAst,
@@ -226,3 +226,5 @@ Register-ArgumentCompleter -CommandName 'git-bug' -ScriptBlock {
 
     }
 }
+
+Register-ArgumentCompleter -CommandName 'git-bug' -ScriptBlock $__git_bugCompleterBlock

repository/config.go 🔗

@@ -26,7 +26,7 @@ type ConfigRead interface {
 	// there is zero or more than one entry for this key
 	ReadBool(key string) (bool, error)
 
-	// ReadBool read a single string value from the config
+	// ReadString read a single string value from the config
 	// Return ErrNoConfigEntry or ErrMultipleConfigEntry if
 	// there is zero or more than one entry for this key
 	ReadString(key string) (string, error)
@@ -38,13 +38,13 @@ type ConfigRead interface {
 }
 
 type ConfigWrite interface {
-	// Store writes a single key/value pair in the config
+	// StoreString writes a single string key/value pair in the config
 	StoreString(key, value string) error
 
-	// Store writes a key and timestamp value to the config
+	// StoreTimestamp writes a key and timestamp value to the config
 	StoreTimestamp(key string, value time.Time) error
 
-	// Store writes a key and boolean value to the config
+	// StoreBool writes a key and boolean value to the config
 	StoreBool(key string, value bool) error
 
 	// RemoveAll removes all key/value pair matching the key prefix