diff options
Diffstat (limited to 'commands')
87 files changed, 1813 insertions, 1722 deletions
diff --git a/commands/add_test.go b/commands/add_test.go deleted file mode 100644 index 077995a6..00000000 --- a/commands/add_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package commands - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/require" -) - -func newTestEnvAndBug(t *testing.T) (*testEnv, string) { - t.Helper() - - testEnv, _ := newTestEnvAndUser(t) - opts := addOptions{ - title: "this is a bug title", - message: "this is a bug message", - messageFile: "", - nonInteractive: true, - } - - require.NoError(t, runAdd(testEnv.env, opts)) - require.Regexp(t, "^[0-9A-Fa-f]{7} created\n$", testEnv.out) - bugID := strings.Split(testEnv.out.String(), " ")[0] - testEnv.out.Reset() - - return testEnv, bugID -} - -func TestAdd(t *testing.T) { - _, bugID := newTestEnvAndBug(t) - require.Regexp(t, "^[0-9A-Fa-f]{7}$", bugID) -} diff --git a/commands/bridge.go b/commands/bridge.go deleted file mode 100644 index 8ce35aa3..00000000 --- a/commands/bridge.go +++ /dev/null @@ -1,42 +0,0 @@ -package commands - -import ( - "github.com/spf13/cobra" - - "github.com/MichaelMure/git-bug/bridge" -) - -func newBridgeCommand() *cobra.Command { - env := newEnv() - - cmd := &cobra.Command{ - Use: "bridge", - Short: "Configure and use bridges to other bug trackers.", - PreRunE: loadBackend(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runBridge(env) - }), - Args: cobra.NoArgs, - } - - cmd.AddCommand(newBridgeAuthCommand()) - cmd.AddCommand(newBridgeConfigureCommand()) - cmd.AddCommand(newBridgePullCommand()) - cmd.AddCommand(newBridgePushCommand()) - cmd.AddCommand(newBridgeRm()) - - return cmd -} - -func runBridge(env *Env) error { - configured, err := bridge.ConfiguredBridges(env.backend) - if err != nil { - return err - } - - for _, c := range configured { - env.out.Println(c) - } - - return nil -} diff --git a/commands/bridge/bridge.go b/commands/bridge/bridge.go new file mode 100644 index 00000000..980a38e2 --- /dev/null +++ b/commands/bridge/bridge.go @@ -0,0 +1,43 @@ +package bridgecmd + +import ( + "github.com/spf13/cobra" + + "github.com/MichaelMure/git-bug/bridge" + "github.com/MichaelMure/git-bug/commands/execenv" +) + +func NewBridgeCommand() *cobra.Command { + env := execenv.NewEnv() + + cmd := &cobra.Command{ + Use: "bridge", + Short: "List bridges to other bug trackers", + PreRunE: execenv.LoadBackend(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runBridge(env) + }), + Args: cobra.NoArgs, + } + + cmd.AddCommand(newBridgeAuthCommand()) + cmd.AddCommand(newBridgeNewCommand()) + cmd.AddCommand(newBridgePullCommand()) + cmd.AddCommand(newBridgePushCommand()) + cmd.AddCommand(newBridgeRm()) + + return cmd +} + +func runBridge(env *execenv.Env) error { + configured, err := bridge.ConfiguredBridges(env.Backend) + if err != nil { + return err + } + + for _, c := range configured { + env.Out.Println(c) + } + + return nil +} diff --git a/commands/bridge_auth.go b/commands/bridge/bridge_auth.go index 50306b8d..52e063e6 100644 --- a/commands/bridge_auth.go +++ b/commands/bridge/bridge_auth.go @@ -1,25 +1,25 @@ -package commands +package bridgecmd import ( "sort" "strings" - "github.com/spf13/cobra" - text "github.com/MichaelMure/go-term-text" + "github.com/spf13/cobra" "github.com/MichaelMure/git-bug/bridge/core/auth" + "github.com/MichaelMure/git-bug/commands/execenv" "github.com/MichaelMure/git-bug/util/colors" ) func newBridgeAuthCommand() *cobra.Command { - env := newEnv() + env := execenv.NewEnv() cmd := &cobra.Command{ Use: "auth", - Short: "List all known bridge authentication credentials.", - PreRunE: loadBackend(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { + Short: "List all known bridge authentication credentials", + PreRunE: execenv.LoadBackend(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { return runBridgeAuth(env) }), Args: cobra.NoArgs, @@ -32,8 +32,8 @@ func newBridgeAuthCommand() *cobra.Command { return cmd } -func runBridgeAuth(env *Env) error { - creds, err := auth.List(env.backend) +func runBridgeAuth(env *execenv.Env) error { + creds, err := auth.List(env.Backend) if err != nil { return err } @@ -54,7 +54,7 @@ func runBridgeAuth(env *Env) error { sort.Strings(meta) metaFmt := strings.Join(meta, ",") - env.out.Printf("%s %s %s %s %s\n", + env.Out.Printf("%s %s %s %s %s\n", colors.Cyan(cred.ID().Human()), colors.Yellow(targetFmt), colors.Magenta(cred.Kind()), diff --git a/commands/bridge_auth_addtoken.go b/commands/bridge/bridge_auth_addtoken.go index dfdc66b6..bcab7fc3 100644 --- a/commands/bridge_auth_addtoken.go +++ b/commands/bridge/bridge_auth_addtoken.go @@ -1,4 +1,4 @@ -package commands +package bridgecmd import ( "bufio" @@ -14,6 +14,8 @@ import ( "github.com/MichaelMure/git-bug/bridge/core" "github.com/MichaelMure/git-bug/bridge/core/auth" "github.com/MichaelMure/git-bug/cache" + "github.com/MichaelMure/git-bug/commands/completion" + "github.com/MichaelMure/git-bug/commands/execenv" ) type bridgeAuthAddTokenOptions struct { @@ -23,14 +25,14 @@ type bridgeAuthAddTokenOptions struct { } func newBridgeAuthAddTokenCommand() *cobra.Command { - env := newEnv() + env := execenv.NewEnv() options := bridgeAuthAddTokenOptions{} cmd := &cobra.Command{ Use: "add-token [TOKEN]", Short: "Store a new token", - PreRunE: loadBackendEnsureUser(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { + PreRunE: execenv.LoadBackendEnsureUser(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { return runBridgeAuthAddToken(env, options, args) }), Args: cobra.MaximumNArgs(1), @@ -41,17 +43,17 @@ func newBridgeAuthAddTokenCommand() *cobra.Command { flags.StringVarP(&options.target, "target", "t", "", fmt.Sprintf("The target of the bridge. Valid values are [%s]", strings.Join(bridge.Targets(), ","))) - cmd.RegisterFlagCompletionFunc("target", completeFrom(bridge.Targets())) + cmd.RegisterFlagCompletionFunc("target", completion.From(bridge.Targets())) flags.StringVarP(&options.login, "login", "l", "", "The login in the remote bug-tracker") flags.StringVarP(&options.user, "user", "u", "", "The user to add the token to. Default is the current user") - cmd.RegisterFlagCompletionFunc("user", completeUser(env)) + cmd.RegisterFlagCompletionFunc("user", completion.User(env)) return cmd } -func runBridgeAuthAddToken(env *Env, opts bridgeAuthAddTokenOptions, args []string) error { +func runBridgeAuthAddToken(env *execenv.Env, opts bridgeAuthAddTokenOptions, args []string) error { // Note: as bridgeAuthAddTokenLogin is not checked against the remote bug-tracker, // it's possible to register a credential with an incorrect login (including bad case). // The consequence is that it will not get picked later by the bridge. I find that @@ -76,7 +78,7 @@ func runBridgeAuthAddToken(env *Env, opts bridgeAuthAddTokenOptions, args []stri } else { // Read from Stdin if isatty.IsTerminal(os.Stdin.Fd()) { - env.err.Println("Enter the token:") + env.Err.Println("Enter the token:") } reader := bufio.NewReader(os.Stdin) raw, err := reader.ReadString('\n') @@ -90,9 +92,9 @@ func runBridgeAuthAddToken(env *Env, opts bridgeAuthAddTokenOptions, args []stri var err error if opts.user == "" { - user, err = env.backend.GetUserIdentity() + user, err = env.Backend.GetUserIdentity() } else { - user, err = env.backend.ResolveIdentityPrefix(opts.user) + user, err = env.Backend.ResolveIdentityPrefix(opts.user) } if err != nil { return err @@ -121,11 +123,11 @@ func runBridgeAuthAddToken(env *Env, opts bridgeAuthAddTokenOptions, args []stri return errors.Wrap(err, "invalid token") } - err = auth.Store(env.repo, token) + err = auth.Store(env.Repo, token) if err != nil { return err } - env.out.Printf("token %s added\n", token.ID()) + env.Out.Printf("token %s added\n", token.ID()) return nil } diff --git a/commands/bridge/bridge_auth_rm.go b/commands/bridge/bridge_auth_rm.go new file mode 100644 index 00000000..d58ca63e --- /dev/null +++ b/commands/bridge/bridge_auth_rm.go @@ -0,0 +1,41 @@ +package bridgecmd + +import ( + "github.com/spf13/cobra" + + "github.com/MichaelMure/git-bug/bridge/core/auth" + "github.com/MichaelMure/git-bug/commands/completion" + "github.com/MichaelMure/git-bug/commands/execenv" +) + +func newBridgeAuthRm() *cobra.Command { + env := execenv.NewEnv() + + cmd := &cobra.Command{ + Use: "rm BRIDGE_ID", + Short: "Remove a credential", + PreRunE: execenv.LoadRepo(env), + RunE: func(cmd *cobra.Command, args []string) error { + return runBridgeAuthRm(env, args) + }, + Args: cobra.ExactArgs(1), + ValidArgsFunction: completion.BridgeAuth(env), + } + + return cmd +} + +func runBridgeAuthRm(env *execenv.Env, args []string) error { + cred, err := auth.LoadWithPrefix(env.Repo, args[0]) + if err != nil { + return err + } + + err = auth.Remove(env.Repo, cred.ID()) + if err != nil { + return err + } + + env.Out.Printf("credential %s removed\n", cred.ID()) + return nil +} diff --git a/commands/bridge/bridge_auth_show.go b/commands/bridge/bridge_auth_show.go new file mode 100644 index 00000000..d373273d --- /dev/null +++ b/commands/bridge/bridge_auth_show.go @@ -0,0 +1,60 @@ +package bridgecmd + +import ( + "fmt" + "sort" + "strings" + "time" + + "github.com/spf13/cobra" + + "github.com/MichaelMure/git-bug/bridge/core/auth" + "github.com/MichaelMure/git-bug/commands/completion" + "github.com/MichaelMure/git-bug/commands/execenv" +) + +func newBridgeAuthShow() *cobra.Command { + env := execenv.NewEnv() + + cmd := &cobra.Command{ + Use: "show", + Short: "Display an authentication credential", + PreRunE: execenv.LoadBackend(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runBridgeAuthShow(env, args) + }), + Args: cobra.ExactArgs(1), + ValidArgsFunction: completion.BridgeAuth(env), + } + + return cmd +} + +func runBridgeAuthShow(env *execenv.Env, args []string) error { + cred, err := auth.LoadWithPrefix(env.Repo, args[0]) + if err != nil { + return err + } + + env.Out.Printf("Id: %s\n", cred.ID()) + env.Out.Printf("Target: %s\n", cred.Target()) + env.Out.Printf("Kind: %s\n", cred.Kind()) + env.Out.Printf("Creation: %s\n", cred.CreateTime().Format(time.RFC822)) + + switch cred := cred.(type) { + case *auth.Token: + env.Out.Printf("Value: %s\n", cred.Value) + } + + env.Out.Println("Metadata:") + + meta := make([]string, 0, len(cred.Metadata())) + for key, value := range cred.Metadata() { + meta = append(meta, fmt.Sprintf(" %s --> %s\n", key, value)) + } + sort.Strings(meta) + + env.Out.Print(strings.Join(meta, "")) + + return nil +} diff --git a/commands/bridge_configure.go b/commands/bridge/bridge_new.go index d5b40dfd..4cfc903d 100644 --- a/commands/bridge_configure.go +++ b/commands/bridge/bridge_new.go @@ -1,4 +1,4 @@ -package commands +package bridgecmd import ( "bufio" @@ -12,10 +12,12 @@ import ( "github.com/MichaelMure/git-bug/bridge" "github.com/MichaelMure/git-bug/bridge/core" "github.com/MichaelMure/git-bug/bridge/core/auth" + "github.com/MichaelMure/git-bug/commands/completion" + "github.com/MichaelMure/git-bug/commands/execenv" "github.com/MichaelMure/git-bug/repository" ) -type bridgeConfigureOptions struct { +type bridgeNewOptions struct { name string target string params core.BridgeParams @@ -24,13 +26,13 @@ type bridgeConfigureOptions struct { nonInteractive bool } -func newBridgeConfigureCommand() *cobra.Command { - env := newEnv() - options := bridgeConfigureOptions{} +func newBridgeNewCommand() *cobra.Command { + env := execenv.NewEnv() + options := bridgeNewOptions{} cmd := &cobra.Command{ - Use: "configure", - Short: "Configure a new bridge.", + Use: "new", + Short: "Configure a new bridge", Long: ` Configure a new bridge by passing flags or/and using interactive terminal prompts. You can avoid all the terminal prompts by passing all the necessary flags to configure your bridge.`, Example: `# Interactive example [1]: github @@ -66,7 +68,7 @@ Enter token: 87cf5c03b64029f18ea5f9ca5679daa08ccbd700 Successfully configured bridge: default # For GitHub -git bug bridge configure \ +git bug bridge new \ --name=default \ --target=github \ --owner=$(OWNER) \ @@ -74,20 +76,20 @@ git bug bridge configure \ --token=$(TOKEN) # For Launchpad -git bug bridge configure \ +git bug bridge new \ --name=default \ --target=launchpad-preview \ --url=https://bugs.launchpad.net/ubuntu/ # For Gitlab -git bug bridge configure \ +git bug bridge new \ --name=default \ --target=github \ --url=https://github.com/michaelmure/git-bug \ --token=$(TOKEN)`, - PreRunE: loadBackend(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runBridgeConfigure(env, options) + PreRunE: execenv.LoadBackend(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runBridgeNew(env, options) }), } @@ -97,7 +99,7 @@ git bug bridge configure \ flags.StringVarP(&options.name, "name", "n", "", "A distinctive name to identify the bridge") flags.StringVarP(&options.target, "target", "t", "", fmt.Sprintf("The target of the bridge. Valid values are [%s]", strings.Join(bridge.Targets(), ","))) - cmd.RegisterFlagCompletionFunc("target", completeFrom(bridge.Targets())) + cmd.RegisterFlagCompletionFunc("target", completion.From(bridge.Targets())) flags.StringVarP(&options.params.URL, "url", "u", "", "The URL of the remote repository") flags.StringVarP(&options.params.BaseURL, "base-url", "b", "", "The base URL of your remote issue tracker") flags.StringVarP(&options.params.Login, "login", "l", "", "The login on your remote issue tracker") @@ -111,7 +113,7 @@ git bug bridge configure \ return cmd } -func runBridgeConfigure(env *Env, opts bridgeConfigureOptions) error { +func runBridgeNew(env *execenv.Env, opts bridgeNewOptions) error { var err error if (opts.tokenStdin || opts.token != "" || opts.params.CredPrefix != "") && @@ -121,7 +123,7 @@ func runBridgeConfigure(env *Env, opts bridgeConfigureOptions) error { // early fail if opts.params.CredPrefix != "" { - if _, err := auth.LoadWithPrefix(env.repo, opts.params.CredPrefix); err != nil { + if _, err := auth.LoadWithPrefix(env.Repo, opts.params.CredPrefix); err != nil { return err } } @@ -146,13 +148,13 @@ func runBridgeConfigure(env *Env, opts bridgeConfigureOptions) error { } if !opts.nonInteractive && opts.name == "" { - opts.name, err = promptName(env.repo) + opts.name, err = promptName(env.Repo) if err != nil { return err } } - b, err := bridge.NewBridge(env.backend, opts.target, opts.name) + b, err := bridge.NewBridge(env.Backend, opts.target, opts.name) if err != nil { return err } @@ -162,7 +164,7 @@ func runBridgeConfigure(env *Env, opts bridgeConfigureOptions) error { return err } - env.out.Printf("Successfully configured bridge: %s\n", opts.name) + env.Out.Printf("Successfully configured bridge: %s\n", opts.name) return nil } diff --git a/commands/bridge_pull.go b/commands/bridge/bridge_pull.go index 9370e088..d1fc279a 100644 --- a/commands/bridge_pull.go +++ b/commands/bridge/bridge_pull.go @@ -1,4 +1,4 @@ -package commands +package bridgecmd import ( "context" @@ -13,6 +13,8 @@ import ( "github.com/MichaelMure/git-bug/bridge" "github.com/MichaelMure/git-bug/bridge/core" + "github.com/MichaelMure/git-bug/commands/completion" + "github.com/MichaelMure/git-bug/commands/execenv" "github.com/MichaelMure/git-bug/util/interrupt" ) @@ -22,18 +24,18 @@ type bridgePullOptions struct { } func newBridgePullCommand() *cobra.Command { - env := newEnv() + env := execenv.NewEnv() options := bridgePullOptions{} cmd := &cobra.Command{ Use: "pull [NAME]", - Short: "Pull updates.", - PreRunE: loadBackend(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { + Short: "Pull updates from a remote bug tracker", + PreRunE: execenv.LoadBackend(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { return runBridgePull(env, options, args) }), Args: cobra.MaximumNArgs(1), - ValidArgsFunction: completeBridge(env), + ValidArgsFunction: completion.Bridge(env), } flags := cmd.Flags() @@ -45,7 +47,7 @@ func newBridgePullCommand() *cobra.Command { return cmd } -func runBridgePull(env *Env, opts bridgePullOptions, args []string) error { +func runBridgePull(env *execenv.Env, opts bridgePullOptions, args []string) error { if opts.noResume && opts.importSince != "" { return fmt.Errorf("only one of --no-resume and --since flags should be used") } @@ -54,9 +56,9 @@ func runBridgePull(env *Env, opts bridgePullOptions, args []string) error { var err error if len(args) == 0 { - b, err = bridge.DefaultBridge(env.backend) + b, err = bridge.DefaultBridge(env.Backend) } else { - b, err = bridge.LoadBridge(env.backend, args[0]) + b, err = bridge.LoadBridge(env.Backend, args[0]) } if err != nil { @@ -75,14 +77,14 @@ func runBridgePull(env *Env, opts bridgePullOptions, args []string) error { interrupt.RegisterCleaner(func() error { mu.Lock() if interruptCount > 0 { - env.err.Println("Received another interrupt before graceful stop, terminating...") + env.Err.Println("Received another interrupt before graceful stop, terminating...") os.Exit(0) } interruptCount++ mu.Unlock() - env.err.Println("Received interrupt signal, stopping the import...\n(Hit ctrl-c again to kill the process.)") + env.Err.Println("Received interrupt signal, stopping the import...\n(Hit ctrl-c again to kill the process.)") // send signal to stop the importer cancel() @@ -119,23 +121,23 @@ func runBridgePull(env *Env, opts bridgePullOptions, args []string) error { case core.ImportEventBug: importedIssues++ - env.out.Println(result.String()) + env.Out.Println(result.String()) case core.ImportEventIdentity: importedIdentities++ - env.out.Println(result.String()) + env.Out.Println(result.String()) case core.ImportEventError: if result.Err != context.Canceled { - env.out.Println(result.String()) + env.Out.Println(result.String()) } default: - env.out.Println(result.String()) + env.Out.Println(result.String()) } } - env.out.Printf("imported %d issues and %d identities with %s bridge\n", importedIssues, importedIdentities, b.Name) + env.Out.Printf("imported %d issues and %d identities with %s bridge\n", importedIssues, importedIdentities, b.Name) // send done signal close(done) diff --git a/commands/bridge_push.go b/commands/bridge/bridge_push.go index ef1f2d3e..51baed4d 100644 --- a/commands/bridge_push.go +++ b/commands/bridge/bridge_push.go @@ -1,4 +1,4 @@ -package commands +package bridgecmd import ( "context" @@ -10,34 +10,36 @@ import ( "github.com/MichaelMure/git-bug/bridge" "github.com/MichaelMure/git-bug/bridge/core" + "github.com/MichaelMure/git-bug/commands/completion" + "github.com/MichaelMure/git-bug/commands/execenv" "github.com/MichaelMure/git-bug/util/interrupt" ) func newBridgePushCommand() *cobra.Command { - env := newEnv() + env := execenv.NewEnv() cmd := &cobra.Command{ Use: "push [NAME]", - Short: "Push updates.", - PreRunE: loadBackendEnsureUser(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { + Short: "Push updates to remote bug tracker", + PreRunE: execenv.LoadBackendEnsureUser(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { return runBridgePush(env, args) }), Args: cobra.MaximumNArgs(1), - ValidArgsFunction: completeBridge(env), + ValidArgsFunction: completion.Bridge(env), } return cmd } -func runBridgePush(env *Env, args []string) error { +func runBridgePush(env *execenv.Env, args []string) error { var b *core.Bridge var err error if len(args) == 0 { - b, err = bridge.DefaultBridge(env.backend) + b, err = bridge.DefaultBridge(env.Backend) } else { - b, err = bridge.LoadBridge(env.backend, args[0]) + b, err = bridge.LoadBridge(env.Backend, args[0]) } if err != nil { @@ -55,14 +57,14 @@ func runBridgePush(env *Env, args []string) error { interrupt.RegisterCleaner(func() error { mu.Lock() if interruptCount > 0 { - env.err.Println("Received another interrupt before graceful stop, terminating...") + env.Err.Println("Received another interrupt before graceful stop, terminating...") os.Exit(0) } interruptCount++ mu.Unlock() - env.err.Println("Received interrupt signal, stopping the import...\n(Hit ctrl-c again to kill the process.)") + env.Err.Println("Received interrupt signal, stopping the import...\n(Hit ctrl-c again to kill the process.)") // send signal to stop the importer cancel() @@ -80,7 +82,7 @@ func runBridgePush(env *Env, args []string) error { exportedIssues := 0 for result := range events { if result.Event != core.ExportEventNothing { - env.out.Println(result.String()) + env.Out.Println(result.String()) } switch result.Event { @@ -89,7 +91,7 @@ func runBridgePush(env *Env, args []string) error { } } - env.out.Printf("exported %d issues with %s bridge\n", exportedIssues, b.Name) + env.Out.Printf("exported %d issues with %s bridge\n", exportedIssues, b.Name) // send done signal close(done) diff --git a/commands/bridge/bridge_rm.go b/commands/bridge/bridge_rm.go new file mode 100644 index 00000000..5d8d23c5 --- /dev/null +++ b/commands/bridge/bridge_rm.go @@ -0,0 +1,36 @@ +package bridgecmd + +import ( + "github.com/spf13/cobra" + + "github.com/MichaelMure/git-bug/bridge" + "github.com/MichaelMure/git-bug/commands/completion" + "github.com/MichaelMure/git-bug/commands/execenv" +) + +func newBridgeRm() *cobra.Command { + env := execenv.NewEnv() + + cmd := &cobra.Command{ + Use: "rm NAME", + Short: "Delete a configured bridge", + PreRunE: execenv.LoadBackend(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runBridgeRm(env, args) + }), + Args: cobra.ExactArgs(1), + ValidArgsFunction: completion.Bridge(env), + } + + return cmd +} + +func runBridgeRm(env *execenv.Env, args []string) error { + err := bridge.RemoveBridge(env.Backend, args[0]) + if err != nil { + return err + } + + env.Out.Printf("Successfully removed bridge configuration %v\n", args[0]) + return nil +} diff --git a/commands/bridge_auth_rm.go b/commands/bridge_auth_rm.go deleted file mode 100644 index a28057de..00000000 --- a/commands/bridge_auth_rm.go +++ /dev/null @@ -1,39 +0,0 @@ -package commands - -import ( - "github.com/spf13/cobra" - - "github.com/MichaelMure/git-bug/bridge/core/auth" -) - -func newBridgeAuthRm() *cobra.Command { - env := newEnv() - - cmd := &cobra.Command{ - Use: "rm ID", - Short: "Remove a credential.", - PreRunE: loadRepo(env), - RunE: func(cmd *cobra.Command, args []string) error { - return runBridgeAuthRm(env, args) - }, - Args: cobra.ExactArgs(1), - ValidArgsFunction: completeBridgeAuth(env), - } - - return cmd -} - -func runBridgeAuthRm(env *Env, args []string) error { - cred, err := auth.LoadWithPrefix(env.repo, args[0]) - if err != nil { - return err - } - - err = auth.Remove(env.repo, cred.ID()) - if err != nil { - return err - } - - env.out.Printf("credential %s removed\n", cred.ID()) - return nil -} diff --git a/commands/bridge_auth_show.go b/commands/bridge_auth_show.go deleted file mode 100644 index 7233bb51..00000000 --- a/commands/bridge_auth_show.go +++ /dev/null @@ -1,58 +0,0 @@ -package commands - -import ( - "fmt" - "sort" - "strings" - "time" - - "github.com/spf13/cobra" - - "github.com/MichaelMure/git-bug/bridge/core/auth" -) - -func newBridgeAuthShow() *cobra.Command { - env := newEnv() - - cmd := &cobra.Command{ - Use: "show", - Short: "Display an authentication credential.", - PreRunE: loadBackend(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runBridgeAuthShow(env, args) - }), - Args: cobra.ExactArgs(1), - ValidArgsFunction: completeBridgeAuth(env), - } - - return cmd -} - -func runBridgeAuthShow(env *Env, args []string) error { - cred, err := auth.LoadWithPrefix(env.repo, args[0]) - if err != nil { - return err - } - - env.out.Printf("Id: %s\n", cred.ID()) - env.out.Printf("Target: %s\n", cred.Target()) - env.out.Printf("Kind: %s\n", cred.Kind()) - env.out.Printf("Creation: %s\n", cred.CreateTime().Format(time.RFC822)) - - switch cred := cred.(type) { - case *auth.Token: - env.out.Printf("Value: %s\n", cred.Value) - } - - env.out.Println("Metadata:") - - meta := make([]string, 0, len(cred.Metadata())) - for key, value := range cred.Metadata() { - meta = append(meta, fmt.Sprintf(" %s --> %s\n", key, value)) - } - sort.Strings(meta) - - env.out.Print(strings.Join(meta, "")) - - return nil -} diff --git a/commands/bridge_rm.go b/commands/bridge_rm.go deleted file mode 100644 index 0306944e..00000000 --- a/commands/bridge_rm.go +++ /dev/null @@ -1,34 +0,0 @@ -package commands - -import ( - "github.com/spf13/cobra" - - "github.com/MichaelMure/git-bug/bridge" -) - -func newBridgeRm() *cobra.Command { - env := newEnv() - - cmd := &cobra.Command{ - Use: "rm NAME", - Short: "Delete a configured bridge.", - PreRunE: loadBackend(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runBridgeRm(env, args) - }), - Args: cobra.ExactArgs(1), - ValidArgsFunction: completeBridge(env), - } - - return cmd -} - -func runBridgeRm(env *Env, args []string) error { - err := bridge.RemoveBridge(env.backend, args[0]) - if err != nil { - return err - } - - env.out.Printf("Successfully removed bridge configuration %v\n", args[0]) - return nil -} diff --git a/commands/ls.go b/commands/bug/bug.go index 7ed897fa..04bf8980 100644 --- a/commands/ls.go +++ b/commands/bug/bug.go @@ -1,4 +1,4 @@ -package commands +package bugcmd import ( "encoding/json" @@ -11,13 +11,16 @@ import ( "github.com/spf13/cobra" "github.com/MichaelMure/git-bug/cache" + "github.com/MichaelMure/git-bug/commands/cmdjson" + "github.com/MichaelMure/git-bug/commands/completion" + "github.com/MichaelMure/git-bug/commands/execenv" "github.com/MichaelMure/git-bug/entities/bug" "github.com/MichaelMure/git-bug/entities/common" "github.com/MichaelMure/git-bug/query" "github.com/MichaelMure/git-bug/util/colors" ) -type lsOptions struct { +type bugOptions struct { statusQuery []string authorQuery []string metadataQuery []string @@ -31,33 +34,33 @@ type lsOptions struct { outputFormat string } -func newLsCommand() *cobra.Command { - env := newEnv() - options := lsOptions{} +func NewBugCommand() *cobra.Command { + env := execenv.NewEnv() + options := bugOptions{} cmd := &cobra.Command{ - Use: "ls [QUERY]", - Short: "List bugs.", + Use: "bug [QUERY]", + Short: "List bugs", Long: `Display a summary of each bugs. You can pass an additional query to filter and order the list. This query can be expressed either with a simple query language, flags, a natural language full text search, or a combination of the aforementioned.`, Example: `List open bugs sorted by last edition with a query: -git bug ls status:open sort:edit-desc +git bug status:open sort:edit-desc List closed bugs sorted by creation with flags: -git bug ls --status closed --by creation +git bug --status closed --by creation Do a full text search of all bugs: -git bug ls "foo bar" baz +git bug "foo bar" baz Use queries, flags, and full text search: -git bug ls status:open --by creation "foo bar" baz +git bug status:open --by creation "foo bar" baz `, - PreRunE: loadBackend(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runLs(env, options, args) + PreRunE: execenv.LoadBackend(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runBug(env, options, args) }), - ValidArgsFunction: completeLs(env), + ValidArgsFunction: completion.Ls(env), } flags := cmd.Flags() @@ -65,41 +68,60 @@ git bug ls status:open --by creation "foo bar" baz flags.StringSliceVarP(&options.statusQuery, "status", "s", nil, "Filter by status. Valid values are [open,closed]") - cmd.RegisterFlagCompletionFunc("status", completeFrom([]string{"open", "closed"})) + cmd.RegisterFlagCompletionFunc("status", completion.From([]string{"open", "closed"})) flags.StringSliceVarP(&options.authorQuery, "author", "a", nil, "Filter by author") flags.StringSliceVarP(&options.metadataQuery, "metadata", "m", nil, "Filter by metadata. Example: github-url=URL") - cmd.RegisterFlagCompletionFunc("author", completeUserForQuery(env)) + cmd.RegisterFlagCompletionFunc("author", completion.UserForQuery(env)) flags.StringSliceVarP(&options.participantQuery, "participant", "p", nil, "Filter by participant") - cmd.RegisterFlagCompletionFunc("participant", completeUserForQuery(env)) + cmd.RegisterFlagCompletionFunc("participant", completion.UserForQuery(env)) flags.StringSliceVarP(&options.actorQuery, "actor", "A", nil, "Filter by actor") - cmd.RegisterFlagCompletionFunc("actor", completeUserForQuery(env)) + cmd.RegisterFlagCompletionFunc("actor", completion.UserForQuery(env)) flags.StringSliceVarP(&options.labelQuery, "label", "l", nil, "Filter by label") - cmd.RegisterFlagCompletionFunc("label", completeLabel(env)) + cmd.RegisterFlagCompletionFunc("label", completion.Label(env)) flags.StringSliceVarP(&options.titleQuery, "title", "t", nil, "Filter by title") flags.StringSliceVarP(&options.noQuery, "no", "n", nil, "Filter by absence of something. Valid values are [label]") - cmd.RegisterFlagCompletionFunc("no", completeLabel(env)) + cmd.RegisterFlagCompletionFunc("no", completion.Label(env)) flags.StringVarP(&options.sortBy, "by", "b", "creation", "Sort the results by a characteristic. Valid values are [id,creation,edit]") - cmd.RegisterFlagCompletionFunc("by", completeFrom([]string{"id", "creation", "edit"})) + cmd.RegisterFlagCompletionFunc("by", completion.From([]string{"id", "creation", "edit"})) flags.StringVarP(&options.sortDirection, "direction", "d", "asc", "Select the sorting direction. Valid values are [asc,desc]") - cmd.RegisterFlagCompletionFunc("direction", completeFrom([]string{"asc", "desc"})) + cmd.RegisterFlagCompletionFunc("direction", completion.From([]string{"asc", "desc"})) flags.StringVarP(&options.outputFormat, "format", "f", "default", "Select the output formatting style. Valid values are [default,plain,compact,id,json,org-mode]") cmd.RegisterFlagCompletionFunc("format", - completeFrom([]string{"default", "plain", "compact", "id", "json", "org-mode"})) + completion.From([]string{"default", "plain", "compact", "id", "json", "org-mode"})) + + const selectGroup = "select" + cmd.AddGroup(&cobra.Group{ID: selectGroup, Title: "Implicit selection"}) + + addCmdWithGroup := func(child *cobra.Command, groupID string) { + cmd.AddCommand(child) + child.GroupID = groupID + } + + addCmdWithGroup(newBugDeselectCommand(), selectGroup) + addCmdWithGroup(newBugSelectCommand(), selectGroup) + + cmd.AddCommand(newBugCommentCommand()) + cmd.AddCommand(newBugLabelCommand()) + cmd.AddCommand(newBugNewCommand()) + cmd.AddCommand(newBugRmCommand()) + cmd.AddCommand(newBugShowCommand()) + cmd.AddCommand(newBugStatusCommand()) + cmd.AddCommand(newBugTitleCommand()) return cmd } -func runLs(env *Env, opts lsOptions, args []string) error { +func runBug(env *execenv.Env, opts bugOptions, args []string) error { var q *query.Query var err error @@ -120,14 +142,14 @@ func runLs(env *Env, opts lsOptions, args []string) error { return err } - allIds, err := env.backend.QueryBugs(q) + allIds, err := env.Backend.QueryBugs(q) if err != nil { return err } bugExcerpt := make([]*cache.BugExcerpt, len(allIds)) for i, id := range allIds { - b, err := env.backend.ResolveBugExcerpt(id) + b, err := env.Backend.ResolveBugExcerpt(id) if err != nil { return err } @@ -136,17 +158,17 @@ func runLs(env *Env, opts lsOptions, args []string) error { switch opts.outputFormat { case "org-mode": - return lsOrgmodeFormatter(env, bugExcerpt) + return bugsOrgmodeFormatter(env, bugExcerpt) case "plain": - return lsPlainFormatter(env, bugExcerpt) + return bugsPlainFormatter(env, bugExcerpt) case "json": - return lsJsonFormatter(env, bugExcerpt) + return bugsJsonFormatter(env, bugExcerpt) case "compact": - return lsCompactFormatter(env, bugExcerpt) + return bugsCompactFormatter(env, bugExcerpt) case "id": - return lsIDFormatter(env, bugExcerpt) + return bugsIDFormatter(env, bugExcerpt) case "default": - return lsDefaultFormatter(env, bugExcerpt) + return bugsDefaultFormatter(env, bugExcerpt) default: return fmt.Errorf("unknown format %s", opts.outputFormat) } @@ -166,30 +188,30 @@ func repairQuery(args []string) string { } type JSONBugExcerpt struct { - Id string `json:"id"` - HumanId string `json:"human_id"` - CreateTime JSONTime `json:"create_time"` - EditTime JSONTime `json:"edit_time"` - - Status string `json:"status"` - Labels []bug.Label `json:"labels"` - Title string `json:"title"` - Actors []JSONIdentity `json:"actors"` - Participants []JSONIdentity `json:"participants"` - Author JSONIdentity `json:"author"` + Id string `json:"id"` + HumanId string `json:"human_id"` + CreateTime cmdjson.Time `json:"create_time"` + EditTime cmdjson.Time `json:"edit_time"` + + Status string `json:"status"` + Labels []bug.Label `json:"labels"` + Title string `json:"title"` + Actors []cmdjson.Identity `json:"actors"` + Participants []cmdjson.Identity `json:"participants"` + Author cmdjson.Identity `json:"author"` Comments int `json:"comments"` Metadata map[string]string `json:"metadata"` } -func lsJsonFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error { +func bugsJsonFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error { jsonBugs := make([]JSONBugExcerpt, len(bugExcerpts)) for i, b := range bugExcerpts { jsonBug := JSONBugExcerpt{ Id: b.Id.String(), HumanId: b.Id.Human(), - CreateTime: NewJSONTime(b.CreateTime(), b.CreateLamportTime), - EditTime: NewJSONTime(b.EditTime(), b.EditLamportTime), + CreateTime: cmdjson.NewTime(b.CreateTime(), b.CreateLamportTime), + EditTime: cmdjson.NewTime(b.EditTime(), b.EditLamportTime), Status: b.Status.String(), Labels: b.Labels, Title: b.Title, @@ -197,40 +219,40 @@ func lsJsonFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error { Metadata: b.CreateMetadata, } - author, err := env.backend.ResolveIdentityExcerpt(b.AuthorId) + author, err := env.Backend.ResolveIdentityExcerpt(b.AuthorId) if err != nil { return err } - jsonBug.Author = NewJSONIdentityFromExcerpt(author) + jsonBug.Author = cmdjson.NewIdentityFromExcerpt(author) - jsonBug.Actors = make([]JSONIdentity, len(b.Actors)) + jsonBug.Actors = make([]cmdjson.Identity, len(b.Actors)) for i, element := range b.Actors { - actor, err := env.backend.ResolveIdentityExcerpt(element) + actor, err := env.Backend.ResolveIdentityExcerpt(element) if err != nil { return err } - jsonBug.Actors[i] = NewJSONIdentityFromExcerpt(actor) + jsonBug.Actors[i] = cmdjson.NewIdentityFromExcerpt(actor) } - jsonBug.Participants = make([]JSONIdentity, len(b.Participants)) + jsonBug.Participants = make([]cmdjson.Identity, len(b.Participants)) for i, element := range b.Participants { - participant, err := env.backend.ResolveIdentityExcerpt(element) + participant, err := env.Backend.ResolveIdentityExcerpt(element) if err != nil { return err } - jsonBug.Participants[i] = NewJSONIdentityFromExcerpt(participant) + jsonBug.Participants[i] = cmdjson.NewIdentityFromExcerpt(participant) } jsonBugs[i] = jsonBug } jsonObject, _ := json.MarshalIndent(jsonBugs, "", " ") - env.out.Printf("%s\n", jsonObject) + env.Out.Printf("%s\n", jsonObject) return nil } -func lsCompactFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error { +func bugsCompactFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error { for _, b := range bugExcerpts { - author, err := env.backend.ResolveIdentityExcerpt(b.AuthorId) + author, err := env.Backend.ResolveIdentityExcerpt(b.AuthorId) if err != nil { return err } @@ -243,7 +265,7 @@ func lsCompactFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error { labelsTxt.WriteString(lc256.Unescape()) } - env.out.Printf("%s %s %s %s %s\n", + env.Out.Printf("%s %s %s %s %s\n", colors.Cyan(b.Id.Human()), colors.Yellow(b.Status), text.LeftPadMaxLine(strings.TrimSpace(b.Title), 40, 0), @@ -254,17 +276,17 @@ func lsCompactFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error { return nil } -func lsIDFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error { +func bugsIDFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error { for _, b := range bugExcerpts { - env.out.Println(b.Id.String()) + env.Out.Println(b.Id.String()) } return nil } -func lsDefaultFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error { +func bugsDefaultFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error { for _, b := range bugExcerpts { - author, err := env.backend.ResolveIdentityExcerpt(b.AuthorId) + author, err := env.Backend.ResolveIdentityExcerpt(b.AuthorId) if err != nil { return err } @@ -290,7 +312,7 @@ func lsDefaultFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error { comments = " ∞ 💬" } - env.out.Printf("%s\t%s\t%s\t%s\t%s\n", + env.Out.Printf("%s\t%s\t%s\t%s\t%s\n", colors.Cyan(b.Id.Human()), colors.Yellow(b.Status), titleFmt+labelsFmt, @@ -301,14 +323,14 @@ func lsDefaultFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error { return nil } -func lsPlainFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error { +func bugsPlainFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error { for _, b := range bugExcerpts { - env.out.Printf("%s [%s] %s\n", b.Id.Human(), b.Status, strings.TrimSpace(b.Title)) + env.Out.Printf("%s [%s] %s\n", b.Id.Human(), b.Status, strings.TrimSpace(b.Title)) } return nil } -func lsOrgmodeFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error { +func bugsOrgmodeFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error { // see https://orgmode.org/manual/Tags.html orgTagRe := regexp.MustCompile("[^[:alpha:]_@]") formatTag := func(l bug.Label) string { @@ -319,7 +341,7 @@ func lsOrgmodeFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error { return time.Format("[2006-01-02 Mon 15:05]") } - env.out.Println("#+TODO: OPEN | CLOSED") + env.Out.Println("#+TODO: OPEN | CLOSED") for _, b := range bugExcerpts { status := strings.ToUpper(b.Status.String()) @@ -331,7 +353,7 @@ func lsOrgmodeFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error { title = b.Title } - author, err := env.backend.ResolveIdentityExcerpt(b.AuthorId) + author, err := env.Backend.ResolveIdentityExcerpt(b.AuthorId) if err != nil { return err } @@ -346,7 +368,7 @@ func lsOrgmodeFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error { } labels.WriteString(":") - env.out.Printf("* %-6s %s %s %s: %s %s\n", + env.Out.Printf("* %-6s %s %s %s: %s %s\n", status, b.Id.Human(), formatTime(b.CreateTime()), @@ -355,29 +377,29 @@ func lsOrgmodeFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error { labels.String(), ) - env.out.Printf("** Last Edited: %s\n", formatTime(b.EditTime())) + env.Out.Printf("** Last Edited: %s\n", formatTime(b.EditTime())) - env.out.Printf("** Actors:\n") + env.Out.Printf("** Actors:\n") for _, element := range b.Actors { - actor, err := env.backend.ResolveIdentityExcerpt(element) + actor, err := env.Backend.ResolveIdentityExcerpt(element) if err != nil { return err } - env.out.Printf(": %s %s\n", + env.Out.Printf(": %s %s\n", actor.Id.Human(), actor.DisplayName(), ) } - env.out.Printf("** Participants:\n") + env.Out.Printf("** Participants:\n") for _, element := range b.Participants { - participant, err := env.backend.ResolveIdentityExcerpt(element) + participant, err := env.Backend.ResolveIdentityExcerpt(element) if err != nil { return err } - env.out.Printf(": %s %s\n", + env.Out.Printf(": %s %s\n", participant.Id.Human(), participant.DisplayName(), ) @@ -388,7 +410,7 @@ func lsOrgmodeFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error { } // Finish the command flags transformation into the query.Query -func completeQuery(q *query.Query, opts lsOptions) error { +func completeQuery(q *query.Query, opts bugOptions) error { for _, str := range opts.statusQuery { status, err := common.StatusFromString(str) if err != nil { diff --git a/commands/bug/bug_comment.go b/commands/bug/bug_comment.go new file mode 100644 index 00000000..bc665f0d --- /dev/null +++ b/commands/bug/bug_comment.go @@ -0,0 +1,52 @@ +package bugcmd + +import ( + text "github.com/MichaelMure/go-term-text" + "github.com/spf13/cobra" + + "github.com/MichaelMure/git-bug/commands/bug/select" + "github.com/MichaelMure/git-bug/commands/completion" + "github.com/MichaelMure/git-bug/commands/execenv" + "github.com/MichaelMure/git-bug/util/colors" +) + +func newBugCommentCommand() *cobra.Command { + env := execenv.NewEnv() + + cmd := &cobra.Command{ + Use: "comment [BUG_ID]", + Short: "List a bug's comments", + PreRunE: execenv.LoadBackend(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runBugComment(env, args) + }), + ValidArgsFunction: completion.Bug(env), + } + + cmd.AddCommand(newBugCommentNewCommand()) + cmd.AddCommand(newBugCommentEditCommand()) + + return cmd +} + +func runBugComment(env *execenv.Env, args []string) error { + b, args, err := _select.ResolveBug(env.Backend, args) + if err != nil { + return err + } + + snap := b.Snapshot() + + for i, comment := range snap.Comments { + if i != 0 { + env.Out.Println() + } + + env.Out.Printf("Author: %s\n", colors.Magenta(comment.Author.DisplayName())) + env.Out.Printf("Id: %s\n", colors.Cyan(comment.CombinedId().Human())) + env.Out.Printf("Date: %s\n\n", comment.FormatTime()) + env.Out.Println(text.LeftPadLines(comment.Message, 4)) + } + + return nil +} diff --git a/commands/comment_add.go b/commands/bug/bug_comment_add.go index acac7994..b676db3a 100644 --- a/commands/comment_add.go +++ b/commands/bug/bug_comment_add.go @@ -1,31 +1,33 @@ -package commands +package bugcmd import ( "github.com/spf13/cobra" + "github.com/MichaelMure/git-bug/commands/bug/select" + "github.com/MichaelMure/git-bug/commands/completion" + "github.com/MichaelMure/git-bug/commands/execenv" "github.com/MichaelMure/git-bug/commands/input" - _select "github.com/MichaelMure/git-bug/commands/select" "github.com/MichaelMure/git-bug/util/text" ) -type commentAddOptions struct { +type bugCommentNewOptions struct { messageFile string message string nonInteractive bool } -func newCommentAddCommand() *cobra.Command { - env := newEnv() - options := commentAddOptions{} +func newBugCommentNewCommand() *cobra.Command { + env := execenv.NewEnv() + options := bugCommentNewOptions{} cmd := &cobra.Command{ - Use: "add [ID]", - Short: "Add a new comment to a bug.", - PreRunE: loadBackendEnsureUser(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runCommentAdd(env, options, args) + Use: "new [BUG_ID]", + Short: "Add a new comment to a bug", + PreRunE: execenv.LoadBackendEnsureUser(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runBugCommentNew(env, options, args) }), - ValidArgsFunction: completeBug(env), + ValidArgsFunction: completion.Bug(env), } flags := cmd.Flags() @@ -41,8 +43,8 @@ func newCommentAddCommand() *cobra.Command { return cmd } -func runCommentAdd(env *Env, opts commentAddOptions, args []string) error { - b, args, err := _select.ResolveBug(env.backend, args) +func runBugCommentNew(env *execenv.Env, opts bugCommentNewOptions, args []string) error { + b, args, err := _select.ResolveBug(env.Backend, args) if err != nil { return err } @@ -56,12 +58,12 @@ func runCommentAdd(env *Env, opts commentAddOptions, args []string) error { if opts.messageFile == "" && opts.message == "" { if opts.nonInteractive { - env.err.Println("No message given. Use -m or -F option to specify a message. Aborting.") + env.Err.Println("No message given. Use -m or -F option to specify a message. Aborting.") return nil } - opts.message, err = input.BugCommentEditorInput(env.backend, "") + opts.message, err = input.BugCommentEditorInput(env.Backend, "") if err == input.ErrEmptyMessage { - env.err.Println("Empty message, aborting.") + env.Err.Println("Empty message, aborting.") return nil } if err != nil { diff --git a/commands/bug/bug_comment_add_test.go b/commands/bug/bug_comment_add_test.go new file mode 100644 index 00000000..55e285f4 --- /dev/null +++ b/commands/bug/bug_comment_add_test.go @@ -0,0 +1,18 @@ +package bugcmd + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/MichaelMure/git-bug/commands/bug/testenv" +) + +func TestBugCommentNew(t *testing.T) { + const golden = "testdata/comment/add" + + env, bugID, _ := testenv.NewTestEnvAndBugWithComment(t) + + require.NoError(t, runBugComment(env, []string{bugID.String()})) + requireCommentsEqual(t, golden, env) +} diff --git a/commands/comment_edit.go b/commands/bug/bug_comment_edit.go index 91c6d809..8be7cb80 100644 --- a/commands/comment_edit.go +++ b/commands/bug/bug_comment_edit.go @@ -1,28 +1,29 @@ -package commands +package bugcmd import ( "github.com/spf13/cobra" + "github.com/MichaelMure/git-bug/commands/execenv" "github.com/MichaelMure/git-bug/commands/input" ) -type commentEditOptions struct { +type bugCommentEditOptions struct { messageFile string message string nonInteractive bool } -func newCommentEditCommand() *cobra.Command { - env := newEnv() - options := commentEditOptions{} +func newBugCommentEditCommand() *cobra.Command { + env := execenv.NewEnv() + options := bugCommentEditOptions{} cmd := &cobra.Command{ Use: "edit [COMMENT_ID]", - Short: "Edit an existing comment on a bug.", + Short: "Edit an existing comment on a bug", Args: cobra.ExactArgs(1), - PreRunE: loadBackendEnsureUser(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runCommentEdit(env, options, args) + PreRunE: execenv.LoadBackendEnsureUser(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runBugCommentEdit(env, options, args) }), } @@ -39,8 +40,8 @@ func newCommentEditCommand() *cobra.Command { return cmd } -func runCommentEdit(env *Env, opts commentEditOptions, args []string) error { - b, commentId, err := env.backend.ResolveComment(args[0]) +func runBugCommentEdit(env *execenv.Env, opts bugCommentEditOptions, args []string) error { + b, commentId, err := env.Backend.ResolveComment(args[0]) if err != nil { return err } @@ -54,12 +55,12 @@ func runCommentEdit(env *Env, opts commentEditOptions, args []string) error { if opts.messageFile == "" && opts.message == "" { if opts.nonInteractive { - env.err.Println("No message given. Use -m or -F option to specify a message. Aborting.") + env.Err.Println("No message given. Use -m or -F option to specify a message. Aborting.") return nil } - opts.message, err = input.BugCommentEditorInput(env.backend, "") + opts.message, err = input.BugCommentEditorInput(env.Backend, "") if err == input.ErrEmptyMessage { - env.err.Println("Empty message, aborting.") + env.Err.Println("Empty message, aborting.") return nil } if err != nil { diff --git a/commands/bug/bug_comment_edit_test.go b/commands/bug/bug_comment_edit_test.go new file mode 100644 index 00000000..9e110a3b --- /dev/null +++ b/commands/bug/bug_comment_edit_test.go @@ -0,0 +1,23 @@ +package bugcmd + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/MichaelMure/git-bug/commands/bug/testenv" +) + +func TestBugCommentEdit(t *testing.T) { + const golden = "testdata/comment/edit" + + env, bugID, commentID := testenv.NewTestEnvAndBugWithComment(t) + + opts := bugCommentEditOptions{ + message: "this is an altered bug comment", + } + require.NoError(t, runBugCommentEdit(env, opts, []string{commentID.Human()})) + + require.NoError(t, runBugComment(env, []string{bugID.Human()})) + requireCommentsEqual(t, golden, env) +} diff --git a/commands/comment_test.go b/commands/bug/bug_comment_test.go index 43062ed0..c1dc9952 100644 --- a/commands/comment_test.go +++ b/commands/bug/bug_comment_test.go @@ -1,4 +1,4 @@ -package commands +package bugcmd import ( "fmt" @@ -8,14 +8,18 @@ import ( "time" "github.com/stretchr/testify/require" + + "github.com/MichaelMure/git-bug/commands/bug/testenv" + "github.com/MichaelMure/git-bug/commands/cmdtest" + "github.com/MichaelMure/git-bug/commands/execenv" ) -func TestComment(t *testing.T) { +func TestBugComment(t *testing.T) { const golden = "testdata/comment/message-only" - env, bug := newTestEnvAndBug(t) + env, bug := testenv.NewTestEnvAndBug(t) - require.NoError(t, runComment(env.env, []string{bug})) + require.NoError(t, runBugComment(env, []string{bug.Human()})) requireCommentsEqual(t, golden, env) } @@ -37,7 +41,7 @@ type commentParser struct { comments []parsedComment } -func parseComments(t *testing.T, env *testEnv) []parsedComment { +func parseComments(t *testing.T, env *execenv.Env) []parsedComment { t.Helper() parser := &commentParser{ @@ -48,7 +52,7 @@ func parseComments(t *testing.T, env *testEnv) []parsedComment { comment := &parsedComment{} parser.fn = parser.parseAuthor - for _, line := range strings.Split(env.out.String(), "\n") { + for _, line := range strings.Split(env.Out.String(), "\n") { parser.fn(comment, line) } @@ -116,7 +120,7 @@ func normalizeParsedComments(t *testing.T, comments []parsedComment) []parsedCom date, err := time.Parse(gitDateFormat, "Fri Aug 19 07:00:00 2022 +1900") require.NoError(t, err) - out := []parsedComment{} + var out []parsedComment for i, comment := range comments { comment.id = fmt.Sprintf("%7x", prefix+i) @@ -127,18 +131,18 @@ func normalizeParsedComments(t *testing.T, comments []parsedComment) []parsedCom return out } -func requireCommentsEqual(t *testing.T, golden string, env *testEnv) { +func requireCommentsEqual(t *testing.T, golden string, env *execenv.Env) { t.Helper() - const goldenFilePatter = "%s-%d-golden.txt" + const goldenFilePattern = "%s-%d-golden.txt" comments := parseComments(t, env) comments = normalizeParsedComments(t, comments) - if *update { + if *cmdtest.Update { t.Log("Got here") for i, comment := range comments { - fileName := fmt.Sprintf(goldenFilePatter, golden, i) + fileName := fmt.Sprintf(goldenFilePattern, golden, i) require.NoError(t, ioutil.WriteFile(fileName, []byte(comment.message), 0644)) } } @@ -152,7 +156,7 @@ func requireCommentsEqual(t *testing.T, golden string, env *testEnv) { require.Equal(t, fmt.Sprintf("%7x", prefix+i), comment.id) require.Equal(t, date.Add(time.Duration(i)*time.Minute), comment.date) - fileName := fmt.Sprintf(goldenFilePatter, golden, i) + fileName := fmt.Sprintf(goldenFilePattern, golden, i) exp, err := ioutil.ReadFile(fileName) require.NoError(t, err) require.Equal(t, strings.ReplaceAll(string(exp), "\r", ""), strings.ReplaceAll(comment.message, "\r", "")) diff --git a/commands/bug/bug_deselect.go b/commands/bug/bug_deselect.go new file mode 100644 index 00000000..7e2a86c9 --- /dev/null +++ b/commands/bug/bug_deselect.go @@ -0,0 +1,37 @@ +package bugcmd + +import ( + "github.com/spf13/cobra" + + "github.com/MichaelMure/git-bug/commands/bug/select" + "github.com/MichaelMure/git-bug/commands/execenv" +) + +func newBugDeselectCommand() *cobra.Command { + env := execenv.NewEnv() + + cmd := &cobra.Command{ + Use: "deselect", + Short: "Clear the implicitly selected bug", + Example: `git bug select 2f15 +git bug comment +git bug status +git bug deselect +`, + PreRunE: execenv.LoadBackend(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runBugDeselect(env) + }), + } + + return cmd +} + +func runBugDeselect(env *execenv.Env) error { + err := _select.Clear(env.Backend) + if err != nil { + return err + } + + return nil +} diff --git a/commands/bug/bug_label.go b/commands/bug/bug_label.go new file mode 100644 index 00000000..657fa2ca --- /dev/null +++ b/commands/bug/bug_label.go @@ -0,0 +1,43 @@ +package bugcmd + +import ( + "github.com/spf13/cobra" + + "github.com/MichaelMure/git-bug/commands/bug/select" + "github.com/MichaelMure/git-bug/commands/completion" + "github.com/MichaelMure/git-bug/commands/execenv" +) + +func newBugLabelCommand() *cobra.Command { + env := execenv.NewEnv() + + cmd := &cobra.Command{ + Use: "label [BUG_ID]", + Short: "Display labels of a bug", + PreRunE: execenv.LoadBackend(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runBugLabel(env, args) + }), + ValidArgsFunction: completion.Bug(env), + } + + cmd.AddCommand(newBugLabelNewCommand()) + cmd.AddCommand(newBugLabelRmCommand()) + + return cmd +} + +func runBugLabel(env *execenv.Env, args []string) error { + b, args, err := _select.ResolveBug(env.Backend, args) + if err != nil { + return err + } + + snap := b.Snapshot() + + for _, l := range snap.Labels { + env.Out.Println(l) + } + + return nil +} diff --git a/commands/bug/bug_label_new.go b/commands/bug/bug_label_new.go new file mode 100644 index 00000000..f94d3dc8 --- /dev/null +++ b/commands/bug/bug_label_new.go @@ -0,0 +1,47 @@ +package bugcmd + +import ( + "github.com/spf13/cobra" + + "github.com/MichaelMure/git-bug/commands/bug/select" + "github.com/MichaelMure/git-bug/commands/completion" + "github.com/MichaelMure/git-bug/commands/execenv" + "github.com/MichaelMure/git-bug/util/text" +) + +func newBugLabelNewCommand() *cobra.Command { + env := execenv.NewEnv() + + cmd := &cobra.Command{ + Use: "new [BUG_ID] LABEL...", + Short: "Add a label to a bug", + PreRunE: execenv.LoadBackendEnsureUser(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runBugLabelNew(env, args) + }), + ValidArgsFunction: completion.BugAndLabels(env, true), + } + + return cmd +} + +func runBugLabelNew(env *execenv.Env, args []string) error { + b, args, err := _select.ResolveBug(env.Backend, args) + if err != nil { + return err + } + + added := args + + changes, _, err := b.ChangeLabels(text.CleanupOneLineArray(added), nil) + + for _, change := range changes { + env.Out.Println(change) + } + + if err != nil { + return err + } + + return b.Commit() +} diff --git a/commands/bug/bug_label_rm.go b/commands/bug/bug_label_rm.go new file mode 100644 index 00000000..13ce4b81 --- /dev/null +++ b/commands/bug/bug_label_rm.go @@ -0,0 +1,47 @@ +package bugcmd + +import ( + "github.com/spf13/cobra" + + "github.com/MichaelMure/git-bug/commands/bug/select" + "github.com/MichaelMure/git-bug/commands/completion" + "github.com/MichaelMure/git-bug/commands/execenv" + "github.com/MichaelMure/git-bug/util/text" +) + +func newBugLabelRmCommand() *cobra.Command { + env := execenv.NewEnv() + + cmd := &cobra.Command{ + Use: "rm [BUG_ID] LABEL...", + Short: "Remove a label from a bug", + PreRunE: execenv.LoadBackend(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runBugLabelRm(env, args) + }), + ValidArgsFunction: completion.BugAndLabels(env, false), + } + + return cmd +} + +func runBugLabelRm(env *execenv.Env, args []string) error { + b, args, err := _select.ResolveBug(env.Backend, args) + if err != nil { + return err + } + + removed := args + + changes, _, err := b.ChangeLabels(nil, text.CleanupOneLineArray(removed)) + + for _, change := range changes { + env.Out.Println(change) + } + + if err != nil { + return err + } + + return b.Commit() +} diff --git a/commands/add.go b/commands/bug/bug_new.go index b43eda36..4f73a09c 100644 --- a/commands/add.go +++ b/commands/bug/bug_new.go @@ -1,29 +1,30 @@ -package commands +package bugcmd import ( "github.com/spf13/cobra" + "github.com/MichaelMure/git-bug/commands/execenv" "github.com/MichaelMure/git-bug/commands/input" "github.com/MichaelMure/git-bug/util/text" ) -type addOptions struct { +type bugNewOptions struct { title string message string messageFile string nonInteractive bool } -func newAddCommand() *cobra.Command { - env := newEnv() - options := addOptions{} +func newBugNewCommand() *cobra.Command { + env := execenv.NewEnv() + options := bugNewOptions{} cmd := &cobra.Command{ - Use: "add", - Short: "Create a new bug.", - PreRunE: loadBackendEnsureUser(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runAdd(env, options) + Use: "new", + Short: "Create a new bug", + PreRunE: execenv.LoadBackendEnsureUser(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runBugNew(env, options) }), } @@ -41,7 +42,7 @@ func newAddCommand() *cobra.Command { return cmd } -func runAdd(env *Env, opts addOptions) error { +func runBugNew(env *execenv.Env, opts bugNewOptions) error { var err error if opts.messageFile != "" && opts.message == "" { opts.title, opts.message, err = input.BugCreateFileInput(opts.messageFile) @@ -51,10 +52,10 @@ func runAdd(env *Env, opts addOptions) error { } if !opts.nonInteractive && opts.messageFile == "" && (opts.message == "" || opts.title == "") { - opts.title, opts.message, err = input.BugCreateEditorInput(env.backend, opts.title, opts.message) + opts.title, opts.message, err = input.BugCreateEditorInput(env.Backend, opts.title, opts.message) if err == input.ErrEmptyTitle { - env.out.Println("Empty title, aborting.") + env.Out.Println("Empty title, aborting.") return nil } if err != nil { @@ -62,7 +63,7 @@ func runAdd(env *Env, opts addOptions) error { } } - b, _, err := env.backend.NewBug( + b, _, err := env.Backend.NewBug( text.CleanupOneLine(opts.title), text.Cleanup(opts.message), ) @@ -70,7 +71,7 @@ func runAdd(env *Env, opts addOptions) error { return err } - env.out.Printf("%s created\n", b.Id().Human()) + env.Out.Printf("%s created\n", b.Id().Human()) return nil } diff --git a/commands/bug/bug_new_test.go b/commands/bug/bug_new_test.go new file mode 100644 index 00000000..210a4b0b --- /dev/null +++ b/commands/bug/bug_new_test.go @@ -0,0 +1,21 @@ +package bugcmd + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/MichaelMure/git-bug/commands/bug/testenv" +) + +func TestBugNew(t *testing.T) { + env, _ := testenv.NewTestEnvAndUser(t) + + err := runBugNew(env, bugNewOptions{ + nonInteractive: true, + message: "message", + title: "title", + }) + require.NoError(t, err) + require.Regexp(t, "^[0-9A-Fa-f]{7} created\n$", env.Out.String()) +} diff --git a/commands/bug/bug_rm.go b/commands/bug/bug_rm.go new file mode 100644 index 00000000..1d2a7524 --- /dev/null +++ b/commands/bug/bug_rm.go @@ -0,0 +1,46 @@ +package bugcmd + +import ( + "errors" + + "github.com/spf13/cobra" + + "github.com/MichaelMure/git-bug/commands/completion" + "github.com/MichaelMure/git-bug/commands/execenv" +) + +func newBugRmCommand() *cobra.Command { + env := execenv.NewEnv() + + cmd := &cobra.Command{ + Use: "rm BUG_ID", + Short: "Remove an existing bug", + Long: "Remove an existing bug in the local repository. Note removing bugs that were imported from bridges will not remove the bug on the remote, and will only remove the local copy of the bug.", + PreRunE: execenv.LoadBackendEnsureUser(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runBugRm(env, args) + }), + ValidArgsFunction: completion.Bug(env), + } + + flags := cmd.Flags() + flags.SortFlags = false + + return cmd +} + +func runBugRm(env *execenv.Env, args []string) (err error) { + if len(args) == 0 { + return errors.New("you must provide a bug prefix to remove") + } + + err = env.Backend.RemoveBug(args[0]) + + if err != nil { + return + } + + env.Out.Printf("bug %s removed\n", args[0]) + + return +} diff --git a/commands/bug/bug_rm_test.go b/commands/bug/bug_rm_test.go new file mode 100644 index 00000000..e0c2bbc5 --- /dev/null +++ b/commands/bug/bug_rm_test.go @@ -0,0 +1,19 @@ +package bugcmd + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/MichaelMure/git-bug/commands/bug/testenv" +) + +func TestBugRm(t *testing.T) { + env, bugID := testenv.NewTestEnvAndBug(t) + + exp := "bug " + bugID.Human() + " removed\n" + + require.NoError(t, runBugRm(env, []string{bugID.Human()})) + require.Equal(t, exp, env.Out.String()) + env.Out.Reset() +} diff --git a/commands/bug/bug_select.go b/commands/bug/bug_select.go new file mode 100644 index 00000000..0b1cb15c --- /dev/null +++ b/commands/bug/bug_select.go @@ -0,0 +1,62 @@ +package bugcmd + +import ( + "errors" + + "github.com/spf13/cobra" + + "github.com/MichaelMure/git-bug/commands/bug/select" + "github.com/MichaelMure/git-bug/commands/completion" + "github.com/MichaelMure/git-bug/commands/execenv" +) + +func newBugSelectCommand() *cobra.Command { + env := execenv.NewEnv() + + cmd := &cobra.Command{ + Use: "select BUG_ID", + Short: "Select a bug for implicit use in future commands", + Example: `git bug select 2f15 +git bug comment +git bug status +`, + Long: `Select a bug for implicit use in future commands. + +This command allows you to omit any bug ID argument, for example: + git bug show +instead of + git bug show 2f153ca + +The complementary command is "git bug deselect" performing the opposite operation. +`, + PreRunE: execenv.LoadBackend(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runBugSelect(env, args) + }), + ValidArgsFunction: completion.Bug(env), + } + + return cmd +} + +func runBugSelect(env *execenv.Env, args []string) error { + if len(args) == 0 { + return errors.New("You must provide a bug id") + } + + prefix := args[0] + + b, err := env.Backend.ResolveBugPrefix(prefix) + if err != nil { + return err + } + + err = _select.Select(env.Backend, b.Id()) + if err != nil { + return err + } + + env.Out.Printf("selected bug %s: %s\n", b.Id().Human(), b.Snapshot().Title) + + return nil +} diff --git a/commands/show.go b/commands/bug/bug_show.go index 1491372e..105b1150 100644 --- a/commands/show.go +++ b/commands/bug/bug_show.go @@ -1,4 +1,4 @@ -package commands +package bugcmd import ( "encoding/json" @@ -8,28 +8,31 @@ import ( "github.com/spf13/cobra" - _select "github.com/MichaelMure/git-bug/commands/select" + "github.com/MichaelMure/git-bug/commands/bug/select" + "github.com/MichaelMure/git-bug/commands/cmdjson" + "github.com/MichaelMure/git-bug/commands/completion" + "github.com/MichaelMure/git-bug/commands/execenv" "github.com/MichaelMure/git-bug/entities/bug" "github.com/MichaelMure/git-bug/util/colors" ) -type showOptions struct { +type bugShowOptions struct { fields string format string } -func newShowCommand() *cobra.Command { - env := newEnv() - options := showOptions{} +func newBugShowCommand() *cobra.Command { + env := execenv.NewEnv() + options := bugShowOptions{} cmd := &cobra.Command{ - Use: "show [ID]", - Short: "Display the details of a bug.", - PreRunE: loadBackend(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runShow(env, options, args) + Use: "show [BUG_ID]", + Short: "Display the details of a bug", + PreRunE: execenv.LoadBackend(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runBugShow(env, options, args) }), - ValidArgsFunction: completeBug(env), + ValidArgsFunction: completion.Bug(env), } flags := cmd.Flags() @@ -39,15 +42,15 @@ func newShowCommand() *cobra.Command { "id", "labels", "shortId", "status", "title", "actors", "participants"} flags.StringVarP(&options.fields, "field", "", "", "Select field to display. Valid values are ["+strings.Join(fields, ",")+"]") - cmd.RegisterFlagCompletionFunc("by", completeFrom(fields)) + cmd.RegisterFlagCompletionFunc("by", completion.From(fields)) flags.StringVarP(&options.format, "format", "f", "default", "Select the output formatting style. Valid values are [default,json,org-mode]") return cmd } -func runShow(env *Env, opts showOptions, args []string) error { - b, args, err := _select.ResolveBug(env.backend, args) +func runBugShow(env *execenv.Env, opts bugShowOptions, args []string) error { + b, args, err := _select.ResolveBug(env.Backend, args) if err != nil { return err } @@ -61,35 +64,35 @@ func runShow(env *Env, opts showOptions, args []string) error { if opts.fields != "" { switch opts.fields { case "author": - env.out.Printf("%s\n", snap.Author.DisplayName()) + env.Out.Printf("%s\n", snap.Author.DisplayName()) case "authorEmail": - env.out.Printf("%s\n", snap.Author.Email()) + env.Out.Printf("%s\n", snap.Author.Email()) case "createTime": - env.out.Printf("%s\n", snap.CreateTime.String()) + env.Out.Printf("%s\n", snap.CreateTime.String()) case "lastEdit": - env.out.Printf("%s\n", snap.EditTime().String()) + env.Out.Printf("%s\n", snap.EditTime().String()) case "humanId": - env.out.Printf("%s\n", snap.Id().Human()) + env.Out.Printf("%s\n", snap.Id().Human()) case "id": - env.out.Printf("%s\n", snap.Id()) + env.Out.Printf("%s\n", snap.Id()) case "labels": for _, l := range snap.Labels { - env.out.Printf("%s\n", l.String()) + env.Out.Printf("%s\n", l.String()) } case "actors": for _, a := range snap.Actors { - env.out.Printf("%s\n", a.DisplayName()) + env.Out.Printf("%s\n", a.DisplayName()) } case "participants": for _, p := range snap.Participants { - env.out.Printf("%s\n", p.DisplayName()) + env.Out.Printf("%s\n", p.DisplayName()) } case "shortId": - env.out.Printf("%s\n", snap.Id().Human()) + env.Out.Printf("%s\n", snap.Id().Human()) case "status": - env.out.Printf("%s\n", snap.Status) + env.Out.Printf("%s\n", snap.Status) case "title": - env.out.Printf("%s\n", snap.Title) + env.Out.Printf("%s\n", snap.Title) default: return fmt.Errorf("\nUnsupported field: %s\n", opts.fields) } @@ -109,20 +112,20 @@ func runShow(env *Env, opts showOptions, args []string) error { } } -func showDefaultFormatter(env *Env, snapshot *bug.Snapshot) error { +func showDefaultFormatter(env *execenv.Env, snapshot *bug.Snapshot) error { // Header - env.out.Printf("%s [%s] %s\n\n", + env.Out.Printf("%s [%s] %s\n\n", colors.Cyan(snapshot.Id().Human()), colors.Yellow(snapshot.Status), snapshot.Title, ) - env.out.Printf("%s opened this issue %s\n", + env.Out.Printf("%s opened this issue %s\n", colors.Magenta(snapshot.Author.DisplayName()), snapshot.CreateTime.String(), ) - env.out.Printf("This was last edited at %s\n\n", + env.Out.Printf("This was last edited at %s\n\n", snapshot.EditTime().String(), ) @@ -132,7 +135,7 @@ func showDefaultFormatter(env *Env, snapshot *bug.Snapshot) error { labels[i] = string(snapshot.Labels[i]) } - env.out.Printf("labels: %s\n", + env.Out.Printf("labels: %s\n", strings.Join(labels, ", "), ) @@ -142,7 +145,7 @@ func showDefaultFormatter(env *Env, snapshot *bug.Snapshot) error { actors[i] = snapshot.Actors[i].DisplayName() } - env.out.Printf("actors: %s\n", + env.Out.Printf("actors: %s\n", strings.Join(actors, ", "), ) @@ -152,7 +155,7 @@ func showDefaultFormatter(env *Env, snapshot *bug.Snapshot) error { participants[i] = snapshot.Participants[i].DisplayName() } - env.out.Printf("participants: %s\n\n", + env.Out.Printf("participants: %s\n\n", strings.Join(participants, ", "), ) @@ -161,7 +164,7 @@ func showDefaultFormatter(env *Env, snapshot *bug.Snapshot) error { for i, comment := range snapshot.Comments { var message string - env.out.Printf("%s%s #%d %s <%s>\n\n", + env.Out.Printf("%s%s #%d %s <%s>\n\n", indent, comment.CombinedId().Human(), i, @@ -175,7 +178,7 @@ func showDefaultFormatter(env *Env, snapshot *bug.Snapshot) error { message = comment.Message } - env.out.Printf("%s%s\n\n\n", + env.Out.Printf("%s%s\n\n\n", indent, message, ) @@ -185,85 +188,85 @@ func showDefaultFormatter(env *Env, snapshot *bug.Snapshot) error { } type JSONBugSnapshot struct { - Id string `json:"id"` - HumanId string `json:"human_id"` - CreateTime JSONTime `json:"create_time"` - EditTime JSONTime `json:"edit_time"` - Status string `json:"status"` - Labels []bug.Label `json:"labels"` - Title string `json:"title"` - Author JSONIdentity `json:"author"` - Actors []JSONIdentity `json:"actors"` - Participants []JSONIdentity `json:"participants"` - Comments []JSONComment `json:"comments"` + Id string `json:"id"` + HumanId string `json:"human_id"` + CreateTime cmdjson.Time `json:"create_time"` + EditTime cmdjson.Time `json:"edit_time"` + Status string `json:"status"` + Labels []bug.Label `json:"labels"` + Title string `json:"title"` + Author cmdjson.Identity `json:"author"` + Actors []cmdjson.Identity `json:"actors"` + Participants []cmdjson.Identity `json:"participants"` + Comments []JSONBugComment `json:"comments"` } -type JSONComment struct { - Id string `json:"id"` - HumanId string `json:"human_id"` - Author JSONIdentity `json:"author"` - Message string `json:"message"` +type JSONBugComment struct { + Id string `json:"id"` + HumanId string `json:"human_id"` + Author cmdjson.Identity `json:"author"` + Message string `json:"message"` } -func NewJSONComment(comment bug.Comment) JSONComment { - return JSONComment{ +func NewJSONComment(comment bug.Comment) JSONBugComment { + return JSONBugComment{ Id: comment.CombinedId().String(), HumanId: comment.CombinedId().Human(), - Author: NewJSONIdentity(comment.Author), + Author: cmdjson.NewIdentity(comment.Author), Message: comment.Message, } } -func showJsonFormatter(env *Env, snapshot *bug.Snapshot) error { +func showJsonFormatter(env *execenv.Env, snapshot *bug.Snapshot) error { jsonBug := JSONBugSnapshot{ Id: snapshot.Id().String(), HumanId: snapshot.Id().Human(), - CreateTime: NewJSONTime(snapshot.CreateTime, 0), - EditTime: NewJSONTime(snapshot.EditTime(), 0), + CreateTime: cmdjson.NewTime(snapshot.CreateTime, 0), + EditTime: cmdjson.NewTime(snapshot.EditTime(), 0), Status: snapshot.Status.String(), Labels: snapshot.Labels, Title: snapshot.Title, - Author: NewJSONIdentity(snapshot.Author), + Author: cmdjson.NewIdentity(snapshot.Author), } - jsonBug.Actors = make([]JSONIdentity, len(snapshot.Actors)) + jsonBug.Actors = make([]cmdjson.Identity, len(snapshot.Actors)) for i, element := range snapshot.Actors { - jsonBug.Actors[i] = NewJSONIdentity(element) + jsonBug.Actors[i] = cmdjson.NewIdentity(element) } - jsonBug.Participants = make([]JSONIdentity, len(snapshot.Participants)) + jsonBug.Participants = make([]cmdjson.Identity, len(snapshot.Participants)) for i, element := range snapshot.Participants { - jsonBug.Participants[i] = NewJSONIdentity(element) + jsonBug.Participants[i] = cmdjson.NewIdentity(element) } - jsonBug.Comments = make([]JSONComment, len(snapshot.Comments)) + jsonBug.Comments = make([]JSONBugComment, len(snapshot.Comments)) for i, comment := range snapshot.Comments { jsonBug.Comments[i] = NewJSONComment(comment) } jsonObject, _ := json.MarshalIndent(jsonBug, "", " ") - env.out.Printf("%s\n", jsonObject) + env.Out.Printf("%s\n", jsonObject) return nil } -func showOrgModeFormatter(env *Env, snapshot *bug.Snapshot) error { +func showOrgModeFormatter(env *execenv.Env, snapshot *bug.Snapshot) error { // Header - env.out.Printf("%s [%s] %s\n", + env.Out.Printf("%s [%s] %s\n", snapshot.Id().Human(), snapshot.Status, snapshot.Title, ) - env.out.Printf("* Author: %s\n", + env.Out.Printf("* Author: %s\n", snapshot.Author.DisplayName(), ) - env.out.Printf("* Creation Time: %s\n", + env.Out.Printf("* Creation Time: %s\n", snapshot.CreateTime.String(), ) - env.out.Printf("* Last Edit: %s\n", + env.Out.Printf("* Last Edit: %s\n", snapshot.EditTime().String(), ) @@ -273,9 +276,9 @@ func showOrgModeFormatter(env *Env, snapshot *bug.Snapshot) error { labels[i] = string(label) } - env.out.Printf("* Labels:\n") + env.Out.Printf("* Labels:\n") if len(labels) > 0 { - env.out.Printf("** %s\n", + env.Out.Printf("** %s\n", strings.Join(labels, "\n** "), ) } @@ -289,7 +292,7 @@ func showOrgModeFormatter(env *Env, snapshot *bug.Snapshot) error { ) } - env.out.Printf("* Actors:\n** %s\n", + env.Out.Printf("* Actors:\n** %s\n", strings.Join(actors, "\n** "), ) @@ -302,15 +305,15 @@ func showOrgModeFormatter(env *Env, snapshot *bug.Snapshot) error { ) } - env.out.Printf("* Participants:\n** %s\n", + env.Out.Printf("* Participants:\n** %s\n", strings.Join(participants, "\n** "), ) - env.out.Printf("* Comments:\n") + env.Out.Printf("* Comments:\n") for i, comment := range snapshot.Comments { var message string - env.out.Printf("** #%d %s\n", + env.Out.Printf("** #%d %s\n", i, comment.Author.DisplayName()) if comment.Message == "" { @@ -319,7 +322,7 @@ func showOrgModeFormatter(env *Env, snapshot *bug.Snapshot) error { message = strings.ReplaceAll(comment.Message, "\n", "\n: ") } - env.out.Printf(": %s\n", message) + env.Out.Printf(": %s\n", message) } return nil diff --git a/commands/bug/bug_status.go b/commands/bug/bug_status.go new file mode 100644 index 00000000..b05f862c --- /dev/null +++ b/commands/bug/bug_status.go @@ -0,0 +1,41 @@ +package bugcmd + +import ( + "github.com/spf13/cobra" + + "github.com/MichaelMure/git-bug/commands/bug/select" + "github.com/MichaelMure/git-bug/commands/completion" + "github.com/MichaelMure/git-bug/commands/execenv" +) + +func newBugStatusCommand() *cobra.Command { + env := execenv.NewEnv() + + cmd := &cobra.Command{ + Use: "status [BUG_ID]", + Short: "Display the status of a bug", + PreRunE: execenv.LoadBackend(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runBugStatus(env, args) + }), + ValidArgsFunction: completion.Bug(env), + } + + cmd.AddCommand(newBugStatusCloseCommand()) + cmd.AddCommand(newBugStatusOpenCommand()) + + return cmd +} + +func runBugStatus(env *execenv.Env, args []string) error { + b, args, err := _select.ResolveBug(env.Backend, args) + if err != nil { + return err + } + + snap := b.Snapshot() + + env.Out.Println(snap.Status) + + return nil +} diff --git a/commands/bug/bug_status_close.go b/commands/bug/bug_status_close.go new file mode 100644 index 00000000..fcd47922 --- /dev/null +++ b/commands/bug/bug_status_close.go @@ -0,0 +1,39 @@ +package bugcmd + +import ( + "github.com/spf13/cobra" + + "github.com/MichaelMure/git-bug/commands/bug/select" + "github.com/MichaelMure/git-bug/commands/completion" + "github.com/MichaelMure/git-bug/commands/execenv" +) + +func newBugStatusCloseCommand() *cobra.Command { + env := execenv.NewEnv() + + cmd := &cobra.Command{ + Use: "close [BUG_ID]", + Short: "Mark a bug as closed", + PreRunE: execenv.LoadBackendEnsureUser(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runBugStatusClose(env, args) + }), + ValidArgsFunction: completion.Bug(env), + } + + return cmd +} + +func runBugStatusClose(env *execenv.Env, args []string) error { + b, args, err := _select.ResolveBug(env.Backend, args) + if err != nil { + return err + } + + _, err = b.Close() + if err != nil { + return err + } + + return b.Commit() +} diff --git a/commands/bug/bug_status_open.go b/commands/bug/bug_status_open.go new file mode 100644 index 00000000..e686add1 --- /dev/null +++ b/commands/bug/bug_status_open.go @@ -0,0 +1,39 @@ +package bugcmd + +import ( + "github.com/spf13/cobra" + + "github.com/MichaelMure/git-bug/commands/bug/select" + "github.com/MichaelMure/git-bug/commands/completion" + "github.com/MichaelMure/git-bug/commands/execenv" +) + +func newBugStatusOpenCommand() *cobra.Command { + env := execenv.NewEnv() + + cmd := &cobra.Command{ + Use: "open [BUG_ID]", + Short: "Mark a bug as open", + PreRunE: execenv.LoadBackendEnsureUser(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runBugStatusOpen(env, args) + }), + ValidArgsFunction: completion.Bug(env), + } + + return cmd +} + +func runBugStatusOpen(env *execenv.Env, args []string) error { + b, args, err := _select.ResolveBug(env.Backend, args) + if err != nil { + return err + } + + _, err = b.Open() + if err != nil { + return err + } + + return b.Commit() +} diff --git a/commands/ls_test.go b/commands/bug/bug_test.go index 22adc1ce..aef0346d 100644 --- a/commands/ls_test.go +++ b/commands/bug/bug_test.go @@ -1,4 +1,4 @@ -package commands +package bugcmd import ( "encoding/json" @@ -6,6 +6,8 @@ import ( "testing" "github.com/stretchr/testify/require" + + "github.com/MichaelMure/git-bug/commands/bug/testenv" ) func Test_repairQuery(t *testing.T) { @@ -44,7 +46,7 @@ func Test_repairQuery(t *testing.T) { } } -func TestLs_Format(t *testing.T) { +func TestBug_Format(t *testing.T) { const expOrgMode = `^#+TODO: OPEN | CLOSED [*] OPEN [0-9a-f]{7} \[\d\d\d\d-\d\d-\d\d [[:alpha:]]{3} \d\d:\d\d\] John Doe: this is a bug title :: [*]{2} Last Edited: \[\d\d\d\d-\d\d-\d\d [[:alpha:]]{3} \d\d:\d\d\] @@ -66,7 +68,7 @@ $` } for _, testcase := range cases { - opts := lsOptions{ + opts := bugOptions{ sortDirection: "asc", sortBy: "creation", outputFormat: testcase.format, @@ -75,26 +77,26 @@ $` name := fmt.Sprintf("with %s format", testcase.format) t.Run(name, func(t *testing.T) { - env, _ := newTestEnvAndBug(t) + env, _ := testenv.NewTestEnvAndBug(t) - require.NoError(t, runLs(env.env, opts, []string{})) - require.Regexp(t, testcase.exp, env.out.String()) + require.NoError(t, runBug(env, opts, []string{})) + require.Regexp(t, testcase.exp, env.Out.String()) }) } t.Run("with JSON format", func(t *testing.T) { - opts := lsOptions{ + opts := bugOptions{ sortDirection: "asc", sortBy: "creation", outputFormat: "json", } - env, _ := newTestEnvAndBug(t) + env, _ := testenv.NewTestEnvAndBug(t) - require.NoError(t, runLs(env.env, opts, []string{})) + require.NoError(t, runBug(env, opts, []string{})) - bugs := []JSONBugExcerpt{} - require.NoError(t, json.Unmarshal(env.out.Bytes(), &bugs)) + var bugs []JSONBugExcerpt + require.NoError(t, json.Unmarshal(env.Out.Bytes(), &bugs)) require.Len(t, bugs, 1) }) diff --git a/commands/bug/bug_title.go b/commands/bug/bug_title.go new file mode 100644 index 00000000..98809b60 --- /dev/null +++ b/commands/bug/bug_title.go @@ -0,0 +1,40 @@ +package bugcmd + +import ( + "github.com/spf13/cobra" + + "github.com/MichaelMure/git-bug/commands/bug/select" + "github.com/MichaelMure/git-bug/commands/completion" + "github.com/MichaelMure/git-bug/commands/execenv" +) + +func newBugTitleCommand() *cobra.Command { + env := execenv.NewEnv() + + cmd := &cobra.Command{ + Use: "title [BUG_ID]", + Short: "Display the title of a bug", + PreRunE: execenv.LoadBackend(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runBugTitle(env, args) + }), + ValidArgsFunction: completion.Bug(env), + } + + cmd.AddCommand(newBugTitleEditCommand()) + + return cmd +} + +func runBugTitle(env *execenv.Env, args []string) error { + b, args, err := _select.ResolveBug(env.Backend, args) + if err != nil { + return err + } + + snap := b.Snapshot() + + env.Out.Println(snap.Title) + + return nil +} diff --git a/commands/bug/bug_title_edit.go b/commands/bug/bug_title_edit.go new file mode 100644 index 00000000..e71330a1 --- /dev/null +++ b/commands/bug/bug_title_edit.go @@ -0,0 +1,76 @@ +package bugcmd + +import ( + "github.com/spf13/cobra" + + "github.com/MichaelMure/git-bug/commands/bug/select" + "github.com/MichaelMure/git-bug/commands/completion" + "github.com/MichaelMure/git-bug/commands/execenv" + "github.com/MichaelMure/git-bug/commands/input" + "github.com/MichaelMure/git-bug/util/text" +) + +type bugTitleEditOptions struct { + title string + nonInteractive bool +} + +func newBugTitleEditCommand() *cobra.Command { + env := execenv.NewEnv() + options := bugTitleEditOptions{} + + cmd := &cobra.Command{ + Use: "edit [BUG_ID]", + Short: "Edit a title of a bug", + PreRunE: execenv.LoadBackendEnsureUser(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runBugTitleEdit(env, options, args) + }), + ValidArgsFunction: completion.Bug(env), + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringVarP(&options.title, "title", "t", "", + "Provide a title to describe the issue", + ) + flags.BoolVar(&options.nonInteractive, "non-interactive", false, "Do not ask for user input") + + return cmd +} + +func runBugTitleEdit(env *execenv.Env, opts bugTitleEditOptions, args []string) error { + b, args, err := _select.ResolveBug(env.Backend, args) + if err != nil { + return err + } + + snap := b.Snapshot() + + if opts.title == "" { + if opts.nonInteractive { + env.Err.Println("No title given. Use -m or -F option to specify a title. Aborting.") + return nil + } + opts.title, err = input.BugTitleEditorInput(env.Repo, snap.Title) + if err == input.ErrEmptyTitle { + env.Out.Println("Empty title, aborting.") + return nil + } + if err != nil { + return err + } + } + + if opts.title == snap.Title { + env.Err.Println("No change, aborting.") + } + + _, err = b.SetTitle(text.CleanupOneLine(opts.title)) + if err != nil { + return err + } + + return b.Commit() +} diff --git a/commands/select/select.go b/commands/bug/select/select.go index 908ad58c..908ad58c 100644 --- a/commands/select/select.go +++ b/commands/bug/select/select.go diff --git a/commands/select/select_test.go b/commands/bug/select/select_test.go index 702700f4..702700f4 100644 --- a/commands/select/select_test.go +++ b/commands/bug/select/select_test.go diff --git a/commands/testdata/comment/add-0-golden.txt b/commands/bug/testdata/comment/add-0-golden.txt index 44ae0c1a..44ae0c1a 100644 --- a/commands/testdata/comment/add-0-golden.txt +++ b/commands/bug/testdata/comment/add-0-golden.txt diff --git a/commands/testdata/comment/add-1-golden.txt b/commands/bug/testdata/comment/add-1-golden.txt index bcf127c0..bcf127c0 100644 --- a/commands/testdata/comment/add-1-golden.txt +++ b/commands/bug/testdata/comment/add-1-golden.txt diff --git a/commands/testdata/comment/edit-0-golden.txt b/commands/bug/testdata/comment/edit-0-golden.txt index 44ae0c1a..44ae0c1a 100644 --- a/commands/testdata/comment/edit-0-golden.txt +++ b/commands/bug/testdata/comment/edit-0-golden.txt diff --git a/commands/testdata/comment/edit-1-golden.txt b/commands/bug/testdata/comment/edit-1-golden.txt index 3d83c02b..3d83c02b 100644 --- a/commands/testdata/comment/edit-1-golden.txt +++ b/commands/bug/testdata/comment/edit-1-golden.txt diff --git a/commands/testdata/comment/message-only-0-golden.txt b/commands/bug/testdata/comment/message-only-0-golden.txt index 44ae0c1a..44ae0c1a 100644 --- a/commands/testdata/comment/message-only-0-golden.txt +++ b/commands/bug/testdata/comment/message-only-0-golden.txt diff --git a/commands/bug/testenv/testenv.go b/commands/bug/testenv/testenv.go new file mode 100644 index 00000000..10f20950 --- /dev/null +++ b/commands/bug/testenv/testenv.go @@ -0,0 +1,63 @@ +package testenv + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/MichaelMure/git-bug/commands/execenv" + "github.com/MichaelMure/git-bug/entity" +) + +const ( + testUserName = "John Doe" + testUserEmail = "jdoe@example.com" +) + +func NewTestEnvAndUser(t *testing.T) (*execenv.Env, entity.Id) { + t.Helper() + + testEnv := execenv.NewTestEnv(t) + + i, err := testEnv.Backend.NewIdentity(testUserName, testUserEmail) + require.NoError(t, err) + + err = testEnv.Backend.SetUserIdentity(i) + require.NoError(t, err) + + return testEnv, i.Id() +} + +const ( + testBugTitle = "this is a bug title" + testBugMessage = "this is a bug message" +) + +func NewTestEnvAndBug(t *testing.T) (*execenv.Env, entity.Id) { + t.Helper() + + testEnv, _ := NewTestEnvAndUser(t) + + b, _, err := testEnv.Backend.NewBug(testBugTitle, testBugMessage) + require.NoError(t, err) + + return testEnv, b.Id() +} + +const ( + testCommentMessage = "this is a bug comment" +) + +func NewTestEnvAndBugWithComment(t *testing.T) (*execenv.Env, entity.Id, entity.CombinedId) { + t.Helper() + + env, bugID := NewTestEnvAndBug(t) + + b, err := env.Backend.ResolveBug(bugID) + require.NoError(t, err) + + commentId, _, err := b.AddComment(testCommentMessage) + require.NoError(t, err) + + return env, bugID, commentId +} diff --git a/commands/json_common.go b/commands/cmdjson/json_common.go index 3ceee1ec..60e6e751 100644 --- a/commands/json_common.go +++ b/commands/cmdjson/json_common.go @@ -1,4 +1,4 @@ -package commands +package cmdjson import ( "time" @@ -8,15 +8,15 @@ import ( "github.com/MichaelMure/git-bug/util/lamport" ) -type JSONIdentity struct { +type Identity struct { Id string `json:"id"` HumanId string `json:"human_id"` Name string `json:"name"` Login string `json:"login"` } -func NewJSONIdentity(i identity.Interface) JSONIdentity { - return JSONIdentity{ +func NewIdentity(i identity.Interface) Identity { + return Identity{ Id: i.Id().String(), HumanId: i.Id().Human(), Name: i.Name(), @@ -24,8 +24,8 @@ func NewJSONIdentity(i identity.Interface) JSONIdentity { } } -func NewJSONIdentityFromExcerpt(excerpt *cache.IdentityExcerpt) JSONIdentity { - return JSONIdentity{ +func NewIdentityFromExcerpt(excerpt *cache.IdentityExcerpt) Identity { + return Identity{ Id: excerpt.Id.String(), HumanId: excerpt.Id.Human(), Name: excerpt.Name, @@ -33,21 +33,14 @@ func NewJSONIdentityFromExcerpt(excerpt *cache.IdentityExcerpt) JSONIdentity { } } -func NewJSONIdentityFromLegacyExcerpt(excerpt *cache.LegacyAuthorExcerpt) JSONIdentity { - return JSONIdentity{ - Name: excerpt.Name, - Login: excerpt.Login, - } -} - -type JSONTime struct { +type Time struct { Timestamp int64 `json:"timestamp"` Time time.Time `json:"time"` Lamport lamport.Time `json:"lamport,omitempty"` } -func NewJSONTime(t time.Time, l lamport.Time) JSONTime { - return JSONTime{ +func NewTime(t time.Time, l lamport.Time) Time { + return Time{ Timestamp: t.Unix(), Time: t, Lamport: l, diff --git a/commands/cmdtest/golden.go b/commands/cmdtest/golden.go new file mode 100644 index 00000000..c9a21f73 --- /dev/null +++ b/commands/cmdtest/golden.go @@ -0,0 +1,5 @@ +package cmdtest + +import "flag" + +var Update = flag.Bool("Update", false, "Update golden files") diff --git a/commands/commands.go b/commands/commands.go index 49c960ab..7d2fc37d 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -4,6 +4,8 @@ import ( "sort" "github.com/spf13/cobra" + + "github.com/MichaelMure/git-bug/commands/execenv" ) type commandOptions struct { @@ -11,7 +13,7 @@ type commandOptions struct { } func newCommandsCommand() *cobra.Command { - env := newEnv() + env := execenv.NewEnv() options := commandOptions{} cmd := &cobra.Command{ @@ -32,7 +34,7 @@ func newCommandsCommand() *cobra.Command { return cmd } -func runCommands(env *Env, opts commandOptions) error { +func runCommands(env *execenv.Env, opts commandOptions) error { first := true var allCmds []*cobra.Command @@ -49,24 +51,24 @@ func runCommands(env *Env, opts commandOptions) error { for _, cmd := range allCmds { if !first { - env.out.Println() + env.Out.Println() } first = false if opts.desc { - env.out.Printf("# %s\n", cmd.Short) + env.Out.Printf("# %s\n", cmd.Short) } - env.out.Print(cmd.UseLine()) + env.Out.Print(cmd.UseLine()) if opts.desc { - env.out.Println() + env.Out.Println() } } if !opts.desc { - env.out.Println() + env.Out.Println() } return nil diff --git a/commands/comment.go b/commands/comment.go deleted file mode 100644 index 7cab447c..00000000 --- a/commands/comment.go +++ /dev/null @@ -1,50 +0,0 @@ -package commands - -import ( - text "github.com/MichaelMure/go-term-text" - "github.com/spf13/cobra" - - _select "github.com/MichaelMure/git-bug/commands/select" - "github.com/MichaelMure/git-bug/util/colors" -) - -func newCommentCommand() *cobra.Command { - env := newEnv() - - cmd := &cobra.Command{ - Use: "comment [ID]", - Short: "Display or add comments to a bug.", - PreRunE: loadBackend(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runComment(env, args) - }), - ValidArgsFunction: completeBug(env), - } - - cmd.AddCommand(newCommentAddCommand()) - cmd.AddCommand(newCommentEditCommand()) - - return cmd -} - -func runComment(env *Env, args []string) error { - b, args, err := _select.ResolveBug(env.backend, args) - if err != nil { - return err - } - - snap := b.Snapshot() - - for i, comment := range snap.Comments { - if i != 0 { - env.out.Println() - } - - env.out.Printf("Author: %s\n", colors.Magenta(comment.Author.DisplayName())) - env.out.Printf("Id: %s\n", colors.Cyan(comment.CombinedId().Human())) - env.out.Printf("Date: %s\n\n", comment.FormatTime()) - env.out.Println(text.LeftPadLines(comment.Message, 4)) - } - - return nil -} diff --git a/commands/comment_add_test.go b/commands/comment_add_test.go deleted file mode 100644 index 34ff3743..00000000 --- a/commands/comment_add_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package commands - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func newTestEnvAndBugWithComment(t *testing.T) (*testEnv, string, string) { - t.Helper() - - env, bugID := newTestEnvAndBug(t) - - opts := commentAddOptions{ - message: "this is a bug comment", - } - require.NoError(t, runCommentAdd(env.env, opts, []string{bugID})) - require.NoError(t, runComment(env.env, []string{bugID})) - comments := parseComments(t, env) - require.Len(t, comments, 2) - - env.out.Reset() - - return env, bugID, comments[1].id -} - -func TestCommentAdd(t *testing.T) { - const golden = "testdata/comment/add" - - env, bugID, _ := newTestEnvAndBugWithComment(t) - require.NoError(t, runComment(env.env, []string{bugID})) - requireCommentsEqual(t, golden, env) -} diff --git a/commands/comment_edit_test.go b/commands/comment_edit_test.go deleted file mode 100644 index 50c1850b..00000000 --- a/commands/comment_edit_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package commands - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestCommentEdit(t *testing.T) { - const golden = "testdata/comment/edit" - - env, bugID, commentID := newTestEnvAndBugWithComment(t) - - opts := commentEditOptions{ - message: "this is an altered bug comment", - } - require.NoError(t, runCommentEdit(env.env, opts, []string{commentID})) - - require.NoError(t, runComment(env.env, []string{bugID})) - requireCommentsEqual(t, golden, env) -} diff --git a/commands/helper_completion.go b/commands/completion/helper_completion.go index 847a0288..27fbd615 100644 --- a/commands/helper_completion.go +++ b/commands/completion/helper_completion.go @@ -1,4 +1,4 @@ -package commands +package completion import ( "fmt" @@ -10,28 +10,29 @@ import ( "github.com/MichaelMure/git-bug/bridge" "github.com/MichaelMure/git-bug/bridge/core/auth" "github.com/MichaelMure/git-bug/cache" - _select "github.com/MichaelMure/git-bug/commands/select" + "github.com/MichaelMure/git-bug/commands/bug/select" + "github.com/MichaelMure/git-bug/commands/execenv" "github.com/MichaelMure/git-bug/entities/bug" ) -type validArgsFunction func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) +type ValidArgsFunction func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) -func completionHandlerError(err error) (completions []string, directives cobra.ShellCompDirective) { +func handleError(err error) (completions []string, directives cobra.ShellCompDirective) { return nil, cobra.ShellCompDirectiveError } -func completeBridge(env *Env) validArgsFunction { +func Bridge(env *execenv.Env) ValidArgsFunction { return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) { - if err := loadBackend(env)(cmd, args); err != nil { - return completionHandlerError(err) + if err := execenv.LoadBackend(env)(cmd, args); err != nil { + return handleError(err) } defer func() { - _ = env.backend.Close() + _ = env.Backend.Close() }() - bridges, err := bridge.ConfiguredBridges(env.backend) + bridges, err := bridge.ConfiguredBridges(env.Backend) if err != nil { - return completionHandlerError(err) + return handleError(err) } completions = make([]string, len(bridges)) @@ -43,18 +44,18 @@ func completeBridge(env *Env) validArgsFunction { } } -func completeBridgeAuth(env *Env) validArgsFunction { +func BridgeAuth(env *execenv.Env) ValidArgsFunction { return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) { - if err := loadBackend(env)(cmd, args); err != nil { - return completionHandlerError(err) + if err := execenv.LoadBackend(env)(cmd, args); err != nil { + return handleError(err) } defer func() { - _ = env.backend.Close() + _ = env.Backend.Close() }() - creds, err := auth.List(env.backend) + creds, err := auth.List(env.Backend) if err != nil { - return completionHandlerError(err) + return handleError(err) } completions = make([]string, len(creds)) @@ -73,27 +74,27 @@ func completeBridgeAuth(env *Env) validArgsFunction { } } -func completeBug(env *Env) validArgsFunction { +func Bug(env *execenv.Env) ValidArgsFunction { return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) { - if err := loadBackend(env)(cmd, args); err != nil { - return completionHandlerError(err) + if err := execenv.LoadBackend(env)(cmd, args); err != nil { + return handleError(err) } defer func() { - _ = env.backend.Close() + _ = env.Backend.Close() }() - return completeBugWithBackend(env.backend, toComplete) + return bugWithBackend(env.Backend, toComplete) } } -func completeBugWithBackend(backend *cache.RepoCache, toComplete string) (completions []string, directives cobra.ShellCompDirective) { +func bugWithBackend(backend *cache.RepoCache, toComplete string) (completions []string, directives cobra.ShellCompDirective) { allIds := backend.AllBugsIds() bugExcerpt := make([]*cache.BugExcerpt, len(allIds)) for i, id := range allIds { var err error bugExcerpt[i], err = backend.ResolveBugExcerpt(id) if err != nil { - return completionHandlerError(err) + return handleError(err) } } @@ -106,22 +107,22 @@ func completeBugWithBackend(backend *cache.RepoCache, toComplete string) (comple return completions, cobra.ShellCompDirectiveNoFileComp } -func completeBugAndLabels(env *Env, addOrRemove bool) validArgsFunction { +func BugAndLabels(env *execenv.Env, addOrRemove bool) ValidArgsFunction { return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) { - if err := loadBackend(env)(cmd, args); err != nil { - return completionHandlerError(err) + if err := execenv.LoadBackend(env)(cmd, args); err != nil { + return handleError(err) } defer func() { - _ = env.backend.Close() + _ = env.Backend.Close() }() - b, args, err := _select.ResolveBug(env.backend, args) + b, args, err := _select.ResolveBug(env.Backend, args) if err == _select.ErrNoValidId { // we need a bug first to complete labels - return completeBugWithBackend(env.backend, toComplete) + return bugWithBackend(env.Backend, toComplete) } if err != nil { - return completionHandlerError(err) + return handleError(err) } snap := b.Snapshot() @@ -137,7 +138,7 @@ func completeBugAndLabels(env *Env, addOrRemove bool) validArgsFunction { seenLabels[label] = true } - allLabels := env.backend.ValidLabels() + allLabels := env.Backend.ValidLabels() labels = make([]bug.Label, 0, len(allLabels)) for _, label := range allLabels { if !seenLabels[label] { @@ -162,24 +163,24 @@ func completeBugAndLabels(env *Env, addOrRemove bool) validArgsFunction { } } -func completeFrom(choices []string) validArgsFunction { +func From(choices []string) ValidArgsFunction { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return choices, cobra.ShellCompDirectiveNoFileComp } } -func completeGitRemote(env *Env) validArgsFunction { +func GitRemote(env *execenv.Env) ValidArgsFunction { return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) { - if err := loadBackend(env)(cmd, args); err != nil { - return completionHandlerError(err) + if err := execenv.LoadBackend(env)(cmd, args); err != nil { + return handleError(err) } defer func() { - _ = env.backend.Close() + _ = env.Backend.Close() }() - remoteMap, err := env.backend.GetRemotes() + remoteMap, err := env.Backend.GetRemotes() if err != nil { - return completionHandlerError(err) + return handleError(err) } completions = make([]string, 0, len(remoteMap)) for remote, url := range remoteMap { @@ -190,16 +191,16 @@ func completeGitRemote(env *Env) validArgsFunction { } } -func completeLabel(env *Env) validArgsFunction { +func Label(env *execenv.Env) ValidArgsFunction { return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) { - if err := loadBackend(env)(cmd, args); err != nil { - return completionHandlerError(err) + if err := execenv.LoadBackend(env)(cmd, args); err != nil { + return handleError(err) } defer func() { - _ = env.backend.Close() + _ = env.Backend.Close() }() - labels := env.backend.ValidLabels() + labels := env.Backend.ValidLabels() completions = make([]string, len(labels)) for i, label := range labels { if strings.Contains(label.String(), " ") { @@ -212,7 +213,7 @@ func completeLabel(env *Env) validArgsFunction { } } -func completeLs(env *Env) validArgsFunction { +func Ls(env *execenv.Env) ValidArgsFunction { return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) { if strings.HasPrefix(toComplete, "status:") { completions = append(completions, "status:open\tOpen bugs") @@ -230,11 +231,11 @@ func completeLs(env *Env) validArgsFunction { } if needBackend { - if err := loadBackend(env)(cmd, args); err != nil { - return completionHandlerError(err) + if err := execenv.LoadBackend(env)(cmd, args); err != nil { + return handleError(err) } defer func() { - _ = env.backend.Close() + _ = env.Backend.Close() }() } @@ -242,12 +243,12 @@ func completeLs(env *Env) validArgsFunction { if !strings.HasPrefix(toComplete, key) { continue } - ids := env.backend.AllIdentityIds() + ids := env.Backend.AllIdentityIds() completions = make([]string, len(ids)) for i, id := range ids { - user, err := env.backend.ResolveIdentityExcerpt(id) + user, err := env.Backend.ResolveIdentityExcerpt(id) if err != nil { - return completionHandlerError(err) + return handleError(err) } var handle string if user.Login != "" { @@ -265,7 +266,7 @@ func completeLs(env *Env) validArgsFunction { if !strings.HasPrefix(toComplete, key) { continue } - labels := env.backend.ValidLabels() + labels := env.Backend.ValidLabels() completions = make([]string, len(labels)) for i, label := range labels { if strings.Contains(label.String(), " ") { @@ -290,21 +291,21 @@ func completeLs(env *Env) validArgsFunction { } } -func completeUser(env *Env) validArgsFunction { +func User(env *execenv.Env) ValidArgsFunction { return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) { - if err := loadBackend(env)(cmd, args); err != nil { - return completionHandlerError(err) + if err := execenv.LoadBackend(env)(cmd, args); err != nil { + return handleError(err) } defer func() { - _ = env.backend.Close() + _ = env.Backend.Close() }() - ids := env.backend.AllIdentityIds() + ids := env.Backend.AllIdentityIds() completions = make([]string, len(ids)) for i, id := range ids { - user, err := env.backend.ResolveIdentityExcerpt(id) + user, err := env.Backend.ResolveIdentityExcerpt(id) if err != nil { - return completionHandlerError(err) + return handleError(err) } completions[i] = user.Id.Human() + "\t" + user.DisplayName() } @@ -312,21 +313,21 @@ func completeUser(env *Env) validArgsFunction { } } -func completeUserForQuery(env *Env) validArgsFunction { +func UserForQuery(env *execenv.Env) ValidArgsFunction { return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) { - if err := loadBackend(env)(cmd, args); err != nil { - return completionHandlerError(err) + if err := execenv.LoadBackend(env)(cmd, args); err != nil { + return handleError(err) } defer func() { - _ = env.backend.Close() + _ = env.Backend.Close() }() - ids := env.backend.AllIdentityIds() + ids := env.Backend.AllIdentityIds() completions = make([]string, len(ids)) for i, id := range ids { - user, err := env.backend.ResolveIdentityExcerpt(id) + user, err := env.Backend.ResolveIdentityExcerpt(id) if err != nil { - return completionHandlerError(err) + return handleError(err) } var handle string if user.Login != "" { diff --git a/commands/deselect.go b/commands/deselect.go deleted file mode 100644 index d8c44dd7..00000000 --- a/commands/deselect.go +++ /dev/null @@ -1,36 +0,0 @@ -package commands - -import ( - "github.com/spf13/cobra" - - "github.com/MichaelMure/git-bug/commands/select" -) - -func newDeselectCommand() *cobra.Command { - env := newEnv() - - cmd := &cobra.Command{ - Use: "deselect", - Short: "Clear the implicitly selected bug.", - Example: `git bug select 2f15 -git bug comment -git bug status -git bug deselect -`, - PreRunE: loadBackend(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runDeselect(env) - }), - } - - return cmd -} - -func runDeselect(env *Env) error { - err := _select.Clear(env.backend) - if err != nil { - return err - } - - return nil -} diff --git a/commands/env_testing.go b/commands/env_testing.go deleted file mode 100644 index 1493a190..00000000 --- a/commands/env_testing.go +++ /dev/null @@ -1,40 +0,0 @@ -package commands - -import ( - "bytes" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/MichaelMure/git-bug/cache" - "github.com/MichaelMure/git-bug/repository" -) - -type testEnv struct { - env *Env - out *bytes.Buffer -} - -func newTestEnv(t *testing.T) *testEnv { - t.Helper() - - repo := repository.CreateGoGitTestRepo(t, false) - - buf := new(bytes.Buffer) - - backend, err := cache.NewRepoCache(repo) - require.NoError(t, err) - t.Cleanup(func() { - backend.Close() - }) - - return &testEnv{ - env: &Env{ - repo: repo, - backend: backend, - out: out{Writer: buf}, - err: out{Writer: buf}, - }, - out: buf, - } -} diff --git a/commands/env.go b/commands/execenv/env.go index 11b91c4b..a63f835a 100644 --- a/commands/env.go +++ b/commands/execenv/env.go @@ -1,4 +1,4 @@ -package commands +package execenv import ( "fmt" @@ -14,24 +14,43 @@ import ( "github.com/MichaelMure/git-bug/util/interrupt" ) +const RootCommandName = "git-bug" + const gitBugNamespace = "git-bug" // Env is the environment of a command type Env struct { - repo repository.ClockedRepo - backend *cache.RepoCache - out out - err out + Repo repository.ClockedRepo + Backend *cache.RepoCache + Out Out + Err Out } -func newEnv() *Env { +func NewEnv() *Env { return &Env{ - repo: nil, - out: out{Writer: os.Stdout}, - err: out{Writer: os.Stderr}, + Repo: nil, + Out: out{Writer: os.Stdout}, + Err: out{Writer: os.Stderr}, } } +type Out interface { + io.Writer + Printf(format string, a ...interface{}) + Print(a ...interface{}) + Println(a ...interface{}) + + // String returns what have been written in the output before, as a string. + // This only works in test scenario. + String() string + // Bytes returns what have been written in the output before, as []byte. + // This only works in test scenario. + Bytes() []byte + // Reset clear what has been recorded as written in the output before. + // This only works in test scenario. + Reset() +} + type out struct { io.Writer } @@ -48,17 +67,29 @@ func (o out) Println(a ...interface{}) { _, _ = fmt.Fprintln(o, a...) } -// loadRepo is a pre-run function that load the repository for use in a command -func loadRepo(env *Env) func(*cobra.Command, []string) error { +func (o out) String() string { + panic("only work with a test env") +} + +func (o out) Bytes() []byte { + panic("only work with a test env") +} + +func (o out) Reset() { + panic("only work with a test env") +} + +// LoadRepo is a pre-run function that load the repository for use in a command +func LoadRepo(env *Env) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { cwd, err := os.Getwd() if err != nil { return fmt.Errorf("unable to get the current working directory: %q", err) } - env.repo, err = repository.OpenGoGitRepo(cwd, gitBugNamespace, []repository.ClockLoader{bug.ClockLoader}) + env.Repo, err = repository.OpenGoGitRepo(cwd, gitBugNamespace, []repository.ClockLoader{bug.ClockLoader}) if err == repository.ErrNotARepo { - return fmt.Errorf("%s must be run from within a git repo", rootCommandName) + return fmt.Errorf("%s must be run from within a git Repo", RootCommandName) } if err != nil { @@ -69,17 +100,17 @@ func loadRepo(env *Env) func(*cobra.Command, []string) error { } } -// loadRepoEnsureUser is the same as loadRepo, but also ensure that the user has configured +// LoadRepoEnsureUser is the same as LoadRepo, but also ensure that the user has configured // an identity. Use this pre-run function when an error after using the configured user won't // do. -func loadRepoEnsureUser(env *Env) func(*cobra.Command, []string) error { +func LoadRepoEnsureUser(env *Env) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { - err := loadRepo(env)(cmd, args) + err := LoadRepo(env)(cmd, args) if err != nil { return err } - _, err = identity.GetUserIdentity(env.repo) + _, err = identity.GetUserIdentity(env.Repo) if err != nil { return err } @@ -88,25 +119,25 @@ func loadRepoEnsureUser(env *Env) func(*cobra.Command, []string) error { } } -// loadBackend is a pre-run function that load the repository and the backend for use in a command -// When using this function you also need to use closeBackend as a post-run -func loadBackend(env *Env) func(*cobra.Command, []string) error { +// LoadBackend is a pre-run function that load the repository and the Backend for use in a command +// When using this function you also need to use CloseBackend as a post-run +func LoadBackend(env *Env) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { - err := loadRepo(env)(cmd, args) + err := LoadRepo(env)(cmd, args) if err != nil { return err } - env.backend, err = cache.NewRepoCache(env.repo) + env.Backend, err = cache.NewRepoCache(env.Repo) if err != nil { return err } cleaner := func(env *Env) interrupt.CleanerFunc { return func() error { - if env.backend != nil { - err := env.backend.Close() - env.backend = nil + if env.Backend != nil { + err := env.Backend.Close() + env.Backend = nil return err } return nil @@ -119,17 +150,17 @@ func loadBackend(env *Env) func(*cobra.Command, []string) error { } } -// loadBackendEnsureUser is the same as loadBackend, but also ensure that the user has configured +// LoadBackendEnsureUser is the same as LoadBackend, but also ensure that the user has configured // an identity. Use this pre-run function when an error after using the configured user won't // do. -func loadBackendEnsureUser(env *Env) func(*cobra.Command, []string) error { +func LoadBackendEnsureUser(env *Env) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { - err := loadBackend(env)(cmd, args) + err := LoadBackend(env)(cmd, args) if err != nil { return err } - _, err = identity.GetUserIdentity(env.repo) + _, err = identity.GetUserIdentity(env.Repo) if err != nil { return err } @@ -138,18 +169,18 @@ func loadBackendEnsureUser(env *Env) func(*cobra.Command, []string) error { } } -// closeBackend is a wrapper for a RunE function that will close the backend properly +// CloseBackend is a wrapper for a RunE function that will close the Backend properly // if it has been opened. // This wrapper style is necessary because a Cobra PostE function does not run if RunE return an error. -func closeBackend(env *Env, runE func(cmd *cobra.Command, args []string) error) func(*cobra.Command, []string) error { +func CloseBackend(env *Env, runE func(cmd *cobra.Command, args []string) error) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { errRun := runE(cmd, args) - if env.backend == nil { + if env.Backend == nil { return nil } - err := env.backend.Close() - env.backend = nil + err := env.Backend.Close() + env.Backend = nil // prioritize the RunE error if errRun != nil { diff --git a/commands/execenv/env_testing.go b/commands/execenv/env_testing.go new file mode 100644 index 00000000..7d9fbd60 --- /dev/null +++ b/commands/execenv/env_testing.go @@ -0,0 +1,48 @@ +package execenv + +import ( + "bytes" + "fmt" + "testing" + + "github.com/MichaelMure/git-bug/cache" + "github.com/MichaelMure/git-bug/repository" + "github.com/stretchr/testify/require" +) + +type TestOut struct { + *bytes.Buffer +} + +func (te *TestOut) Printf(format string, a ...interface{}) { + _, _ = fmt.Fprintf(te.Buffer, format, a...) +} + +func (te *TestOut) Print(a ...interface{}) { + _, _ = fmt.Fprint(te.Buffer, a...) +} + +func (te *TestOut) Println(a ...interface{}) { + _, _ = fmt.Fprintln(te.Buffer, a...) +} + +func NewTestEnv(t *testing.T) *Env { + t.Helper() + + repo := repository.CreateGoGitTestRepo(t, false) + + buf := new(bytes.Buffer) + + backend, err := cache.NewRepoCache(repo) + require.NoError(t, err) + t.Cleanup(func() { + backend.Close() + }) + + return &Env{ + Repo: repo, + Backend: backend, + Out: &TestOut{buf}, + Err: &TestOut{buf}, + } +} diff --git a/commands/golden_test.go b/commands/golden_test.go deleted file mode 100644 index 9fcee0d6..00000000 --- a/commands/golden_test.go +++ /dev/null @@ -1,5 +0,0 @@ -package commands - -import "flag" - -var update = flag.Bool("update", false, "update golden files") diff --git a/commands/input/input.go b/commands/input/input.go index e9c1be1b..ee343cd8 100644 --- a/commands/input/input.go +++ b/commands/input/input.go @@ -25,7 +25,7 @@ const messageFilename = "BUG_MESSAGE_EDITMSG" // ErrEmptyMessage is returned when the required message has not been entered var ErrEmptyMessage = errors.New("empty message") -// ErrEmptyMessage is returned when the required title has not been entered +// ErrEmptyTitle is returned when the required title has not been entered var ErrEmptyTitle = errors.New("empty title") const bugTitleCommentTemplate = `%s%s diff --git a/commands/label.go b/commands/label.go index ff4d0151..70090d26 100644 --- a/commands/label.go +++ b/commands/label.go @@ -3,39 +3,32 @@ package commands import ( "github.com/spf13/cobra" - _select "github.com/MichaelMure/git-bug/commands/select" + "github.com/MichaelMure/git-bug/commands/execenv" ) func newLabelCommand() *cobra.Command { - env := newEnv() + env := execenv.NewEnv() cmd := &cobra.Command{ - Use: "label [ID]", - Short: "Display, add or remove labels to/from a bug.", - PreRunE: loadBackend(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runLabel(env, args) + Use: "label", + Short: "List valid labels", + Long: `List valid labels. + +Note: in the future, a proper label policy could be implemented where valid labels are defined in a configuration file. Until that, the default behavior is to return the list of labels already used.`, + PreRunE: execenv.LoadBackend(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runLabel(env) }), - ValidArgsFunction: completeBug(env), } - cmd.AddCommand(newLabelAddCommand()) - cmd.AddCommand(newLabelLsCommand()) - cmd.AddCommand(newLabelRmCommand()) - return cmd } -func runLabel(env *Env, args []string) error { - b, _, err := _select.ResolveBug(env.backend, args) - if err != nil { - return err - } - - snap := b.Snapshot() +func runLabel(env *execenv.Env) error { + labels := env.Backend.ValidLabels() - for _, l := range snap.Labels { - env.out.Println(l) + for _, l := range labels { + env.Out.Println(l) } return nil diff --git a/commands/label_add.go b/commands/label_add.go deleted file mode 100644 index 65439a4a..00000000 --- a/commands/label_add.go +++ /dev/null @@ -1,45 +0,0 @@ -package commands - -import ( - "github.com/spf13/cobra" - - _select "github.com/MichaelMure/git-bug/commands/select" - "github.com/MichaelMure/git-bug/util/text" -) - -func newLabelAddCommand() *cobra.Command { - env := newEnv() - - cmd := &cobra.Command{ - Use: "add [ID] LABEL...", - Short: "Add a label to a bug.", - PreRunE: loadBackendEnsureUser(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runLabelAdd(env, args) - }), - ValidArgsFunction: completeBugAndLabels(env, true), - } - - return cmd -} - -func runLabelAdd(env *Env, args []string) error { - b, args, err := _select.ResolveBug(env.backend, args) - if err != nil { - return err - } - - added := args - - changes, _, err := b.ChangeLabels(text.CleanupOneLineArray(added), nil) - - for _, change := range changes { - env.out.Println(change) - } - - if err != nil { - return err - } - - return b.Commit() -} diff --git a/commands/label_ls.go b/commands/label_ls.go deleted file mode 100644 index 242eb00c..00000000 --- a/commands/label_ls.go +++ /dev/null @@ -1,33 +0,0 @@ -package commands - -import ( - "github.com/spf13/cobra" -) - -func newLabelLsCommand() *cobra.Command { - env := newEnv() - - cmd := &cobra.Command{ - Use: "ls", - Short: "List valid labels.", - Long: `List valid labels. - -Note: in the future, a proper label policy could be implemented where valid labels are defined in a configuration file. Until that, the default behavior is to return the list of labels already used.`, - PreRunE: loadBackend(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runLabelLs(env) - }), - } - - return cmd -} - -func runLabelLs(env *Env) error { - labels := env.backend.ValidLabels() - - for _, l := range labels { - env.out.Println(l) - } - - return nil -} diff --git a/commands/label_rm.go b/commands/label_rm.go deleted file mode 100644 index 3f4e1958..00000000 --- a/commands/label_rm.go +++ /dev/null @@ -1,45 +0,0 @@ -package commands - -import ( - "github.com/spf13/cobra" - - _select "github.com/MichaelMure/git-bug/commands/select" - "github.com/MichaelMure/git-bug/util/text" -) - -func newLabelRmCommand() *cobra.Command { - env := newEnv() - - cmd := &cobra.Command{ - Use: "rm [ID] LABEL...", - Short: "Remove a label from a bug.", - PreRunE: loadBackend(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runLabelRm(env, args) - }), - ValidArgsFunction: completeBugAndLabels(env, false), - } - - return cmd -} - -func runLabelRm(env *Env, args []string) error { - b, args, err := _select.ResolveBug(env.backend, args) - if err != nil { - return err - } - - removed := args - - changes, _, err := b.ChangeLabels(nil, text.CleanupOneLineArray(removed)) - - for _, change := range changes { - env.out.Println(change) - } - - if err != nil { - return err - } - - return b.Commit() -} diff --git a/commands/ls-id.go b/commands/ls-id.go deleted file mode 100644 index 31107e87..00000000 --- a/commands/ls-id.go +++ /dev/null @@ -1,42 +0,0 @@ -package commands - -import ( - "github.com/spf13/cobra" -) - -func newLsIdCommand() *cobra.Command { - env := newEnv() - - cmd := &cobra.Command{ - Use: "ls-id [PREFIX]", - Short: "List bug identifiers.", - PreRunE: loadBackend(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runLsId(env, args) - }), - Deprecated: `and will be removed in v1.0. - -Please use the "ls" command which allows filtering and sorting of the resulting -list of ids. The following example would print a new-line separated list containing -the ids of all open bugs: -git-bug ls --format id --status open -`, - } - - return cmd -} - -func runLsId(env *Env, args []string) error { - var prefix = "" - if len(args) != 0 { - prefix = args[0] - } - - for _, id := range env.backend.AllBugsIds() { - if prefix == "" || id.HasPrefix(prefix) { - env.out.Println(id) - } - } - - return nil -} diff --git a/commands/ls-labels.go b/commands/ls-labels.go deleted file mode 100644 index 00fc3fe6..00000000 --- a/commands/ls-labels.go +++ /dev/null @@ -1,29 +0,0 @@ -package commands - -import ( - "github.com/spf13/cobra" -) - -func newLsLabelCommand() *cobra.Command { - env := newEnv() - - cmd := &cobra.Command{ - Use: "ls-label", - Short: "List valid labels.", - Long: `List valid labels. - -Note: in the future, a proper label policy could be implemented where valid labels are defined in a configuration file. Until that, the default behavior is to return the list of labels already used.`, - PreRunE: loadBackend(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runLabelLs(env) - }), - Deprecated: ` and will be removed in v1.0. - -The functionality provided by this command is now provided by -the following (equivalent) command: -git-bug label ls -`, - } - - return cmd -} diff --git a/commands/pull.go b/commands/pull.go index 29c9f034..2e2639e1 100644 --- a/commands/pull.go +++ b/commands/pull.go @@ -5,26 +5,28 @@ import ( "github.com/spf13/cobra" + "github.com/MichaelMure/git-bug/commands/completion" + "github.com/MichaelMure/git-bug/commands/execenv" "github.com/MichaelMure/git-bug/entity" ) func newPullCommand() *cobra.Command { - env := newEnv() + env := execenv.NewEnv() cmd := &cobra.Command{ Use: "pull [REMOTE]", - Short: "Pull bugs update from a git remote.", - PreRunE: loadBackend(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { + Short: "Pull updates from a git remote", + PreRunE: execenv.LoadBackend(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { return runPull(env, args) }), - ValidArgsFunction: completeGitRemote(env), + ValidArgsFunction: completion.GitRemote(env), } return cmd } -func runPull(env *Env, args []string) error { +func runPull(env *execenv.Env, args []string) error { if len(args) > 1 { return errors.New("Only pulling from one remote at a time is supported") } @@ -34,24 +36,24 @@ func runPull(env *Env, args []string) error { remote = args[0] } - env.out.Println("Fetching remote ...") + env.Out.Println("Fetching remote ...") - stdout, err := env.backend.Fetch(remote) + stdout, err := env.Backend.Fetch(remote) if err != nil { return err } - env.out.Println(stdout) + env.Out.Println(stdout) - env.out.Println("Merging data ...") + env.Out.Println("Merging data ...") - for result := range env.backend.MergeAll(remote) { + for result := range env.Backend.MergeAll(remote) { if result.Err != nil { - env.err.Println(result.Err) + env.Err.Println(result.Err) } if result.Status != entity.MergeStatusNothing { - env.out.Printf("%s: %s\n", result.Id.Human(), result) + env.Out.Printf("%s: %s\n", result.Id.Human(), result) } } diff --git a/commands/push.go b/commands/push.go index adba6bef..d45e301a 100644 --- a/commands/push.go +++ b/commands/push.go @@ -4,25 +4,28 @@ import ( "errors" "github.com/spf13/cobra" + + "github.com/MichaelMure/git-bug/commands/completion" + "github.com/MichaelMure/git-bug/commands/execenv" ) func newPushCommand() *cobra.Command { - env := newEnv() + env := execenv.NewEnv() cmd := &cobra.Command{ Use: "push [REMOTE]", - Short: "Push bugs update to a git remote.", - PreRunE: loadBackend(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { + Short: "Push updates to a git remote", + PreRunE: execenv.LoadBackend(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { return runPush(env, args) }), - ValidArgsFunction: completeGitRemote(env), + ValidArgsFunction: completion.GitRemote(env), } return cmd } -func runPush(env *Env, args []string) error { +func runPush(env *execenv.Env, args []string) error { if len(args) > 1 { return errors.New("Only pushing to one remote at a time is supported") } @@ -32,12 +35,12 @@ func runPush(env *Env, args []string) error { remote = args[0] } - stdout, err := env.backend.Push(remote) + stdout, err := env.Backend.Push(remote) if err != nil { return err } - env.out.Println(stdout) + env.Out.Println(stdout) return nil } diff --git a/commands/rm.go b/commands/rm.go deleted file mode 100644 index 2e1d924d..00000000 --- a/commands/rm.go +++ /dev/null @@ -1,43 +0,0 @@ -package commands - -import ( - "errors" - - "github.com/spf13/cobra" -) - -func newRmCommand() *cobra.Command { - env := newEnv() - - cmd := &cobra.Command{ - Use: "rm ID", - Short: "Remove an existing bug.", - Long: "Remove an existing bug in the local repository. Note removing bugs that were imported from bridges will not remove the bug on the remote, and will only remove the local copy of the bug.", - PreRunE: loadBackendEnsureUser(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runRm(env, args) - }), - ValidArgsFunction: completeBug(env), - } - - flags := cmd.Flags() - flags.SortFlags = false - - return cmd -} - -func runRm(env *Env, args []string) (err error) { - if len(args) == 0 { - return errors.New("you must provide a bug prefix to remove") - } - - err = env.backend.RemoveBug(args[0]) - - if err != nil { - return - } - - env.out.Printf("bug %s removed\n", args[0]) - - return -} diff --git a/commands/rm_test.go b/commands/rm_test.go deleted file mode 100644 index 0156bbd4..00000000 --- a/commands/rm_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package commands - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestRm(t *testing.T) { - testEnv, bugID := newTestEnvAndBug(t) - - exp := "bug " + bugID + " removed\n" - - require.NoError(t, runRm(testEnv.env, []string{bugID})) - require.Equal(t, exp, testEnv.out.String()) - testEnv.out.Reset() -} diff --git a/commands/root.go b/commands/root.go index e012bd83..b28b77b8 100644 --- a/commands/root.go +++ b/commands/root.go @@ -6,9 +6,13 @@ import ( "os" "github.com/spf13/cobra" -) -const rootCommandName = "git-bug" + "github.com/MichaelMure/git-bug/commands/bridge" + usercmd "github.com/MichaelMure/git-bug/commands/user" + + "github.com/MichaelMure/git-bug/commands/bug" + "github.com/MichaelMure/git-bug/commands/execenv" +) // These variables are initialized externally during the build. See the Makefile. var GitCommit string @@ -17,8 +21,8 @@ var GitExactTag string func NewRootCommand() *cobra.Command { cmd := &cobra.Command{ - Use: rootCommandName, - Short: "A bug tracker embedded in Git.", + Use: execenv.RootCommandName, + Short: "A bug tracker embedded in Git", Long: `git-bug is a bug tracker embedded in git. git-bug use git objects to store the bug tracking separated from the files @@ -52,26 +56,32 @@ the same git remote you are already using to collaborate with other people. DisableAutoGenTag: true, } - cmd.AddCommand(newAddCommand()) - cmd.AddCommand(newBridgeCommand()) + const entityGroup = "entity" + const uiGroup = "ui" + const remoteGroup = "remote" + + cmd.AddGroup(&cobra.Group{ID: entityGroup, Title: "Entities"}) + cmd.AddGroup(&cobra.Group{ID: uiGroup, Title: "User interfaces"}) + cmd.AddGroup(&cobra.Group{ID: remoteGroup, Title: "Interaction with the outside world"}) + + addCmdWithGroup := func(child *cobra.Command, groupID string) { + cmd.AddCommand(child) + child.GroupID = groupID + } + + addCmdWithGroup(bugcmd.NewBugCommand(), entityGroup) + addCmdWithGroup(usercmd.NewUserCommand(), entityGroup) + addCmdWithGroup(newLabelCommand(), entityGroup) + + addCmdWithGroup(newTermUICommand(), uiGroup) + addCmdWithGroup(newWebUICommand(), uiGroup) + + addCmdWithGroup(newPullCommand(), remoteGroup) + addCmdWithGroup(newPushCommand(), remoteGroup) + addCmdWithGroup(bridgecmd.NewBridgeCommand(), remoteGroup) + cmd.AddCommand(newCommandsCommand()) - cmd.AddCommand(newCommentCommand()) - cmd.AddCommand(newDeselectCommand()) - cmd.AddCommand(newLabelCommand()) - cmd.AddCommand(newLsCommand()) - cmd.AddCommand(newLsIdCommand()) - cmd.AddCommand(newLsLabelCommand()) - cmd.AddCommand(newPullCommand()) - cmd.AddCommand(newPushCommand()) - cmd.AddCommand(newRmCommand()) - cmd.AddCommand(newSelectCommand()) - cmd.AddCommand(newShowCommand()) - cmd.AddCommand(newStatusCommand()) - cmd.AddCommand(newTermUICommand()) - cmd.AddCommand(newTitleCommand()) - cmd.AddCommand(newUserCommand()) cmd.AddCommand(newVersionCommand()) - cmd.AddCommand(newWebUICommand()) return cmd } diff --git a/commands/select.go b/commands/select.go deleted file mode 100644 index f9e6ece7..00000000 --- a/commands/select.go +++ /dev/null @@ -1,60 +0,0 @@ -package commands - -import ( - "errors" - - "github.com/spf13/cobra" - - _select "github.com/MichaelMure/git-bug/commands/select" -) - -func newSelectCommand() *cobra.Command { - env := newEnv() - - cmd := &cobra.Command{ - Use: "select ID", - Short: "Select a bug for implicit use in future commands.", - Example: `git bug select 2f15 -git bug comment -git bug status -`, - Long: `Select a bug for implicit use in future commands. - -This command allows you to omit any bug ID argument, for example: - git bug show -instead of - git bug show 2f153ca - -The complementary command is "git bug deselect" performing the opposite operation. -`, - PreRunE: loadBackend(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runSelect(env, args) - }), - ValidArgsFunction: completeBug(env), - } - - return cmd -} - -func runSelect(env *Env, args []string) error { - if len(args) == 0 { - return errors.New("You must provide a bug id") - } - - prefix := args[0] - - b, err := env.backend.ResolveBugPrefix(prefix) - if err != nil { - return err - } - - err = _select.Select(env.backend, b.Id()) - if err != nil { - return err - } - - env.out.Printf("selected bug %s: %s\n", b.Id().Human(), b.Snapshot().Title) - - return nil -} diff --git a/commands/status.go b/commands/status.go deleted file mode 100644 index c3e860b6..00000000 --- a/commands/status.go +++ /dev/null @@ -1,38 +0,0 @@ -package commands - -import ( - _select "github.com/MichaelMure/git-bug/commands/select" - "github.com/spf13/cobra" -) - -func newStatusCommand() *cobra.Command { - env := newEnv() - - cmd := &cobra.Command{ - Use: "status [ID]", - Short: "Display or change a bug status.", - PreRunE: loadBackend(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runStatus(env, args) - }), - ValidArgsFunction: completeBug(env), - } - - cmd.AddCommand(newStatusCloseCommand()) - cmd.AddCommand(newStatusOpenCommand()) - - return cmd -} - -func runStatus(env *Env, args []string) error { - b, args, err := _select.ResolveBug(env.backend, args) - if err != nil { - return err - } - - snap := b.Snapshot() - - env.out.Println(snap.Status) - - return nil -} diff --git a/commands/status_close.go b/commands/status_close.go deleted file mode 100644 index 8541aa0b..00000000 --- a/commands/status_close.go +++ /dev/null @@ -1,35 +0,0 @@ -package commands - -import ( - _select "github.com/MichaelMure/git-bug/commands/select" - "github.com/spf13/cobra" -) - -func newStatusCloseCommand() *cobra.Command { - env := newEnv() - - cmd := &cobra.Command{ - Use: "close [ID]", - Short: "Mark a bug as closed.", - PreRunE: loadBackendEnsureUser(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runStatusClose(env, args) - }), - } - - return cmd -} - -func runStatusClose(env *Env, args []string) error { - b, args, err := _select.ResolveBug(env.backend, args) - if err != nil { - return err - } - - _, err = b.Close() - if err != nil { - return err - } - - return b.Commit() -} diff --git a/commands/status_open.go b/commands/status_open.go deleted file mode 100644 index ee6bd27a..00000000 --- a/commands/status_open.go +++ /dev/null @@ -1,35 +0,0 @@ -package commands - -import ( - _select "github.com/MichaelMure/git-bug/commands/select" - "github.com/spf13/cobra" -) - -func newStatusOpenCommand() *cobra.Command { - env := newEnv() - - cmd := &cobra.Command{ - Use: "open [ID]", - Short: "Mark a bug as open.", - PreRunE: loadBackendEnsureUser(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runStatusOpen(env, args) - }), - } - - return cmd -} - -func runStatusOpen(env *Env, args []string) error { - b, args, err := _select.ResolveBug(env.backend, args) - if err != nil { - return err - } - - _, err = b.Open() - if err != nil { - return err - } - - return b.Commit() -} diff --git a/commands/termui.go b/commands/termui.go index 4df6cdaf..1cfdd8f3 100644 --- a/commands/termui.go +++ b/commands/termui.go @@ -3,18 +3,19 @@ package commands import ( "github.com/spf13/cobra" + "github.com/MichaelMure/git-bug/commands/execenv" "github.com/MichaelMure/git-bug/termui" ) func newTermUICommand() *cobra.Command { - env := newEnv() + env := execenv.NewEnv() cmd := &cobra.Command{ Use: "termui", Aliases: []string{"tui"}, - Short: "Launch the terminal UI.", - PreRunE: loadBackendEnsureUser(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { + Short: "Launch the terminal UI", + PreRunE: execenv.LoadBackendEnsureUser(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { return runTermUI(env) }), } @@ -22,6 +23,6 @@ func newTermUICommand() *cobra.Command { return cmd } -func runTermUI(env *Env) error { - return termui.Run(env.backend) +func runTermUI(env *execenv.Env) error { + return termui.Run(env.Backend) } diff --git a/commands/title.go b/commands/title.go deleted file mode 100644 index f99c6eff..00000000 --- a/commands/title.go +++ /dev/null @@ -1,37 +0,0 @@ -package commands - -import ( - _select "github.com/MichaelMure/git-bug/commands/select" - "github.com/spf13/cobra" -) - -func newTitleCommand() *cobra.Command { - env := newEnv() - - cmd := &cobra.Command{ - Use: "title [ID]", - Short: "Display or change a title of a bug.", - PreRunE: loadBackend(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runTitle(env, args) - }), - ValidArgsFunction: completeBug(env), - } - - cmd.AddCommand(newTitleEditCommand()) - - return cmd -} - -func runTitle(env *Env, args []string) error { - b, args, err := _select.ResolveBug(env.backend, args) - if err != nil { - return err - } - - snap := b.Snapshot() - - env.out.Println(snap.Title) - - return nil -} diff --git a/commands/title_edit.go b/commands/title_edit.go deleted file mode 100644 index a1ba0324..00000000 --- a/commands/title_edit.go +++ /dev/null @@ -1,74 +0,0 @@ -package commands - -import ( - "github.com/spf13/cobra" - - "github.com/MichaelMure/git-bug/commands/input" - _select "github.com/MichaelMure/git-bug/commands/select" - "github.com/MichaelMure/git-bug/util/text" -) - -type titleEditOptions struct { - title string - nonInteractive bool -} - -func newTitleEditCommand() *cobra.Command { - env := newEnv() - options := titleEditOptions{} - - cmd := &cobra.Command{ - Use: "edit [ID]", - Short: "Edit a title of a bug.", - PreRunE: loadBackendEnsureUser(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runTitleEdit(env, options, args) - }), - ValidArgsFunction: completeBug(env), - } - - flags := cmd.Flags() - flags.SortFlags = false - - flags.StringVarP(&options.title, "title", "t", "", - "Provide a title to describe the issue", - ) - flags.BoolVar(&options.nonInteractive, "non-interactive", false, "Do not ask for user input") - - return cmd -} - -func runTitleEdit(env *Env, opts titleEditOptions, args []string) error { - b, args, err := _select.ResolveBug(env.backend, args) - if err != nil { - return err - } - - snap := b.Snapshot() - - if opts.title == "" { - if opts.nonInteractive { - env.err.Println("No title given. Use -m or -F option to specify a title. Aborting.") - return nil - } - opts.title, err = input.BugTitleEditorInput(env.repo, snap.Title) - if err == input.ErrEmptyTitle { - env.out.Println("Empty title, aborting.") - return nil - } - if err != nil { - return err - } - } - - if opts.title == snap.Title { - env.err.Println("No change, aborting.") - } - - _, err = b.SetTitle(text.CleanupOneLine(opts.title)) - if err != nil { - return err - } - - return b.Commit() -} diff --git a/commands/user.go b/commands/user.go deleted file mode 100644 index 0fe3be4d..00000000 --- a/commands/user.go +++ /dev/null @@ -1,110 +0,0 @@ -package commands - -import ( - "errors" - "fmt" - "strings" - - "github.com/spf13/cobra" - - "github.com/MichaelMure/git-bug/cache" -) - -type userOptions struct { - fields string -} - -func newUserCommand() *cobra.Command { - env := newEnv() - options := userOptions{} - - cmd := &cobra.Command{ - Use: "user [USER-ID]", - Short: "Display or change the user identity.", - PreRunE: loadBackendEnsureUser(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runUser(env, options, args) - }), - ValidArgsFunction: completeUser(env), - } - - cmd.AddCommand(newUserAdoptCommand()) - cmd.AddCommand(newUserCreateCommand()) - cmd.AddCommand(newUserLsCommand()) - - flags := cmd.Flags() - flags.SortFlags = false - - fields := []string{"email", "humanId", "id", "lastModification", "lastModificationLamports", "login", "metadata", "name"} - flags.StringVarP(&options.fields, "field", "f", "", - "Select field to display. Valid values are ["+strings.Join(fields, ",")+"]") - cmd.RegisterFlagCompletionFunc("field", completeFrom(fields)) - - return cmd -} - -func runUser(env *Env, opts userOptions, args []string) error { - if len(args) > 1 { - return errors.New("only one identity can be displayed at a time") - } - - var id *cache.IdentityCache - var err error - if len(args) == 1 { - id, err = env.backend.ResolveIdentityPrefix(args[0]) - } else { - id, err = env.backend.GetUserIdentity() - } - - if err != nil { - return err - } - - if opts.fields != "" { - switch opts.fields { - case "email": - env.out.Printf("%s\n", id.Email()) - case "login": - env.out.Printf("%s\n", id.Login()) - case "humanId": - env.out.Printf("%s\n", id.Id().Human()) - case "id": - env.out.Printf("%s\n", id.Id()) - case "lastModification": - env.out.Printf("%s\n", id.LastModification(). - Time().Format("Mon Jan 2 15:04:05 2006 +0200")) - case "lastModificationLamport": - for name, t := range id.LastModificationLamports() { - env.out.Printf("%s\n%d\n", name, t) - } - case "metadata": - for key, value := range id.ImmutableMetadata() { - env.out.Printf("%s\n%s\n", key, value) - } - case "name": - env.out.Printf("%s\n", id.Name()) - - default: - return fmt.Errorf("\nUnsupported field: %s\n", opts.fields) - } - - return nil - } - - env.out.Printf("Id: %s\n", id.Id()) - env.out.Printf("Name: %s\n", id.Name()) - env.out.Printf("Email: %s\n", id.Email()) - env.out.Printf("Login: %s\n", id.Login()) - env.out.Printf("Last modification: %s\n", id.LastModification().Time().Format("Mon Jan 2 15:04:05 2006 +0200")) - env.out.Printf("Last moditication (lamport):\n") - for name, t := range id.LastModificationLamports() { - env.out.Printf("\t%s: %d", name, t) - } - env.out.Println("Metadata:") - for key, value := range id.ImmutableMetadata() { - env.out.Printf(" %s --> %s\n", key, value) - } - // env.out.Printf("Protected: %v\n", id.IsProtected()) - - return nil -} diff --git a/commands/user/user.go b/commands/user/user.go new file mode 100644 index 00000000..191fb828 --- /dev/null +++ b/commands/user/user.go @@ -0,0 +1,89 @@ +package usercmd + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + + json2 "github.com/MichaelMure/git-bug/commands/cmdjson" + + "github.com/MichaelMure/git-bug/cache" + "github.com/MichaelMure/git-bug/commands/completion" + "github.com/MichaelMure/git-bug/commands/execenv" + "github.com/MichaelMure/git-bug/util/colors" +) + +type userOptions struct { + format string +} + +func NewUserCommand() *cobra.Command { + env := execenv.NewEnv() + options := userOptions{} + + cmd := &cobra.Command{ + Use: "user", + Short: "List identities", + PreRunE: execenv.LoadBackend(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runUser(env, options) + }), + } + + cmd.AddCommand(newUserNewCommand()) + cmd.AddCommand(newUserShowCommand()) + cmd.AddCommand(newUserAdoptCommand()) + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringVarP(&options.format, "format", "f", "default", + "Select the output formatting style. Valid values are [default,json]") + cmd.RegisterFlagCompletionFunc("format", completion.From([]string{"default", "json"})) + + return cmd +} + +func runUser(env *execenv.Env, opts userOptions) error { + ids := env.Backend.AllIdentityIds() + var users []*cache.IdentityExcerpt + for _, id := range ids { + user, err := env.Backend.ResolveIdentityExcerpt(id) + if err != nil { + return err + } + users = append(users, user) + } + + switch opts.format { + case "json": + return userJsonFormatter(env, users) + case "default": + return userDefaultFormatter(env, users) + default: + return fmt.Errorf("unknown format %s", opts.format) + } +} + +func userDefaultFormatter(env *execenv.Env, users []*cache.IdentityExcerpt) error { + for _, user := range users { + env.Out.Printf("%s %s\n", + colors.Cyan(user.Id.Human()), + user.DisplayName(), + ) + } + + return nil +} + +func userJsonFormatter(env *execenv.Env, users []*cache.IdentityExcerpt) error { + jsonUsers := make([]json2.Identity, len(users)) + for i, user := range users { + jsonUsers[i] = json2.NewIdentityFromExcerpt(user) + } + + jsonObject, _ := json.MarshalIndent(jsonUsers, "", " ") + env.Out.Printf("%s\n", jsonObject) + return nil +} diff --git a/commands/user/user_adopt.go b/commands/user/user_adopt.go new file mode 100644 index 00000000..f5944053 --- /dev/null +++ b/commands/user/user_adopt.go @@ -0,0 +1,43 @@ +package usercmd + +import ( + "github.com/spf13/cobra" + + "github.com/MichaelMure/git-bug/commands/completion" + "github.com/MichaelMure/git-bug/commands/execenv" +) + +func newUserAdoptCommand() *cobra.Command { + env := execenv.NewEnv() + + cmd := &cobra.Command{ + Use: "adopt USER_ID", + Short: "Adopt an existing identity as your own", + Args: cobra.ExactArgs(1), + PreRunE: execenv.LoadBackend(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runUserAdopt(env, args) + }), + ValidArgsFunction: completion.User(env), + } + + return cmd +} + +func runUserAdopt(env *execenv.Env, args []string) error { + prefix := args[0] + + i, err := env.Backend.ResolveIdentityPrefix(prefix) + if err != nil { + return err + } + + err = env.Backend.SetUserIdentity(i) + if err != nil { + return err + } + + env.Out.Printf("Your identity is now: %s\n", i.DisplayName()) + + return nil +} diff --git a/commands/user_create.go b/commands/user/user_new.go index 6941cff5..d7224512 100644 --- a/commands/user_create.go +++ b/commands/user/user_new.go @@ -1,28 +1,29 @@ -package commands +package usercmd import ( "github.com/spf13/cobra" + "github.com/MichaelMure/git-bug/commands/execenv" "github.com/MichaelMure/git-bug/commands/input" ) -type createUserOptions struct { +type userNewOptions struct { name string email string avatarURL string nonInteractive bool } -func newUserCreateCommand() *cobra.Command { - env := newEnv() +func newUserNewCommand() *cobra.Command { + env := execenv.NewEnv() - options := createUserOptions{} + options := userNewOptions{} cmd := &cobra.Command{ - Use: "create", - Short: "Create a new identity.", - PreRunE: loadBackend(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runUserCreate(env, options) + Use: "new", + Short: "Create a new identity", + PreRunE: execenv.LoadBackend(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runUserNew(env, options) }), } @@ -35,10 +36,10 @@ func newUserCreateCommand() *cobra.Command { return cmd } -func runUserCreate(env *Env, opts createUserOptions) error { +func runUserNew(env *execenv.Env, opts userNewOptions) error { if !opts.nonInteractive && opts.name == "" { - preName, err := env.backend.GetUserName() + preName, err := env.Backend.GetUserName() if err != nil { return err } @@ -49,7 +50,7 @@ func runUserCreate(env *Env, opts createUserOptions) error { } if !opts.nonInteractive && opts.email == "" { - preEmail, err := env.backend.GetUserEmail() + preEmail, err := env.Backend.GetUserEmail() if err != nil { return err } @@ -68,7 +69,7 @@ func runUserCreate(env *Env, opts createUserOptions) error { } } - id, err := env.backend.NewIdentityRaw(opts.name, opts.email, "", opts.avatarURL, nil, nil) + id, err := env.Backend.NewIdentityRaw(opts.name, opts.email, "", opts.avatarURL, nil, nil) if err != nil { return err } @@ -78,20 +79,20 @@ func runUserCreate(env *Env, opts createUserOptions) error { return err } - set, err := env.backend.IsUserIdentitySet() + set, err := env.Backend.IsUserIdentitySet() if err != nil { return err } if !set { - err = env.backend.SetUserIdentity(id) + err = env.Backend.SetUserIdentity(id) if err != nil { return err } } - env.err.Println() - env.out.Println(id.Id()) + env.Err.Println() + env.Out.Println(id.Id()) return nil } diff --git a/commands/user/user_new_test.go b/commands/user/user_new_test.go new file mode 100644 index 00000000..619e5de6 --- /dev/null +++ b/commands/user/user_new_test.go @@ -0,0 +1,14 @@ +package usercmd + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/MichaelMure/git-bug/commands/bug/testenv" +) + +func TestUserNewCommand(t *testing.T) { + _, userID := testenv.NewTestEnvAndUser(t) + require.Regexp(t, "[0-9a-f]{64}", userID) +} diff --git a/commands/user/user_show.go b/commands/user/user_show.go new file mode 100644 index 00000000..36c09e8e --- /dev/null +++ b/commands/user/user_show.go @@ -0,0 +1,108 @@ +package usercmd + +import ( + "errors" + "fmt" + "strings" + + "github.com/spf13/cobra" + + "github.com/MichaelMure/git-bug/cache" + "github.com/MichaelMure/git-bug/commands/completion" + "github.com/MichaelMure/git-bug/commands/execenv" +) + +type userShowOptions struct { + fields string +} + +func newUserShowCommand() *cobra.Command { + env := execenv.NewEnv() + options := userShowOptions{} + + cmd := &cobra.Command{ + Use: "user show [USER_ID]", + Short: "Display a user identity", + PreRunE: execenv.LoadBackendEnsureUser(env), + RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + return runUserShow(env, options, args) + }), + ValidArgsFunction: completion.User(env), + } + + flags := cmd.Flags() + flags.SortFlags = false + + fields := []string{"email", "humanId", "id", "lastModification", "lastModificationLamports", "login", "metadata", "name"} + flags.StringVarP(&options.fields, "field", "f", "", + "Select field to display. Valid values are ["+strings.Join(fields, ",")+"]") + cmd.RegisterFlagCompletionFunc("field", completion.From(fields)) + + return cmd +} + +func runUserShow(env *execenv.Env, opts userShowOptions, args []string) error { + if len(args) > 1 { + return errors.New("only one identity can be displayed at a time") + } + + var id *cache.IdentityCache + var err error + if len(args) == 1 { + id, err = env.Backend.ResolveIdentityPrefix(args[0]) + } else { + id, err = env.Backend.GetUserIdentity() + } + + if err != nil { + return err + } + + if opts.fields != "" { + switch opts.fields { + case "email": + env.Out.Printf("%s\n", id.Email()) + case "login": + env.Out.Printf("%s\n", id.Login()) + case "humanId": + env.Out.Printf("%s\n", id.Id().Human()) + case "id": + env.Out.Printf("%s\n", id.Id()) + case "lastModification": + env.Out.Printf("%s\n", id.LastModification(). + Time().Format("Mon Jan 2 15:04:05 2006 +0200")) + case "lastModificationLamport": + for name, t := range id.LastModificationLamports() { + env.Out.Printf("%s\n%d\n", name, t) + } + case "metadata": + for key, value := range id.ImmutableMetadata() { + env.Out.Printf("%s\n%s\n", key, value) + } + case "name": + env.Out.Printf("%s\n", id.Name()) + + default: + return fmt.Errorf("\nUnsupported field: %s\n", opts.fields) + } + + return nil + } + + env.Out.Printf("Id: %s\n", id.Id()) + env.Out.Printf("Name: %s\n", id.Name()) + env.Out.Printf("Email: %s\n", id.Email()) + env.Out.Printf("Login: %s\n", id.Login()) + env.Out.Printf("Last modification: %s\n", id.LastModification().Time().Format("Mon Jan 2 15:04:05 2006 +0200")) + env.Out.Printf("Last moditication (lamport):\n") + for name, t := range id.LastModificationLamports() { + env.Out.Printf("\t%s: %d", name, t) + } + env.Out.Println("Metadata:") + for key, value := range id.ImmutableMetadata() { + env.Out.Printf(" %s --> %s\n", key, value) + } + // env.Out.Printf("Protected: %v\n", id.IsProtected()) + + return nil +} diff --git a/commands/user_adopt.go b/commands/user_adopt.go deleted file mode 100644 index afef94ea..00000000 --- a/commands/user_adopt.go +++ /dev/null @@ -1,40 +0,0 @@ -package commands - -import ( - "github.com/spf13/cobra" -) - -func newUserAdoptCommand() *cobra.Command { - env := newEnv() - - cmd := &cobra.Command{ - Use: "adopt USER-ID", - Short: "Adopt an existing identity as your own.", - Args: cobra.ExactArgs(1), - PreRunE: loadBackend(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runUserAdopt(env, args) - }), - ValidArgsFunction: completeUser(env), - } - - return cmd -} - -func runUserAdopt(env *Env, args []string) error { - prefix := args[0] - - i, err := env.backend.ResolveIdentityPrefix(prefix) - if err != nil { - return err - } - - err = env.backend.SetUserIdentity(i) - if err != nil { - return err - } - - env.out.Printf("Your identity is now: %s\n", i.DisplayName()) - - return nil -} diff --git a/commands/user_create_test.go b/commands/user_create_test.go deleted file mode 100644 index 08958344..00000000 --- a/commands/user_create_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package commands - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/require" -) - -const ( - testUserName = "John Doe" - testUserEmail = "jdoe@example.com" -) - -func newTestEnvAndUser(t *testing.T) (*testEnv, string) { - t.Helper() - - testEnv := newTestEnv(t) - - opts := createUserOptions{ - name: testUserName, - email: testUserEmail, - avatarURL: "", - nonInteractive: true, - } - - require.NoError(t, runUserCreate(testEnv.env, opts)) - - userID := strings.TrimSpace(testEnv.out.String()) - testEnv.out.Reset() - - return testEnv, userID -} - -func TestUserCreateCommand(t *testing.T) { - _, userID := newTestEnvAndUser(t) - require.Regexp(t, "[0-9a-f]{64}", userID) -} diff --git a/commands/user_ls.go b/commands/user_ls.go deleted file mode 100644 index 341f0dc1..00000000 --- a/commands/user_ls.go +++ /dev/null @@ -1,81 +0,0 @@ -package commands - -import ( - "encoding/json" - "fmt" - - "github.com/spf13/cobra" - - "github.com/MichaelMure/git-bug/cache" - "github.com/MichaelMure/git-bug/util/colors" -) - -type userLsOptions struct { - format string -} - -func newUserLsCommand() *cobra.Command { - env := newEnv() - options := userLsOptions{} - - cmd := &cobra.Command{ - Use: "ls", - Short: "List identities.", - PreRunE: loadBackend(env), - RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error { - return runUserLs(env, options) - }), - } - - flags := cmd.Flags() - flags.SortFlags = false - - flags.StringVarP(&options.format, "format", "f", "default", - "Select the output formatting style. Valid values are [default,json]") - cmd.RegisterFlagCompletionFunc("format", completeFrom([]string{"default", "json"})) - - return cmd -} - -func runUserLs(env *Env, opts userLsOptions) error { - ids := env.backend.AllIdentityIds() - var users []*cache.IdentityExcerpt - for _, id := range ids { - user, err := env.backend.ResolveIdentityExcerpt(id) - if err != nil { - return err - } - users = append(users, user) - } - - switch opts.format { - case "json": - return userLsJsonFormatter(env, users) - case "default": - return userLsDefaultFormatter(env, users) - default: - return fmt.Errorf("unknown format %s", opts.format) - } -} - -func userLsDefaultFormatter(env *Env, users []*cache.IdentityExcerpt) error { - for _, user := range users { - env.out.Printf("%s %s\n", - colors.Cyan(user.Id.Human()), - user.DisplayName(), - ) - } - - return nil -} - -func userLsJsonFormatter(env *Env, users []*cache.IdentityExcerpt) error { - jsonUsers := make([]JSONIdentity, len(users)) - for i, user := range users { - jsonUsers[i] = NewJSONIdentityFromExcerpt(user) - } - - jsonObject, _ := json.MarshalIndent(jsonUsers, "", " ") - env.out.Printf("%s\n", jsonObject) - return nil -} diff --git a/commands/version.go b/commands/version.go index 71baba40..0e54bb92 100644 --- a/commands/version.go +++ b/commands/version.go @@ -4,6 +4,8 @@ import ( "runtime" "github.com/spf13/cobra" + + "github.com/MichaelMure/git-bug/commands/execenv" ) type versionOptions struct { @@ -13,12 +15,12 @@ type versionOptions struct { } func newVersionCommand() *cobra.Command { - env := newEnv() + env := execenv.NewEnv() options := versionOptions{} cmd := &cobra.Command{ Use: "version", - Short: "Show git-bug version information.", + Short: "Show git-bug version information", Run: func(cmd *cobra.Command, args []string) { runVersion(env, options, cmd.Root()) }, @@ -40,23 +42,23 @@ func newVersionCommand() *cobra.Command { return cmd } -func runVersion(env *Env, opts versionOptions, root *cobra.Command) { +func runVersion(env *execenv.Env, opts versionOptions, root *cobra.Command) { if opts.all { - env.out.Printf("%s version: %s\n", rootCommandName, root.Version) - env.out.Printf("System version: %s/%s\n", runtime.GOARCH, runtime.GOOS) - env.out.Printf("Golang version: %s\n", runtime.Version()) + env.Out.Printf("%s version: %s\n", execenv.RootCommandName, root.Version) + env.Out.Printf("System version: %s/%s\n", runtime.GOARCH, runtime.GOOS) + env.Out.Printf("Golang version: %s\n", runtime.Version()) return } if opts.number { - env.out.Println(root.Version) + env.Out.Println(root.Version) return } if opts.commit { - env.out.Println(GitCommit) + env.Out.Println(GitCommit) return } - env.out.Printf("%s version: %s\n", rootCommandName, root.Version) + env.Out.Printf("%s version: %s\n", execenv.RootCommandName, root.Version) } diff --git a/commands/webui.go b/commands/webui.go index 758a153b..5fe66aa7 100644 --- a/commands/webui.go +++ b/commands/webui.go @@ -23,6 +23,7 @@ import ( "github.com/MichaelMure/git-bug/api/graphql" httpapi "github.com/MichaelMure/git-bug/api/http" "github.com/MichaelMure/git-bug/cache" + "github.com/MichaelMure/git-bug/commands/execenv" "github.com/MichaelMure/git-bug/entities/identity" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/webui" @@ -41,18 +42,18 @@ type webUIOptions struct { } func newWebUICommand() *cobra.Command { - env := newEnv() + env := execenv.NewEnv() options := webUIOptions{} cmd := &cobra.Command{ Use: "webui", - Short: "Launch the web UI.", + Short: "Launch the web UI", Long: `Launch the web UI. Available git config: git-bug.webui.open [bool]: control the automatic opening of the web UI in the default browser `, - PreRunE: loadRepo(env), + PreRunE: execenv.LoadRepo(env), RunE: func(cmd *cobra.Command, args []string) error { return runWebUI(env, options) }, @@ -72,7 +73,7 @@ Available git config: return cmd } -func runWebUI(env *Env, opts webUIOptions) error { +func runWebUI(env *execenv.Env, opts webUIOptions) error { if opts.port == 0 { var err error opts.port, err = freeport.GetFreePort() @@ -96,7 +97,7 @@ func runWebUI(env *Env, opts webUIOptions) error { // fixed identity: the default user of the repo // TODO: support dynamic authentication with OAuth if !opts.readOnly { - author, err := identity.GetUserIdentity(env.repo) + author, err := identity.GetUserIdentity(env.Repo) if err != nil { return err } @@ -104,14 +105,14 @@ func runWebUI(env *Env, opts webUIOptions) error { } mrc := cache.NewMultiRepoCache() - _, err := mrc.RegisterDefaultRepository(env.repo) + _, err := mrc.RegisterDefaultRepository(env.Repo) if err != nil { return err } var errOut io.Writer if opts.logErrors { - errOut = env.err + errOut = env.Err } graphqlHandler := graphql.NewHandler(mrc, errOut) @@ -136,7 +137,7 @@ func runWebUI(env *Env, opts webUIOptions) error { go func() { <-quit - env.out.Println("WebUI is shutting down...") + env.Out.Println("WebUI is shutting down...") ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -149,18 +150,18 @@ func runWebUI(env *Env, opts webUIOptions) error { // Teardown err := graphqlHandler.Close() if err != nil { - env.out.Println(err) + env.Out.Println(err) } close(done) }() - env.out.Printf("Web UI: %s\n", webUiAddr) - env.out.Printf("Graphql API: http://%s/graphql\n", addr) - env.out.Printf("Graphql Playground: http://%s/playground\n", addr) - env.out.Println("Press Ctrl+c to quit") + env.Out.Printf("Web UI: %s\n", webUiAddr) + env.Out.Printf("Graphql API: http://%s/graphql\n", addr) + env.Out.Printf("Graphql Playground: http://%s/playground\n", addr) + env.Out.Println("Press Ctrl+c to quit") - configOpen, err := env.repo.AnyConfig().ReadBool(webUIOpenConfigKey) + configOpen, err := env.Repo.AnyConfig().ReadBool(webUIOpenConfigKey) if err == repository.ErrNoConfigEntry { // default to true configOpen = true @@ -173,7 +174,7 @@ func runWebUI(env *Env, opts webUIOptions) error { if shouldOpen { err = open.Run(toOpen) if err != nil { - env.out.Println(err) + env.Out.Println(err) } } @@ -184,6 +185,6 @@ func runWebUI(env *Env, opts webUIOptions) error { <-done - env.out.Println("WebUI stopped") + env.Out.Println("WebUI stopped") return nil } |