commands: generic "select" code, move bug completion in bugcmd

Michael MurΓ© created

Change summary

commands/bug/bug_comment.go              |   6 
commands/bug/bug_comment_add.go          |   6 
commands/bug/bug_deselect.go             |   5 
commands/bug/bug_label.go                |   6 
commands/bug/bug_label_new.go            |   6 
commands/bug/bug_label_rm.go             |   6 
commands/bug/bug_rm.go                   |   3 
commands/bug/bug_select.go               |  13 +
commands/bug/bug_show.go                 |   5 
commands/bug/bug_status.go               |   6 
commands/bug/bug_status_close.go         |   6 
commands/bug/bug_status_open.go          |   6 
commands/bug/bug_title.go                |   6 
commands/bug/bug_title_edit.go           |   6 
commands/bug/completion.go               |  98 ++++++++++++++++
commands/bug/select/select.go            | 128 ---------------------
commands/completion/helper_completion.go | 120 ++-----------------
commands/select/select.go                | 156 ++++++++++++++++++++++++++
commands/select/select_test.go           |  41 ++++--
19 files changed, 327 insertions(+), 302 deletions(-)

Detailed changes

commands/bug/bug_comment.go πŸ”—

@@ -4,8 +4,6 @@ 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"
 )
@@ -20,7 +18,7 @@ func newBugCommentCommand() *cobra.Command {
 		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
 			return runBugComment(env, args)
 		}),
-		ValidArgsFunction: completion.Bug(env),
+		ValidArgsFunction: BugCompletion(env),
 	}
 
 	cmd.AddCommand(newBugCommentNewCommand())
@@ -30,7 +28,7 @@ func newBugCommentCommand() *cobra.Command {
 }
 
 func runBugComment(env *execenv.Env, args []string) error {
-	b, args, err := _select.ResolveBug(env.Backend, args)
+	b, args, err := ResolveSelected(env.Backend, args)
 	if err != nil {
 		return err
 	}

commands/bug/bug_comment_add.go πŸ”—

@@ -4,8 +4,6 @@ import (
 	"github.com/spf13/cobra"
 
 	buginput "github.com/MichaelMure/git-bug/commands/bug/input"
-	"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"
 )
@@ -27,7 +25,7 @@ func newBugCommentNewCommand() *cobra.Command {
 		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
 			return runBugCommentNew(env, options, args)
 		}),
-		ValidArgsFunction: completion.Bug(env),
+		ValidArgsFunction: BugCompletion(env),
 	}
 
 	flags := cmd.Flags()
@@ -44,7 +42,7 @@ func newBugCommentNewCommand() *cobra.Command {
 }
 
 func runBugCommentNew(env *execenv.Env, opts bugCommentNewOptions, args []string) error {
-	b, args, err := _select.ResolveBug(env.Backend, args)
+	b, args, err := ResolveSelected(env.Backend, args)
 	if err != nil {
 		return err
 	}

commands/bug/bug_deselect.go πŸ”—

@@ -3,8 +3,9 @@ package bugcmd
 import (
 	"github.com/spf13/cobra"
 
-	"github.com/MichaelMure/git-bug/commands/bug/select"
 	"github.com/MichaelMure/git-bug/commands/execenv"
+	_select "github.com/MichaelMure/git-bug/commands/select"
+	"github.com/MichaelMure/git-bug/entities/bug"
 )
 
 func newBugDeselectCommand() *cobra.Command {
@@ -28,7 +29,7 @@ git bug deselect
 }
 
 func runBugDeselect(env *execenv.Env) error {
-	err := _select.Clear(env.Backend)
+	err := _select.Clear(env.Backend, bug.Namespace)
 	if err != nil {
 		return err
 	}

commands/bug/bug_label.go πŸ”—

@@ -3,8 +3,6 @@ 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"
 )
 
@@ -18,7 +16,7 @@ func newBugLabelCommand() *cobra.Command {
 		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
 			return runBugLabel(env, args)
 		}),
-		ValidArgsFunction: completion.Bug(env),
+		ValidArgsFunction: BugCompletion(env),
 	}
 
 	cmd.AddCommand(newBugLabelNewCommand())
@@ -28,7 +26,7 @@ func newBugLabelCommand() *cobra.Command {
 }
 
 func runBugLabel(env *execenv.Env, args []string) error {
-	b, args, err := _select.ResolveBug(env.Backend, args)
+	b, args, err := ResolveSelected(env.Backend, args)
 	if err != nil {
 		return err
 	}

commands/bug/bug_label_new.go πŸ”—

@@ -3,8 +3,6 @@ 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"
 )
@@ -19,14 +17,14 @@ func newBugLabelNewCommand() *cobra.Command {
 		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
 			return runBugLabelNew(env, args)
 		}),
-		ValidArgsFunction: completion.BugAndLabels(env, true),
+		ValidArgsFunction: BugAndLabelsCompletion(env, true),
 	}
 
 	return cmd
 }
 
 func runBugLabelNew(env *execenv.Env, args []string) error {
-	b, args, err := _select.ResolveBug(env.Backend, args)
+	b, args, err := ResolveSelected(env.Backend, args)
 	if err != nil {
 		return err
 	}

commands/bug/bug_label_rm.go πŸ”—

@@ -3,8 +3,6 @@ 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"
 )
@@ -19,14 +17,14 @@ func newBugLabelRmCommand() *cobra.Command {
 		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
 			return runBugLabelRm(env, args)
 		}),
-		ValidArgsFunction: completion.BugAndLabels(env, false),
+		ValidArgsFunction: BugAndLabelsCompletion(env, false),
 	}
 
 	return cmd
 }
 
 func runBugLabelRm(env *execenv.Env, args []string) error {
-	b, args, err := _select.ResolveBug(env.Backend, args)
+	b, args, err := ResolveSelected(env.Backend, args)
 	if err != nil {
 		return err
 	}

commands/bug/bug_rm.go πŸ”—

@@ -5,7 +5,6 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"github.com/MichaelMure/git-bug/commands/completion"
 	"github.com/MichaelMure/git-bug/commands/execenv"
 )
 
@@ -20,7 +19,7 @@ func newBugRmCommand() *cobra.Command {
 		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
 			return runBugRm(env, args)
 		}),
-		ValidArgsFunction: completion.Bug(env),
+		ValidArgsFunction: BugCompletion(env),
 	}
 
 	flags := cmd.Flags()

commands/bug/bug_select.go πŸ”—

@@ -5,11 +5,16 @@ 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/cache"
 	"github.com/MichaelMure/git-bug/commands/execenv"
+	_select "github.com/MichaelMure/git-bug/commands/select"
+	"github.com/MichaelMure/git-bug/entities/bug"
 )
 
+func ResolveSelected(repo *cache.RepoCache, args []string) (*cache.BugCache, []string, error) {
+	return _select.Resolve[*cache.BugCache](repo, bug.Typename, bug.Namespace, repo.Bugs(), args)
+}
+
 func newBugSelectCommand() *cobra.Command {
 	env := execenv.NewEnv()
 
@@ -33,7 +38,7 @@ The complementary command is "git bug deselect" performing the opposite operatio
 		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
 			return runBugSelect(env, args)
 		}),
-		ValidArgsFunction: completion.Bug(env),
+		ValidArgsFunction: BugCompletion(env),
 	}
 
 	return cmd
@@ -51,7 +56,7 @@ func runBugSelect(env *execenv.Env, args []string) error {
 		return err
 	}
 
-	err = _select.Select(env.Backend, b.Id())
+	err = _select.Select(env.Backend, bug.Namespace, b.Id())
 	if err != nil {
 		return err
 	}

commands/bug/bug_show.go πŸ”—

@@ -8,7 +8,6 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"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"
@@ -32,7 +31,7 @@ func newBugShowCommand() *cobra.Command {
 		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
 			return runBugShow(env, options, args)
 		}),
-		ValidArgsFunction: completion.Bug(env),
+		ValidArgsFunction: BugCompletion(env),
 	}
 
 	flags := cmd.Flags()
@@ -50,7 +49,7 @@ func newBugShowCommand() *cobra.Command {
 }
 
 func runBugShow(env *execenv.Env, opts bugShowOptions, args []string) error {
-	b, args, err := _select.ResolveBug(env.Backend, args)
+	b, args, err := ResolveSelected(env.Backend, args)
 	if err != nil {
 		return err
 	}

commands/bug/bug_status.go πŸ”—

@@ -3,8 +3,6 @@ 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"
 )
 
@@ -18,7 +16,7 @@ func newBugStatusCommand() *cobra.Command {
 		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
 			return runBugStatus(env, args)
 		}),
-		ValidArgsFunction: completion.Bug(env),
+		ValidArgsFunction: BugCompletion(env),
 	}
 
 	cmd.AddCommand(newBugStatusCloseCommand())
@@ -28,7 +26,7 @@ func newBugStatusCommand() *cobra.Command {
 }
 
 func runBugStatus(env *execenv.Env, args []string) error {
-	b, args, err := _select.ResolveBug(env.Backend, args)
+	b, args, err := ResolveSelected(env.Backend, args)
 	if err != nil {
 		return err
 	}

commands/bug/bug_status_close.go πŸ”—

@@ -3,8 +3,6 @@ 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"
 )
 
@@ -18,14 +16,14 @@ func newBugStatusCloseCommand() *cobra.Command {
 		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
 			return runBugStatusClose(env, args)
 		}),
-		ValidArgsFunction: completion.Bug(env),
+		ValidArgsFunction: BugCompletion(env),
 	}
 
 	return cmd
 }
 
 func runBugStatusClose(env *execenv.Env, args []string) error {
-	b, args, err := _select.ResolveBug(env.Backend, args)
+	b, args, err := ResolveSelected(env.Backend, args)
 	if err != nil {
 		return err
 	}

commands/bug/bug_status_open.go πŸ”—

@@ -3,8 +3,6 @@ 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"
 )
 
@@ -18,14 +16,14 @@ func newBugStatusOpenCommand() *cobra.Command {
 		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
 			return runBugStatusOpen(env, args)
 		}),
-		ValidArgsFunction: completion.Bug(env),
+		ValidArgsFunction: BugCompletion(env),
 	}
 
 	return cmd
 }
 
 func runBugStatusOpen(env *execenv.Env, args []string) error {
-	b, args, err := _select.ResolveBug(env.Backend, args)
+	b, args, err := ResolveSelected(env.Backend, args)
 	if err != nil {
 		return err
 	}

commands/bug/bug_title.go πŸ”—

@@ -3,8 +3,6 @@ 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"
 )
 
@@ -18,7 +16,7 @@ func newBugTitleCommand() *cobra.Command {
 		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
 			return runBugTitle(env, args)
 		}),
-		ValidArgsFunction: completion.Bug(env),
+		ValidArgsFunction: BugCompletion(env),
 	}
 
 	cmd.AddCommand(newBugTitleEditCommand())
@@ -27,7 +25,7 @@ func newBugTitleCommand() *cobra.Command {
 }
 
 func runBugTitle(env *execenv.Env, args []string) error {
-	b, args, err := _select.ResolveBug(env.Backend, args)
+	b, args, err := ResolveSelected(env.Backend, args)
 	if err != nil {
 		return err
 	}

commands/bug/bug_title_edit.go πŸ”—

@@ -4,8 +4,6 @@ import (
 	"github.com/spf13/cobra"
 
 	buginput "github.com/MichaelMure/git-bug/commands/bug/input"
-	"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"
 )
@@ -26,7 +24,7 @@ func newBugTitleEditCommand() *cobra.Command {
 		RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
 			return runBugTitleEdit(env, options, args)
 		}),
-		ValidArgsFunction: completion.Bug(env),
+		ValidArgsFunction: BugCompletion(env),
 	}
 
 	flags := cmd.Flags()
@@ -41,7 +39,7 @@ func newBugTitleEditCommand() *cobra.Command {
 }
 
 func runBugTitleEdit(env *execenv.Env, opts bugTitleEditOptions, args []string) error {
-	b, args, err := _select.ResolveBug(env.Backend, args)
+	b, args, err := ResolveSelected(env.Backend, args)
 	if err != nil {
 		return err
 	}

commands/bug/completion.go πŸ”—

@@ -0,0 +1,98 @@
+package bugcmd
+
+import (
+	"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"
+	_select "github.com/MichaelMure/git-bug/commands/select"
+	"github.com/MichaelMure/git-bug/entities/bug"
+)
+
+// BugCompletion complete a bug id
+func BugCompletion(env *execenv.Env) completion.ValidArgsFunction {
+	return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
+		if err := execenv.LoadBackend(env)(cmd, args); err != nil {
+			return completion.HandleError(err)
+		}
+		defer func() {
+			_ = env.Backend.Close()
+		}()
+
+		return bugWithBackend(env.Backend, toComplete)
+	}
+}
+
+func bugWithBackend(backend *cache.RepoCache, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
+	for _, id := range backend.Bugs().AllIds() {
+		if strings.Contains(id.String(), strings.TrimSpace(toComplete)) {
+			excerpt, err := backend.Bugs().ResolveExcerpt(id)
+			if err != nil {
+				return completion.HandleError(err)
+			}
+			completions = append(completions, id.Human()+"\t"+excerpt.Title)
+		}
+	}
+
+	return completions, cobra.ShellCompDirectiveNoFileComp
+}
+
+// BugAndLabelsCompletion complete either a bug ID or a label if we know about the bug
+func BugAndLabelsCompletion(env *execenv.Env, addOrRemove bool) completion.ValidArgsFunction {
+	return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
+		if err := execenv.LoadBackend(env)(cmd, args); err != nil {
+			return completion.HandleError(err)
+		}
+		defer func() {
+			_ = env.Backend.Close()
+		}()
+
+		b, args, err := ResolveSelected(env.Backend, args)
+		if _select.IsErrNoValidId(err) {
+			// we need a bug first to complete labels
+			return bugWithBackend(env.Backend, toComplete)
+		}
+		if err != nil {
+			return completion.HandleError(err)
+		}
+
+		snap := b.Snapshot()
+
+		seenLabels := map[bug.Label]bool{}
+		for _, label := range args {
+			seenLabels[bug.Label(label)] = addOrRemove
+		}
+
+		var labels []bug.Label
+		if addOrRemove {
+			for _, label := range snap.Labels {
+				seenLabels[label] = true
+			}
+
+			allLabels := env.Backend.Bugs().ValidLabels()
+			labels = make([]bug.Label, 0, len(allLabels))
+			for _, label := range allLabels {
+				if !seenLabels[label] {
+					labels = append(labels, label)
+				}
+			}
+		} else {
+			labels = make([]bug.Label, 0, len(snap.Labels))
+			for _, label := range snap.Labels {
+				if seenLabels[label] {
+					labels = append(labels, label)
+				}
+			}
+		}
+
+		completions = make([]string, len(labels))
+		for i, label := range labels {
+			completions[i] = string(label) + "\t" + "Label"
+		}
+
+		return completions, cobra.ShellCompDirectiveNoFileComp
+	}
+}

commands/bug/select/select.go πŸ”—

@@ -1,128 +0,0 @@
-package _select
-
-import (
-	"fmt"
-	"io"
-	"io/ioutil"
-	"os"
-
-	"github.com/pkg/errors"
-
-	"github.com/MichaelMure/git-bug/cache"
-	"github.com/MichaelMure/git-bug/entity"
-)
-
-const selectFile = "select"
-
-var ErrNoValidId = errors.New("you must provide a bug id or use the \"select\" command first")
-
-// ResolveBug first try to resolve a bug using the first argument of the command
-// line. If it fails, it falls back to the select mechanism.
-//
-// Returns:
-// - the bug if any
-// - the new list of command line arguments with the bug prefix removed if it
-//   has been used
-// - an error if the process failed
-func ResolveBug(repo *cache.RepoCache, args []string) (*cache.BugCache, []string, error) {
-	// At first, try to use the first argument as a bug prefix
-	if len(args) > 0 {
-		b, err := repo.Bugs().ResolvePrefix(args[0])
-
-		if err == nil {
-			return b, args[1:], nil
-		}
-
-		if !entity.IsErrNotFound(err) {
-			return nil, nil, err
-		}
-	}
-
-	// first arg is not a valid bug prefix, we can safely use the preselected bug if any
-
-	b, err := selected(repo)
-
-	// selected bug is invalid
-	if entity.IsErrNotFound(err) {
-		// we clear the selected bug
-		err = Clear(repo)
-		if err != nil {
-			return nil, nil, err
-		}
-		return nil, nil, ErrNoValidId
-	}
-
-	// another error when reading the bug
-	if err != nil {
-		return nil, nil, err
-	}
-
-	// bug is successfully retrieved
-	if b != nil {
-		return b, args, nil
-	}
-
-	// no selected bug and no valid first argument
-	return nil, nil, ErrNoValidId
-}
-
-// Select will select a bug for future use
-func Select(repo *cache.RepoCache, id entity.Id) error {
-	f, err := repo.LocalStorage().OpenFile(selectFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
-	if err != nil {
-		return err
-	}
-
-	_, err = f.Write([]byte(id.String()))
-	if err != nil {
-		return err
-	}
-
-	return f.Close()
-}
-
-// Clear will clear the selected bug, if any
-func Clear(repo *cache.RepoCache) error {
-	return repo.LocalStorage().Remove(selectFile)
-}
-
-func selected(repo *cache.RepoCache) (*cache.BugCache, error) {
-	f, err := repo.LocalStorage().Open(selectFile)
-	if err != nil {
-		if os.IsNotExist(err) {
-			return nil, nil
-		} else {
-			return nil, err
-		}
-	}
-
-	buf, err := ioutil.ReadAll(io.LimitReader(f, 100))
-	if err != nil {
-		return nil, err
-	}
-	if len(buf) == 100 {
-		return nil, fmt.Errorf("the select file should be < 100 bytes")
-	}
-
-	id := entity.Id(buf)
-	if err := id.Validate(); err != nil {
-		err = repo.LocalStorage().Remove(selectFile)
-		if err != nil {
-			return nil, errors.Wrap(err, "error while removing invalid select file")
-		}
-
-		return nil, fmt.Errorf("select file in invalid, removing it")
-	}
-
-	b, err := repo.Bugs().Resolve(id)
-	if err != nil {
-		return nil, err
-	}
-
-	err = f.Close()
-	if err != nil {
-		return nil, err
-	}
-
-	return b, nil
-}

commands/completion/helper_completion.go πŸ”—

@@ -9,22 +9,19 @@ import (
 
 	"github.com/MichaelMure/git-bug/bridge"
 	"github.com/MichaelMure/git-bug/bridge/core/auth"
-	"github.com/MichaelMure/git-bug/cache"
-	"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)
 
-func handleError(err error) (completions []string, directives cobra.ShellCompDirective) {
+func HandleError(err error) (completions []string, directives cobra.ShellCompDirective) {
 	return nil, cobra.ShellCompDirectiveError
 }
 
 func Bridge(env *execenv.Env) ValidArgsFunction {
 	return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
 		if err := execenv.LoadBackend(env)(cmd, args); err != nil {
-			return handleError(err)
+			return HandleError(err)
 		}
 		defer func() {
 			_ = env.Backend.Close()
@@ -32,7 +29,7 @@ func Bridge(env *execenv.Env) ValidArgsFunction {
 
 		bridges, err := bridge.ConfiguredBridges(env.Backend)
 		if err != nil {
-			return handleError(err)
+			return HandleError(err)
 		}
 
 		completions = make([]string, len(bridges))
@@ -47,7 +44,7 @@ func Bridge(env *execenv.Env) ValidArgsFunction {
 func BridgeAuth(env *execenv.Env) ValidArgsFunction {
 	return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
 		if err := execenv.LoadBackend(env)(cmd, args); err != nil {
-			return handleError(err)
+			return HandleError(err)
 		}
 		defer func() {
 			_ = env.Backend.Close()
@@ -55,7 +52,7 @@ func BridgeAuth(env *execenv.Env) ValidArgsFunction {
 
 		creds, err := auth.List(env.Backend)
 		if err != nil {
-			return handleError(err)
+			return HandleError(err)
 		}
 
 		completions = make([]string, len(creds))
@@ -74,95 +71,6 @@ func BridgeAuth(env *execenv.Env) ValidArgsFunction {
 	}
 }
 
-func Bug(env *execenv.Env) ValidArgsFunction {
-	return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
-		if err := execenv.LoadBackend(env)(cmd, args); err != nil {
-			return handleError(err)
-		}
-		defer func() {
-			_ = env.Backend.Close()
-		}()
-
-		return bugWithBackend(env.Backend, toComplete)
-	}
-}
-
-func bugWithBackend(backend *cache.RepoCache, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
-	allIds := backend.Bugs().AllIds()
-	bugExcerpt := make([]*cache.BugExcerpt, len(allIds))
-	for i, id := range allIds {
-		var err error
-		bugExcerpt[i], err = backend.Bugs().ResolveExcerpt(id)
-		if err != nil {
-			return handleError(err)
-		}
-	}
-
-	for i, id := range allIds {
-		if strings.Contains(id.String(), strings.TrimSpace(toComplete)) {
-			completions = append(completions, id.Human()+"\t"+bugExcerpt[i].Title)
-		}
-	}
-
-	return completions, cobra.ShellCompDirectiveNoFileComp
-}
-
-func BugAndLabels(env *execenv.Env, addOrRemove bool) ValidArgsFunction {
-	return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
-		if err := execenv.LoadBackend(env)(cmd, args); err != nil {
-			return handleError(err)
-		}
-		defer func() {
-			_ = env.Backend.Close()
-		}()
-
-		b, args, err := _select.ResolveBug(env.Backend, args)
-		if err == _select.ErrNoValidId {
-			// we need a bug first to complete labels
-			return bugWithBackend(env.Backend, toComplete)
-		}
-		if err != nil {
-			return handleError(err)
-		}
-
-		snap := b.Snapshot()
-
-		seenLabels := map[bug.Label]bool{}
-		for _, label := range args {
-			seenLabels[bug.Label(label)] = addOrRemove
-		}
-
-		var labels []bug.Label
-		if addOrRemove {
-			for _, label := range snap.Labels {
-				seenLabels[label] = true
-			}
-
-			allLabels := env.Backend.Bugs().ValidLabels()
-			labels = make([]bug.Label, 0, len(allLabels))
-			for _, label := range allLabels {
-				if !seenLabels[label] {
-					labels = append(labels, label)
-				}
-			}
-		} else {
-			labels = make([]bug.Label, 0, len(snap.Labels))
-			for _, label := range snap.Labels {
-				if seenLabels[label] {
-					labels = append(labels, label)
-				}
-			}
-		}
-
-		completions = make([]string, len(labels))
-		for i, label := range labels {
-			completions[i] = string(label) + "\t" + "Label"
-		}
-
-		return completions, cobra.ShellCompDirectiveNoFileComp
-	}
-}
-
 func From(choices []string) ValidArgsFunction {
 	return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 		return choices, cobra.ShellCompDirectiveNoFileComp
@@ -172,7 +80,7 @@ func From(choices []string) ValidArgsFunction {
 func GitRemote(env *execenv.Env) ValidArgsFunction {
 	return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
 		if err := execenv.LoadBackend(env)(cmd, args); err != nil {
-			return handleError(err)
+			return HandleError(err)
 		}
 		defer func() {
 			_ = env.Backend.Close()
@@ -180,7 +88,7 @@ func GitRemote(env *execenv.Env) ValidArgsFunction {
 
 		remoteMap, err := env.Backend.GetRemotes()
 		if err != nil {
-			return handleError(err)
+			return HandleError(err)
 		}
 		completions = make([]string, 0, len(remoteMap))
 		for remote, url := range remoteMap {
@@ -194,7 +102,7 @@ func GitRemote(env *execenv.Env) ValidArgsFunction {
 func Label(env *execenv.Env) ValidArgsFunction {
 	return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
 		if err := execenv.LoadBackend(env)(cmd, args); err != nil {
-			return handleError(err)
+			return HandleError(err)
 		}
 		defer func() {
 			_ = env.Backend.Close()
@@ -232,7 +140,7 @@ func Ls(env *execenv.Env) ValidArgsFunction {
 
 		if needBackend {
 			if err := execenv.LoadBackend(env)(cmd, args); err != nil {
-				return handleError(err)
+				return HandleError(err)
 			}
 			defer func() {
 				_ = env.Backend.Close()
@@ -248,7 +156,7 @@ func Ls(env *execenv.Env) ValidArgsFunction {
 			for i, id := range ids {
 				user, err := env.Backend.Identities().ResolveExcerpt(id)
 				if err != nil {
-					return handleError(err)
+					return HandleError(err)
 				}
 				var handle string
 				if user.Login != "" {
@@ -294,7 +202,7 @@ func Ls(env *execenv.Env) ValidArgsFunction {
 func User(env *execenv.Env) ValidArgsFunction {
 	return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
 		if err := execenv.LoadBackend(env)(cmd, args); err != nil {
-			return handleError(err)
+			return HandleError(err)
 		}
 		defer func() {
 			_ = env.Backend.Close()
@@ -305,7 +213,7 @@ func User(env *execenv.Env) ValidArgsFunction {
 		for i, id := range ids {
 			user, err := env.Backend.Identities().ResolveExcerpt(id)
 			if err != nil {
-				return handleError(err)
+				return HandleError(err)
 			}
 			completions[i] = user.Id().Human() + "\t" + user.DisplayName()
 		}
@@ -316,7 +224,7 @@ func User(env *execenv.Env) ValidArgsFunction {
 func UserForQuery(env *execenv.Env) ValidArgsFunction {
 	return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
 		if err := execenv.LoadBackend(env)(cmd, args); err != nil {
-			return handleError(err)
+			return HandleError(err)
 		}
 		defer func() {
 			_ = env.Backend.Close()
@@ -327,7 +235,7 @@ func UserForQuery(env *execenv.Env) ValidArgsFunction {
 		for i, id := range ids {
 			user, err := env.Backend.Identities().ResolveExcerpt(id)
 			if err != nil {
-				return handleError(err)
+				return HandleError(err)
 			}
 			var handle string
 			if user.Login != "" {

commands/select/select.go πŸ”—

@@ -0,0 +1,156 @@
+package _select
+
+import (
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+
+	"github.com/pkg/errors"
+
+	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/entity"
+)
+
+type ErrNoValidId struct {
+	typename string
+}
+
+func NewErrNoValidId(typename string) *ErrNoValidId {
+	return &ErrNoValidId{typename: typename}
+}
+
+func (e ErrNoValidId) Error() string {
+	return fmt.Sprintf("you must provide a %s id or use the \"select\" command first", e.typename)
+}
+
+func IsErrNoValidId(err error) bool {
+	_, ok := err.(*ErrNoValidId)
+	return ok
+}
+
+type Resolver[CacheT cache.CacheEntity] interface {
+	Resolve(id entity.Id) (CacheT, error)
+	ResolvePrefix(prefix string) (CacheT, error)
+}
+
+// Resolve first try to resolve an entity using the first argument of the command
+// line. If it fails, it falls back to the select mechanism.
+//
+// Returns:
+// - the entity if any
+// - the new list of command line arguments with the entity prefix removed if it
+//   has been used
+// - an error if the process failed
+func Resolve[CacheT cache.CacheEntity](repo *cache.RepoCache,
+	typename string, namespace string, resolver Resolver[CacheT],
+	args []string) (CacheT, []string, error) {
+	// At first, try to use the first argument as an entity prefix
+	if len(args) > 0 {
+		cached, err := resolver.ResolvePrefix(args[0])
+
+		if err == nil {
+			return cached, args[1:], nil
+		}
+
+		if !entity.IsErrNotFound(err) {
+			return *new(CacheT), nil, err
+		}
+	}
+
+	// first arg is not a valid entity prefix, we can safely use the preselected entity if any
+
+	cached, err := selected(repo, resolver, namespace)
+
+	// selected entity is invalid
+	if entity.IsErrNotFound(err) {
+		// we clear the selected bug
+		err = Clear(repo, namespace)
+		if err != nil {
+			return *new(CacheT), nil, err
+		}
+		return *new(CacheT), nil, NewErrNoValidId(typename)
+	}
+
+	// another error when reading the entity
+	if err != nil {
+		return *new(CacheT), nil, err
+	}
+
+	// entity is successfully retrieved
+	if cached != nil {
+		return *cached, args, nil
+	}
+
+	// no selected bug and no valid first argument
+	return *new(CacheT), nil, NewErrNoValidId(typename)
+}
+
+func selectFileName(namespace string) string {
+	return filepath.Join("select", namespace)
+}
+
+// Select will select a bug for future use
+func Select(repo *cache.RepoCache, namespace string, id entity.Id) error {
+	filename := selectFileName(namespace)
+	f, err := repo.LocalStorage().OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+	if err != nil {
+		return err
+	}
+
+	_, err = f.Write([]byte(id.String()))
+	if err != nil {
+		return err
+	}
+
+	return f.Close()
+}
+
+// Clear will clear the selected entity, if any
+func Clear(repo *cache.RepoCache, namespace string) error {
+	filename := selectFileName(namespace)
+	return repo.LocalStorage().Remove(filename)
+}
+
+func selected[CacheT cache.CacheEntity](repo *cache.RepoCache, resolver Resolver[CacheT], namespace string) (*CacheT, error) {
+	filename := selectFileName(namespace)
+	f, err := repo.LocalStorage().Open(filename)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return nil, nil
+		} else {
+			return nil, err
+		}
+	}
+
+	buf, err := ioutil.ReadAll(io.LimitReader(f, 100))
+	if err != nil {
+		return nil, err
+	}
+	if len(buf) == 100 {
+		return nil, fmt.Errorf("the select file should be < 100 bytes")
+	}
+
+	id := entity.Id(buf)
+	if err := id.Validate(); err != nil {
+		err = repo.LocalStorage().Remove(filename)
+		if err != nil {
+			return nil, errors.Wrap(err, "error while removing invalid select file")
+		}
+
+		return nil, fmt.Errorf("select file in invalid, removing it")
+	}
+
+	cached, err := resolver.Resolve(id)
+	if err != nil {
+		return nil, err
+	}
+
+	err = f.Close()
+	if err != nil {
+		return nil, err
+	}
+
+	return &cached, nil
+}

commands/bug/select/select_test.go β†’ commands/select/select_test.go πŸ”—

@@ -13,67 +13,74 @@ import (
 func TestSelect(t *testing.T) {
 	repo := repository.CreateGoGitTestRepo(t, false)
 
-	repoCache, err := cache.NewRepoCacheNoEvents(repo)
+	backend, err := cache.NewRepoCacheNoEvents(repo)
 	require.NoError(t, err)
 
-	_, _, err = ResolveBug(repoCache, []string{})
-	require.Equal(t, ErrNoValidId, err)
+	const typename = "foo"
+	const namespace = "foos"
 
-	err = Select(repoCache, "invalid")
+	resolve := func(args []string) (*cache.BugCache, []string, error) {
+		return Resolve[*cache.BugCache](backend, typename, namespace, backend.Bugs(), args)
+	}
+
+	_, _, err = resolve([]string{})
+	require.True(t, IsErrNoValidId(err))
+
+	err = Select(backend, namespace, "invalid")
 	require.NoError(t, err)
 
 	// Resolve without a pattern should fail when no bug is selected
-	_, _, err = ResolveBug(repoCache, []string{})
+	_, _, err = resolve([]string{})
 	require.Error(t, err)
 
 	// generate a bunch of bugs
 
-	rene, err := repoCache.Identities().New("RenΓ© Descartes", "rene@descartes.fr")
+	rene, err := backend.Identities().New("RenΓ© Descartes", "rene@descartes.fr")
 	require.NoError(t, err)
 
 	for i := 0; i < 10; i++ {
-		_, _, err := repoCache.Bugs().NewRaw(rene, time.Now().Unix(), "title", "message", nil, nil)
+		_, _, err := backend.Bugs().NewRaw(rene, time.Now().Unix(), "title", "message", nil, nil)
 		require.NoError(t, err)
 	}
 
 	// and two more for testing
-	b1, _, err := repoCache.Bugs().NewRaw(rene, time.Now().Unix(), "title", "message", nil, nil)
+	b1, _, err := backend.Bugs().NewRaw(rene, time.Now().Unix(), "title", "message", nil, nil)
 	require.NoError(t, err)
-	b2, _, err := repoCache.Bugs().NewRaw(rene, time.Now().Unix(), "title", "message", nil, nil)
+	b2, _, err := backend.Bugs().NewRaw(rene, time.Now().Unix(), "title", "message", nil, nil)
 	require.NoError(t, err)
 
-	err = Select(repoCache, b1.Id())
+	err = Select(backend, namespace, b1.Id())
 	require.NoError(t, err)
 
 	// normal select without args
-	b3, _, err := ResolveBug(repoCache, []string{})
+	b3, _, err := resolve([]string{})
 	require.NoError(t, err)
 	require.Equal(t, b1.Id(), b3.Id())
 
 	// override selection with same id
-	b4, _, err := ResolveBug(repoCache, []string{b1.Id().String()})
+	b4, _, err := resolve([]string{b1.Id().String()})
 	require.NoError(t, err)
 	require.Equal(t, b1.Id(), b4.Id())
 
 	// override selection with a prefix
-	b5, _, err := ResolveBug(repoCache, []string{b1.Id().Human()})
+	b5, _, err := resolve([]string{b1.Id().Human()})
 	require.NoError(t, err)
 	require.Equal(t, b1.Id(), b5.Id())
 
 	// args that shouldn't override
-	b6, _, err := ResolveBug(repoCache, []string{"arg"})
+	b6, _, err := resolve([]string{"arg"})
 	require.NoError(t, err)
 	require.Equal(t, b1.Id(), b6.Id())
 
 	// override with a different id
-	b7, _, err := ResolveBug(repoCache, []string{b2.Id().String()})
+	b7, _, err := resolve([]string{b2.Id().String()})
 	require.NoError(t, err)
 	require.Equal(t, b2.Id(), b7.Id())
 
-	err = Clear(repoCache)
+	err = Clear(backend, namespace)
 	require.NoError(t, err)
 
 	// Resolve without a pattern should error again after clearing the selected bug
-	_, _, err = ResolveBug(repoCache, []string{})
+	_, _, err = resolve([]string{})
 	require.Error(t, err)
 }