From 9ccd098eb0664f80d4ea9fd2a4d400c381203531 Mon Sep 17 00:00:00 2001 From: Kousik Mitra Date: Mon, 5 Jun 2023 22:07:04 +0530 Subject: [PATCH] Add gist selector option to gist edit command --- pkg/cmd/gist/edit/edit.go | 45 ++++++++----- pkg/cmd/gist/shared/shared.go | 57 ++++++++++++++++ pkg/cmd/gist/shared/shared_test.go | 100 +++++++++++++++++++++++++++++ pkg/cmd/gist/view/view.go | 60 ++--------------- pkg/cmd/gist/view/view_test.go | 94 --------------------------- 5 files changed, 190 insertions(+), 166 deletions(-) diff --git a/pkg/cmd/gist/edit/edit.go b/pkg/cmd/gist/edit/edit.go index 773ea41df..bbe62d80b 100644 --- a/pkg/cmd/gist/edit/edit.go +++ b/pkg/cmd/gist/edit/edit.go @@ -55,16 +55,15 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman Use: "edit { | } []", Short: "Edit one of your gists", Args: func(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return cmdutil.FlagErrorf("cannot edit: gist argument required") - } if len(args) > 2 { return cmdutil.FlagErrorf("too many arguments") } return nil }, RunE: func(c *cobra.Command, args []string) error { - opts.Selector = args[0] + if len(args) > 0 { + opts.Selector = args[0] + } if len(args) > 1 { opts.SourceFile = args[1] } @@ -85,7 +84,33 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman } func editRun(opts *EditOptions) error { + client, err := opts.HttpClient() + if err != nil { + return err + } + + cfg, err := opts.Config() + if err != nil { + return err + } + + host, _ := cfg.Authentication().DefaultHost() + gistID := opts.Selector + if gistID == "" { + cs := opts.IO.ColorScheme() + if gistID == "" { + gistID, err = shared.PromptGists(client, host, cs) + if err != nil { + return err + } + + if gistID == "" { + fmt.Fprintln(opts.IO.Out, "No gists found.") + return nil + } + } + } if strings.Contains(gistID, "/") { id, err := shared.GistIDFromURL(gistID) @@ -95,20 +120,8 @@ func editRun(opts *EditOptions) error { gistID = id } - client, err := opts.HttpClient() - if err != nil { - return err - } - apiClient := api.NewClientFromHTTP(client) - cfg, err := opts.Config() - if err != nil { - return err - } - - host, _ := cfg.Authentication().DefaultHost() - gist, err := shared.GetGist(client, host, gistID) if err != nil { if errors.Is(err, shared.NotFoundErr) { diff --git a/pkg/cmd/gist/shared/shared.go b/pkg/cmd/gist/shared/shared.go index 52c86caa7..547140838 100644 --- a/pkg/cmd/gist/shared/shared.go +++ b/pkg/cmd/gist/shared/shared.go @@ -5,10 +5,15 @@ import ( "fmt" "net/http" "net/url" + "sort" "strings" "time" + "github.com/AlecAivazis/survey/v2" "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/text" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/prompt" "github.com/gabriel-vasile/mimetype" "github.com/shurcooL/githubv4" ) @@ -171,3 +176,55 @@ func IsBinaryContents(contents []byte) bool { } return isBinary } + +func PromptGists(client *http.Client, host string, cs *iostreams.ColorScheme) (gistID string, err error) { + gists, err := ListGists(client, host, 10, "all") + if err != nil { + return "", err + } + + if len(gists) == 0 { + return "", nil + } + + var opts []string + var result int + var gistIDs = make([]string, len(gists)) + + for i, gist := range gists { + gistIDs[i] = gist.ID + description := "" + gistName := "" + + if gist.Description != "" { + description = gist.Description + } + + filenames := make([]string, 0, len(gist.Files)) + for fn := range gist.Files { + filenames = append(filenames, fn) + } + sort.Strings(filenames) + gistName = filenames[0] + + gistTime := text.FuzzyAgo(time.Now(), gist.UpdatedAt) + // TODO: support dynamic maxWidth + description = text.Truncate(100, text.RemoveExcessiveWhitespace(description)) + opt := fmt.Sprintf("%s %s %s", cs.Bold(gistName), description, cs.Gray(gistTime)) + opts = append(opts, opt) + } + + questions := &survey.Select{ + Message: "Select a gist", + Options: opts, + } + + //nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter + err = prompt.SurveyAskOne(questions, &result) + + if err != nil { + return "", err + } + + return gistIDs[result], nil +} diff --git a/pkg/cmd/gist/shared/shared_test.go b/pkg/cmd/gist/shared/shared_test.go index c55cbea67..0811464e5 100644 --- a/pkg/cmd/gist/shared/shared_test.go +++ b/pkg/cmd/gist/shared/shared_test.go @@ -1,8 +1,14 @@ package shared import ( + "fmt" + "net/http" "testing" + "time" + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/prompt" "github.com/stretchr/testify/assert" ) @@ -85,3 +91,97 @@ func TestIsBinaryContents(t *testing.T) { assert.Equal(t, tt.want, IsBinaryContents(tt.fileContent)) } } + +func TestPromptGists(t *testing.T) { + tests := []struct { + name string + askStubs func(as *prompt.AskStubber) + response string + wantOut string + gist *Gist + wantErr bool + }{ + { + name: "multiple files, select first gist", + askStubs: func(as *prompt.AskStubber) { + as.StubPrompt("Select a gist").AnswerWith("cool.txt about 6 hours ago") + }, + response: `{ "data": { "viewer": { "gists": { "nodes": [ + { + "name": "gistid1", + "files": [{ "name": "cool.txt" }], + "description": "", + "updatedAt": "%[1]v", + "isPublic": true + }, + { + "name": "gistid2", + "files": [{ "name": "gistfile0.txt" }], + "description": "", + "updatedAt": "%[1]v", + "isPublic": true + } + ] } } } }`, + wantOut: "gistid1", + }, + { + name: "multiple files, select second gist", + askStubs: func(as *prompt.AskStubber) { + as.StubPrompt("Select a gist").AnswerWith("gistfile0.txt about 6 hours ago") + }, + response: `{ "data": { "viewer": { "gists": { "nodes": [ + { + "name": "gistid1", + "files": [{ "name": "cool.txt" }], + "description": "", + "updatedAt": "%[1]v", + "isPublic": true + }, + { + "name": "gistid2", + "files": [{ "name": "gistfile0.txt" }], + "description": "", + "updatedAt": "%[1]v", + "isPublic": true + } + ] } } } }`, + wantOut: "gistid2", + }, + { + name: "no files", + response: `{ "data": { "viewer": { "gists": { "nodes": [] } } } }`, + wantOut: "", + }, + } + + ios, _, _, _ := iostreams.Test() + + for _, tt := range tests { + reg := &httpmock.Registry{} + + const query = `query GistList\b` + sixHours, _ := time.ParseDuration("6h") + sixHoursAgo := time.Now().Add(-sixHours) + reg.Register( + httpmock.GraphQL(query), + httpmock.StringResponse(fmt.Sprintf( + tt.response, + sixHoursAgo.Format(time.RFC3339), + )), + ) + client := &http.Client{Transport: reg} + + t.Run(tt.name, func(t *testing.T) { + //nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock + as := prompt.NewAskStubber(t) + if tt.askStubs != nil { + tt.askStubs(as) + } + + gistID, err := PromptGists(client, "github.com", ios.ColorScheme()) + assert.NoError(t, err) + assert.Equal(t, tt.wantOut, gistID) + reg.Verify(t) + }) + } +} diff --git a/pkg/cmd/gist/view/view.go b/pkg/cmd/gist/view/view.go index 545681522..ffc5d54a9 100644 --- a/pkg/cmd/gist/view/view.go +++ b/pkg/cmd/gist/view/view.go @@ -5,17 +5,15 @@ import ( "net/http" "sort" "strings" - "time" - "github.com/AlecAivazis/survey/v2" "github.com/cli/cli/v2/internal/config" "github.com/cli/cli/v2/internal/ghinstance" + "github.com/cli/cli/v2/internal/prompter" "github.com/cli/cli/v2/internal/text" "github.com/cli/cli/v2/pkg/cmd/gist/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/cli/cli/v2/pkg/markdown" - "github.com/cli/cli/v2/pkg/prompt" "github.com/spf13/cobra" ) @@ -28,6 +26,7 @@ type ViewOptions struct { Config func() (config.Config, error) HttpClient func() (*http.Client, error) Browser browser + Prompter prompter.Prompter Selector string Filename string @@ -42,6 +41,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman Config: f.Config, HttpClient: f.HttpClient, Browser: f.Browser, + Prompter: f.Prompter, } cmd := &cobra.Command{ @@ -89,7 +89,7 @@ func viewRun(opts *ViewOptions) error { cs := opts.IO.ColorScheme() if gistID == "" { - gistID, err = promptGists(client, hostname, cs) + gistID, err = shared.PromptGists(client, hostname, cs) if err != nil { return err } @@ -204,55 +204,3 @@ func viewRun(opts *ViewOptions) error { return nil } - -func promptGists(client *http.Client, host string, cs *iostreams.ColorScheme) (gistID string, err error) { - gists, err := shared.ListGists(client, host, 10, "all") - if err != nil { - return "", err - } - - if len(gists) == 0 { - return "", nil - } - - var opts []string - var result int - var gistIDs = make([]string, len(gists)) - - for i, gist := range gists { - gistIDs[i] = gist.ID - description := "" - gistName := "" - - if gist.Description != "" { - description = gist.Description - } - - filenames := make([]string, 0, len(gist.Files)) - for fn := range gist.Files { - filenames = append(filenames, fn) - } - sort.Strings(filenames) - gistName = filenames[0] - - gistTime := text.FuzzyAgo(time.Now(), gist.UpdatedAt) - // TODO: support dynamic maxWidth - description = text.Truncate(100, text.RemoveExcessiveWhitespace(description)) - opt := fmt.Sprintf("%s %s %s", cs.Bold(gistName), description, cs.Gray(gistTime)) - opts = append(opts, opt) - } - - questions := &survey.Select{ - Message: "Select a gist", - Options: opts, - } - - //nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter - err = prompt.SurveyAskOne(questions, &result) - - if err != nil { - return "", err - } - - return gistIDs[result], nil -} diff --git a/pkg/cmd/gist/view/view_test.go b/pkg/cmd/gist/view/view_test.go index 8dbc9a748..49cf878ec 100644 --- a/pkg/cmd/gist/view/view_test.go +++ b/pkg/cmd/gist/view/view_test.go @@ -389,97 +389,3 @@ func Test_viewRun(t *testing.T) { }) } } - -func Test_promptGists(t *testing.T) { - tests := []struct { - name string - askStubs func(as *prompt.AskStubber) - response string - wantOut string - gist *shared.Gist - wantErr bool - }{ - { - name: "multiple files, select first gist", - askStubs: func(as *prompt.AskStubber) { - as.StubPrompt("Select a gist").AnswerWith("cool.txt about 6 hours ago") - }, - response: `{ "data": { "viewer": { "gists": { "nodes": [ - { - "name": "gistid1", - "files": [{ "name": "cool.txt" }], - "description": "", - "updatedAt": "%[1]v", - "isPublic": true - }, - { - "name": "gistid2", - "files": [{ "name": "gistfile0.txt" }], - "description": "", - "updatedAt": "%[1]v", - "isPublic": true - } - ] } } } }`, - wantOut: "gistid1", - }, - { - name: "multiple files, select second gist", - askStubs: func(as *prompt.AskStubber) { - as.StubPrompt("Select a gist").AnswerWith("gistfile0.txt about 6 hours ago") - }, - response: `{ "data": { "viewer": { "gists": { "nodes": [ - { - "name": "gistid1", - "files": [{ "name": "cool.txt" }], - "description": "", - "updatedAt": "%[1]v", - "isPublic": true - }, - { - "name": "gistid2", - "files": [{ "name": "gistfile0.txt" }], - "description": "", - "updatedAt": "%[1]v", - "isPublic": true - } - ] } } } }`, - wantOut: "gistid2", - }, - { - name: "no files", - response: `{ "data": { "viewer": { "gists": { "nodes": [] } } } }`, - wantOut: "", - }, - } - - ios, _, _, _ := iostreams.Test() - - for _, tt := range tests { - reg := &httpmock.Registry{} - - const query = `query GistList\b` - sixHours, _ := time.ParseDuration("6h") - sixHoursAgo := time.Now().Add(-sixHours) - reg.Register( - httpmock.GraphQL(query), - httpmock.StringResponse(fmt.Sprintf( - tt.response, - sixHoursAgo.Format(time.RFC3339), - )), - ) - client := &http.Client{Transport: reg} - - t.Run(tt.name, func(t *testing.T) { - //nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock - as := prompt.NewAskStubber(t) - if tt.askStubs != nil { - tt.askStubs(as) - } - - gistID, err := promptGists(client, "github.com", ios.ColorScheme()) - assert.NoError(t, err) - assert.Equal(t, tt.wantOut, gistID) - reg.Verify(t) - }) - } -}