Merge pull request #414 from MichaelMure/cmd-rework

Michael Muré created

commands: refactor to avoid globals

Change summary

commands/add.go                  |  85 +++++-------
commands/bridge.go               |  48 +++---
commands/bridge_auth.go          |  46 +++---
commands/bridge_auth_addtoken.go | 100 +++++++-------
commands/bridge_auth_rm.go       |  38 +++--
commands/bridge_auth_show.go     |  52 +++----
commands/bridge_configure.go     | 222 +++++++++++++++++----------------
commands/bridge_pull.go          |  81 ++++++-----
commands/bridge_push.go          |  49 +++---
commands/bridge_rm.go            |  42 ++---
commands/commands.go             |  69 +++++----
commands/comment.go              |  63 +++-----
commands/comment_add.go          |  79 +++++------
commands/deselect.go             |  50 +++----
commands/env.go                  | 150 ++++++++++++++++++++++
commands/label.go                |  46 +++---
commands/label_add.go            |  41 ++---
commands/label_rm.go             |  40 ++---
commands/ls-id.go                |  42 ++---
commands/ls-labels.go            |  44 ++---
commands/ls.go                   | 210 ++++++++++++++++---------------
commands/pull.go                 |  54 +++----
commands/push.go                 |  44 ++---
commands/root.go                 | 123 +++++++++---------
commands/select.go               |  72 +++++-----
commands/show.go                 | 159 +++++++++++------------
commands/status.go               |  44 +++---
commands/status_close.go         |  39 ++---
commands/status_open.go          |  39 ++---
commands/termui.go               |  37 ++---
commands/title.go                |  45 +++---
commands/title_edit.go           |  71 +++++-----
commands/user.go                 |  97 +++++++-------
commands/user_adopt.go           |  47 ++----
commands/user_create.go          |  53 +++----
commands/user_ls.go              |  69 +++++-----
commands/version.go              |  89 ++++++-------
commands/webui.go                | 100 ++++++++-------
doc/gen_docs.go                  |   4 
doc/man/git-bug-bridge-pull.1    |   8 
doc/md/git-bug_bridge_pull.md    |   2 
go.sum                           |  10 +
misc/gen_completion.go           |   8 
43 files changed, 1,431 insertions(+), 1,380 deletions(-)

Detailed changes

commands/add.go 🔗

@@ -1,42 +1,58 @@
 package commands
 
 import (
-	"fmt"
+	"github.com/spf13/cobra"
 
-	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/input"
-	"github.com/MichaelMure/git-bug/util/interrupt"
-	"github.com/spf13/cobra"
 )
 
-var (
-	addTitle       string
-	addMessage     string
-	addMessageFile string
-)
+type addOptions struct {
+	title       string
+	message     string
+	messageFile string
+}
 
-func runAddBug(cmd *cobra.Command, args []string) error {
-	var err error
+func newAddCommand() *cobra.Command {
+	env := newEnv()
+	options := addOptions{}
 
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
+	cmd := &cobra.Command{
+		Use:      "add",
+		Short:    "Create a new bug.",
+		PreRunE:  loadBackendEnsureUser(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runAdd(env, options)
+		},
 	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
 
-	if addMessageFile != "" && addMessage == "" {
-		addTitle, addMessage, err = input.BugCreateFileInput(addMessageFile)
+	flags := cmd.Flags()
+	flags.SortFlags = false
+
+	flags.StringVarP(&options.title, "title", "t", "",
+		"Provide a title to describe the issue")
+	flags.StringVarP(&options.message, "message", "m", "",
+		"Provide a message to describe the issue")
+	flags.StringVarP(&options.messageFile, "file", "F", "",
+		"Take the message from the given file. Use - to read the message from the standard input")
+
+	return cmd
+}
+
+func runAdd(env *Env, opts addOptions) error {
+	var err error
+	if opts.messageFile != "" && opts.message == "" {
+		opts.title, opts.message, err = input.BugCreateFileInput(opts.messageFile)
 		if err != nil {
 			return err
 		}
 	}
 
-	if addMessageFile == "" && (addMessage == "" || addTitle == "") {
-		addTitle, addMessage, err = input.BugCreateEditorInput(backend, addTitle, addMessage)
+	if opts.messageFile == "" && (opts.message == "" || opts.title == "") {
+		opts.title, opts.message, err = input.BugCreateEditorInput(env.backend, opts.title, opts.message)
 
 		if err == input.ErrEmptyTitle {
-			fmt.Println("Empty title, aborting.")
+			env.out.Println("Empty title, aborting.")
 			return nil
 		}
 		if err != nil {
@@ -44,35 +60,12 @@ func runAddBug(cmd *cobra.Command, args []string) error {
 		}
 	}
 
-	b, _, err := backend.NewBug(addTitle, addMessage)
+	b, _, err := env.backend.NewBug(opts.title, opts.message)
 	if err != nil {
 		return err
 	}
 
-	fmt.Printf("%s created\n", b.Id().Human())
+	env.out.Printf("%s created\n", b.Id().Human())
 
 	return nil
 }
-
-var addCmd = &cobra.Command{
-	Use:     "add",
-	Short:   "Create a new bug.",
-	PreRunE: loadRepoEnsureUser,
-	RunE:    runAddBug,
-}
-
-func init() {
-	RootCmd.AddCommand(addCmd)
-
-	addCmd.Flags().SortFlags = false
-
-	addCmd.Flags().StringVarP(&addTitle, "title", "t", "",
-		"Provide a title to describe the issue",
-	)
-	addCmd.Flags().StringVarP(&addMessage, "message", "m", "",
-		"Provide a message to describe the issue",
-	)
-	addCmd.Flags().StringVarP(&addMessageFile, "file", "F", "",
-		"Take the message from the given file. Use - to read the message from the standard input",
-	)
-}

commands/bridge.go 🔗

@@ -1,43 +1,43 @@
 package commands
 
 import (
-	"fmt"
-
 	"github.com/spf13/cobra"
 
 	"github.com/MichaelMure/git-bug/bridge"
-	"github.com/MichaelMure/git-bug/cache"
-	"github.com/MichaelMure/git-bug/util/interrupt"
 )
 
-func runBridge(cmd *cobra.Command, args []string) error {
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
+func newBridgeCommand() *cobra.Command {
+	env := newEnv()
+
+	cmd := &cobra.Command{
+		Use:      "bridge",
+		Short:    "Configure and use bridges to other bug trackers.",
+		PreRunE:  loadBackend(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runBridge(env)
+		},
+		Args: cobra.NoArgs,
 	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
 
-	configured, err := bridge.ConfiguredBridges(backend)
+	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 {
-		fmt.Println(c)
+		env.out.Println(c)
 	}
 
 	return nil
 }
-
-var bridgeCmd = &cobra.Command{
-	Use:     "bridge",
-	Short:   "Configure and use bridges to other bug trackers.",
-	PreRunE: loadRepo,
-	RunE:    runBridge,
-	Args:    cobra.NoArgs,
-}
-
-func init() {
-	RootCmd.AddCommand(bridgeCmd)
-}

commands/bridge_auth.go 🔗

@@ -1,7 +1,6 @@
 package commands
 
 import (
-	"fmt"
 	"sort"
 	"strings"
 
@@ -10,20 +9,32 @@ import (
 	text "github.com/MichaelMure/go-term-text"
 
 	"github.com/MichaelMure/git-bug/bridge/core/auth"
-	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/util/colors"
-	"github.com/MichaelMure/git-bug/util/interrupt"
 )
 
-func runBridgeAuth(cmd *cobra.Command, args []string) error {
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
+func newBridgeAuthCommand() *cobra.Command {
+	env := newEnv()
+
+	cmd := &cobra.Command{
+		Use:      "auth",
+		Short:    "List all known bridge authentication credentials.",
+		PreRunE:  loadBackend(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runBridgeAuth(env)
+		},
+		Args: cobra.NoArgs,
 	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
 
-	creds, err := auth.List(backend)
+	cmd.AddCommand(newBridgeAuthAddTokenCommand())
+	cmd.AddCommand(newBridgeAuthRm())
+	cmd.AddCommand(newBridgeAuthShow())
+
+	return cmd
+}
+
+func runBridgeAuth(env *Env) error {
+	creds, err := auth.List(env.backend)
 	if err != nil {
 		return err
 	}
@@ -44,7 +55,7 @@ func runBridgeAuth(cmd *cobra.Command, args []string) error {
 		sort.Strings(meta)
 		metaFmt := strings.Join(meta, ",")
 
-		fmt.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()),
@@ -55,16 +66,3 @@ func runBridgeAuth(cmd *cobra.Command, args []string) error {
 
 	return nil
 }
-
-var bridgeAuthCmd = &cobra.Command{
-	Use:     "auth",
-	Short:   "List all known bridge authentication credentials.",
-	PreRunE: loadRepo,
-	RunE:    runBridgeAuth,
-	Args:    cobra.NoArgs,
-}
-
-func init() {
-	bridgeCmd.AddCommand(bridgeAuthCmd)
-	bridgeAuthCmd.Flags().SortFlags = false
-}

commands/bridge_auth_addtoken.go 🔗

@@ -14,37 +14,57 @@ 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/util/interrupt"
 )
 
-var (
-	bridgeAuthAddTokenTarget string
-	bridgeAuthAddTokenLogin  string
-	bridgeAuthAddTokenUser   string
-)
+type bridgeAuthAddTokenOptions struct {
+	target string
+	login  string
+	user   string
+}
+
+func newBridgeAuthAddTokenCommand() *cobra.Command {
+	env := newEnv()
+	options := bridgeAuthAddTokenOptions{}
+
+	cmd := &cobra.Command{
+		Use:      "add-token [<token>]",
+		Short:    "Store a new token",
+		PreRunE:  loadBackendEnsureUser(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runBridgeAuthAddToken(env, options, args)
+		},
+		Args: cobra.MaximumNArgs(1),
+	}
+
+	flags := cmd.Flags()
+	flags.SortFlags = false
+
+	flags.StringVarP(&options.target, "target", "t", "",
+		fmt.Sprintf("The target of the bridge. Valid values are [%s]", strings.Join(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")
+
+	return cmd
+}
 
-func runBridgeTokenAdd(cmd *cobra.Command, args []string) error {
+func runBridgeAuthAddToken(env *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
 	// checking it would require a cumbersome UX (need to provide a base URL for some bridges, ...)
 	// so it's probably not worth it, unless we refactor that entirely.
 
-	if bridgeAuthAddTokenTarget == "" {
+	if opts.target == "" {
 		return fmt.Errorf("flag --target is required")
 	}
-	if bridgeAuthAddTokenLogin == "" {
+	if opts.login == "" {
 		return fmt.Errorf("flag --login is required")
 	}
 
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
-	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
-
-	if !core.TargetExist(bridgeAuthAddTokenTarget) {
+	if !core.TargetExist(opts.target) {
 		return fmt.Errorf("unknown target")
 	}
 
@@ -55,7 +75,7 @@ func runBridgeTokenAdd(cmd *cobra.Command, args []string) error {
 	} else {
 		// Read from Stdin
 		if isatty.IsTerminal(os.Stdin.Fd()) {
-			fmt.Println("Enter the token:")
+			env.err.Println("Enter the token:")
 		}
 		reader := bufio.NewReader(os.Stdin)
 		raw, err := reader.ReadString('\n')
@@ -66,63 +86,45 @@ func runBridgeTokenAdd(cmd *cobra.Command, args []string) error {
 	}
 
 	var user *cache.IdentityCache
+	var err error
 
-	if bridgeAuthAddTokenUser == "" {
-		user, err = backend.GetUserIdentity()
+	if opts.user == "" {
+		user, err = env.backend.GetUserIdentity()
 	} else {
-		user, err = backend.ResolveIdentityPrefix(bridgeAuthAddTokenUser)
+		user, err = env.backend.ResolveIdentityPrefix(opts.user)
 	}
 	if err != nil {
 		return err
 	}
 
-	metaKey, _ := bridge.LoginMetaKey(bridgeAuthAddTokenTarget)
+	metaKey, _ := bridge.LoginMetaKey(opts.target)
 	login, ok := user.ImmutableMetadata()[metaKey]
 
 	switch {
-	case ok && login == bridgeAuthAddTokenLogin:
+	case ok && login == opts.login:
 		// nothing to do
-	case ok && login != bridgeAuthAddTokenLogin:
-		return fmt.Errorf("this user is already tagged with a different %s login", bridgeAuthAddTokenTarget)
+	case ok && login != opts.login:
+		return fmt.Errorf("this user is already tagged with a different %s login", opts.target)
 	default:
-		user.SetMetadata(metaKey, bridgeAuthAddTokenLogin)
+		user.SetMetadata(metaKey, opts.login)
 		err = user.Commit()
 		if err != nil {
 			return err
 		}
 	}
 
-	token := auth.NewToken(bridgeAuthAddTokenTarget, value)
-	token.SetMetadata(auth.MetaKeyLogin, bridgeAuthAddTokenLogin)
+	token := auth.NewToken(opts.target, value)
+	token.SetMetadata(auth.MetaKeyLogin, opts.login)
 
 	if err := token.Validate(); err != nil {
 		return errors.Wrap(err, "invalid token")
 	}
 
-	err = auth.Store(repo, token)
+	err = auth.Store(env.repo, token)
 	if err != nil {
 		return err
 	}
 
-	fmt.Printf("token %s added\n", token.ID())
+	env.out.Printf("token %s added\n", token.ID())
 	return nil
 }
-
-var bridgeAuthAddTokenCmd = &cobra.Command{
-	Use:     "add-token [<token>]",
-	Short:   "Store a new token",
-	PreRunE: loadRepoEnsureUser,
-	RunE:    runBridgeTokenAdd,
-	Args:    cobra.MaximumNArgs(1),
-}
-
-func init() {
-	bridgeAuthCmd.AddCommand(bridgeAuthAddTokenCmd)
-	bridgeAuthAddTokenCmd.Flags().StringVarP(&bridgeAuthAddTokenTarget, "target", "t", "",
-		fmt.Sprintf("The target of the bridge. Valid values are [%s]", strings.Join(bridge.Targets(), ",")))
-	bridgeAuthAddTokenCmd.Flags().StringVarP(&bridgeAuthAddTokenLogin,
-		"login", "l", "", "The login in the remote bug-tracker")
-	bridgeAuthAddTokenCmd.Flags().StringVarP(&bridgeAuthAddTokenUser,
-		"user", "u", "", "The user to add the token to. Default is the current user")
-	bridgeAuthAddTokenCmd.Flags().SortFlags = false
-}

commands/bridge_auth_rm.go 🔗

@@ -1,36 +1,38 @@
 package commands
 
 import (
-	"fmt"
-
 	"github.com/spf13/cobra"
 
 	"github.com/MichaelMure/git-bug/bridge/core/auth"
 )
 
-func runBridgeAuthRm(cmd *cobra.Command, args []string) error {
-	cred, err := auth.LoadWithPrefix(repo, args[0])
+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),
+	}
+
+	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(repo, cred.ID())
+	err = auth.Remove(env.repo, cred.ID())
 	if err != nil {
 		return err
 	}
 
-	fmt.Printf("credential %s removed\n", cred.ID())
+	env.out.Printf("credential %s removed\n", cred.ID())
 	return nil
 }
-
-var bridgeAuthRmCmd = &cobra.Command{
-	Use:     "rm <id>",
-	Short:   "Remove a credential.",
-	PreRunE: loadRepo,
-	RunE:    runBridgeAuthRm,
-	Args:    cobra.ExactArgs(1),
-}
-
-func init() {
-	bridgeAuthCmd.AddCommand(bridgeAuthRmCmd)
-}

commands/bridge_auth_show.go 🔗

@@ -9,34 +9,42 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/MichaelMure/git-bug/bridge/core/auth"
-	"github.com/MichaelMure/git-bug/cache"
-	"github.com/MichaelMure/git-bug/util/interrupt"
 )
 
-func runBridgeAuthShow(cmd *cobra.Command, args []string) error {
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
+func newBridgeAuthShow() *cobra.Command {
+	env := newEnv()
+
+	cmd := &cobra.Command{
+		Use:      "show",
+		Short:    "Display an authentication credential.",
+		PreRunE:  loadBackend(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runBridgeAuthShow(env, args)
+		},
+		Args: cobra.ExactArgs(1),
 	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
 
-	cred, err := auth.LoadWithPrefix(repo, args[0])
+	return cmd
+}
+
+func runBridgeAuthShow(env *Env, args []string) error {
+	cred, err := auth.LoadWithPrefix(env.repo, args[0])
 	if err != nil {
 		return err
 	}
 
-	fmt.Printf("Id: %s\n", cred.ID())
-	fmt.Printf("Target: %s\n", cred.Target())
-	fmt.Printf("Kind: %s\n", cred.Kind())
-	fmt.Printf("Creation: %s\n", cred.CreateTime().Format(time.RFC822))
+	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:
-		fmt.Printf("Value: %s\n", cred.Value)
+		env.out.Printf("Value: %s\n", cred.Value)
 	}
 
-	fmt.Println("Metadata:")
+	env.out.Println("Metadata:")
 
 	meta := make([]string, 0, len(cred.Metadata()))
 	for key, value := range cred.Metadata() {
@@ -44,19 +52,7 @@ func runBridgeAuthShow(cmd *cobra.Command, args []string) error {
 	}
 	sort.Strings(meta)
 
-	fmt.Print(strings.Join(meta, ""))
+	env.out.Print(strings.Join(meta, ""))
 
 	return nil
 }
-
-var bridgeAuthShowCmd = &cobra.Command{
-	Use:     "show",
-	Short:   "Display an authentication credential.",
-	PreRunE: loadRepo,
-	RunE:    runBridgeAuthShow,
-	Args:    cobra.ExactArgs(1),
-}
-
-func init() {
-	bridgeAuthCmd.AddCommand(bridgeAuthShowCmd)
-}

commands/bridge_configure.go 🔗

@@ -12,84 +12,160 @@ 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/cache"
 	"github.com/MichaelMure/git-bug/repository"
-	"github.com/MichaelMure/git-bug/util/interrupt"
 )
 
-const (
-	defaultName = "default"
-)
+type bridgeConfigureOptions struct {
+	name       string
+	target     string
+	params     core.BridgeParams
+	token      string
+	tokenStdin bool
+}
 
-var (
-	bridgeConfigureName       string
-	bridgeConfigureTarget     string
-	bridgeConfigureParams     core.BridgeParams
-	bridgeConfigureToken      string
-	bridgeConfigureTokenStdin bool
-)
+func newBridgeConfigureCommand() *cobra.Command {
+	env := newEnv()
+	options := bridgeConfigureOptions{}
 
-func runBridgeConfigure(cmd *cobra.Command, args []string) error {
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
+	cmd := &cobra.Command{
+		Use:   "configure",
+		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
+[2]: gitlab
+[3]: jira
+[4]: launchpad-preview
+
+target: 1
+name [default]: default
+
+Detected projects:
+[1]: github.com/a-hilaly/git-bug
+[2]: github.com/MichaelMure/git-bug
+
+[0]: Another project
+
+Select option: 1
+
+[1]: user provided token
+[2]: interactive token creation
+Select option: 1
+
+You can generate a new token by visiting https://github.com/settings/tokens.
+Choose 'Generate new token' and set the necessary access scope for your repository.
+
+The access scope depend on the type of repository.
+Public:
+	- 'public_repo': to be able to read public repositories
+Private:
+	- 'repo'       : to be able to read private repositories
+
+Enter token: 87cf5c03b64029f18ea5f9ca5679daa08ccbd700
+Successfully configured bridge: default
+
+# For GitHub
+git bug bridge configure \
+    --name=default \
+    --target=github \
+    --owner=$(OWNER) \
+    --project=$(PROJECT) \
+    --token=$(TOKEN)
+
+# For Launchpad
+git bug bridge configure \
+    --name=default \
+    --target=launchpad-preview \
+	--url=https://bugs.launchpad.net/ubuntu/
+
+# For Gitlab
+git bug bridge configure \
+    --name=default \
+    --target=github \
+    --url=https://github.com/michaelmure/git-bug \
+    --token=$(TOKEN)`,
+		PreRunE:  loadBackend(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runBridgeConfigure(env, options)
+		},
 	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
 
-	if (bridgeConfigureTokenStdin || bridgeConfigureToken != "" || bridgeConfigureParams.CredPrefix != "") &&
-		(bridgeConfigureName == "" || bridgeConfigureTarget == "") {
+	flags := cmd.Flags()
+	flags.SortFlags = false
+
+	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(), ",")))
+	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")
+	flags.StringVarP(&options.params.CredPrefix, "credential", "c", "", "The identifier or prefix of an already known credential for your remote issue tracker (see \"git-bug bridge auth\")")
+	flags.StringVar(&options.token, "token", "", "A raw authentication token for the remote issue tracker")
+	flags.BoolVar(&options.tokenStdin, "token-stdin", false, "Will read the token from stdin and ignore --token")
+	flags.StringVarP(&options.params.Owner, "owner", "o", "", "The owner of the remote repository")
+	flags.StringVarP(&options.params.Project, "project", "p", "", "The name of the remote repository")
+
+	return cmd
+}
+
+func runBridgeConfigure(env *Env, opts bridgeConfigureOptions) error {
+	var err error
+
+	if (opts.tokenStdin || opts.token != "" || opts.params.CredPrefix != "") &&
+		(opts.name == "" || opts.target == "") {
 		return fmt.Errorf("you must provide a bridge name and target to configure a bridge with a credential")
 	}
 
 	// early fail
-	if bridgeConfigureParams.CredPrefix != "" {
-		if _, err := auth.LoadWithPrefix(repo, bridgeConfigureParams.CredPrefix); err != nil {
+	if opts.params.CredPrefix != "" {
+		if _, err := auth.LoadWithPrefix(env.repo, opts.params.CredPrefix); err != nil {
 			return err
 		}
 	}
 
 	switch {
-	case bridgeConfigureTokenStdin:
+	case opts.tokenStdin:
 		reader := bufio.NewReader(os.Stdin)
 		token, err := reader.ReadString('\n')
 		if err != nil {
 			return fmt.Errorf("reading from stdin: %v", err)
 		}
-		bridgeConfigureParams.TokenRaw = strings.TrimSpace(token)
-	case bridgeConfigureToken != "":
-		bridgeConfigureParams.TokenRaw = bridgeConfigureToken
+		opts.params.TokenRaw = strings.TrimSpace(token)
+	case opts.token != "":
+		opts.params.TokenRaw = opts.token
 	}
 
-	if bridgeConfigureTarget == "" {
-		bridgeConfigureTarget, err = promptTarget()
+	if opts.target == "" {
+		opts.target, err = promptTarget()
 		if err != nil {
 			return err
 		}
 	}
 
-	if bridgeConfigureName == "" {
-		bridgeConfigureName, err = promptName(repo)
+	if opts.name == "" {
+		opts.name, err = promptName(env.repo)
 		if err != nil {
 			return err
 		}
 	}
 
-	b, err := bridge.NewBridge(backend, bridgeConfigureTarget, bridgeConfigureName)
+	b, err := bridge.NewBridge(env.backend, opts.target, opts.name)
 	if err != nil {
 		return err
 	}
 
-	err = b.Configure(bridgeConfigureParams)
+	err = b.Configure(opts.params)
 	if err != nil {
 		return err
 	}
 
-	fmt.Printf("Successfully configured bridge: %s\n", bridgeConfigureName)
+	env.out.Printf("Successfully configured bridge: %s\n", opts.name)
 	return nil
 }
 
 func promptTarget() (string, error) {
+	// TODO: use the reusable prompt from the input package
 	targets := bridge.Targets()
 
 	for {
@@ -117,6 +193,9 @@ func promptTarget() (string, error) {
 }
 
 func promptName(repo repository.RepoConfig) (string, error) {
+	// TODO: use the reusable prompt from the input package
+	const defaultName = "default"
+
 	defaultExist := core.BridgeExist(repo, defaultName)
 
 	for {
@@ -149,80 +228,3 @@ func promptName(repo repository.RepoConfig) (string, error) {
 		fmt.Println("a bridge with the same name already exist")
 	}
 }
-
-var bridgeConfigureCmd = &cobra.Command{
-	Use:   "configure",
-	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
-[2]: gitlab
-[3]: jira
-[4]: launchpad-preview
-
-target: 1
-name [default]: default
-
-Detected projects:
-[1]: github.com/a-hilaly/git-bug
-[2]: github.com/MichaelMure/git-bug
-
-[0]: Another project
-
-Select option: 1
-
-[1]: user provided token
-[2]: interactive token creation
-Select option: 1
-
-You can generate a new token by visiting https://github.com/settings/tokens.
-Choose 'Generate new token' and set the necessary access scope for your repository.
-
-The access scope depend on the type of repository.
-Public:
-	- 'public_repo': to be able to read public repositories
-Private:
-	- 'repo'       : to be able to read private repositories
-
-Enter token: 87cf5c03b64029f18ea5f9ca5679daa08ccbd700
-Successfully configured bridge: default
-
-# For GitHub
-git bug bridge configure \
-    --name=default \
-    --target=github \
-    --owner=$(OWNER) \
-    --project=$(PROJECT) \
-    --token=$(TOKEN)
-
-# For Launchpad
-git bug bridge configure \
-    --name=default \
-    --target=launchpad-preview \
-	--url=https://bugs.launchpad.net/ubuntu/
-
-# For Gitlab
-git bug bridge configure \
-    --name=default \
-    --target=github \
-    --url=https://github.com/michaelmure/git-bug \
-    --token=$(TOKEN)`,
-	PreRunE: loadRepo,
-	RunE:    runBridgeConfigure,
-}
-
-func init() {
-	bridgeCmd.AddCommand(bridgeConfigureCmd)
-	bridgeConfigureCmd.Flags().StringVarP(&bridgeConfigureName, "name", "n", "", "A distinctive name to identify the bridge")
-	bridgeConfigureCmd.Flags().StringVarP(&bridgeConfigureTarget, "target", "t", "",
-		fmt.Sprintf("The target of the bridge. Valid values are [%s]", strings.Join(bridge.Targets(), ",")))
-	bridgeConfigureCmd.Flags().StringVarP(&bridgeConfigureParams.URL, "url", "u", "", "The URL of the remote repository")
-	bridgeConfigureCmd.Flags().StringVarP(&bridgeConfigureParams.BaseURL, "base-url", "b", "", "The base URL of your remote issue tracker")
-	bridgeConfigureCmd.Flags().StringVarP(&bridgeConfigureParams.Login, "login", "l", "", "The login on your remote issue tracker")
-	bridgeConfigureCmd.Flags().StringVarP(&bridgeConfigureParams.CredPrefix, "credential", "c", "", "The identifier or prefix of an already known credential for your remote issue tracker (see \"git-bug bridge auth\")")
-	bridgeConfigureCmd.Flags().StringVar(&bridgeConfigureToken, "token", "", "A raw authentication token for the remote issue tracker")
-	bridgeConfigureCmd.Flags().BoolVar(&bridgeConfigureTokenStdin, "token-stdin", false, "Will read the token from stdin and ignore --token")
-	bridgeConfigureCmd.Flags().StringVarP(&bridgeConfigureParams.Owner, "owner", "o", "", "The owner of the remote repository")
-	bridgeConfigureCmd.Flags().StringVarP(&bridgeConfigureParams.Project, "project", "p", "", "The name of the remote repository")
-	bridgeConfigureCmd.Flags().SortFlags = false
-}

commands/bridge_pull.go 🔗

@@ -13,33 +13,50 @@ import (
 
 	"github.com/MichaelMure/git-bug/bridge"
 	"github.com/MichaelMure/git-bug/bridge/core"
-	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/util/interrupt"
 )
 
-var (
-	bridgePullImportSince string
-	bridgePullNoResume    bool
-)
+type bridgePullOptions struct {
+	importSince string
+	noResume    bool
+}
 
-func runBridgePull(cmd *cobra.Command, args []string) error {
-	if bridgePullNoResume && bridgePullImportSince != "" {
-		return fmt.Errorf("only one of --no-resume and --since flags should be used")
+func newBridgePullCommand() *cobra.Command {
+	env := newEnv()
+	options := bridgePullOptions{}
+
+	cmd := &cobra.Command{
+		Use:      "pull [<name>]",
+		Short:    "Pull updates.",
+		PreRunE:  loadBackend(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runBridgePull(env, options, args)
+		},
+		Args: cobra.MaximumNArgs(1),
 	}
 
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
+	flags := cmd.Flags()
+	flags.SortFlags = false
+
+	flags.BoolVarP(&options.noResume, "no-resume", "n", false, "force importing all bugs")
+	flags.StringVarP(&options.importSince, "since", "s", "", "import only bugs updated after the given date (ex: \"200h\" or \"june 2 2019\")")
+
+	return cmd
+}
+
+func runBridgePull(env *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")
 	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
 
 	var b *core.Bridge
+	var err error
 
 	if len(args) == 0 {
-		b, err = bridge.DefaultBridge(backend)
+		b, err = bridge.DefaultBridge(env.backend)
 	} else {
-		b, err = bridge.LoadBridge(backend, args[0])
+		b, err = bridge.LoadBridge(env.backend, args[0])
 	}
 
 	if err != nil {
@@ -58,14 +75,14 @@ func runBridgePull(cmd *cobra.Command, args []string) error {
 	interrupt.RegisterCleaner(func() error {
 		mu.Lock()
 		if interruptCount > 0 {
-			fmt.Println("Received another interrupt before graceful stop, terminating...")
+			env.err.Println("Received another interrupt before graceful stop, terminating...")
 			os.Exit(0)
 		}
 
 		interruptCount++
 		mu.Unlock()
 
-		fmt.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()
@@ -77,10 +94,10 @@ func runBridgePull(cmd *cobra.Command, args []string) error {
 
 	var events <-chan core.ImportResult
 	switch {
-	case bridgePullNoResume:
+	case opts.noResume:
 		events, err = b.ImportAllSince(ctx, time.Time{})
-	case bridgePullImportSince != "":
-		since, err2 := parseSince(bridgePullImportSince)
+	case opts.importSince != "":
+		since, err2 := parseSince(opts.importSince)
 		if err2 != nil {
 			return errors.Wrap(err2, "import time parsing")
 		}
@@ -102,23 +119,23 @@ func runBridgePull(cmd *cobra.Command, args []string) error {
 
 		case core.ImportEventBug:
 			importedIssues++
-			fmt.Println(result.String())
+			env.out.Println(result.String())
 
 		case core.ImportEventIdentity:
 			importedIdentities++
-			fmt.Println(result.String())
+			env.out.Println(result.String())
 
 		case core.ImportEventError:
 			if result.Err != context.Canceled {
-				fmt.Println(result.String())
+				env.out.Println(result.String())
 			}
 
 		default:
-			fmt.Println(result.String())
+			env.out.Println(result.String())
 		}
 	}
 
-	fmt.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)
@@ -134,17 +151,3 @@ func parseSince(since string) (time.Time, error) {
 
 	return dateparse.ParseLocal(since)
 }
-
-var bridgePullCmd = &cobra.Command{
-	Use:     "pull [<name>]",
-	Short:   "Pull updates.",
-	PreRunE: loadRepo,
-	RunE:    runBridgePull,
-	Args:    cobra.MaximumNArgs(1),
-}
-
-func init() {
-	bridgeCmd.AddCommand(bridgePullCmd)
-	bridgePullCmd.Flags().BoolVarP(&bridgePullNoResume, "no-resume", "n", false, "force importing all bugs")
-	bridgePullCmd.Flags().StringVarP(&bridgePullImportSince, "since", "s", "", "import only bugs updated after the given date (ex: \"200h\" or \"june 2 2019\")")
-}

commands/bridge_push.go 🔗

@@ -2,7 +2,6 @@ package commands
 
 import (
 	"context"
-	"fmt"
 	"os"
 	"sync"
 	"time"
@@ -11,24 +10,34 @@ import (
 
 	"github.com/MichaelMure/git-bug/bridge"
 	"github.com/MichaelMure/git-bug/bridge/core"
-	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/util/interrupt"
 )
 
-func runBridgePush(cmd *cobra.Command, args []string) error {
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
+func newBridgePushCommand() *cobra.Command {
+	env := newEnv()
+
+	cmd := &cobra.Command{
+		Use:      "push [<name>]",
+		Short:    "Push updates.",
+		PreRunE:  loadBackendEnsureUser(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runBridgePush(env, args)
+		},
+		Args: cobra.MaximumNArgs(1),
 	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
 
+	return cmd
+}
+
+func runBridgePush(env *Env, args []string) error {
 	var b *core.Bridge
+	var err error
 
 	if len(args) == 0 {
-		b, err = bridge.DefaultBridge(backend)
+		b, err = bridge.DefaultBridge(env.backend)
 	} else {
-		b, err = bridge.LoadBridge(backend, args[0])
+		b, err = bridge.LoadBridge(env.backend, args[0])
 	}
 
 	if err != nil {
@@ -46,14 +55,14 @@ func runBridgePush(cmd *cobra.Command, args []string) error {
 	interrupt.RegisterCleaner(func() error {
 		mu.Lock()
 		if interruptCount > 0 {
-			fmt.Println("Received another interrupt before graceful stop, terminating...")
+			env.err.Println("Received another interrupt before graceful stop, terminating...")
 			os.Exit(0)
 		}
 
 		interruptCount++
 		mu.Unlock()
 
-		fmt.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()
@@ -71,7 +80,7 @@ func runBridgePush(cmd *cobra.Command, args []string) error {
 	exportedIssues := 0
 	for result := range events {
 		if result.Event != core.ExportEventNothing {
-			fmt.Println(result.String())
+			env.out.Println(result.String())
 		}
 
 		switch result.Event {
@@ -80,21 +89,9 @@ func runBridgePush(cmd *cobra.Command, args []string) error {
 		}
 	}
 
-	fmt.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)
 	return nil
 }
-
-var bridgePushCmd = &cobra.Command{
-	Use:     "push [<name>]",
-	Short:   "Push updates.",
-	PreRunE: loadRepoEnsureUser,
-	RunE:    runBridgePush,
-	Args:    cobra.MaximumNArgs(1),
-}
-
-func init() {
-	bridgeCmd.AddCommand(bridgePushCmd)
-}

commands/bridge_rm.go 🔗

@@ -1,40 +1,34 @@
 package commands
 
 import (
-	"fmt"
-
 	"github.com/spf13/cobra"
 
 	"github.com/MichaelMure/git-bug/bridge"
-	"github.com/MichaelMure/git-bug/cache"
-	"github.com/MichaelMure/git-bug/util/interrupt"
 )
 
-func runBridgeRm(cmd *cobra.Command, args []string) error {
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
+func newBridgeRm() *cobra.Command {
+	env := newEnv()
+
+	cmd := &cobra.Command{
+		Use:      "rm <name>",
+		Short:    "Delete a configured bridge.",
+		PreRunE:  loadBackend(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runBridgeRm(env, args)
+		},
+		Args: cobra.ExactArgs(1),
 	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
 
-	err = bridge.RemoveBridge(backend, args[0])
+	return cmd
+}
+
+func runBridgeRm(env *Env, args []string) error {
+	err := bridge.RemoveBridge(env.backend, args[0])
 	if err != nil {
 		return err
 	}
 
-	fmt.Printf("Successfully removed bridge configuration %v\n", args[0])
+	env.out.Printf("Successfully removed bridge configuration %v\n", args[0])
 	return nil
 }
-
-var bridgeRmCmd = &cobra.Command{
-	Use:     "rm <name>",
-	Short:   "Delete a configured bridge.",
-	PreRunE: loadRepo,
-	RunE:    runBridgeRm,
-	Args:    cobra.ExactArgs(1),
-}
-
-func init() {
-	bridgeCmd.AddCommand(bridgeRmCmd)
-}

commands/commands.go 🔗

@@ -1,27 +1,42 @@
 package commands
 
 import (
-	"fmt"
 	"sort"
 
 	"github.com/spf13/cobra"
 )
 
-var (
-	commandsDesc bool
-)
+type commandOptions struct {
+	desc bool
+}
 
-type commandSorterByName []*cobra.Command
+func newCommandsCommand() *cobra.Command {
+	env := newEnv()
+	options := commandOptions{}
 
-func (c commandSorterByName) Len() int           { return len(c) }
-func (c commandSorterByName) Swap(i, j int)      { c[i], c[j] = c[j], c[i] }
-func (c commandSorterByName) Less(i, j int) bool { return c[i].CommandPath() < c[j].CommandPath() }
+	cmd := &cobra.Command{
+		Use:   "commands [<option>...]",
+		Short: "Display available commands.",
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runCommands(env, options)
+		},
+	}
+
+	flags := cmd.Flags()
+	flags.SortFlags = false
+
+	flags.BoolVarP(&options.desc, "pretty", "p", false,
+		"Output the command description as well as Markdown compatible comment",
+	)
 
-func runCommands(cmd *cobra.Command, args []string) error {
+	return cmd
+}
+
+func runCommands(env *Env, opts commandOptions) error {
 	first := true
 
 	var allCmds []*cobra.Command
-	queue := []*cobra.Command{RootCmd}
+	queue := []*cobra.Command{NewRootCommand()}
 
 	for len(queue) > 0 {
 		cmd := queue[0]
@@ -34,41 +49,31 @@ func runCommands(cmd *cobra.Command, args []string) error {
 
 	for _, cmd := range allCmds {
 		if !first {
-			fmt.Println()
+			env.out.Println()
 		}
 
 		first = false
 
-		if commandsDesc {
-			fmt.Printf("# %s\n", cmd.Short)
+		if opts.desc {
+			env.out.Printf("# %s\n", cmd.Short)
 		}
 
-		fmt.Print(cmd.UseLine())
+		env.out.Print(cmd.UseLine())
 
-		if commandsDesc {
-			fmt.Println()
+		if opts.desc {
+			env.out.Println()
 		}
 	}
 
-	if !commandsDesc {
-		fmt.Println()
+	if !opts.desc {
+		env.out.Println()
 	}
 
 	return nil
 }
 
-var commandsCmd = &cobra.Command{
-	Use:   "commands [<option>...]",
-	Short: "Display available commands.",
-	RunE:  runCommands,
-}
-
-func init() {
-	RootCmd.AddCommand(commandsCmd)
-
-	commandsCmd.Flags().SortFlags = false
+type commandSorterByName []*cobra.Command
 
-	commandsCmd.Flags().BoolVarP(&commandsDesc, "pretty", "p", false,
-		"Output the command description as well as Markdown compatible comment",
-	)
-}
+func (c commandSorterByName) Len() int           { return len(c) }
+func (c commandSorterByName) Swap(i, j int)      { c[i], c[j] = c[j], c[i] }
+func (c commandSorterByName) Less(i, j int) bool { return c[i].CommandPath() < c[j].CommandPath() }

commands/comment.go 🔗

@@ -1,60 +1,49 @@
 package commands
 
 import (
-	"fmt"
-
-	"github.com/MichaelMure/go-term-text"
+	text "github.com/MichaelMure/go-term-text"
 	"github.com/spf13/cobra"
 
-	"github.com/MichaelMure/git-bug/bug"
-	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/commands/select"
 	"github.com/MichaelMure/git-bug/util/colors"
-	"github.com/MichaelMure/git-bug/util/interrupt"
 )
 
-func runComment(cmd *cobra.Command, args []string) error {
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
+func newCommentCommand() *cobra.Command {
+	env := newEnv()
+
+	cmd := &cobra.Command{
+		Use:      "comment [<id>]",
+		Short:    "Display or add comments to a bug.",
+		PreRunE:  loadBackend(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runComment(env, args)
+		},
 	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
 
-	b, args, err := _select.ResolveBug(backend, args)
+	cmd.AddCommand(newCommentAddCommand())
+
+	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()
 
-	commentsTextOutput(snap.Comments)
-
-	return nil
-}
-
-func commentsTextOutput(comments []bug.Comment) {
-	for i, comment := range comments {
+	for i, comment := range snap.Comments {
 		if i != 0 {
-			fmt.Println()
+			env.out.Println()
 		}
 
-		fmt.Printf("Author: %s\n", colors.Magenta(comment.Author.DisplayName()))
-		fmt.Printf("Id: %s\n", colors.Cyan(comment.Id().Human()))
-		fmt.Printf("Date: %s\n\n", comment.FormatTime())
-		fmt.Println(text.LeftPadLines(comment.Message, 4))
+		env.out.Printf("Author: %s\n", colors.Magenta(comment.Author.DisplayName()))
+		env.out.Printf("Id: %s\n", colors.Cyan(comment.Id().Human()))
+		env.out.Printf("Date: %s\n\n", comment.FormatTime())
+		env.out.Println(text.LeftPadLines(comment.Message, 4))
 	}
-}
 
-var commentCmd = &cobra.Command{
-	Use:     "comment [<id>]",
-	Short:   "Display or add comments to a bug.",
-	PreRunE: loadRepo,
-	RunE:    runComment,
-}
-
-func init() {
-	RootCmd.AddCommand(commentCmd)
-
-	commentCmd.Flags().SortFlags = false
+	return nil
 }

commands/comment_add.go 🔗

@@ -1,44 +1,60 @@
 package commands
 
 import (
-	"fmt"
+	"github.com/spf13/cobra"
 
-	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/commands/select"
 	"github.com/MichaelMure/git-bug/input"
-	"github.com/MichaelMure/git-bug/util/interrupt"
-	"github.com/spf13/cobra"
 )
 
-var (
-	commentAddMessageFile string
-	commentAddMessage     string
-)
+type commentAddOptions struct {
+	messageFile string
+	message     string
+}
 
-func runCommentAdd(cmd *cobra.Command, args []string) error {
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
+func newCommentAddCommand() *cobra.Command {
+	env := newEnv()
+	options := commentAddOptions{}
+
+	cmd := &cobra.Command{
+		Use:      "add [<id>]",
+		Short:    "Add a new comment to a bug.",
+		PreRunE:  loadBackendEnsureUser(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runCommentAdd(env, options, args)
+		},
 	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
 
-	b, args, err := _select.ResolveBug(backend, args)
+	flags := cmd.Flags()
+	flags.SortFlags = false
+
+	flags.StringVarP(&options.messageFile, "file", "F", "",
+		"Take the message from the given file. Use - to read the message from the standard input")
+
+	flags.StringVarP(&options.message, "message", "m", "",
+		"Provide the new message from the command line")
+
+	return cmd
+}
+
+func runCommentAdd(env *Env, opts commentAddOptions, args []string) error {
+	b, args, err := _select.ResolveBug(env.backend, args)
 	if err != nil {
 		return err
 	}
 
-	if commentAddMessageFile != "" && commentAddMessage == "" {
-		commentAddMessage, err = input.BugCommentFileInput(commentAddMessageFile)
+	if opts.messageFile != "" && opts.message == "" {
+		opts.message, err = input.BugCommentFileInput(opts.messageFile)
 		if err != nil {
 			return err
 		}
 	}
 
-	if commentAddMessageFile == "" && commentAddMessage == "" {
-		commentAddMessage, err = input.BugCommentEditorInput(backend, "")
+	if opts.messageFile == "" && opts.message == "" {
+		opts.message, err = input.BugCommentEditorInput(env.backend, "")
 		if err == input.ErrEmptyMessage {
-			fmt.Println("Empty message, aborting.")
+			env.err.Println("Empty message, aborting.")
 			return nil
 		}
 		if err != nil {
@@ -46,31 +62,10 @@ func runCommentAdd(cmd *cobra.Command, args []string) error {
 		}
 	}
 
-	_, err = b.AddComment(commentAddMessage)
+	_, err = b.AddComment(opts.message)
 	if err != nil {
 		return err
 	}
 
 	return b.Commit()
 }
-
-var commentAddCmd = &cobra.Command{
-	Use:     "add [<id>]",
-	Short:   "Add a new comment to a bug.",
-	PreRunE: loadRepoEnsureUser,
-	RunE:    runCommentAdd,
-}
-
-func init() {
-	commentCmd.AddCommand(commentAddCmd)
-
-	commentAddCmd.Flags().SortFlags = false
-
-	commentAddCmd.Flags().StringVarP(&commentAddMessageFile, "file", "F", "",
-		"Take the message from the given file. Use - to read the message from the standard input",
-	)
-
-	commentAddCmd.Flags().StringVarP(&commentAddMessage, "message", "m", "",
-		"Provide the new message from the command line",
-	)
-}

commands/deselect.go 🔗

@@ -1,41 +1,37 @@
 package commands
 
 import (
-	"github.com/MichaelMure/git-bug/cache"
-	"github.com/MichaelMure/git-bug/commands/select"
-	"github.com/MichaelMure/git-bug/util/interrupt"
 	"github.com/spf13/cobra"
-)
-
-func runDeselect(cmd *cobra.Command, args []string) error {
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
-	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
 
-	err = _select.Clear(backend)
-	if err != nil {
-		return err
-	}
+	"github.com/MichaelMure/git-bug/commands/select"
+)
 
-	return nil
-}
+func newDeselectCommand() *cobra.Command {
+	env := newEnv()
 
-var deselectCmd = &cobra.Command{
-	Use:   "deselect",
-	Short: "Clear the implicitly selected bug.",
-	Example: `git bug select 2f15
+	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: loadRepo,
-	RunE:    runDeselect,
+		PreRunE:  loadBackend(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runDeselect(env)
+		},
+	}
+
+	return cmd
 }
 
-func init() {
-	RootCmd.AddCommand(deselectCmd)
-	deselectCmd.Flags().SortFlags = false
+func runDeselect(env *Env) error {
+	err := _select.Clear(env.backend)
+	if err != nil {
+		return err
+	}
+
+	return nil
 }

commands/env.go 🔗

@@ -0,0 +1,150 @@
+package commands
+
+import (
+	"fmt"
+	"io"
+	"os"
+
+	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/bug"
+	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/identity"
+	"github.com/MichaelMure/git-bug/repository"
+	"github.com/MichaelMure/git-bug/util/interrupt"
+)
+
+// Env is the environment of a command
+type Env struct {
+	repo    repository.ClockedRepo
+	backend *cache.RepoCache
+	out     out
+	err     out
+}
+
+func newEnv() *Env {
+	return &Env{
+		repo: nil,
+		out:  out{Writer: os.Stdout},
+		err:  out{Writer: os.Stderr},
+	}
+}
+
+type out struct {
+	io.Writer
+}
+
+func (o out) Printf(format string, a ...interface{}) {
+	_, _ = fmt.Fprintf(o, format, a...)
+}
+
+func (o out) Print(a ...interface{}) {
+	_, _ = fmt.Fprint(o, a...)
+}
+
+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 {
+	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.NewGitRepo(cwd, []repository.ClockLoader{bug.ClockLoader})
+		if err == repository.ErrNotARepo {
+			return fmt.Errorf("%s must be run from within a git repo", rootCommandName)
+		}
+
+		if err != nil {
+			return err
+		}
+
+		return nil
+	}
+}
+
+// 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 {
+	return func(cmd *cobra.Command, args []string) error {
+		err := loadRepo(env)(cmd, args)
+		if err != nil {
+			return err
+		}
+
+		_, err = identity.GetUserIdentity(env.repo)
+		if err != nil {
+			return err
+		}
+
+		return nil
+	}
+}
+
+// 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)
+		if err != nil {
+			return err
+		}
+
+		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
+					return err
+				}
+				return nil
+			}
+		}
+
+		// Cleanup properly on interrupt
+		interrupt.RegisterCleaner(cleaner(env))
+		return nil
+	}
+}
+
+// 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 {
+	return func(cmd *cobra.Command, args []string) error {
+		err := loadRepo(env)(cmd, args)
+		if err != nil {
+			return err
+		}
+
+		_, err = identity.GetUserIdentity(env.repo)
+		if err != nil {
+			return err
+		}
+
+		return nil
+	}
+}
+
+// closeBackend is a post-run function that will close the backend properly
+// if it has been opened.
+func closeBackend(env *Env) func(*cobra.Command, []string) error {
+	return func(cmd *cobra.Command, args []string) error {
+		if env.backend == nil {
+			return nil
+		}
+		err := env.backend.Close()
+		env.backend = nil
+		return err
+	}
+}

commands/label.go 🔗

@@ -1,23 +1,32 @@
 package commands
 
 import (
-	"fmt"
+	"github.com/spf13/cobra"
 
-	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/commands/select"
-	"github.com/MichaelMure/git-bug/util/interrupt"
-	"github.com/spf13/cobra"
 )
 
-func runLabel(cmd *cobra.Command, args []string) error {
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
+func newLabelCommand() *cobra.Command {
+	env := newEnv()
+
+	cmd := &cobra.Command{
+		Use:      "label [<id>]",
+		Short:    "Display, add or remove labels to/from a bug.",
+		PreRunE:  loadBackend(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runLabel(env, args)
+		},
 	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
 
-	b, args, err := _select.ResolveBug(backend, args)
+	cmd.AddCommand(newLabelAddCommand())
+	cmd.AddCommand(newLabelRmCommand())
+
+	return cmd
+}
+
+func runLabel(env *Env, args []string) error {
+	b, args, err := _select.ResolveBug(env.backend, args)
 	if err != nil {
 		return err
 	}
@@ -25,21 +34,8 @@ func runLabel(cmd *cobra.Command, args []string) error {
 	snap := b.Snapshot()
 
 	for _, l := range snap.Labels {
-		fmt.Println(l)
+		env.out.Println(l)
 	}
 
 	return nil
 }
-
-var labelCmd = &cobra.Command{
-	Use:     "label [<id>]",
-	Short:   "Display, add or remove labels to/from a bug.",
-	PreRunE: loadRepo,
-	RunE:    runLabel,
-}
-
-func init() {
-	RootCmd.AddCommand(labelCmd)
-
-	labelCmd.Flags().SortFlags = false
-}

commands/label_add.go 🔗

@@ -1,23 +1,29 @@
 package commands
 
 import (
-	"fmt"
+	"github.com/spf13/cobra"
 
-	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/commands/select"
-	"github.com/MichaelMure/git-bug/util/interrupt"
-	"github.com/spf13/cobra"
 )
 
-func runLabelAdd(cmd *cobra.Command, args []string) error {
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
+func newLabelAddCommand() *cobra.Command {
+	env := newEnv()
+
+	cmd := &cobra.Command{
+		Use:      "add [<id>] <label>[...]",
+		Short:    "Add a label to a bug.",
+		PreRunE:  loadBackendEnsureUser(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runLabelAdd(env, args)
+		},
 	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
 
-	b, args, err := _select.ResolveBug(backend, args)
+	return cmd
+}
+
+func runLabelAdd(env *Env, args []string) error {
+	b, args, err := _select.ResolveBug(env.backend, args)
 	if err != nil {
 		return err
 	}
@@ -25,7 +31,7 @@ func runLabelAdd(cmd *cobra.Command, args []string) error {
 	changes, _, err := b.ChangeLabels(args, nil)
 
 	for _, change := range changes {
-		fmt.Println(change)
+		env.out.Println(change)
 	}
 
 	if err != nil {
@@ -34,14 +40,3 @@ func runLabelAdd(cmd *cobra.Command, args []string) error {
 
 	return b.Commit()
 }
-
-var labelAddCmd = &cobra.Command{
-	Use:     "add [<id>] <label>[...]",
-	Short:   "Add a label to a bug.",
-	PreRunE: loadRepoEnsureUser,
-	RunE:    runLabelAdd,
-}
-
-func init() {
-	labelCmd.AddCommand(labelAddCmd)
-}

commands/label_rm.go 🔗

@@ -1,24 +1,29 @@
 package commands
 
 import (
-	"fmt"
-
 	"github.com/spf13/cobra"
 
-	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/commands/select"
-	"github.com/MichaelMure/git-bug/util/interrupt"
 )
 
-func runLabelRm(cmd *cobra.Command, args []string) error {
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
+func newLabelRmCommand() *cobra.Command {
+	env := newEnv()
+
+	cmd := &cobra.Command{
+		Use:      "rm [<id>] <label>[...]",
+		Short:    "Remove a label from a bug.",
+		PreRunE:  loadBackend(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runLabelRm(env, args)
+		},
 	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
 
-	b, args, err := _select.ResolveBug(backend, args)
+	return cmd
+}
+
+func runLabelRm(env *Env, args []string) error {
+	b, args, err := _select.ResolveBug(env.backend, args)
 	if err != nil {
 		return err
 	}
@@ -26,7 +31,7 @@ func runLabelRm(cmd *cobra.Command, args []string) error {
 	changes, _, err := b.ChangeLabels(nil, args)
 
 	for _, change := range changes {
-		fmt.Println(change)
+		env.out.Println(change)
 	}
 
 	if err != nil {
@@ -35,14 +40,3 @@ func runLabelRm(cmd *cobra.Command, args []string) error {
 
 	return b.Commit()
 }
-
-var labelRmCmd = &cobra.Command{
-	Use:     "rm [<id>] <label>[...]",
-	Short:   "Remove a label from a bug.",
-	PreRunE: loadRepo,
-	RunE:    runLabelRm,
-}
-
-func init() {
-	labelCmd.AddCommand(labelRmCmd)
-}

commands/ls-id.go 🔗

@@ -1,44 +1,36 @@
 package commands
 
 import (
-	"fmt"
-
 	"github.com/spf13/cobra"
-
-	"github.com/MichaelMure/git-bug/cache"
-	"github.com/MichaelMure/git-bug/util/interrupt"
 )
 
-func runLsID(cmd *cobra.Command, args []string) error {
-
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
+func newLsIdCommand() *cobra.Command {
+	env := newEnv()
+
+	cmd := &cobra.Command{
+		Use:      "ls-id [<prefix>]",
+		Short:    "List bug identifiers.",
+		PreRunE:  loadBackend(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runLsId(env, args)
+		},
 	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
 
+	return cmd
+}
+
+func runLsId(env *Env, args []string) error {
 	var prefix = ""
 	if len(args) != 0 {
 		prefix = args[0]
 	}
 
-	for _, id := range backend.AllBugsIds() {
+	for _, id := range env.backend.AllBugsIds() {
 		if prefix == "" || id.HasPrefix(prefix) {
-			fmt.Println(id)
+			env.out.Println(id)
 		}
 	}
 
 	return nil
 }
-
-var listBugIDCmd = &cobra.Command{
-	Use:     "ls-id [<prefix>]",
-	Short:   "List bug identifiers.",
-	PreRunE: loadRepo,
-	RunE:    runLsID,
-}
-
-func init() {
-	RootCmd.AddCommand(listBugIDCmd)
-}

commands/ls-labels.go 🔗

@@ -1,40 +1,34 @@
 package commands
 
 import (
-	"fmt"
-
-	"github.com/MichaelMure/git-bug/cache"
-	"github.com/MichaelMure/git-bug/util/interrupt"
 	"github.com/spf13/cobra"
 )
 
-func runLsLabel(cmd *cobra.Command, args []string) error {
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
-	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
+func newLsLabelCommand() *cobra.Command {
+	env := newEnv()
 
-	labels := backend.ValidLabels()
+	cmd := &cobra.Command{
+		Use:   "ls-label",
+		Short: "List valid labels.",
+		Long: `List valid labels.
 
-	for _, l := range labels {
-		fmt.Println(l)
+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),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runLsLabel(env)
+		},
 	}
 
-	return nil
+	return cmd
 }
 
-var lsLabelCmd = &cobra.Command{
-	Use:   "ls-label",
-	Short: "List valid labels.",
-	Long: `List valid labels.
+func runLsLabel(env *Env) error {
+	labels := env.backend.ValidLabels()
 
-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: loadRepo,
-	RunE:    runLsLabel,
-}
+	for _, l := range labels {
+		env.out.Println(l)
+	}
 
-func init() {
-	RootCmd.AddCommand(lsLabelCmd)
+	return nil
 }

commands/ls.go 🔗

@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"strings"
+	"time"
 
 	text "github.com/MichaelMure/go-term-text"
 	"github.com/spf13/cobra"
@@ -12,28 +13,74 @@ import (
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/query"
 	"github.com/MichaelMure/git-bug/util/colors"
-	"github.com/MichaelMure/git-bug/util/interrupt"
 )
 
-var (
-	lsQuery query.Query
+type lsOptions struct {
+	query query.Query
 
-	lsStatusQuery   []string
-	lsNoQuery       []string
-	lsSortBy        string
-	lsSortDirection string
-	lsOutputFormat  string
-)
+	statusQuery   []string
+	noQuery       []string
+	sortBy        string
+	sortDirection string
+	outputFormat  string
+}
+
+func newLsCommand() *cobra.Command {
+	env := newEnv()
+	options := lsOptions{}
+
+	cmd := &cobra.Command{
+		Use:   "ls [<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 or with flags.`,
+		Example: `List open bugs sorted by last edition with a query:
+git bug ls status:open sort:edit-desc
 
-func runLsBug(_ *cobra.Command, args []string) error {
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
+List closed bugs sorted by creation with flags:
+git bug ls --status closed --by creation
+`,
+		PreRunE:  loadBackend(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runLs(env, options, args)
+		},
 	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
+
+	flags := cmd.Flags()
+	flags.SortFlags = false
+
+	flags.StringSliceVarP(&options.statusQuery, "status", "s", nil,
+		"Filter by status. Valid values are [open,closed]")
+	flags.StringSliceVarP(&options.query.Author, "author", "a", nil,
+		"Filter by author")
+	flags.StringSliceVarP(&options.query.Participant, "participant", "p", nil,
+		"Filter by participant")
+	flags.StringSliceVarP(&options.query.Actor, "actor", "A", nil,
+		"Filter by actor")
+	flags.StringSliceVarP(&options.query.Label, "label", "l", nil,
+		"Filter by label")
+	flags.StringSliceVarP(&options.query.Title, "title", "t", nil,
+		"Filter by title")
+	flags.StringSliceVarP(&options.noQuery, "no", "n", nil,
+		"Filter by absence of something. Valid values are [label]")
+	flags.StringVarP(&options.sortBy, "by", "b", "creation",
+		"Sort the results by a characteristic. Valid values are [id,creation,edit]")
+	flags.StringVarP(&options.sortDirection, "direction", "d", "asc",
+		"Select the sorting direction. Valid values are [asc,desc]")
+	flags.StringVarP(&options.outputFormat, "format", "f", "default",
+		"Select the output formatting style. Valid values are [default,plain,json,org-mode]")
+
+	return cmd
+}
+
+func runLs(env *Env, opts lsOptions, args []string) error {
+	time.Sleep(5 * time.Second)
 
 	var q *query.Query
+	var err error
+
 	if len(args) >= 1 {
 		q, err = query.Parse(strings.Join(args, " "))
 
@@ -41,35 +88,35 @@ func runLsBug(_ *cobra.Command, args []string) error {
 			return err
 		}
 	} else {
-		err = completeQuery()
+		err = completeQuery(&opts)
 		if err != nil {
 			return err
 		}
-		q = &lsQuery
+		q = &opts.query
 	}
 
-	allIds := backend.QueryBugs(q)
+	allIds := env.backend.QueryBugs(q)
 
 	bugExcerpt := make([]*cache.BugExcerpt, len(allIds))
 	for i, id := range allIds {
-		b, err := backend.ResolveBugExcerpt(id)
+		b, err := env.backend.ResolveBugExcerpt(id)
 		if err != nil {
 			return err
 		}
 		bugExcerpt[i] = b
 	}
 
-	switch lsOutputFormat {
+	switch opts.outputFormat {
 	case "org-mode":
-		return lsOrgmodeFormatter(backend, bugExcerpt)
+		return lsOrgmodeFormatter(env, bugExcerpt)
 	case "plain":
-		return lsPlainFormatter(backend, bugExcerpt)
+		return lsPlainFormatter(env, bugExcerpt)
 	case "json":
-		return lsJsonFormatter(backend, bugExcerpt)
+		return lsJsonFormatter(env, bugExcerpt)
 	case "default":
-		return lsDefaultFormatter(backend, bugExcerpt)
+		return lsDefaultFormatter(env, bugExcerpt)
 	default:
-		return fmt.Errorf("unknown format %s", lsOutputFormat)
+		return fmt.Errorf("unknown format %s", opts.outputFormat)
 	}
 }
 
@@ -90,7 +137,7 @@ type JSONBugExcerpt struct {
 	Metadata map[string]string `json:"metadata"`
 }
 
-func lsJsonFormatter(backend *cache.RepoCache, bugExcerpts []*cache.BugExcerpt) error {
+func lsJsonFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
 	jsonBugs := make([]JSONBugExcerpt, len(bugExcerpts))
 	for i, b := range bugExcerpts {
 		jsonBug := JSONBugExcerpt{
@@ -106,7 +153,7 @@ func lsJsonFormatter(backend *cache.RepoCache, bugExcerpts []*cache.BugExcerpt)
 		}
 
 		if b.AuthorId != "" {
-			author, err := backend.ResolveIdentityExcerpt(b.AuthorId)
+			author, err := env.backend.ResolveIdentityExcerpt(b.AuthorId)
 			if err != nil {
 				return err
 			}
@@ -117,7 +164,7 @@ func lsJsonFormatter(backend *cache.RepoCache, bugExcerpts []*cache.BugExcerpt)
 
 		jsonBug.Actors = make([]JSONIdentity, len(b.Actors))
 		for i, element := range b.Actors {
-			actor, err := backend.ResolveIdentityExcerpt(element)
+			actor, err := env.backend.ResolveIdentityExcerpt(element)
 			if err != nil {
 				return err
 			}
@@ -126,7 +173,7 @@ func lsJsonFormatter(backend *cache.RepoCache, bugExcerpts []*cache.BugExcerpt)
 
 		jsonBug.Participants = make([]JSONIdentity, len(b.Participants))
 		for i, element := range b.Participants {
-			participant, err := backend.ResolveIdentityExcerpt(element)
+			participant, err := env.backend.ResolveIdentityExcerpt(element)
 			if err != nil {
 				return err
 			}
@@ -136,15 +183,15 @@ func lsJsonFormatter(backend *cache.RepoCache, bugExcerpts []*cache.BugExcerpt)
 		jsonBugs[i] = jsonBug
 	}
 	jsonObject, _ := json.MarshalIndent(jsonBugs, "", "    ")
-	fmt.Printf("%s\n", jsonObject)
+	env.out.Printf("%s\n", jsonObject)
 	return nil
 }
 
-func lsDefaultFormatter(backend *cache.RepoCache, bugExcerpts []*cache.BugExcerpt) error {
+func lsDefaultFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
 	for _, b := range bugExcerpts {
 		var name string
 		if b.AuthorId != "" {
-			author, err := backend.ResolveIdentityExcerpt(b.AuthorId)
+			author, err := env.backend.ResolveIdentityExcerpt(b.AuthorId)
 			if err != nil {
 				return err
 			}
@@ -171,7 +218,7 @@ func lsDefaultFormatter(backend *cache.RepoCache, bugExcerpts []*cache.BugExcerp
 			comments = "    ∞ 💬"
 		}
 
-		fmt.Printf("%s %s\t%s\t%s\t%s\n",
+		env.out.Printf("%s %s\t%s\t%s\t%s\n",
 			colors.Cyan(b.Id.Human()),
 			colors.Yellow(b.Status),
 			titleFmt+labelsFmt,
@@ -182,15 +229,15 @@ func lsDefaultFormatter(backend *cache.RepoCache, bugExcerpts []*cache.BugExcerp
 	return nil
 }
 
-func lsPlainFormatter(_ *cache.RepoCache, bugExcerpts []*cache.BugExcerpt) error {
+func lsPlainFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
 	for _, b := range bugExcerpts {
-		fmt.Printf("%s [%s] %s\n", b.Id.Human(), b.Status, b.Title)
+		env.out.Printf("%s [%s] %s\n", b.Id.Human(), b.Status, b.Title)
 	}
 	return nil
 }
 
-func lsOrgmodeFormatter(backend *cache.RepoCache, bugExcerpts []*cache.BugExcerpt) error {
-	fmt.Println("+TODO: OPEN | CLOSED")
+func lsOrgmodeFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
+	env.out.Println("+TODO: OPEN | CLOSED")
 
 	for _, b := range bugExcerpts {
 		status := strings.Title(b.Status.String())
@@ -204,7 +251,7 @@ func lsOrgmodeFormatter(backend *cache.RepoCache, bugExcerpts []*cache.BugExcerp
 
 		var name string
 		if b.AuthorId != "" {
-			author, err := backend.ResolveIdentityExcerpt(b.AuthorId)
+			author, err := env.backend.ResolveIdentityExcerpt(b.AuthorId)
 			if err != nil {
 				return err
 			}
@@ -221,7 +268,7 @@ func lsOrgmodeFormatter(backend *cache.RepoCache, bugExcerpts []*cache.BugExcerp
 			labelsString = ""
 		}
 
-		fmt.Printf("* %s %s [%s] %s: %s %s\n",
+		env.out.Printf("* %s %s [%s] %s: %s %s\n",
 			b.Id.Human(),
 			status,
 			b.CreateTime(),
@@ -230,29 +277,29 @@ func lsOrgmodeFormatter(backend *cache.RepoCache, bugExcerpts []*cache.BugExcerp
 			labelsString,
 		)
 
-		fmt.Printf("** Last Edited: %s\n", b.EditTime().String())
+		env.out.Printf("** Last Edited: %s\n", b.EditTime().String())
 
-		fmt.Printf("** Actors:\n")
+		env.out.Printf("** Actors:\n")
 		for _, element := range b.Actors {
-			actor, err := backend.ResolveIdentityExcerpt(element)
+			actor, err := env.backend.ResolveIdentityExcerpt(element)
 			if err != nil {
 				return err
 			}
 
-			fmt.Printf(": %s %s\n",
+			env.out.Printf(": %s %s\n",
 				actor.Id.Human(),
 				actor.DisplayName(),
 			)
 		}
 
-		fmt.Printf("** Participants:\n")
+		env.out.Printf("** Participants:\n")
 		for _, element := range b.Participants {
-			participant, err := backend.ResolveIdentityExcerpt(element)
+			participant, err := env.backend.ResolveIdentityExcerpt(element)
 			if err != nil {
 				return err
 			}
 
-			fmt.Printf(": %s %s\n",
+			env.out.Printf(": %s %s\n",
 				participant.Id.Human(),
 				participant.DisplayName(),
 			)
@@ -263,86 +310,43 @@ func lsOrgmodeFormatter(backend *cache.RepoCache, bugExcerpts []*cache.BugExcerp
 }
 
 // Finish the command flags transformation into the query.Query
-func completeQuery() error {
-	for _, str := range lsStatusQuery {
+func completeQuery(opts *lsOptions) error {
+	for _, str := range opts.statusQuery {
 		status, err := bug.StatusFromString(str)
 		if err != nil {
 			return err
 		}
-		lsQuery.Status = append(lsQuery.Status, status)
+		opts.query.Status = append(opts.query.Status, status)
 	}
 
-	for _, no := range lsNoQuery {
+	for _, no := range opts.noQuery {
 		switch no {
 		case "label":
-			lsQuery.NoLabel = true
+			opts.query.NoLabel = true
 		default:
 			return fmt.Errorf("unknown \"no\" filter %s", no)
 		}
 	}
 
-	switch lsSortBy {
+	switch opts.sortBy {
 	case "id":
-		lsQuery.OrderBy = query.OrderById
+		opts.query.OrderBy = query.OrderById
 	case "creation":
-		lsQuery.OrderBy = query.OrderByCreation
+		opts.query.OrderBy = query.OrderByCreation
 	case "edit":
-		lsQuery.OrderBy = query.OrderByEdit
+		opts.query.OrderBy = query.OrderByEdit
 	default:
-		return fmt.Errorf("unknown sort flag %s", lsSortBy)
+		return fmt.Errorf("unknown sort flag %s", opts.sortBy)
 	}
 
-	switch lsSortDirection {
+	switch opts.sortDirection {
 	case "asc":
-		lsQuery.OrderDirection = query.OrderAscending
+		opts.query.OrderDirection = query.OrderAscending
 	case "desc":
-		lsQuery.OrderDirection = query.OrderDescending
+		opts.query.OrderDirection = query.OrderDescending
 	default:
-		return fmt.Errorf("unknown sort direction %s", lsSortDirection)
+		return fmt.Errorf("unknown sort direction %s", opts.sortDirection)
 	}
 
 	return nil
 }
-
-var lsCmd = &cobra.Command{
-	Use:   "ls [<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 or with flags.`,
-	Example: `List open bugs sorted by last edition with a query:
-git bug ls status:open sort:edit-desc
-
-List closed bugs sorted by creation with flags:
-git bug ls --status closed --by creation
-`,
-	PreRunE: loadRepo,
-	RunE:    runLsBug,
-}
-
-func init() {
-	RootCmd.AddCommand(lsCmd)
-
-	lsCmd.Flags().SortFlags = false
-
-	lsCmd.Flags().StringSliceVarP(&lsStatusQuery, "status", "s", nil,
-		"Filter by status. Valid values are [open,closed]")
-	lsCmd.Flags().StringSliceVarP(&lsQuery.Author, "author", "a", nil,
-		"Filter by author")
-	lsCmd.Flags().StringSliceVarP(&lsQuery.Participant, "participant", "p", nil,
-		"Filter by participant")
-	lsCmd.Flags().StringSliceVarP(&lsQuery.Actor, "actor", "A", nil,
-		"Filter by actor")
-	lsCmd.Flags().StringSliceVarP(&lsQuery.Label, "label", "l", nil,
-		"Filter by label")
-	lsCmd.Flags().StringSliceVarP(&lsQuery.Title, "title", "t", nil,
-		"Filter by title")
-	lsCmd.Flags().StringSliceVarP(&lsNoQuery, "no", "n", nil,
-		"Filter by absence of something. Valid values are [label]")
-	lsCmd.Flags().StringVarP(&lsSortBy, "by", "b", "creation",
-		"Sort the results by a characteristic. Valid values are [id,creation,edit]")
-	lsCmd.Flags().StringVarP(&lsSortDirection, "direction", "d", "asc",
-		"Select the sorting direction. Valid values are [asc,desc]")
-	lsCmd.Flags().StringVarP(&lsOutputFormat, "format", "f", "default",
-		"Select the output formatting style. Valid values are [default,plain,json,org-mode]")
-}

commands/pull.go 🔗

@@ -2,16 +2,29 @@ package commands
 
 import (
 	"errors"
-	"fmt"
 
 	"github.com/spf13/cobra"
 
-	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/entity"
-	"github.com/MichaelMure/git-bug/util/interrupt"
 )
 
-func runPull(cmd *cobra.Command, args []string) error {
+func newPullCommand() *cobra.Command {
+	env := newEnv()
+
+	cmd := &cobra.Command{
+		Use:      "pull [<remote>]",
+		Short:    "Pull bugs update from a git remote.",
+		PreRunE:  loadBackend(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runPull(env, args)
+		},
+	}
+
+	return cmd
+}
+
+func runPull(env *Env, args []string) error {
 	if len(args) > 1 {
 		return errors.New("Only pulling from one remote at a time is supported")
 	}
@@ -21,45 +34,26 @@ func runPull(cmd *cobra.Command, args []string) error {
 		remote = args[0]
 	}
 
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
-	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
-
-	fmt.Println("Fetching remote ...")
+	env.out.Println("Fetching remote ...")
 
-	stdout, err := backend.Fetch(remote)
+	stdout, err := env.backend.Fetch(remote)
 	if err != nil {
 		return err
 	}
 
-	fmt.Println(stdout)
+	env.out.Println(stdout)
 
-	fmt.Println("Merging data ...")
+	env.out.Println("Merging data ...")
 
-	for result := range backend.MergeAll(remote) {
+	for result := range env.backend.MergeAll(remote) {
 		if result.Err != nil {
-			fmt.Println(result.Err)
+			env.err.Println(result.Err)
 		}
 
 		if result.Status != entity.MergeStatusNothing {
-			fmt.Printf("%s: %s\n", result.Id.Human(), result)
+			env.out.Printf("%s: %s\n", result.Id.Human(), result)
 		}
 	}
 
 	return nil
 }
-
-// showCmd defines the "push" subcommand.
-var pullCmd = &cobra.Command{
-	Use:     "pull [<remote>]",
-	Short:   "Pull bugs update from a git remote.",
-	PreRunE: loadRepo,
-	RunE:    runPull,
-}
-
-func init() {
-	RootCmd.AddCommand(pullCmd)
-}

commands/push.go 🔗

@@ -2,14 +2,27 @@ package commands
 
 import (
 	"errors"
-	"fmt"
 
-	"github.com/MichaelMure/git-bug/cache"
-	"github.com/MichaelMure/git-bug/util/interrupt"
 	"github.com/spf13/cobra"
 )
 
-func runPush(cmd *cobra.Command, args []string) error {
+func newPushCommand() *cobra.Command {
+	env := newEnv()
+
+	cmd := &cobra.Command{
+		Use:      "push [<remote>]",
+		Short:    "Push bugs update to a git remote.",
+		PreRunE:  loadBackend(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runPush(env, args)
+		},
+	}
+
+	return cmd
+}
+
+func runPush(env *Env, args []string) error {
 	if len(args) > 1 {
 		return errors.New("Only pushing to one remote at a time is supported")
 	}
@@ -19,31 +32,12 @@ func runPush(cmd *cobra.Command, args []string) error {
 		remote = args[0]
 	}
 
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
-	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
-
-	stdout, err := backend.Push(remote)
+	stdout, err := env.backend.Push(remote)
 	if err != nil {
 		return err
 	}
 
-	fmt.Println(stdout)
+	env.out.Println(stdout)
 
 	return nil
 }
-
-// showCmd defines the "push" subcommand.
-var pushCmd = &cobra.Command{
-	Use:     "push [<remote>]",
-	Short:   "Push bugs update to a git remote.",
-	PreRunE: loadRepo,
-	RunE:    runPush,
-}
-
-func init() {
-	RootCmd.AddCommand(pushCmd)
-}

commands/root.go 🔗

@@ -6,22 +6,20 @@ import (
 	"os"
 
 	"github.com/spf13/cobra"
-
-	"github.com/MichaelMure/git-bug/bug"
-	"github.com/MichaelMure/git-bug/identity"
-	"github.com/MichaelMure/git-bug/repository"
 )
 
 const rootCommandName = "git-bug"
 
-// package scoped var to hold the repo after the PreRun execution
-var repo repository.ClockedRepo
+// These variables are initialized externally during the build. See the Makefile.
+var GitCommit string
+var GitLastTag string
+var GitExactTag string
 
-// RootCmd represents the base command when called without any subcommands
-var RootCmd = &cobra.Command{
-	Use:   rootCommandName,
-	Short: "A bug tracker embedded in Git.",
-	Long: `git-bug is a bug tracker embedded in git.
+func NewRootCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   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
 history. As bugs are regular git objects, they can be pushed and pulled from/to
@@ -29,65 +27,64 @@ the same git remote you are already using to collaborate with other people.
 
 `,
 
-	// For the root command, force the execution of the PreRun
-	// even if we just display the help. This is to make sure that we check
-	// the repository and give the user early feedback.
-	Run: func(cmd *cobra.Command, args []string) {
-		if err := cmd.Help(); err != nil {
-			os.Exit(1)
-		}
-	},
-
-	SilenceUsage:      true,
-	DisableAutoGenTag: true,
-
-	// Custom bash code to connect the git completion for "git bug" to the
-	// git-bug completion for "git-bug"
-	BashCompletionFunction: `
+		PersistentPreRun: func(cmd *cobra.Command, args []string) {
+			root := cmd.Root()
+
+			if GitExactTag == "undefined" {
+				GitExactTag = ""
+			}
+			root.Version = GitLastTag
+			if GitExactTag == "" {
+				root.Version = fmt.Sprintf("%s-dev-%.10s", root.Version, GitCommit)
+			}
+		},
+
+		// For the root command, force the execution of the PreRun
+		// even if we just display the help. This is to make sure that we check
+		// the repository and give the user early feedback.
+		Run: func(cmd *cobra.Command, args []string) {
+			if err := cmd.Help(); err != nil {
+				os.Exit(1)
+			}
+		},
+
+		SilenceUsage:      true,
+		DisableAutoGenTag: true,
+
+		// Custom bash code to connect the git completion for "git bug" to the
+		// git-bug completion for "git-bug"
+		BashCompletionFunction: `
 _git_bug() {
     __start_git-bug "$@"
 }
 `,
-}
-
-func Execute() {
-	if err := RootCmd.Execute(); err != nil {
-		os.Exit(1)
-	}
-}
-
-// loadRepo is a pre-run function that load the repository for use in a command
-func loadRepo(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)
 	}
 
-	repo, err = repository.NewGitRepo(cwd, []repository.ClockLoader{bug.ClockLoader})
-	if err == repository.ErrNotARepo {
-		return fmt.Errorf("%s must be run from within a git repo", rootCommandName)
-	}
-
-	if err != nil {
-		return err
-	}
-
-	return nil
+	cmd.AddCommand(newAddCommand())
+	cmd.AddCommand(newBridgeCommand())
+	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(newSelectCommand())
+	cmd.AddCommand(newShowCommand())
+	cmd.AddCommand(newStatusCommand())
+	cmd.AddCommand(newTermUICommand())
+	cmd.AddCommand(newTitleCommand())
+	cmd.AddCommand(newUserCommand())
+	cmd.AddCommand(newVersionCommand())
+	cmd.AddCommand(newWebUICommand())
+
+	return cmd
 }
 
-// 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(cmd *cobra.Command, args []string) error {
-	err := loadRepo(cmd, args)
-	if err != nil {
-		return err
-	}
-
-	_, err = identity.GetUserIdentity(repo)
-	if err != nil {
-		return err
+func Execute() {
+	if err := NewRootCommand().Execute(); err != nil {
+		os.Exit(1)
 	}
-
-	return nil
 }

commands/select.go 🔗

@@ -2,65 +2,59 @@ package commands
 
 import (
 	"errors"
-	"fmt"
 
 	"github.com/spf13/cobra"
 
-	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/commands/select"
-	"github.com/MichaelMure/git-bug/util/interrupt"
 )
 
-func runSelect(cmd *cobra.Command, args []string) error {
-	if len(args) == 0 {
-		return errors.New("You must provide a bug id")
+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),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runSelect(env, args)
+		},
 	}
 
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
+	return cmd
+}
+
+func runSelect(env *Env, args []string) error {
+	if len(args) == 0 {
+		return errors.New("You must provide a bug id")
 	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
 
 	prefix := args[0]
 
-	b, err := backend.ResolveBugPrefix(prefix)
+	b, err := env.backend.ResolveBugPrefix(prefix)
 	if err != nil {
 		return err
 	}
 
-	err = _select.Select(backend, b.Id())
+	err = _select.Select(env.backend, b.Id())
 	if err != nil {
 		return err
 	}
 
-	fmt.Printf("selected bug %s: %s\n", b.Id().Human(), b.Snapshot().Title)
+	env.out.Printf("selected bug %s: %s\n", b.Id().Human(), b.Snapshot().Title)
 
 	return nil
 }
-
-var selectCmd = &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: loadRepo,
-	RunE:    runSelect,
-}
-
-func init() {
-	RootCmd.AddCommand(selectCmd)
-	selectCmd.Flags().SortFlags = false
-}

commands/show.go 🔗

@@ -9,101 +9,117 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/MichaelMure/git-bug/bug"
-	"github.com/MichaelMure/git-bug/cache"
 	_select "github.com/MichaelMure/git-bug/commands/select"
 	"github.com/MichaelMure/git-bug/util/colors"
-	"github.com/MichaelMure/git-bug/util/interrupt"
 )
 
-var (
-	showFieldsQuery  string
-	showOutputFormat string
-)
+type showOptions struct {
+	query  string
+	format string
+}
 
-func runShowBug(_ *cobra.Command, args []string) error {
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
+func newShowCommand() *cobra.Command {
+	env := newEnv()
+	options := showOptions{}
+
+	cmd := &cobra.Command{
+		Use:      "show [<id>]",
+		Short:    "Display the details of a bug.",
+		PreRunE:  loadBackend(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runShow(env, options, args)
+		},
 	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
 
-	b, args, err := _select.ResolveBug(backend, args)
+	flags := cmd.Flags()
+	flags.SortFlags = false
+
+	flags.StringVarP(&options.query, "field", "", "",
+		"Select field to display. Valid values are [author,authorEmail,createTime,lastEdit,humanId,id,labels,shortId,status,title,actors,participants]")
+	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)
 	if err != nil {
 		return err
 	}
 
-	snapshot := b.Snapshot()
+	snap := b.Snapshot()
 
-	if len(snapshot.Comments) == 0 {
+	if len(snap.Comments) == 0 {
 		return errors.New("invalid bug: no comment")
 	}
 
-	if showFieldsQuery != "" {
-		switch showFieldsQuery {
+	if opts.query != "" {
+		switch opts.query {
 		case "author":
-			fmt.Printf("%s\n", snapshot.Author.DisplayName())
+			env.out.Printf("%s\n", snap.Author.DisplayName())
 		case "authorEmail":
-			fmt.Printf("%s\n", snapshot.Author.Email())
+			env.out.Printf("%s\n", snap.Author.Email())
 		case "createTime":
-			fmt.Printf("%s\n", snapshot.CreateTime.String())
+			env.out.Printf("%s\n", snap.CreateTime.String())
 		case "lastEdit":
-			fmt.Printf("%s\n", snapshot.EditTime().String())
+			env.out.Printf("%s\n", snap.EditTime().String())
 		case "humanId":
-			fmt.Printf("%s\n", snapshot.Id().Human())
+			env.out.Printf("%s\n", snap.Id().Human())
 		case "id":
-			fmt.Printf("%s\n", snapshot.Id())
+			env.out.Printf("%s\n", snap.Id())
 		case "labels":
-			for _, l := range snapshot.Labels {
-				fmt.Printf("%s\n", l.String())
+			for _, l := range snap.Labels {
+				env.out.Printf("%s\n", l.String())
 			}
 		case "actors":
-			for _, a := range snapshot.Actors {
-				fmt.Printf("%s\n", a.DisplayName())
+			for _, a := range snap.Actors {
+				env.out.Printf("%s\n", a.DisplayName())
 			}
 		case "participants":
-			for _, p := range snapshot.Participants {
-				fmt.Printf("%s\n", p.DisplayName())
+			for _, p := range snap.Participants {
+				env.out.Printf("%s\n", p.DisplayName())
 			}
 		case "shortId":
-			fmt.Printf("%s\n", snapshot.Id().Human())
+			env.out.Printf("%s\n", snap.Id().Human())
 		case "status":
-			fmt.Printf("%s\n", snapshot.Status)
+			env.out.Printf("%s\n", snap.Status)
 		case "title":
-			fmt.Printf("%s\n", snapshot.Title)
+			env.out.Printf("%s\n", snap.Title)
 		default:
-			return fmt.Errorf("\nUnsupported field: %s\n", showFieldsQuery)
+			return fmt.Errorf("\nUnsupported field: %s\n", opts.query)
 		}
 
 		return nil
 	}
 
-	switch showOutputFormat {
+	switch opts.format {
 	case "org-mode":
-		return showOrgmodeFormatter(snapshot)
+		return showOrgModeFormatter(env, snap)
 	case "json":
-		return showJsonFormatter(snapshot)
+		return showJsonFormatter(env, snap)
 	case "default":
-		return showDefaultFormatter(snapshot)
+		return showDefaultFormatter(env, snap)
 	default:
-		return fmt.Errorf("unknown format %s", showOutputFormat)
+		return fmt.Errorf("unknown format %s", opts.format)
 	}
 }
 
-func showDefaultFormatter(snapshot *bug.Snapshot) error {
+func showDefaultFormatter(env *Env, snapshot *bug.Snapshot) error {
 	// Header
-	fmt.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,
 	)
 
-	fmt.Printf("%s opened this issue %s\n",
+	env.out.Printf("%s opened this issue %s\n",
 		colors.Magenta(snapshot.Author.DisplayName()),
 		snapshot.CreateTime.String(),
 	)
 
-	fmt.Printf("This was last edited at %s\n\n",
+	env.out.Printf("This was last edited at %s\n\n",
 		snapshot.EditTime().String(),
 	)
 
@@ -113,7 +129,7 @@ func showDefaultFormatter(snapshot *bug.Snapshot) error {
 		labels[i] = string(snapshot.Labels[i])
 	}
 
-	fmt.Printf("labels: %s\n",
+	env.out.Printf("labels: %s\n",
 		strings.Join(labels, ", "),
 	)
 
@@ -123,7 +139,7 @@ func showDefaultFormatter(snapshot *bug.Snapshot) error {
 		actors[i] = snapshot.Actors[i].DisplayName()
 	}
 
-	fmt.Printf("actors: %s\n",
+	env.out.Printf("actors: %s\n",
 		strings.Join(actors, ", "),
 	)
 
@@ -133,7 +149,7 @@ func showDefaultFormatter(snapshot *bug.Snapshot) error {
 		participants[i] = snapshot.Participants[i].DisplayName()
 	}
 
-	fmt.Printf("participants: %s\n\n",
+	env.out.Printf("participants: %s\n\n",
 		strings.Join(participants, ", "),
 	)
 
@@ -142,7 +158,7 @@ func showDefaultFormatter(snapshot *bug.Snapshot) error {
 
 	for i, comment := range snapshot.Comments {
 		var message string
-		fmt.Printf("%s#%d %s <%s>\n\n",
+		env.out.Printf("%s#%d %s <%s>\n\n",
 			indent,
 			i,
 			comment.Author.DisplayName(),
@@ -155,7 +171,7 @@ func showDefaultFormatter(snapshot *bug.Snapshot) error {
 			message = comment.Message
 		}
 
-		fmt.Printf("%s%s\n\n\n",
+		env.out.Printf("%s%s\n\n\n",
 			indent,
 			message,
 		)
@@ -194,7 +210,7 @@ func NewJSONComment(comment bug.Comment) JSONComment {
 	}
 }
 
-func showJsonFormatter(snapshot *bug.Snapshot) error {
+func showJsonFormatter(env *Env, snapshot *bug.Snapshot) error {
 	jsonBug := JSONBugSnapshot{
 		Id:         snapshot.Id().String(),
 		HumanId:    snapshot.Id().Human(),
@@ -222,28 +238,28 @@ func showJsonFormatter(snapshot *bug.Snapshot) error {
 	}
 
 	jsonObject, _ := json.MarshalIndent(jsonBug, "", "    ")
-	fmt.Printf("%s\n", jsonObject)
+	env.out.Printf("%s\n", jsonObject)
 
 	return nil
 }
 
-func showOrgmodeFormatter(snapshot *bug.Snapshot) error {
+func showOrgModeFormatter(env *Env, snapshot *bug.Snapshot) error {
 	// Header
-	fmt.Printf("%s [%s] %s\n",
+	env.out.Printf("%s [%s] %s\n",
 		snapshot.Id().Human(),
 		snapshot.Status,
 		snapshot.Title,
 	)
 
-	fmt.Printf("* Author: %s\n",
+	env.out.Printf("* Author: %s\n",
 		snapshot.Author.DisplayName(),
 	)
 
-	fmt.Printf("* Creation Time: %s\n",
+	env.out.Printf("* Creation Time: %s\n",
 		snapshot.CreateTime.String(),
 	)
 
-	fmt.Printf("* Last Edit: %s\n",
+	env.out.Printf("* Last Edit: %s\n",
 		snapshot.EditTime().String(),
 	)
 
@@ -253,9 +269,9 @@ func showOrgmodeFormatter(snapshot *bug.Snapshot) error {
 		labels[i] = string(label)
 	}
 
-	fmt.Printf("* Labels:\n")
+	env.out.Printf("* Labels:\n")
 	if len(labels) > 0 {
-		fmt.Printf("** %s\n",
+		env.out.Printf("** %s\n",
 			strings.Join(labels, "\n** "),
 		)
 	}
@@ -269,7 +285,7 @@ func showOrgmodeFormatter(snapshot *bug.Snapshot) error {
 		)
 	}
 
-	fmt.Printf("* Actors:\n** %s\n",
+	env.out.Printf("* Actors:\n** %s\n",
 		strings.Join(actors, "\n** "),
 	)
 
@@ -282,18 +298,16 @@ func showOrgmodeFormatter(snapshot *bug.Snapshot) error {
 		)
 	}
 
-	fmt.Printf("* Participants:\n** %s\n",
+	env.out.Printf("* Participants:\n** %s\n",
 		strings.Join(participants, "\n** "),
 	)
 
-	fmt.Printf("* Comments:\n")
+	env.out.Printf("* Comments:\n")
 
 	for i, comment := range snapshot.Comments {
 		var message string
-		fmt.Printf("** #%d %s\n",
-			i,
-			comment.Author.DisplayName(),
-		)
+		env.out.Printf("** #%d %s\n",
+			i, comment.Author.DisplayName())
 
 		if comment.Message == "" {
 			message = "No description provided."
@@ -301,25 +315,8 @@ func showOrgmodeFormatter(snapshot *bug.Snapshot) error {
 			message = strings.ReplaceAll(comment.Message, "\n", "\n: ")
 		}
 
-		fmt.Printf(": %s\n",
-			message,
-		)
+		env.out.Printf(": %s\n", message)
 	}
 
 	return nil
 }
-
-var showCmd = &cobra.Command{
-	Use:     "show [<id>]",
-	Short:   "Display the details of a bug.",
-	PreRunE: loadRepo,
-	RunE:    runShowBug,
-}
-
-func init() {
-	RootCmd.AddCommand(showCmd)
-	showCmd.Flags().StringVarP(&showFieldsQuery, "field", "", "",
-		"Select field to display. Valid values are [author,authorEmail,createTime,lastEdit,humanId,id,labels,shortId,status,title,actors,participants]")
-	showCmd.Flags().StringVarP(&showOutputFormat, "format", "f", "default",
-		"Select the output formatting style. Valid values are [default,json,org-mode]")
-}

commands/status.go 🔗

@@ -1,41 +1,39 @@
 package commands
 
 import (
-	"fmt"
+	"github.com/spf13/cobra"
 
-	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/commands/select"
-	"github.com/MichaelMure/git-bug/util/interrupt"
-	"github.com/spf13/cobra"
 )
 
-func runStatus(cmd *cobra.Command, args []string) error {
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
+func newStatusCommand() *cobra.Command {
+	env := newEnv()
+
+	cmd := &cobra.Command{
+		Use:      "status [<id>]",
+		Short:    "Display or change a bug status.",
+		PreRunE:  loadBackend(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runStatus(env, args)
+		},
 	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
 
-	b, args, err := _select.ResolveBug(backend, args)
+	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()
 
-	fmt.Println(snap.Status)
+	env.out.Println(snap.Status)
 
 	return nil
 }
-
-var statusCmd = &cobra.Command{
-	Use:     "status [<id>]",
-	Short:   "Display or change a bug status.",
-	PreRunE: loadRepo,
-	RunE:    runStatus,
-}
-
-func init() {
-	RootCmd.AddCommand(statusCmd)
-}

commands/status_close.go 🔗

@@ -1,21 +1,29 @@
 package commands
 
 import (
-	"github.com/MichaelMure/git-bug/cache"
-	"github.com/MichaelMure/git-bug/commands/select"
-	"github.com/MichaelMure/git-bug/util/interrupt"
 	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/commands/select"
 )
 
-func runStatusClose(cmd *cobra.Command, args []string) error {
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
+func newStatusCloseCommand() *cobra.Command {
+	env := newEnv()
+
+	cmd := &cobra.Command{
+		Use:      "close [<id>]",
+		Short:    "Mark a bug as closed.",
+		PreRunE:  loadBackendEnsureUser(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runStatusClose(env, args)
+		},
 	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
 
-	b, args, err := _select.ResolveBug(backend, args)
+	return cmd
+}
+
+func runStatusClose(env *Env, args []string) error {
+	b, args, err := _select.ResolveBug(env.backend, args)
 	if err != nil {
 		return err
 	}
@@ -27,14 +35,3 @@ func runStatusClose(cmd *cobra.Command, args []string) error {
 
 	return b.Commit()
 }
-
-var closeCmd = &cobra.Command{
-	Use:     "close [<id>]",
-	Short:   "Mark a bug as closed.",
-	PreRunE: loadRepoEnsureUser,
-	RunE:    runStatusClose,
-}
-
-func init() {
-	statusCmd.AddCommand(closeCmd)
-}

commands/status_open.go 🔗

@@ -1,21 +1,29 @@
 package commands
 
 import (
-	"github.com/MichaelMure/git-bug/cache"
-	"github.com/MichaelMure/git-bug/commands/select"
-	"github.com/MichaelMure/git-bug/util/interrupt"
 	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/commands/select"
 )
 
-func runStatusOpen(cmd *cobra.Command, args []string) error {
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
+func newStatusOpenCommand() *cobra.Command {
+	env := newEnv()
+
+	cmd := &cobra.Command{
+		Use:      "open [<id>]",
+		Short:    "Mark a bug as open.",
+		PreRunE:  loadBackendEnsureUser(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runStatusOpen(env, args)
+		},
 	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
 
-	b, args, err := _select.ResolveBug(backend, args)
+	return cmd
+}
+
+func runStatusOpen(env *Env, args []string) error {
+	b, args, err := _select.ResolveBug(env.backend, args)
 	if err != nil {
 		return err
 	}
@@ -27,14 +35,3 @@ func runStatusOpen(cmd *cobra.Command, args []string) error {
 
 	return b.Commit()
 }
-
-var openCmd = &cobra.Command{
-	Use:     "open [<id>]",
-	Short:   "Mark a bug as open.",
-	PreRunE: loadRepoEnsureUser,
-	RunE:    runStatusOpen,
-}
-
-func init() {
-	statusCmd.AddCommand(openCmd)
-}

commands/termui.go 🔗

@@ -1,31 +1,28 @@
 package commands
 
 import (
-	"github.com/MichaelMure/git-bug/cache"
-	"github.com/MichaelMure/git-bug/termui"
-	"github.com/MichaelMure/git-bug/util/interrupt"
 	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/termui"
 )
 
-func runTermUI(cmd *cobra.Command, args []string) error {
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
-	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
+func newTermUICommand() *cobra.Command {
+	env := newEnv()
 
-	return termui.Run(backend)
-}
+	cmd := &cobra.Command{
+		Use:      "termui",
+		Aliases:  []string{"tui"},
+		Short:    "Launch the terminal UI.",
+		PreRunE:  loadBackendEnsureUser(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runTermUI(env)
+		},
+	}
 
-var termUICmd = &cobra.Command{
-	Use:     "termui",
-	Aliases: []string{"tui"},
-	Short:   "Launch the terminal UI.",
-	PreRunE: loadRepoEnsureUser,
-	RunE:    runTermUI,
+	return cmd
 }
 
-func init() {
-	RootCmd.AddCommand(termUICmd)
+func runTermUI(env *Env) error {
+	return termui.Run(env.backend)
 }

commands/title.go 🔗

@@ -1,43 +1,38 @@
 package commands
 
 import (
-	"fmt"
+	"github.com/spf13/cobra"
 
-	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/commands/select"
-	"github.com/MichaelMure/git-bug/util/interrupt"
-	"github.com/spf13/cobra"
 )
 
-func runTitle(cmd *cobra.Command, args []string) error {
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
+func newTitleCommand() *cobra.Command {
+	env := newEnv()
+
+	cmd := &cobra.Command{
+		Use:      "title [<id>]",
+		Short:    "Display or change a title of a bug.",
+		PreRunE:  loadBackend(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runTitle(env, args)
+		},
 	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
 
-	b, args, err := _select.ResolveBug(backend, args)
+	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()
 
-	fmt.Println(snap.Title)
+	env.out.Println(snap.Title)
 
 	return nil
 }
-
-var titleCmd = &cobra.Command{
-	Use:     "title [<id>]",
-	Short:   "Display or change a title of a bug.",
-	PreRunE: loadRepo,
-	RunE:    runTitle,
-}
-
-func init() {
-	RootCmd.AddCommand(titleCmd)
-
-	titleCmd.Flags().SortFlags = false
-}

commands/title_edit.go 🔗

@@ -1,38 +1,52 @@
 package commands
 
 import (
-	"fmt"
+	"github.com/spf13/cobra"
 
-	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/commands/select"
 	"github.com/MichaelMure/git-bug/input"
-	"github.com/MichaelMure/git-bug/util/interrupt"
-	"github.com/spf13/cobra"
 )
 
-var (
-	titleEditTitle string
-)
+type titleEditOptions struct {
+	title string
+}
 
-func runTitleEdit(cmd *cobra.Command, args []string) error {
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
+func newTitleEditCommand() *cobra.Command {
+	env := newEnv()
+	options := titleEditOptions{}
+
+	cmd := &cobra.Command{
+		Use:      "edit [<id>]",
+		Short:    "Edit a title of a bug.",
+		PreRunE:  loadBackendEnsureUser(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runTitleEdit(env, options, args)
+		},
 	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
 
-	b, args, err := _select.ResolveBug(backend, args)
+	flags := cmd.Flags()
+	flags.SortFlags = false
+
+	flags.StringVarP(&options.title, "title", "t", "",
+		"Provide a title to describe the issue",
+	)
+
+	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 titleEditTitle == "" {
-		titleEditTitle, err = input.BugTitleEditorInput(repo, snap.Title)
+	if opts.title == "" {
+		opts.title, err = input.BugTitleEditorInput(env.repo, snap.Title)
 		if err == input.ErrEmptyTitle {
-			fmt.Println("Empty title, aborting.")
+			env.out.Println("Empty title, aborting.")
 			return nil
 		}
 		if err != nil {
@@ -40,31 +54,14 @@ func runTitleEdit(cmd *cobra.Command, args []string) error {
 		}
 	}
 
-	if titleEditTitle == snap.Title {
-		fmt.Println("No change, aborting.")
+	if opts.title == snap.Title {
+		env.err.Println("No change, aborting.")
 	}
 
-	_, err = b.SetTitle(titleEditTitle)
+	_, err = b.SetTitle(opts.title)
 	if err != nil {
 		return err
 	}
 
 	return b.Commit()
 }
-
-var titleEditCmd = &cobra.Command{
-	Use:     "edit [<id>]",
-	Short:   "Edit a title of a bug.",
-	PreRunE: loadRepoEnsureUser,
-	RunE:    runTitleEdit,
-}
-
-func init() {
-	titleCmd.AddCommand(titleEditCmd)
-
-	titleEditCmd.Flags().SortFlags = false
-
-	titleEditCmd.Flags().StringVarP(&titleEditTitle, "title", "t", "",
-		"Provide a title to describe the issue",
-	)
-}

commands/user.go 🔗

@@ -7,92 +7,97 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/MichaelMure/git-bug/cache"
-	"github.com/MichaelMure/git-bug/util/interrupt"
 )
 
-var (
-	userFieldsQuery string
-)
+type userOptions struct {
+	fieldsQuery string
+}
 
-func runUser(cmd *cobra.Command, args []string) error {
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
+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),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runUser(env, options, args)
+		},
 	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
 
+	cmd.AddCommand(newUserAdoptCommand())
+	cmd.AddCommand(newUserCreateCommand())
+	cmd.AddCommand(newUserLsCommand())
+
+	flags := cmd.Flags()
+	flags.SortFlags = false
+
+	flags.StringVarP(&options.fieldsQuery, "field", "f", "",
+		"Select field to display. Valid values are [email,humanId,id,lastModification,lastModificationLamport,login,metadata,name]")
+
+	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 = backend.ResolveIdentityPrefix(args[0])
+		id, err = env.backend.ResolveIdentityPrefix(args[0])
 	} else {
-		id, err = backend.GetUserIdentity()
+		id, err = env.backend.GetUserIdentity()
 	}
 
 	if err != nil {
 		return err
 	}
 
-	if userFieldsQuery != "" {
-		switch userFieldsQuery {
+	if opts.fieldsQuery != "" {
+		switch opts.fieldsQuery {
 		case "email":
-			fmt.Printf("%s\n", id.Email())
+			env.out.Printf("%s\n", id.Email())
 		case "login":
-			fmt.Printf("%s\n", id.Login())
+			env.out.Printf("%s\n", id.Login())
 		case "humanId":
-			fmt.Printf("%s\n", id.Id().Human())
+			env.out.Printf("%s\n", id.Id().Human())
 		case "id":
-			fmt.Printf("%s\n", id.Id())
+			env.out.Printf("%s\n", id.Id())
 		case "lastModification":
-			fmt.Printf("%s\n", id.LastModification().
+			env.out.Printf("%s\n", id.LastModification().
 				Time().Format("Mon Jan 2 15:04:05 2006 +0200"))
 		case "lastModificationLamport":
-			fmt.Printf("%d\n", id.LastModificationLamport())
+			env.out.Printf("%d\n", id.LastModificationLamport())
 		case "metadata":
 			for key, value := range id.ImmutableMetadata() {
-				fmt.Printf("%s\n%s\n", key, value)
+				env.out.Printf("%s\n%s\n", key, value)
 			}
 		case "name":
-			fmt.Printf("%s\n", id.Name())
+			env.out.Printf("%s\n", id.Name())
 
 		default:
-			return fmt.Errorf("\nUnsupported field: %s\n", userFieldsQuery)
+			return fmt.Errorf("\nUnsupported field: %s\n", opts.fieldsQuery)
 		}
 
 		return nil
 	}
 
-	fmt.Printf("Id: %s\n", id.Id())
-	fmt.Printf("Name: %s\n", id.Name())
-	fmt.Printf("Email: %s\n", id.Email())
-	fmt.Printf("Login: %s\n", id.Login())
-	fmt.Printf("Last modification: %s (lamport %d)\n",
+	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 (lamport %d)\n",
 		id.LastModification().Time().Format("Mon Jan 2 15:04:05 2006 +0200"),
 		id.LastModificationLamport())
-	fmt.Println("Metadata:")
+	env.out.Println("Metadata:")
 	for key, value := range id.ImmutableMetadata() {
-		fmt.Printf("    %s --> %s\n", key, value)
+		env.out.Printf("    %s --> %s\n", key, value)
 	}
-	// fmt.Printf("Protected: %v\n", id.IsProtected())
+	// env.out.Printf("Protected: %v\n", id.IsProtected())
 
 	return nil
 }
-
-var userCmd = &cobra.Command{
-	Use:     "user [<user-id>]",
-	Short:   "Display or change the user identity.",
-	PreRunE: loadRepoEnsureUser,
-	RunE:    runUser,
-}
-
-func init() {
-	RootCmd.AddCommand(userCmd)
-	userCmd.Flags().SortFlags = false
-
-	userCmd.Flags().StringVarP(&userFieldsQuery, "field", "f", "",
-		"Select field to display. Valid values are [email,humanId,id,lastModification,lastModificationLamport,login,metadata,name]")
-}

commands/user_adopt.go 🔗

@@ -1,49 +1,40 @@
 package commands
 
 import (
-	"fmt"
-	"os"
-
 	"github.com/spf13/cobra"
-
-	"github.com/MichaelMure/git-bug/cache"
-	"github.com/MichaelMure/git-bug/util/interrupt"
 )
 
-func runUserAdopt(cmd *cobra.Command, args []string) error {
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
+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),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runUserAdopt(env, args)
+		},
 	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
 
+	return cmd
+}
+
+func runUserAdopt(env *Env, args []string) error {
 	prefix := args[0]
 
-	i, err := backend.ResolveIdentityPrefix(prefix)
+	i, err := env.backend.ResolveIdentityPrefix(prefix)
 	if err != nil {
 		return err
 	}
 
-	err = backend.SetUserIdentity(i)
+	err = env.backend.SetUserIdentity(i)
 	if err != nil {
 		return err
 	}
 
-	_, _ = fmt.Fprintf(os.Stderr, "Your identity is now: %s\n", i.DisplayName())
+	env.out.Printf("Your identity is now: %s\n", i.DisplayName())
 
 	return nil
 }
-
-var userAdoptCmd = &cobra.Command{
-	Use:     "adopt <user-id>",
-	Short:   "Adopt an existing identity as your own.",
-	PreRunE: loadRepo,
-	RunE:    runUserAdopt,
-	Args:    cobra.ExactArgs(1),
-}
-
-func init() {
-	userCmd.AddCommand(userAdoptCmd)
-	userAdoptCmd.Flags().SortFlags = false
-}

commands/user_create.go 🔗

@@ -1,24 +1,29 @@
 package commands
 
 import (
-	"fmt"
-	"os"
+	"github.com/spf13/cobra"
 
-	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/input"
-	"github.com/MichaelMure/git-bug/util/interrupt"
-	"github.com/spf13/cobra"
 )
 
-func runUserCreate(cmd *cobra.Command, args []string) error {
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
+func newUserCreateCommand() *cobra.Command {
+	env := newEnv()
+
+	cmd := &cobra.Command{
+		Use:      "create",
+		Short:    "Create a new identity.",
+		PreRunE:  loadBackend(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runUserCreate(env)
+		},
 	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
 
-	preName, err := backend.GetUserName()
+	return cmd
+}
+
+func runUserCreate(env *Env) error {
+	preName, err := env.backend.GetUserName()
 	if err != nil {
 		return err
 	}
@@ -28,7 +33,7 @@ func runUserCreate(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	preEmail, err := backend.GetUserEmail()
+	preEmail, err := env.backend.GetUserEmail()
 	if err != nil {
 		return err
 	}
@@ -43,7 +48,7 @@ func runUserCreate(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	id, err := backend.NewIdentityRaw(name, email, "", avatarURL, nil)
+	id, err := env.backend.NewIdentityRaw(name, email, "", avatarURL, nil)
 	if err != nil {
 		return err
 	}
@@ -53,32 +58,20 @@ func runUserCreate(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	set, err := backend.IsUserIdentitySet()
+	set, err := env.backend.IsUserIdentitySet()
 	if err != nil {
 		return err
 	}
 
 	if !set {
-		err = backend.SetUserIdentity(id)
+		err = env.backend.SetUserIdentity(id)
 		if err != nil {
 			return err
 		}
 	}
 
-	_, _ = fmt.Fprintln(os.Stderr)
-	fmt.Println(id.Id())
+	env.err.Println()
+	env.out.Println(id.Id())
 
 	return nil
 }
-
-var userCreateCmd = &cobra.Command{
-	Use:     "create",
-	Short:   "Create a new identity.",
-	PreRunE: loadRepo,
-	RunE:    runUserCreate,
-}
-
-func init() {
-	userCmd.AddCommand(userCreateCmd)
-	userCreateCmd.Flags().SortFlags = false
-}

commands/user_ls.go 🔗

@@ -8,44 +8,59 @@ import (
 
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/util/colors"
-	"github.com/MichaelMure/git-bug/util/interrupt"
 )
 
-var (
-	userLsOutputFormat string
-)
+type userLsOptions struct {
+	format string
+}
 
-func runUserLs(_ *cobra.Command, _ []string) error {
-	backend, err := cache.NewRepoCache(repo)
-	if err != nil {
-		return err
+func newUserLsCommand() *cobra.Command {
+	env := newEnv()
+	options := userLsOptions{}
+
+	cmd := &cobra.Command{
+		Use:      "ls",
+		Short:    "List identities.",
+		PreRunE:  loadBackend(env),
+		PostRunE: closeBackend(env),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runUserLs(env, options)
+		},
 	}
-	defer backend.Close()
-	interrupt.RegisterCleaner(backend.Close)
 
-	ids := backend.AllIdentityIds()
+	flags := cmd.Flags()
+	flags.SortFlags = false
+
+	flags.StringVarP(&options.format, "format", "f", "default",
+		"Select the output formatting style. Valid values are [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 := backend.ResolveIdentityExcerpt(id)
+		user, err := env.backend.ResolveIdentityExcerpt(id)
 		if err != nil {
 			return err
 		}
 		users = append(users, user)
 	}
 
-	switch userLsOutputFormat {
+	switch opts.format {
 	case "json":
-		return userLsJsonFormatter(users)
+		return userLsJsonFormatter(env, users)
 	case "default":
-		return userLsDefaultFormatter(users)
+		return userLsDefaultFormatter(env, users)
 	default:
-		return fmt.Errorf("unknown format %s", userLsOutputFormat)
+		return fmt.Errorf("unknown format %s", opts.format)
 	}
 }
 
-func userLsDefaultFormatter(users []*cache.IdentityExcerpt) error {
+func userLsDefaultFormatter(env *Env, users []*cache.IdentityExcerpt) error {
 	for _, user := range users {
-		fmt.Printf("%s %s\n",
+		env.out.Printf("%s %s\n",
 			colors.Cyan(user.Id.Human()),
 			user.DisplayName(),
 		)
@@ -54,27 +69,13 @@ func userLsDefaultFormatter(users []*cache.IdentityExcerpt) error {
 	return nil
 }
 
-func userLsJsonFormatter(users []*cache.IdentityExcerpt) error {
+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, "", "    ")
-	fmt.Printf("%s\n", jsonObject)
+	env.out.Printf("%s\n", jsonObject)
 	return nil
 }
-
-var userLsCmd = &cobra.Command{
-	Use:     "ls",
-	Short:   "List identities.",
-	PreRunE: loadRepo,
-	RunE:    runUserLs,
-}
-
-func init() {
-	userCmd.AddCommand(userLsCmd)
-	userLsCmd.Flags().SortFlags = false
-	userLsCmd.Flags().StringVarP(&userLsOutputFormat, "format", "f", "default",
-		"Select the output formatting style. Valid values are [default,json]")
-}

commands/version.go 🔗

@@ -1,71 +1,62 @@
 package commands
 
 import (
-	"fmt"
 	"runtime"
 
 	"github.com/spf13/cobra"
 )
 
-// These variables are initialized externally during the build. See the Makefile.
-var GitCommit string
-var GitLastTag string
-var GitExactTag string
+type versionOptions struct {
+	number bool
+	commit bool
+	all    bool
+}
 
-var (
-	versionNumber bool
-	versionCommit bool
-	versionAll    bool
-)
+func newVersionCommand() *cobra.Command {
+	env := newEnv()
+	options := versionOptions{}
 
-func runVersionCmd(cmd *cobra.Command, args []string) {
-	if versionAll {
-		fmt.Printf("%s version: %s\n", rootCommandName, RootCmd.Version)
-		fmt.Printf("System version: %s/%s\n", runtime.GOARCH, runtime.GOOS)
-		fmt.Printf("Golang version: %s\n", runtime.Version())
-		return
+	cmd := &cobra.Command{
+		Use:   "version",
+		Short: "Show git-bug version information.",
+		Run: func(cmd *cobra.Command, args []string) {
+			runVersion(env, options, cmd.Root())
+		},
 	}
 
-	if versionNumber {
-		fmt.Println(RootCmd.Version)
-		return
-	}
+	flags := cmd.Flags()
+	flags.SortFlags = false
 
-	if versionCommit {
-		fmt.Println(GitCommit)
-		return
-	}
-
-	fmt.Printf("%s version: %s\n", rootCommandName, RootCmd.Version)
-}
+	flags.BoolVarP(&options.number, "number", "n", false,
+		"Only show the version number",
+	)
+	flags.BoolVarP(&options.commit, "commit", "c", false,
+		"Only show the commit hash",
+	)
+	flags.BoolVarP(&options.all, "all", "a", false,
+		"Show all version information",
+	)
 
-var versionCmd = &cobra.Command{
-	Use:   "version",
-	Short: "Show git-bug version information.",
-	Run:   runVersionCmd,
+	return cmd
 }
 
-func init() {
-	if GitExactTag == "undefined" {
-		GitExactTag = ""
+func runVersion(env *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())
+		return
 	}
 
-	RootCmd.Version = GitLastTag
-
-	if GitExactTag == "" {
-		RootCmd.Version = fmt.Sprintf("%s-dev-%.10s", RootCmd.Version, GitCommit)
+	if opts.number {
+		env.out.Println(root.Version)
+		return
 	}
 
-	RootCmd.AddCommand(versionCmd)
-	versionCmd.Flags().SortFlags = false
+	if opts.commit {
+		env.out.Println(GitCommit)
+		return
+	}
 
-	versionCmd.Flags().BoolVarP(&versionNumber, "number", "n", false,
-		"Only show the version number",
-	)
-	versionCmd.Flags().BoolVarP(&versionCommit, "commit", "c", false,
-		"Only show the commit hash",
-	)
-	versionCmd.Flags().BoolVarP(&versionAll, "all", "a", false,
-		"Show all version information",
-	)
+	env.out.Printf("%s version: %s\n", rootCommandName, root.Version)
 }

commands/webui.go 🔗

@@ -24,25 +24,54 @@ import (
 	"github.com/MichaelMure/git-bug/webui"
 )
 
-var (
-	webUIPort     int
-	webUIOpen     bool
-	webUINoOpen   bool
-	webUIReadOnly bool
-)
-
 const webUIOpenConfigKey = "git-bug.webui.open"
 
-func runWebUI(cmd *cobra.Command, args []string) error {
-	if webUIPort == 0 {
+type webUIOptions struct {
+	port     int
+	open     bool
+	noOpen   bool
+	readOnly bool
+}
+
+func newWebUICommand() *cobra.Command {
+	env := newEnv()
+	options := webUIOptions{}
+
+	cmd := &cobra.Command{
+		Use:   "webui",
+		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),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runWebUI(env, options, args)
+		},
+	}
+
+	flags := cmd.Flags()
+	flags.SortFlags = false
+
+	flags.BoolVar(&options.open, "open", false, "Automatically open the web UI in the default browser")
+	flags.BoolVar(&options.noOpen, "no-open", false, "Prevent the automatic opening of the web UI in the default browser")
+	flags.IntVarP(&options.port, "port", "p", 0, "Port to listen to (default is random)")
+	flags.BoolVar(&options.readOnly, "read-only", false, "Whether to run the web UI in read-only mode")
+
+	return cmd
+}
+
+func runWebUI(env *Env, opts webUIOptions, args []string) error {
+	if opts.port == 0 {
 		var err error
-		webUIPort, err = freeport.GetFreePort()
+		opts.port, err = freeport.GetFreePort()
 		if err != nil {
 			return err
 		}
 	}
 
-	addr := fmt.Sprintf("127.0.0.1:%d", webUIPort)
+	addr := fmt.Sprintf("127.0.0.1:%d", opts.port)
 	webUiAddr := fmt.Sprintf("http://%s", addr)
 
 	router := mux.NewRouter()
@@ -50,8 +79,8 @@ func runWebUI(cmd *cobra.Command, args []string) error {
 	// If the webUI is not read-only, use an authentication middleware with a
 	// fixed identity: the default user of the repo
 	// TODO: support dynamic authentication with OAuth
-	if !webUIReadOnly {
-		author, err := identity.GetUserIdentity(repo)
+	if !opts.readOnly {
+		author, err := identity.GetUserIdentity(env.repo)
 		if err != nil {
 			return err
 		}
@@ -59,7 +88,7 @@ func runWebUI(cmd *cobra.Command, args []string) error {
 	}
 
 	mrc := cache.NewMultiRepoCache()
-	_, err := mrc.RegisterDefaultRepository(repo)
+	_, err := mrc.RegisterDefaultRepository(env.repo)
 	if err != nil {
 		return err
 	}
@@ -86,7 +115,7 @@ func runWebUI(cmd *cobra.Command, args []string) error {
 
 	go func() {
 		<-quit
-		fmt.Println("WebUI is shutting down...")
+		env.out.Println("WebUI is shutting down...")
 
 		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
 		defer cancel()
@@ -99,18 +128,18 @@ func runWebUI(cmd *cobra.Command, args []string) error {
 		// Teardown
 		err := graphqlHandler.Close()
 		if err != nil {
-			fmt.Println(err)
+			env.out.Println(err)
 		}
 
 		close(done)
 	}()
 
-	fmt.Printf("Web UI: %s\n", webUiAddr)
-	fmt.Printf("Graphql API: http://%s/graphql\n", addr)
-	fmt.Printf("Graphql Playground: http://%s/playground\n", addr)
-	fmt.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 := repo.LocalConfig().ReadBool(webUIOpenConfigKey)
+	configOpen, err := env.repo.LocalConfig().ReadBool(webUIOpenConfigKey)
 	if err == repository.ErrNoConfigEntry {
 		// default to true
 		configOpen = true
@@ -118,12 +147,12 @@ func runWebUI(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	shouldOpen := (configOpen && !webUINoOpen) || webUIOpen
+	shouldOpen := (configOpen && !opts.noOpen) || opts.open
 
 	if shouldOpen {
 		err = open.Run(webUiAddr)
 		if err != nil {
-			fmt.Println(err)
+			env.out.Println(err)
 		}
 	}
 
@@ -134,29 +163,6 @@ func runWebUI(cmd *cobra.Command, args []string) error {
 
 	<-done
 
-	fmt.Println("WebUI stopped")
+	env.out.Println("WebUI stopped")
 	return nil
 }
-
-var webUICmd = &cobra.Command{
-	Use:   "webui",
-	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,
-	RunE:    runWebUI,
-}
-
-func init() {
-	RootCmd.AddCommand(webUICmd)
-
-	webUICmd.Flags().SortFlags = false
-
-	webUICmd.Flags().BoolVar(&webUIOpen, "open", false, "Automatically open the web UI in the default browser")
-	webUICmd.Flags().BoolVar(&webUINoOpen, "no-open", false, "Prevent the automatic opening of the web UI in the default browser")
-	webUICmd.Flags().IntVarP(&webUIPort, "port", "p", 0, "Port to listen to (default is random)")
-	webUICmd.Flags().BoolVar(&webUIReadOnly, "read-only", false, "Whether to run the web UI in read-only mode")
-}

doc/gen_docs.go 🔗

@@ -62,7 +62,7 @@ func genManPage() error {
 		}
 	}
 
-	return doc.GenManTree(commands.RootCmd, header, dir)
+	return doc.GenManTree(commands.NewRootCommand(), header, dir)
 }
 
 func genMarkdown() error {
@@ -79,5 +79,5 @@ func genMarkdown() error {
 		}
 	}
 
-	return doc.GenMarkdownTree(commands.RootCmd, dir)
+	return doc.GenMarkdownTree(commands.NewRootCommand(), dir)
 }

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

@@ -18,10 +18,6 @@ Pull updates.
 
 
 .SH OPTIONS
-.PP
-\fB\-h\fP, \fB\-\-help\fP[=false]
-	help for pull
-
 .PP
 \fB\-n\fP, \fB\-\-no\-resume\fP[=false]
 	force importing all bugs
@@ -30,6 +26,10 @@ Pull updates.
 \fB\-s\fP, \fB\-\-since\fP=""
 	import only bugs updated after the given date (ex: "200h" or "june 2 2019")
 
+.PP
+\fB\-h\fP, \fB\-\-help\fP[=false]
+	help for pull
+
 
 .SH SEE ALSO
 .PP

doc/md/git-bug_bridge_pull.md 🔗

@@ -13,9 +13,9 @@ git-bug bridge pull [<name>] [flags]
 ### Options
 
 ```
-  -h, --help           help for pull
   -n, --no-resume      force importing all bugs
   -s, --since string   import only bugs updated after the given date (ex: "200h" or "june 2 2019")
+  -h, --help           help for pull
 ```
 
 ### SEE ALSO

go.sum 🔗

@@ -28,6 +28,7 @@ github.com/awesome-gocui/gocui v0.6.1-0.20191115151952-a34ffb055986/go.mod h1:1Q
 github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc h1:wGNpKcHU8Aadr9yOzsT3GEsFLS7HQu8HxQIomnekqf0=
 github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc/go.mod h1:tOy3o5Nf1bA17mnK4W41gD7PS3u4Cv0P0pqFcoWMy8s=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
@@ -36,11 +37,15 @@ github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9 h1:a1zrFsLFac2xoM
 github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA=
+github.com/coreos/bbolt v1.3.2 h1:wZwiHHUieZCquLkDL0B8UhzreNWsPHooDAG3q34zk0s=
 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04=
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8=
 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg=
 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
 github.com/corpix/uarand v0.1.1 h1:RMr1TWc9F4n5jiPDzFHtmaUXLKLNUFK0SgCLo4BhX/U=
 github.com/corpix/uarand v0.1.1/go.mod h1:SFKZvkcRoLqVRFZ4u25xPmp6m9ktANfbpXZ7SJ0/FNU=
@@ -139,6 +144,7 @@ github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuuj
 github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
 github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
@@ -160,12 +166,16 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
 github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
 github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=
 github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=

misc/gen_completion.go 🔗

@@ -39,23 +39,23 @@ func main() {
 func genBash() error {
 	cwd, _ := os.Getwd()
 	dir := path.Join(cwd, "misc", "bash_completion", "git-bug")
-	return commands.RootCmd.GenBashCompletionFile(dir)
+	return commands.NewRootCommand().GenBashCompletionFile(dir)
 }
 
 func genFish() error {
 	cwd, _ := os.Getwd()
 	dir := path.Join(cwd, "misc", "fish_completion", "git-bug")
-	return commands.RootCmd.GenFishCompletionFile(dir, true)
+	return commands.NewRootCommand().GenFishCompletionFile(dir, true)
 }
 
 func genPowerShell() error {
 	cwd, _ := os.Getwd()
 	filepath := path.Join(cwd, "misc", "powershell_completion", "git-bug")
-	return commands.RootCmd.GenPowerShellCompletionFile(filepath)
+	return commands.NewRootCommand().GenPowerShellCompletionFile(filepath)
 }
 
 func genZsh() error {
 	cwd, _ := os.Getwd()
 	filepath := path.Join(cwd, "misc", "zsh_completion", "git-bug")
-	return commands.RootCmd.GenZshCompletionFile(filepath)
+	return commands.NewRootCommand().GenZshCompletionFile(filepath)
 }