From e920987860dd9392fefc4b222aa4d2446b74f3d8 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Tue, 27 Dec 2022 19:40:40 +0100 Subject: commands: generic "select" code, move bug completion in bugcmd --- 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/bug/select/select_test.go | 79 ---------------- commands/completion/helper_completion.go | 120 +++--------------------- commands/select/select.go | 156 +++++++++++++++++++++++++++++++ commands/select/select_test.go | 86 +++++++++++++++++ 20 files changed, 389 insertions(+), 364 deletions(-) create mode 100644 commands/bug/completion.go delete mode 100644 commands/bug/select/select.go delete mode 100644 commands/bug/select/select_test.go create mode 100644 commands/select/select.go create mode 100644 commands/select/select_test.go (limited to 'commands') diff --git a/commands/bug/bug_comment.go b/commands/bug/bug_comment.go index bc665f0d..4dc8dc1f 100644 --- a/commands/bug/bug_comment.go +++ b/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 } diff --git a/commands/bug/bug_comment_add.go b/commands/bug/bug_comment_add.go index b445ffad..ff406b4f 100644 --- a/commands/bug/bug_comment_add.go +++ b/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 } diff --git a/commands/bug/bug_deselect.go b/commands/bug/bug_deselect.go index 7e2a86c9..090a7bf2 100644 --- a/commands/bug/bug_deselect.go +++ b/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 } diff --git a/commands/bug/bug_label.go b/commands/bug/bug_label.go index 657fa2ca..e6d0e603 100644 --- a/commands/bug/bug_label.go +++ b/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 } diff --git a/commands/bug/bug_label_new.go b/commands/bug/bug_label_new.go index f94d3dc8..aa4f9463 100644 --- a/commands/bug/bug_label_new.go +++ b/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 } diff --git a/commands/bug/bug_label_rm.go b/commands/bug/bug_label_rm.go index 13ce4b81..18510bbd 100644 --- a/commands/bug/bug_label_rm.go +++ b/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 } diff --git a/commands/bug/bug_rm.go b/commands/bug/bug_rm.go index 04881d54..386c57ec 100644 --- a/commands/bug/bug_rm.go +++ b/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() diff --git a/commands/bug/bug_select.go b/commands/bug/bug_select.go index 2a4d1201..bfad899d 100644 --- a/commands/bug/bug_select.go +++ b/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 } diff --git a/commands/bug/bug_show.go b/commands/bug/bug_show.go index 105b1150..6cf50015 100644 --- a/commands/bug/bug_show.go +++ b/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 } diff --git a/commands/bug/bug_status.go b/commands/bug/bug_status.go index b05f862c..807a9a60 100644 --- a/commands/bug/bug_status.go +++ b/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 } diff --git a/commands/bug/bug_status_close.go b/commands/bug/bug_status_close.go index fcd47922..e52959b2 100644 --- a/commands/bug/bug_status_close.go +++ b/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 } diff --git a/commands/bug/bug_status_open.go b/commands/bug/bug_status_open.go index e686add1..74177974 100644 --- a/commands/bug/bug_status_open.go +++ b/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 } diff --git a/commands/bug/bug_title.go b/commands/bug/bug_title.go index 98809b60..e59a1fdc 100644 --- a/commands/bug/bug_title.go +++ b/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 } diff --git a/commands/bug/bug_title_edit.go b/commands/bug/bug_title_edit.go index bf9f6375..59898530 100644 --- a/commands/bug/bug_title_edit.go +++ b/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 } diff --git a/commands/bug/completion.go b/commands/bug/completion.go new file mode 100644 index 00000000..4754f97d --- /dev/null +++ b/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 + } +} diff --git a/commands/bug/select/select.go b/commands/bug/select/select.go deleted file mode 100644 index 7096dde4..00000000 --- a/commands/bug/select/select.go +++ /dev/null @@ -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 -} diff --git a/commands/bug/select/select_test.go b/commands/bug/select/select_test.go deleted file mode 100644 index 83ca6643..00000000 --- a/commands/bug/select/select_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package _select - -import ( - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/MichaelMure/git-bug/cache" - "github.com/MichaelMure/git-bug/repository" -) - -func TestSelect(t *testing.T) { - repo := repository.CreateGoGitTestRepo(t, false) - - repoCache, err := cache.NewRepoCacheNoEvents(repo) - require.NoError(t, err) - - _, _, err = ResolveBug(repoCache, []string{}) - require.Equal(t, ErrNoValidId, err) - - err = Select(repoCache, "invalid") - require.NoError(t, err) - - // Resolve without a pattern should fail when no bug is selected - _, _, err = ResolveBug(repoCache, []string{}) - require.Error(t, err) - - // generate a bunch of bugs - - rene, err := repoCache.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) - require.NoError(t, err) - } - - // and two more for testing - b1, _, err := repoCache.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) - require.NoError(t, err) - - err = Select(repoCache, b1.Id()) - require.NoError(t, err) - - // normal select without args - b3, _, err := ResolveBug(repoCache, []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()}) - require.NoError(t, err) - require.Equal(t, b1.Id(), b4.Id()) - - // override selection with a prefix - b5, _, err := ResolveBug(repoCache, []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"}) - require.NoError(t, err) - require.Equal(t, b1.Id(), b6.Id()) - - // override with a different id - b7, _, err := ResolveBug(repoCache, []string{b2.Id().String()}) - require.NoError(t, err) - require.Equal(t, b2.Id(), b7.Id()) - - err = Clear(repoCache) - require.NoError(t, err) - - // Resolve without a pattern should error again after clearing the selected bug - _, _, err = ResolveBug(repoCache, []string{}) - require.Error(t, err) -} diff --git a/commands/completion/helper_completion.go b/commands/completion/helper_completion.go index 691f0895..db6d8969 100644 --- a/commands/completion/helper_completion.go +++ b/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 != "" { diff --git a/commands/select/select.go b/commands/select/select.go new file mode 100644 index 00000000..b821ba59 --- /dev/null +++ b/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 +} diff --git a/commands/select/select_test.go b/commands/select/select_test.go new file mode 100644 index 00000000..4425c275 --- /dev/null +++ b/commands/select/select_test.go @@ -0,0 +1,86 @@ +package _select + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/MichaelMure/git-bug/cache" + "github.com/MichaelMure/git-bug/repository" +) + +func TestSelect(t *testing.T) { + repo := repository.CreateGoGitTestRepo(t, false) + + backend, err := cache.NewRepoCacheNoEvents(repo) + require.NoError(t, err) + + const typename = "foo" + const namespace = "foos" + + 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 = resolve([]string{}) + require.Error(t, err) + + // generate a bunch of bugs + + rene, err := backend.Identities().New("René Descartes", "rene@descartes.fr") + require.NoError(t, err) + + for i := 0; i < 10; i++ { + _, _, err := backend.Bugs().NewRaw(rene, time.Now().Unix(), "title", "message", nil, nil) + require.NoError(t, err) + } + + // and two more for testing + b1, _, err := backend.Bugs().NewRaw(rene, time.Now().Unix(), "title", "message", nil, nil) + require.NoError(t, err) + b2, _, err := backend.Bugs().NewRaw(rene, time.Now().Unix(), "title", "message", nil, nil) + require.NoError(t, err) + + err = Select(backend, namespace, b1.Id()) + require.NoError(t, err) + + // normal select without args + b3, _, err := resolve([]string{}) + require.NoError(t, err) + require.Equal(t, b1.Id(), b3.Id()) + + // override selection with same id + 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 := resolve([]string{b1.Id().Human()}) + require.NoError(t, err) + require.Equal(t, b1.Id(), b5.Id()) + + // args that shouldn't override + b6, _, err := resolve([]string{"arg"}) + require.NoError(t, err) + require.Equal(t, b1.Id(), b6.Id()) + + // override with a different id + b7, _, err := resolve([]string{b2.Id().String()}) + require.NoError(t, err) + require.Equal(t, b2.Id(), b7.Id()) + + err = Clear(backend, namespace) + require.NoError(t, err) + + // Resolve without a pattern should error again after clearing the selected bug + _, _, err = resolve([]string{}) + require.Error(t, err) +} -- cgit