Merge pull request #962 from MichaelMure/select-completion

commands: generic "select" code, move bug completion in bugcmd
This commit is contained in:
Michael Muré 2022-12-27 19:48:19 +01:00 committed by GitHub
commit d11ea5c2ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 327 additions and 302 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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()

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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 != "" {

156
commands/select/select.go Normal file
View File

@ -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
}

View File

@ -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)
}