From f7c4a0cf3f8436e24ebfc5fed2756da5bf580c2f Mon Sep 17 00:00:00 2001 From: vilmibm Date: Thu, 10 Sep 2020 20:14:29 -0500 Subject: [PATCH 01/32] gh gist list --- pkg/cmd/gist/gist.go | 2 + pkg/cmd/gist/list/http.go | 52 ++++++++++++++++++++ pkg/cmd/gist/list/list.go | 90 ++++++++++++++++++++++++++++++++++ pkg/cmd/gist/list/list_test.go | 84 +++++++++++++++++++++++++++++++ 4 files changed, 228 insertions(+) create mode 100644 pkg/cmd/gist/list/http.go create mode 100644 pkg/cmd/gist/list/list.go create mode 100644 pkg/cmd/gist/list/list_test.go diff --git a/pkg/cmd/gist/gist.go b/pkg/cmd/gist/gist.go index ea10a8c40..e47f055db 100644 --- a/pkg/cmd/gist/gist.go +++ b/pkg/cmd/gist/gist.go @@ -2,6 +2,7 @@ package gist import ( gistCreateCmd "github.com/cli/cli/pkg/cmd/gist/create" + gistListCmd "github.com/cli/cli/pkg/cmd/gist/list" "github.com/cli/cli/pkg/cmdutil" "github.com/spf13/cobra" ) @@ -14,6 +15,7 @@ func NewCmdGist(f *cmdutil.Factory) *cobra.Command { } cmd.AddCommand(gistCreateCmd.NewCmdCreate(f, nil)) + cmd.AddCommand(gistListCmd.NewCmdList(f, nil)) return cmd } diff --git a/pkg/cmd/gist/list/http.go b/pkg/cmd/gist/list/http.go new file mode 100644 index 000000000..e64243977 --- /dev/null +++ b/pkg/cmd/gist/list/http.go @@ -0,0 +1,52 @@ +package list + +import ( + "fmt" + "net/http" + "net/url" + "sort" + "time" + + "github.com/cli/cli/api" +) + +type Gist struct { + Description string `json:"description"` + HTMLURL string `json:"html_url"` + UpdatedAt time.Time `json:"updated_at"` + Public bool +} + +func listGists(client *http.Client, hostname string, limit int, visibility string) ([]Gist, error) { + result := []Gist{} + + query := url.Values{} + if visibility == "all" { + query.Add("per_page", fmt.Sprintf("%d", limit)) + } else { + query.Add("per_page", "100") + } + + apiClient := api.NewClientFromHTTP(client) + err := apiClient.REST(hostname, "GET", "gists?"+query.Encode(), nil, &result) + if err != nil { + return nil, err + } + + gists := []Gist{} + + for _, gist := range result { + if len(gists) == limit { + break + } + if visibility == "all" || (visibility == "private" && !gist.Public) || (visibility == "public" && gist.Public) { + gists = append(gists, gist) + } + } + + sort.SliceStable(gists, func(i, j int) bool { + return gists[i].UpdatedAt.After(gists[j].UpdatedAt) + }) + + return gists, nil +} diff --git a/pkg/cmd/gist/list/list.go b/pkg/cmd/gist/list/list.go new file mode 100644 index 000000000..bf59497f3 --- /dev/null +++ b/pkg/cmd/gist/list/list.go @@ -0,0 +1,90 @@ +package list + +import ( + "fmt" + "net/http" + + "github.com/cli/cli/internal/ghinstance" + "github.com/cli/cli/pkg/cmdutil" + "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/utils" + "github.com/spf13/cobra" +) + +type ListOptions struct { + IO *iostreams.IOStreams + HttpClient func() (*http.Client, error) + + Limit int + Visibility string // all, secret, public +} + +func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command { + opts := &ListOptions{ + IO: f.IOStreams, + HttpClient: f.HttpClient, + } + + cmd := &cobra.Command{ + Use: "list", + Short: "List your gists", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if opts.Limit < 1 { + return &cmdutil.FlagError{Err: fmt.Errorf("invalid limit: %v", opts.Limit)} + } + + pub := cmd.Flags().Changed("public") + priv := cmd.Flags().Changed("private") + + opts.Visibility = "all" + if pub && !priv { + opts.Visibility = "public" + } else if priv && !pub { + opts.Visibility = "private" + } + + if runF != nil { + return runF(opts) + } + + return listRun(opts) + }, + } + + cmd.Flags().IntVarP(&opts.Limit, "limit", "L", 10, "Maximum number of gists to fetch") + cmd.Flags().Bool("public", false, "Show only public gists") + cmd.Flags().Bool("private", false, "Show only private gists") + + return cmd +} + +func listRun(opts *ListOptions) error { + client, err := opts.HttpClient() + if err != nil { + return err + } + + gists, err := listGists(client, ghinstance.OverridableDefault(), opts.Limit, opts.Visibility) + if err != nil { + return err + } + + cs := opts.IO.ColorScheme() + + tp := utils.NewTablePrinter(opts.IO) + + for _, gist := range gists { + // TODO i was getting confusing results with table printer's truncation + description := gist.Description + if len(description) > 40 { + description = description[0:37] + "..." + } + + tp.AddField(description, nil, cs.Bold) + tp.AddField(gist.HTMLURL, nil, nil) + tp.EndRow() + } + + return tp.Render() +} diff --git a/pkg/cmd/gist/list/list_test.go b/pkg/cmd/gist/list/list_test.go new file mode 100644 index 000000000..03266cdf2 --- /dev/null +++ b/pkg/cmd/gist/list/list_test.go @@ -0,0 +1,84 @@ +package list + +import ( + "bytes" + "testing" + + "github.com/cli/cli/pkg/cmdutil" + "github.com/google/shlex" + "github.com/stretchr/testify/assert" +) + +func TestNewCmdList(t *testing.T) { + tests := []struct { + name string + cli string + wants ListOptions + }{ + { + name: "no arguments", + cli: "", + wants: ListOptions{ + Limit: 10, + Visibility: "all", + }, + }, + { + name: "public", + cli: "--public", + wants: ListOptions{ + Limit: 10, + Visibility: "public", + }, + }, + { + name: "private", + cli: "--private", + wants: ListOptions{ + Limit: 10, + Visibility: "private", + }, + }, + { + name: "public and private", + cli: "--private --public", + wants: ListOptions{ + Limit: 10, + Visibility: "all", + }, + }, + { + name: "limit", + cli: "--limit 30", + wants: ListOptions{ + Limit: 30, + Visibility: "all", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := &cmdutil.Factory{} + + argv, err := shlex.Split(tt.cli) + assert.NoError(t, err) + + var gotOpts *ListOptions + cmd := NewCmdList(f, func(opts *ListOptions) error { + gotOpts = opts + return nil + }) + cmd.SetArgs(argv) + cmd.SetIn(&bytes.Buffer{}) + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + + _, err = cmd.ExecuteC() + assert.NoError(t, err) + + assert.Equal(t, tt.wants.Visibility, gotOpts.Visibility) + assert.Equal(t, tt.wants.Limit, gotOpts.Limit) + }) + } +} From b17124157c062ed43268412767b82bd0ad4adc76 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Fri, 11 Sep 2020 13:42:05 -0500 Subject: [PATCH 02/32] start on gist view --- pkg/cmd/gist/gist.go | 2 + pkg/cmd/gist/list/list_test.go | 2 + pkg/cmd/gist/view/http.go | 33 ++++++++++++ pkg/cmd/gist/view/view.go | 94 ++++++++++++++++++++++++++++++++++ pkg/cmd/gist/view/view_test.go | 70 +++++++++++++++++++++++++ 5 files changed, 201 insertions(+) create mode 100644 pkg/cmd/gist/view/http.go create mode 100644 pkg/cmd/gist/view/view.go create mode 100644 pkg/cmd/gist/view/view_test.go diff --git a/pkg/cmd/gist/gist.go b/pkg/cmd/gist/gist.go index e47f055db..612ef0bcf 100644 --- a/pkg/cmd/gist/gist.go +++ b/pkg/cmd/gist/gist.go @@ -3,6 +3,7 @@ package gist import ( gistCreateCmd "github.com/cli/cli/pkg/cmd/gist/create" gistListCmd "github.com/cli/cli/pkg/cmd/gist/list" + gistViewCmd "github.com/cli/cli/pkg/cmd/gist/view" "github.com/cli/cli/pkg/cmdutil" "github.com/spf13/cobra" ) @@ -16,6 +17,7 @@ func NewCmdGist(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(gistCreateCmd.NewCmdCreate(f, nil)) cmd.AddCommand(gistListCmd.NewCmdList(f, nil)) + cmd.AddCommand(gistViewCmd.NewCmdView(f, nil)) return cmd } diff --git a/pkg/cmd/gist/list/list_test.go b/pkg/cmd/gist/list/list_test.go index 03266cdf2..5541a51bc 100644 --- a/pkg/cmd/gist/list/list_test.go +++ b/pkg/cmd/gist/list/list_test.go @@ -82,3 +82,5 @@ func TestNewCmdList(t *testing.T) { }) } } + +// TODO execution tests diff --git a/pkg/cmd/gist/view/http.go b/pkg/cmd/gist/view/http.go new file mode 100644 index 000000000..617fa2ec5 --- /dev/null +++ b/pkg/cmd/gist/view/http.go @@ -0,0 +1,33 @@ +package view + +import ( + "fmt" + "net/http" + + "github.com/cli/cli/api" +) + +type GistFile struct { + Filename string + Type string + Language string + Content string +} + +type Gist struct { + Description string + Files map[string]GistFile +} + +func getGist(client *http.Client, hostname, gistID string) (*Gist, error) { + gist := Gist{} + path := fmt.Sprintf("gists/%s", gistID) + + apiClient := api.NewClientFromHTTP(client) + err := apiClient.REST(hostname, "GET", path, nil, &gist) + if err != nil { + return nil, err + } + + return &gist, nil +} diff --git a/pkg/cmd/gist/view/view.go b/pkg/cmd/gist/view/view.go new file mode 100644 index 000000000..be9ac4bde --- /dev/null +++ b/pkg/cmd/gist/view/view.go @@ -0,0 +1,94 @@ +package view + +import ( + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/cli/cli/internal/ghinstance" + "github.com/cli/cli/pkg/cmdutil" + "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/utils" + "github.com/spf13/cobra" +) + +type ViewOptions struct { + IO *iostreams.IOStreams + HttpClient func() (*http.Client, error) + + Selector string + Raw bool +} + +func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Command { + opts := &ViewOptions{ + IO: f.IOStreams, + HttpClient: f.HttpClient, + } + + cmd := &cobra.Command{ + Use: "view { | }", + Short: "View a gist", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.Selector = args[0] + + if !opts.IO.IsStdoutTTY() { + opts.Raw = true + } + + if runF != nil { + return runF(opts) + } + return viewRun(opts) + }, + } + + cmd.Flags().BoolVarP(&opts.Raw, "raw", "r", false, "do not try and render markdown") + + return cmd +} + +func viewRun(opts *ViewOptions) error { + gistID := opts.Selector + + u, err := url.Parse(opts.Selector) + if err == nil { + if strings.HasPrefix(u.Path, "/") { + gistID = u.Path[1:] + } + } + + client, err := opts.HttpClient() + if err != nil { + return err + } + + gist, err := getGist(client, ghinstance.OverridableDefault(), gistID) + if err != nil { + return err + } + + cs := opts.IO.ColorScheme() + if gist.Description != "" { + fmt.Fprintf(opts.IO.ErrOut, "%s\n", cs.Bold(gist.Description)) + } + + for filename, gistFile := range gist.Files { + fmt.Fprintf(opts.IO.ErrOut, "%s\n", cs.Gray(filename)) + fmt.Fprintln(opts.IO.ErrOut) + content := gistFile.Content + if strings.Contains(gistFile.Type, "markdown") && !opts.Raw { + rendered, err := utils.RenderMarkdown(gistFile.Content) + if err == nil { + content = rendered + } + } + fmt.Fprintf(opts.IO.Out, "%s\n", content) + fmt.Fprintln(opts.IO.Out) + } + + // TODO print gist files, possibly with rendered markdown + return nil +} diff --git a/pkg/cmd/gist/view/view_test.go b/pkg/cmd/gist/view/view_test.go new file mode 100644 index 000000000..d77ab946d --- /dev/null +++ b/pkg/cmd/gist/view/view_test.go @@ -0,0 +1,70 @@ +package view + +import ( + "bytes" + "testing" + + "github.com/cli/cli/pkg/cmdutil" + "github.com/cli/cli/pkg/iostreams" + "github.com/google/shlex" + "github.com/stretchr/testify/assert" +) + +func TestNewCmdView(t *testing.T) { + tests := []struct { + name string + cli string + wants ViewOptions + tty bool + }{ + { + name: "tty no arguments", + tty: true, + cli: "123", + wants: ViewOptions{ + Raw: false, + Selector: "123", + }, + }, + { + name: "nontty no arguments", + cli: "123", + wants: ViewOptions{ + Raw: true, + Selector: "123", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + io, _, _, _ := iostreams.Test() + io.SetStdoutTTY(tt.tty) + + f := &cmdutil.Factory{ + IOStreams: io, + } + + argv, err := shlex.Split(tt.cli) + assert.NoError(t, err) + + var gotOpts *ViewOptions + cmd := NewCmdView(f, func(opts *ViewOptions) error { + gotOpts = opts + return nil + }) + cmd.SetArgs(argv) + cmd.SetIn(&bytes.Buffer{}) + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + + _, err = cmd.ExecuteC() + assert.NoError(t, err) + + assert.Equal(t, tt.wants.Raw, gotOpts.Raw) + assert.Equal(t, tt.wants.Selector, gotOpts.Selector) + }) + } +} + +// TODO execution tests From f3b4493448e5c6a6199e4287dff848f574aa8f62 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Fri, 11 Sep 2020 17:08:14 -0500 Subject: [PATCH 03/32] print aliases as valid yaml when piping --- pkg/cmd/alias/list/list.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pkg/cmd/alias/list/list.go b/pkg/cmd/alias/list/list.go index b9d67c2ae..a8576db5c 100644 --- a/pkg/cmd/alias/list/list.go +++ b/pkg/cmd/alias/list/list.go @@ -69,12 +69,7 @@ func listRun(opts *ListOptions) error { sort.Strings(keys) for _, alias := range keys { - if tp.IsTTY() { - // ensure that screen readers pause - tp.AddField(alias+":", nil, nil) - } else { - tp.AddField(alias, nil, nil) - } + tp.AddField(alias+":", nil, nil) tp.AddField(aliasMap[alias], nil, nil) tp.EndRow() } From b6502deb24e5911121b754ce31107375f10e4f8b Mon Sep 17 00:00:00 2001 From: vilmibm Date: Fri, 11 Sep 2020 17:08:59 -0500 Subject: [PATCH 04/32] allow naming stdin gist files --- pkg/cmd/gist/create/create.go | 18 ++++++++++++------ pkg/cmd/gist/create/create_test.go | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/gist/create/create.go b/pkg/cmd/gist/create/create.go index e2d6975b8..8024cfc24 100644 --- a/pkg/cmd/gist/create/create.go +++ b/pkg/cmd/gist/create/create.go @@ -23,9 +23,10 @@ import ( type CreateOptions struct { IO *iostreams.IOStreams - Description string - Public bool - Filenames []string + Description string + Public bool + Filenames []string + FilenameOverride string HttpClient func() (*http.Client, error) } @@ -84,6 +85,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co cmd.Flags().StringVarP(&opts.Description, "desc", "d", "", "A description for this gist") cmd.Flags().BoolVarP(&opts.Public, "public", "p", false, "List the gist publicly (default: private)") + cmd.Flags().StringVarP(&opts.FilenameOverride, "filename", "f", "", "Provide a filename to be used when reading from STDIN") return cmd } @@ -93,7 +95,7 @@ func createRun(opts *CreateOptions) error { fileArgs = []string{"-"} } - files, err := processFiles(opts.IO.In, fileArgs) + files, err := processFiles(opts.IO.In, opts.FilenameOverride, fileArgs) if err != nil { return fmt.Errorf("failed to collect files for posting: %w", err) } @@ -137,7 +139,7 @@ func createRun(opts *CreateOptions) error { return nil } -func processFiles(stdin io.ReadCloser, filenames []string) (map[string]string, error) { +func processFiles(stdin io.ReadCloser, filenameOverride string, filenames []string) (map[string]string, error) { fs := map[string]string{} if len(filenames) == 0 { @@ -149,7 +151,11 @@ func processFiles(stdin io.ReadCloser, filenames []string) (map[string]string, e var content []byte var err error if f == "-" { - filename = fmt.Sprintf("gistfile%d.txt", i) + if filenameOverride != "" { + filename = filenameOverride + } else { + filename = fmt.Sprintf("gistfile%d.txt", i) + } content, err = ioutil.ReadAll(stdin) if err != nil { return fs, fmt.Errorf("failed to read from stdin: %w", err) diff --git a/pkg/cmd/gist/create/create_test.go b/pkg/cmd/gist/create/create_test.go index bd57cfa2e..043b6f2d1 100644 --- a/pkg/cmd/gist/create/create_test.go +++ b/pkg/cmd/gist/create/create_test.go @@ -21,7 +21,7 @@ const ( func Test_processFiles(t *testing.T) { fakeStdin := strings.NewReader("hey cool how is it going") - files, err := processFiles(ioutil.NopCloser(fakeStdin), []string{"-"}) + files, err := processFiles(ioutil.NopCloser(fakeStdin), "", []string{"-"}) if err != nil { t.Fatalf("unexpected error processing files: %s", err) } From 39b6ec8aec41141e122a0f68fb2bc5b42642742c Mon Sep 17 00:00:00 2001 From: vilmibm Date: Fri, 11 Sep 2020 17:37:45 -0500 Subject: [PATCH 05/32] share gist getting --- pkg/cmd/gist/{view/http.go => shared/shared.go} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename pkg/cmd/gist/{view/http.go => shared/shared.go} (85%) diff --git a/pkg/cmd/gist/view/http.go b/pkg/cmd/gist/shared/shared.go similarity index 85% rename from pkg/cmd/gist/view/http.go rename to pkg/cmd/gist/shared/shared.go index 617fa2ec5..7cfbc5866 100644 --- a/pkg/cmd/gist/view/http.go +++ b/pkg/cmd/gist/shared/shared.go @@ -1,4 +1,4 @@ -package view +package shared import ( "fmt" @@ -19,7 +19,7 @@ type Gist struct { Files map[string]GistFile } -func getGist(client *http.Client, hostname, gistID string) (*Gist, error) { +func GetGist(client *http.Client, hostname, gistID string) (*Gist, error) { gist := Gist{} path := fmt.Sprintf("gists/%s", gistID) From 0d3056d9a75d7d07a80b38e87c987626cd538880 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Fri, 11 Sep 2020 17:41:24 -0500 Subject: [PATCH 06/32] gh gist edit --- pkg/cmd/gist/edit/edit.go | 203 ++++++++++++++++++++++++++++++++++ pkg/cmd/gist/gist.go | 2 + pkg/cmd/gist/list/http.go | 2 +- pkg/cmd/gist/shared/shared.go | 15 ++- pkg/cmd/gist/view/view.go | 3 +- 5 files changed, 217 insertions(+), 8 deletions(-) create mode 100644 pkg/cmd/gist/edit/edit.go diff --git a/pkg/cmd/gist/edit/edit.go b/pkg/cmd/gist/edit/edit.go new file mode 100644 index 000000000..097cfad6b --- /dev/null +++ b/pkg/cmd/gist/edit/edit.go @@ -0,0 +1,203 @@ +package edit + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "os" + "sort" + "strings" + + "github.com/AlecAivazis/survey/v2" + "github.com/cli/cli/api" + "github.com/cli/cli/internal/config" + "github.com/cli/cli/internal/ghinstance" + "github.com/cli/cli/pkg/cmd/gist/shared" + "github.com/cli/cli/pkg/cmdutil" + "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/pkg/prompt" + "github.com/cli/cli/pkg/surveyext" + "github.com/spf13/cobra" +) + +type EditOptions struct { + IO *iostreams.IOStreams + HttpClient func() (*http.Client, error) + Config func() (config.Config, error) + + Selector string + Filename string +} + +func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Command { + opts := EditOptions{ + IO: f.IOStreams, + HttpClient: f.HttpClient, + Config: f.Config, + } + + cmd := &cobra.Command{ + Use: "edit { | }", + Short: "Edit one of your gists", + Args: cobra.ExactArgs(1), + RunE: func(c *cobra.Command, args []string) error { + opts.Selector = args[0] + + if runF != nil { + return runF(&opts) + } + + return editRun(&opts) + }, + } + cmd.Flags().StringVarP(&opts.Filename, "filename", "f", "", "a specific file to edit") + + return cmd +} + +func editRun(opts *EditOptions) error { + gistID := opts.Selector + + u, err := url.Parse(opts.Selector) + if err == nil { + if strings.HasPrefix(u.Path, "/") { + gistID = u.Path[1:] + } + } + + client, err := opts.HttpClient() + if err != nil { + return err + } + + gist, err := shared.GetGist(client, ghinstance.OverridableDefault(), gistID) + if err != nil { + return err + } + + filesToUpdate := map[string]string{} + + for true { + filename := opts.Filename + candidates := []string{} + for filename, _ := range gist.Files { + candidates = append(candidates, filename) + } + + sort.Strings(candidates) + + if filename == "" { + if len(candidates) == 1 { + filename = candidates[0] + } else { + if !opts.IO.CanPrompt() { + return errors.New("unsure what file to edit; either specify --filename or run interactively") + } + err = prompt.SurveyAskOne(&survey.Select{ + Message: "Edit which file?", + Options: candidates, + }, &filename) + } + } + + if _, ok := gist.Files[filename]; !ok { + return fmt.Errorf("gist has no file %q", filename) + } + + editorCommand, err := cmdutil.DetermineEditor(opts.Config) + if err != nil { + return err + } + text, err := surveyext.Edit( + editorCommand, + "*."+filename, + gist.Files[filename].Content, + // TODO: consider using iostreams here + os.Stdin, os.Stdout, os.Stderr, nil) + + if err != nil { + return err + } + + if text != gist.Files[filename].Content { + gistFile := gist.Files[filename] + gistFile.Content = text // so it appears if they re-edit + filesToUpdate[filename] = text + } + + if !opts.IO.CanPrompt() { + break + } + + if len(candidates) == 1 { + break + } + + choice := "" + + err = prompt.SurveyAskOne(&survey.Select{ + Message: "What next?", + Options: []string{ + "Edit another file", + "Submit", + "Cancel", + }, + }, &choice) + + if err != nil { + return err + } + + stop := false + + switch choice { + case "Edit another file": + continue + case "Submit": + stop = true + case "Cancel": + return cmdutil.SilentError + } + + if stop { + break + } + } + + err = updateGist(client, ghinstance.OverridableDefault(), gist) + if err != nil { + return err + } + + return nil +} + +func updateGist(client *http.Client, hostname string, gist *shared.Gist) error { + body := shared.Gist{ + Description: gist.Description, + Files: gist.Files, + } + + path := "gists/" + gist.ID + + requestByte, err := json.Marshal(body) + if err != nil { + return err + } + + requestBody := bytes.NewReader(requestByte) + + result := shared.Gist{} + + apiClient := api.NewClientFromHTTP(client) + err = apiClient.REST(hostname, "POST", path, requestBody, &result) + + if err != nil { + return err + } + + return nil +} diff --git a/pkg/cmd/gist/gist.go b/pkg/cmd/gist/gist.go index 612ef0bcf..7496a72f0 100644 --- a/pkg/cmd/gist/gist.go +++ b/pkg/cmd/gist/gist.go @@ -2,6 +2,7 @@ package gist import ( gistCreateCmd "github.com/cli/cli/pkg/cmd/gist/create" + gistEditCmd "github.com/cli/cli/pkg/cmd/gist/edit" gistListCmd "github.com/cli/cli/pkg/cmd/gist/list" gistViewCmd "github.com/cli/cli/pkg/cmd/gist/view" "github.com/cli/cli/pkg/cmdutil" @@ -18,6 +19,7 @@ func NewCmdGist(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(gistCreateCmd.NewCmdCreate(f, nil)) cmd.AddCommand(gistListCmd.NewCmdList(f, nil)) cmd.AddCommand(gistViewCmd.NewCmdView(f, nil)) + cmd.AddCommand(gistEditCmd.NewCmdEdit(f, nil)) return cmd } diff --git a/pkg/cmd/gist/list/http.go b/pkg/cmd/gist/list/http.go index e64243977..4fcde88f8 100644 --- a/pkg/cmd/gist/list/http.go +++ b/pkg/cmd/gist/list/http.go @@ -11,7 +11,7 @@ import ( ) type Gist struct { - Description string `json:"description"` + Description string HTMLURL string `json:"html_url"` UpdatedAt time.Time `json:"updated_at"` Public bool diff --git a/pkg/cmd/gist/shared/shared.go b/pkg/cmd/gist/shared/shared.go index 7cfbc5866..af67e25af 100644 --- a/pkg/cmd/gist/shared/shared.go +++ b/pkg/cmd/gist/shared/shared.go @@ -7,16 +7,19 @@ import ( "github.com/cli/cli/api" ) +// TODO make gist create use this file + type GistFile struct { - Filename string - Type string - Language string - Content string + Filename string `json:"filename"` + Type string `json:"type,omitempty"` + Language string `json:"language,omitempty"` + Content string `json:"content"` } type Gist struct { - Description string - Files map[string]GistFile + ID string `json:"id,omitempty"` + Description string `json:"description"` + Files map[string]*GistFile `json:"files"` } func GetGist(client *http.Client, hostname, gistID string) (*Gist, error) { diff --git a/pkg/cmd/gist/view/view.go b/pkg/cmd/gist/view/view.go index be9ac4bde..313529d20 100644 --- a/pkg/cmd/gist/view/view.go +++ b/pkg/cmd/gist/view/view.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/cli/cli/internal/ghinstance" + "github.com/cli/cli/pkg/cmd/gist/shared" "github.com/cli/cli/pkg/cmdutil" "github.com/cli/cli/pkg/iostreams" "github.com/cli/cli/utils" @@ -65,7 +66,7 @@ func viewRun(opts *ViewOptions) error { return err } - gist, err := getGist(client, ghinstance.OverridableDefault(), gistID) + gist, err := shared.GetGist(client, ghinstance.OverridableDefault(), gistID) if err != nil { return err } From dc345a9f738145e147a47db6d9758ac4004cee22 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Fri, 11 Sep 2020 20:50:25 -0500 Subject: [PATCH 07/32] support --web for gist view --- internal/ghinstance/host.go | 7 +++++++ pkg/cmd/gist/view/view.go | 14 ++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/internal/ghinstance/host.go b/internal/ghinstance/host.go index 560cc9c5c..642dd0846 100644 --- a/internal/ghinstance/host.go +++ b/internal/ghinstance/host.go @@ -55,3 +55,10 @@ func RESTPrefix(hostname string) string { } return "https://api.github.com/" } + +func GistPrefix(hostname string) string { + if IsEnterprise(hostname) { + return fmt.Sprintf("https://%s/gist/", hostname) + } + return fmt.Sprintf("https://gist.%s/", hostname) +} diff --git a/pkg/cmd/gist/view/view.go b/pkg/cmd/gist/view/view.go index 313529d20..5e4da1e69 100644 --- a/pkg/cmd/gist/view/view.go +++ b/pkg/cmd/gist/view/view.go @@ -20,6 +20,7 @@ type ViewOptions struct { Selector string Raw bool + Web bool } func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Command { @@ -47,6 +48,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman } cmd.Flags().BoolVarP(&opts.Raw, "raw", "r", false, "do not try and render markdown") + cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "open gist in browser") return cmd } @@ -54,6 +56,18 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman func viewRun(opts *ViewOptions) error { gistID := opts.Selector + if opts.Web { + gistURL := gistID + if !strings.Contains(gistURL, "/") { + hostname := ghinstance.OverridableDefault() + gistURL = ghinstance.GistPrefix(hostname) + gistID + } + if opts.IO.IsStderrTTY() { + fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(gistURL)) + } + return utils.OpenInBrowser(gistURL) + } + u, err := url.Parse(opts.Selector) if err == nil { if strings.HasPrefix(u.Path, "/") { From 02d94d6f93741f18095a684d9c3d378a34899462 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Sat, 12 Sep 2020 10:57:48 -0500 Subject: [PATCH 08/32] hmm --- pkg/cmd/gist/list/list.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/gist/list/list.go b/pkg/cmd/gist/list/list.go index bf59497f3..477ec3f9c 100644 --- a/pkg/cmd/gist/list/list.go +++ b/pkg/cmd/gist/list/list.go @@ -77,8 +77,8 @@ func listRun(opts *ListOptions) error { for _, gist := range gists { // TODO i was getting confusing results with table printer's truncation description := gist.Description - if len(description) > 40 { - description = description[0:37] + "..." + if len(description) > 30 { + description = description[0:27] + "..." } tp.AddField(description, nil, cs.Bold) From 269adab75a6a5c0cdc1a270ae53f3af8f574422f Mon Sep 17 00:00:00 2001 From: vilmibm Date: Mon, 14 Sep 2020 10:32:31 -0500 Subject: [PATCH 09/32] improve list output --- pkg/cmd/gist/list/http.go | 15 ++++----------- pkg/cmd/gist/list/list.go | 23 +++++++++++++++++------ pkg/cmd/gist/shared/shared.go | 3 +++ 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/pkg/cmd/gist/list/http.go b/pkg/cmd/gist/list/http.go index 4fcde88f8..9c462f456 100644 --- a/pkg/cmd/gist/list/http.go +++ b/pkg/cmd/gist/list/http.go @@ -5,20 +5,13 @@ import ( "net/http" "net/url" "sort" - "time" "github.com/cli/cli/api" + "github.com/cli/cli/pkg/cmd/gist/shared" ) -type Gist struct { - Description string - HTMLURL string `json:"html_url"` - UpdatedAt time.Time `json:"updated_at"` - Public bool -} - -func listGists(client *http.Client, hostname string, limit int, visibility string) ([]Gist, error) { - result := []Gist{} +func listGists(client *http.Client, hostname string, limit int, visibility string) ([]shared.Gist, error) { + result := []shared.Gist{} query := url.Values{} if visibility == "all" { @@ -33,7 +26,7 @@ func listGists(client *http.Client, hostname string, limit int, visibility strin return nil, err } - gists := []Gist{} + gists := []shared.Gist{} for _, gist := range result { if len(gists) == limit { diff --git a/pkg/cmd/gist/list/list.go b/pkg/cmd/gist/list/list.go index 477ec3f9c..8aba36fc4 100644 --- a/pkg/cmd/gist/list/list.go +++ b/pkg/cmd/gist/list/list.go @@ -3,6 +3,7 @@ package list import ( "fmt" "net/http" + "time" "github.com/cli/cli/internal/ghinstance" "github.com/cli/cli/pkg/cmdutil" @@ -75,14 +76,24 @@ func listRun(opts *ListOptions) error { tp := utils.NewTablePrinter(opts.IO) for _, gist := range gists { - // TODO i was getting confusing results with table printer's truncation - description := gist.Description - if len(description) > 30 { - description = description[0:27] + "..." + fileCount := 0 + for _, _ = range gist.Files { + fileCount++ } - tp.AddField(description, nil, cs.Bold) - tp.AddField(gist.HTMLURL, nil, nil) + visibility := "public" + visColor := cs.Green + if !gist.Public { + visibility = "secret" + visColor = cs.Red + } + + updatedAt := utils.FuzzyAgo(time.Since(gist.UpdatedAt)) + tp.AddField(gist.ID, nil, nil) + tp.AddField(gist.Description, nil, cs.Bold) + tp.AddField(utils.Pluralize(fileCount, "file"), nil, nil) + tp.AddField(visibility, nil, visColor) + tp.AddField(updatedAt, nil, utils.Gray) tp.EndRow() } diff --git a/pkg/cmd/gist/shared/shared.go b/pkg/cmd/gist/shared/shared.go index af67e25af..75e24c197 100644 --- a/pkg/cmd/gist/shared/shared.go +++ b/pkg/cmd/gist/shared/shared.go @@ -3,6 +3,7 @@ package shared import ( "fmt" "net/http" + "time" "github.com/cli/cli/api" ) @@ -20,6 +21,8 @@ type Gist struct { ID string `json:"id,omitempty"` Description string `json:"description"` Files map[string]*GistFile `json:"files"` + UpdatedAt time.Time `json:"updated_at"` + Public bool } func GetGist(client *http.Client, hostname, gistID string) (*Gist, error) { From 1edff18ad41f6fc369091acf93cab95c81071f1f Mon Sep 17 00:00:00 2001 From: vilmibm Date: Mon, 14 Sep 2020 10:38:35 -0500 Subject: [PATCH 10/32] s/private/secret --- pkg/cmd/gist/list/http.go | 2 +- pkg/cmd/gist/list/list.go | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/gist/list/http.go b/pkg/cmd/gist/list/http.go index 9c462f456..abc497423 100644 --- a/pkg/cmd/gist/list/http.go +++ b/pkg/cmd/gist/list/http.go @@ -32,7 +32,7 @@ func listGists(client *http.Client, hostname string, limit int, visibility strin if len(gists) == limit { break } - if visibility == "all" || (visibility == "private" && !gist.Public) || (visibility == "public" && gist.Public) { + if visibility == "all" || (visibility == "secret" && !gist.Public) || (visibility == "public" && gist.Public) { gists = append(gists, gist) } } diff --git a/pkg/cmd/gist/list/list.go b/pkg/cmd/gist/list/list.go index 8aba36fc4..1febf5c41 100644 --- a/pkg/cmd/gist/list/list.go +++ b/pkg/cmd/gist/list/list.go @@ -36,13 +36,13 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman } pub := cmd.Flags().Changed("public") - priv := cmd.Flags().Changed("private") + secret := cmd.Flags().Changed("secret") opts.Visibility = "all" - if pub && !priv { + if pub && !secret { opts.Visibility = "public" - } else if priv && !pub { - opts.Visibility = "private" + } else if secret && !pub { + opts.Visibility = "secret" } if runF != nil { @@ -55,7 +55,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman cmd.Flags().IntVarP(&opts.Limit, "limit", "L", 10, "Maximum number of gists to fetch") cmd.Flags().Bool("public", false, "Show only public gists") - cmd.Flags().Bool("private", false, "Show only private gists") + cmd.Flags().Bool("secret", false, "Show only secret gists") return cmd } From 415c2ac4829617818f5262213485d469353ac6b0 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Mon, 14 Sep 2020 10:43:34 -0500 Subject: [PATCH 11/32] put gist in core commands --- pkg/cmd/gist/gist.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/cmd/gist/gist.go b/pkg/cmd/gist/gist.go index 7496a72f0..6c86cc431 100644 --- a/pkg/cmd/gist/gist.go +++ b/pkg/cmd/gist/gist.go @@ -1,6 +1,7 @@ package gist import ( + "github.com/MakeNowJust/heredoc" gistCreateCmd "github.com/cli/cli/pkg/cmd/gist/create" gistEditCmd "github.com/cli/cli/pkg/cmd/gist/edit" gistListCmd "github.com/cli/cli/pkg/cmd/gist/list" @@ -14,6 +15,14 @@ func NewCmdGist(f *cmdutil.Factory) *cobra.Command { Use: "gist", Short: "Create gists", Long: `Work with GitHub gists.`, + Annotations: map[string]string{ + "IsCore": "true", + "help:arguments": heredoc.Doc(` + A gist can be supplied as argument in any of the following formats: + - by ID, e.g. 5b0e0062eb8e9654adad7bb1d81cc75f + - by URL, e.g. "https://gist.github.com/OWNER/5b0e0062eb8e9654adad7bb1d81cc75f"; or + `), + }, } cmd.AddCommand(gistCreateCmd.NewCmdCreate(f, nil)) From 2df6a6eb8cac7c195e0bc959e35f710ecb3e9dd8 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Mon, 14 Sep 2020 11:04:37 -0500 Subject: [PATCH 12/32] s/private/secret/ --- pkg/cmd/gist/list/list_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/gist/list/list_test.go b/pkg/cmd/gist/list/list_test.go index 5541a51bc..b2e50e99c 100644 --- a/pkg/cmd/gist/list/list_test.go +++ b/pkg/cmd/gist/list/list_test.go @@ -32,16 +32,16 @@ func TestNewCmdList(t *testing.T) { }, }, { - name: "private", - cli: "--private", + name: "secret", + cli: "--secret", wants: ListOptions{ Limit: 10, - Visibility: "private", + Visibility: "secret", }, }, { - name: "public and private", - cli: "--private --public", + name: "public and secret", + cli: "--secret --public", wants: ListOptions{ Limit: 10, Visibility: "all", From e7ab1b753eac35141f3d74cede3dc06397dbca74 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Mon, 14 Sep 2020 11:05:26 -0500 Subject: [PATCH 13/32] linter appeasement --- pkg/cmd/gist/edit/edit.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/gist/edit/edit.go b/pkg/cmd/gist/edit/edit.go index 097cfad6b..3480993f5 100644 --- a/pkg/cmd/gist/edit/edit.go +++ b/pkg/cmd/gist/edit/edit.go @@ -80,7 +80,7 @@ func editRun(opts *EditOptions) error { filesToUpdate := map[string]string{} - for true { + for { filename := opts.Filename candidates := []string{} for filename, _ := range gist.Files { From ecfbaaa31c05f82b9eac1f5b530a2ac5288fa464 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Mon, 14 Sep 2020 11:07:33 -0500 Subject: [PATCH 14/32] linter appeasement --- pkg/cmd/gist/edit/edit.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/cmd/gist/edit/edit.go b/pkg/cmd/gist/edit/edit.go index 3480993f5..76285ed6a 100644 --- a/pkg/cmd/gist/edit/edit.go +++ b/pkg/cmd/gist/edit/edit.go @@ -100,6 +100,10 @@ func editRun(opts *EditOptions) error { Message: "Edit which file?", Options: candidates, }, &filename) + + if err != nil { + return fmt.Errorf("could not prompt: %w", err) + } } } From 190d76abc5e665425f27024a94086408b157e51a Mon Sep 17 00:00:00 2001 From: vilmibm Date: Mon, 14 Sep 2020 11:08:32 -0500 Subject: [PATCH 15/32] linter appeasement --- pkg/cmd/gist/edit/edit.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/gist/edit/edit.go b/pkg/cmd/gist/edit/edit.go index 76285ed6a..de94d09c7 100644 --- a/pkg/cmd/gist/edit/edit.go +++ b/pkg/cmd/gist/edit/edit.go @@ -83,7 +83,7 @@ func editRun(opts *EditOptions) error { for { filename := opts.Filename candidates := []string{} - for filename, _ := range gist.Files { + for filename := range gist.Files { candidates = append(candidates, filename) } From 41382a558b94eb175a14ec7b51233225f4003218 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Mon, 14 Sep 2020 11:09:50 -0500 Subject: [PATCH 16/32] better err --- pkg/cmd/gist/edit/edit.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/gist/edit/edit.go b/pkg/cmd/gist/edit/edit.go index de94d09c7..3caf69b34 100644 --- a/pkg/cmd/gist/edit/edit.go +++ b/pkg/cmd/gist/edit/edit.go @@ -152,7 +152,7 @@ func editRun(opts *EditOptions) error { }, &choice) if err != nil { - return err + return fmt.Errorf("could not prompt: %w", err) } stop := false From b3266d94544fe3e3d8c9c58bc68f0f6b946a7983 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Mon, 14 Sep 2020 11:10:17 -0500 Subject: [PATCH 17/32] linter appeasement --- pkg/cmd/gist/list/list.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/gist/list/list.go b/pkg/cmd/gist/list/list.go index 1febf5c41..dbb9969fb 100644 --- a/pkg/cmd/gist/list/list.go +++ b/pkg/cmd/gist/list/list.go @@ -77,7 +77,7 @@ func listRun(opts *ListOptions) error { for _, gist := range gists { fileCount := 0 - for _, _ = range gist.Files { + for range gist.Files { fileCount++ } From 00b4eab573875f371ccd3e25a88610cb7730fae9 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Mon, 14 Sep 2020 11:29:11 -0500 Subject: [PATCH 18/32] include in readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e3aa83aa4..c5a14ddc2 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ We'd love to hear your feedback about `gh`. If you spot bugs or have features th - `gh pr [status, list, view, checkout, create]` - `gh issue [status, list, view, create]` - `gh repo [view, create, clone, fork]` +- `gh gist [create, list, view, edit]` - `gh auth [login, logout, refresh, status]` - `gh config [get, set]` - `gh help` From 1887fc07c9ae697004ff126f00cd4ffa4a0bebd9 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Tue, 15 Sep 2020 09:39:30 -0500 Subject: [PATCH 19/32] working on list tests, need to debug --- pkg/cmd/gist/list/http.go | 3 ++ pkg/cmd/gist/list/list.go | 7 +++- pkg/cmd/gist/list/list_test.go | 76 ++++++++++++++++++++++++++++++++++ pkg/cmd/gist/shared/shared.go | 2 +- pkg/httpmock/stub.go | 3 ++ 5 files changed, 89 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/gist/list/http.go b/pkg/cmd/gist/list/http.go index abc497423..4e5445ad8 100644 --- a/pkg/cmd/gist/list/http.go +++ b/pkg/cmd/gist/list/http.go @@ -26,6 +26,9 @@ func listGists(client *http.Client, hostname string, limit int, visibility strin return nil, err } + // TODO in tests the api call is matching properly and encoding json properly but i'm getting no + // result and no parse error, wtf? + gists := []shared.Gist{} for _, gist := range result { diff --git a/pkg/cmd/gist/list/list.go b/pkg/cmd/gist/list/list.go index dbb9969fb..ec9e45c0d 100644 --- a/pkg/cmd/gist/list/list.go +++ b/pkg/cmd/gist/list/list.go @@ -16,6 +16,8 @@ type ListOptions struct { IO *iostreams.IOStreams HttpClient func() (*http.Client, error) + Since func(t time.Time) time.Duration + Limit int Visibility string // all, secret, public } @@ -24,6 +26,9 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman opts := &ListOptions{ IO: f.IOStreams, HttpClient: f.HttpClient, + Since: func(t time.Time) time.Duration { + return time.Since(t) + }, } cmd := &cobra.Command{ @@ -88,7 +93,7 @@ func listRun(opts *ListOptions) error { visColor = cs.Red } - updatedAt := utils.FuzzyAgo(time.Since(gist.UpdatedAt)) + updatedAt := utils.FuzzyAgo(opts.Since(gist.UpdatedAt)) tp.AddField(gist.ID, nil, nil) tp.AddField(gist.Description, nil, cs.Bold) tp.AddField(utils.Pluralize(fileCount, "file"), nil, nil) diff --git a/pkg/cmd/gist/list/list_test.go b/pkg/cmd/gist/list/list_test.go index b2e50e99c..dcae4db60 100644 --- a/pkg/cmd/gist/list/list_test.go +++ b/pkg/cmd/gist/list/list_test.go @@ -2,9 +2,14 @@ package list import ( "bytes" + "net/http" "testing" + "time" + "github.com/cli/cli/pkg/cmd/gist/shared" "github.com/cli/cli/pkg/cmdutil" + "github.com/cli/cli/pkg/httpmock" + "github.com/cli/cli/pkg/iostreams" "github.com/google/shlex" "github.com/stretchr/testify/assert" ) @@ -84,3 +89,74 @@ func TestNewCmdList(t *testing.T) { } // TODO execution tests + +func Test_listRun(t *testing.T) { + tests := []struct { + name string + opts *ListOptions + wantOut string + stubs func(*httpmock.Registry) + }{ + { + name: "no gists", + opts: &ListOptions{}, + stubs: func(reg *httpmock.Registry) { + reg.Register(httpmock.REST("GET", "gists"), + httpmock.JSONResponse([]shared.Gist{})) + + }, + wantOut: "", + }, + + { + name: "default behavior", + opts: &ListOptions{}, + wantOut: "TODO", + }, + // TODO public filter + // TODO secret filter + // TODO limit specified + } + + for _, tt := range tests { + reg := &httpmock.Registry{} + if tt.stubs == nil { + reg.Register(httpmock.REST("GET", "gists"), + httpmock.JSONResponse([]shared.Gist{ + { + ID: "1234567890", + Description: "", + Files: map[string]*shared.GistFile{ + "cool.txt": { + Content: "lol", + }, + }, + Public: true, + UpdatedAt: time.Time{}, + }, + })) + } else { + tt.stubs(reg) + } + + tt.opts.HttpClient = func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + } + + tt.opts.Since = func(t time.Time) time.Duration { + d, _ := time.ParseDuration("6h") + return d + } + + io, _, stdout, _ := iostreams.Test() + tt.opts.IO = io + + t.Run(tt.name, func(t *testing.T) { + err := listRun(tt.opts) + assert.NoError(t, err) + + assert.Equal(t, tt.wantOut, stdout.String()) + reg.Verify(t) + }) + } +} diff --git a/pkg/cmd/gist/shared/shared.go b/pkg/cmd/gist/shared/shared.go index 75e24c197..95d8bae31 100644 --- a/pkg/cmd/gist/shared/shared.go +++ b/pkg/cmd/gist/shared/shared.go @@ -22,7 +22,7 @@ type Gist struct { Description string `json:"description"` Files map[string]*GistFile `json:"files"` UpdatedAt time.Time `json:"updated_at"` - Public bool + Public bool `json:"public"` } func GetGist(client *http.Client, hostname, gistID string) (*Gist, error) { diff --git a/pkg/httpmock/stub.go b/pkg/httpmock/stub.go index a1dcefaa3..e4572efe9 100644 --- a/pkg/httpmock/stub.go +++ b/pkg/httpmock/stub.go @@ -3,6 +3,7 @@ package httpmock import ( "bytes" "encoding/json" + "fmt" "io" "io/ioutil" "net/http" @@ -86,6 +87,8 @@ func StatusStringResponse(status int, body string) Responder { func JSONResponse(body interface{}) Responder { return func(req *http.Request) (*http.Response, error) { b, _ := json.Marshal(body) + fmt.Printf("DEBUG %#v\n", "COOOOOL") + fmt.Printf("DEBUG %#v\n", string(b)) return httpResponse(200, req, bytes.NewBuffer(b)), nil } } From 425f707c7d7d258ba99b1458b41fb4ec52c5df37 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Tue, 15 Sep 2020 13:36:32 -0500 Subject: [PATCH 20/32] fix tests --- pkg/cmd/gist/list/http.go | 3 --- pkg/cmd/gist/list/list_test.go | 12 ++++++++++-- pkg/httpmock/stub.go | 3 --- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/gist/list/http.go b/pkg/cmd/gist/list/http.go index 4e5445ad8..abc497423 100644 --- a/pkg/cmd/gist/list/http.go +++ b/pkg/cmd/gist/list/http.go @@ -26,9 +26,6 @@ func listGists(client *http.Client, hostname string, limit int, visibility strin return nil, err } - // TODO in tests the api call is matching properly and encoding json properly but i'm getting no - // result and no parse error, wtf? - gists := []shared.Gist{} for _, gist := range result { diff --git a/pkg/cmd/gist/list/list_test.go b/pkg/cmd/gist/list/list_test.go index dcae4db60..d6f707e71 100644 --- a/pkg/cmd/gist/list/list_test.go +++ b/pkg/cmd/gist/list/list_test.go @@ -88,14 +88,13 @@ func TestNewCmdList(t *testing.T) { } } -// TODO execution tests - func Test_listRun(t *testing.T) { tests := []struct { name string opts *ListOptions wantOut string stubs func(*httpmock.Registry) + nontty bool }{ { name: "no gists", @@ -116,6 +115,7 @@ func Test_listRun(t *testing.T) { // TODO public filter // TODO secret filter // TODO limit specified + // TODO nontty output } for _, tt := range tests { @@ -149,8 +149,16 @@ func Test_listRun(t *testing.T) { } io, _, stdout, _ := iostreams.Test() + io.SetStdoutTTY(!tt.nontty) tt.opts.IO = io + if tt.opts.Limit == 0 { + tt.opts.Limit = 10 + } + + if tt.opts.Visibility == "" { + tt.opts.Visibility = "all" + } t.Run(tt.name, func(t *testing.T) { err := listRun(tt.opts) assert.NoError(t, err) diff --git a/pkg/httpmock/stub.go b/pkg/httpmock/stub.go index e4572efe9..a1dcefaa3 100644 --- a/pkg/httpmock/stub.go +++ b/pkg/httpmock/stub.go @@ -3,7 +3,6 @@ package httpmock import ( "bytes" "encoding/json" - "fmt" "io" "io/ioutil" "net/http" @@ -87,8 +86,6 @@ func StatusStringResponse(status int, body string) Responder { func JSONResponse(body interface{}) Responder { return func(req *http.Request) (*http.Response, error) { b, _ := json.Marshal(body) - fmt.Printf("DEBUG %#v\n", "COOOOOL") - fmt.Printf("DEBUG %#v\n", string(b)) return httpResponse(200, req, bytes.NewBuffer(b)), nil } } From 9fd87faadc223e9a1ab50c6172eaf83401974eaf Mon Sep 17 00:00:00 2001 From: vilmibm Date: Tue, 15 Sep 2020 14:15:44 -0500 Subject: [PATCH 21/32] wip tests --- pkg/cmd/gist/list/http.go | 1 + pkg/cmd/gist/list/list_test.go | 62 +++++++++++++++++++++++++++++----- 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/gist/list/http.go b/pkg/cmd/gist/list/http.go index abc497423..aa75eef25 100644 --- a/pkg/cmd/gist/list/http.go +++ b/pkg/cmd/gist/list/http.go @@ -20,6 +20,7 @@ func listGists(client *http.Client, hostname string, limit int, visibility strin query.Add("per_page", "100") } + // TODO switch to graphql apiClient := api.NewClientFromHTTP(client) err := apiClient.REST(hostname, "GET", "gists?"+query.Encode(), nil, &result) if err != nil { diff --git a/pkg/cmd/gist/list/list_test.go b/pkg/cmd/gist/list/list_test.go index d6f707e71..d08973532 100644 --- a/pkg/cmd/gist/list/list_test.go +++ b/pkg/cmd/gist/list/list_test.go @@ -106,16 +106,32 @@ func Test_listRun(t *testing.T) { }, wantOut: "", }, - { name: "default behavior", opts: &ListOptions{}, - wantOut: "TODO", + wantOut: "1234567890 1 file public about 6 hours ago\n2345678901 tea leaves thwart... 2 files secret about 6 hours ago\n3456789012 short desc 11 files secret about 6 hours ago\n", + }, + { + name: "with public filter", + opts: &ListOptions{Visibility: "public"}, + wantOut: "1234567890 1 file public about 6 hours ago\n", + }, + { + name: "with secret filter", + opts: &ListOptions{Visibility: "secret"}, + wantOut: "2345678901 tea leaves thwart... 2 files secret about 6 hours ago\n3456789012 short desc 11 files secret about 6 hours ago\n", + }, + { + name: "with limit", + opts: &ListOptions{Limit: 1}, + wantOut: "1234567890 1 file public about 6 hours ago\n", + }, + { + name: "nontty output", + opts: &ListOptions{}, + wantOut: "", + nontty: true, }, - // TODO public filter - // TODO secret filter - // TODO limit specified - // TODO nontty output } for _, tt := range tests { @@ -131,8 +147,38 @@ func Test_listRun(t *testing.T) { Content: "lol", }, }, - Public: true, - UpdatedAt: time.Time{}, + Public: true, + }, + { + ID: "2345678901", + Description: "tea leaves thwart those who court catastrophe", + Files: map[string]*shared.GistFile{ + "gistfile0.txt": { + Content: "lolol", + }, + "gistfile1.txt": { + Content: "lololol", + }, + }, + Public: false, + }, + { + ID: "3456789012", + Description: "short desc", + Files: map[string]*shared.GistFile{ + "gistfile0.txt": {}, + "gistfile1.txt": {}, + "gistfile2.txt": {}, + "gistfile3.txt": {}, + "gistfile4.txt": {}, + "gistfile5.txt": {}, + "gistfile6.txt": {}, + "gistfile7.txt": {}, + "gistfile8.txt": {}, + "gistfile9.txt": {}, + "gistfile10.txt": {}, + }, + Public: false, }, })) } else { From ba5b639be437ecd9bead6fb4d8763d0e9c37caf7 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Tue, 15 Sep 2020 14:33:14 -0500 Subject: [PATCH 22/32] finish list tests --- pkg/cmd/gist/list/list.go | 8 ++++++-- pkg/cmd/gist/list/list_test.go | 3 +-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/gist/list/list.go b/pkg/cmd/gist/list/list.go index ec9e45c0d..03882152d 100644 --- a/pkg/cmd/gist/list/list.go +++ b/pkg/cmd/gist/list/list.go @@ -93,12 +93,16 @@ func listRun(opts *ListOptions) error { visColor = cs.Red } - updatedAt := utils.FuzzyAgo(opts.Since(gist.UpdatedAt)) tp.AddField(gist.ID, nil, nil) tp.AddField(gist.Description, nil, cs.Bold) tp.AddField(utils.Pluralize(fileCount, "file"), nil, nil) tp.AddField(visibility, nil, visColor) - tp.AddField(updatedAt, nil, utils.Gray) + if tp.IsTTY() { + updatedAt := utils.FuzzyAgo(opts.Since(gist.UpdatedAt)) + tp.AddField(updatedAt, nil, cs.Gray) + } else { + tp.AddField(gist.UpdatedAt.String(), nil, nil) + } tp.EndRow() } diff --git a/pkg/cmd/gist/list/list_test.go b/pkg/cmd/gist/list/list_test.go index d08973532..2507f3e6b 100644 --- a/pkg/cmd/gist/list/list_test.go +++ b/pkg/cmd/gist/list/list_test.go @@ -22,7 +22,6 @@ func TestNewCmdList(t *testing.T) { }{ { name: "no arguments", - cli: "", wants: ListOptions{ Limit: 10, Visibility: "all", @@ -129,7 +128,7 @@ func Test_listRun(t *testing.T) { { name: "nontty output", opts: &ListOptions{}, - wantOut: "", + wantOut: "1234567890\t\t1 file\tpublic\t0001-01-01 00:00:00 +0000 UTC\n2345678901\ttea leaves thwart those who court catastrophe\t2 files\tsecret\t0001-01-01 00:00:00 +0000 UTC\n3456789012\tshort desc\t11 files\tsecret\t0001-01-01 00:00:00 +0000 UTC\n", nontty: true, }, } From a61c897e4ce2777ee3eb866891554f6513808aa7 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Tue, 15 Sep 2020 14:55:55 -0500 Subject: [PATCH 23/32] show filename if no description --- pkg/cmd/gist/list/list.go | 13 ++++++++++++- pkg/cmd/gist/list/list_test.go | 28 +++++++++++++++------------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/pkg/cmd/gist/list/list.go b/pkg/cmd/gist/list/list.go index 03882152d..1040d5f87 100644 --- a/pkg/cmd/gist/list/list.go +++ b/pkg/cmd/gist/list/list.go @@ -3,6 +3,7 @@ package list import ( "fmt" "net/http" + "strings" "time" "github.com/cli/cli/internal/ghinstance" @@ -93,8 +94,18 @@ func listRun(opts *ListOptions) error { visColor = cs.Red } + description := gist.Description + if description == "" { + for filename, _ := range gist.Files { + if !strings.HasPrefix(filename, "gistfile") { + description = filename + break + } + } + } + tp.AddField(gist.ID, nil, nil) - tp.AddField(gist.Description, nil, cs.Bold) + tp.AddField(description, nil, cs.Bold) tp.AddField(utils.Pluralize(fileCount, "file"), nil, nil) tp.AddField(visibility, nil, visColor) if tp.IsTTY() { diff --git a/pkg/cmd/gist/list/list_test.go b/pkg/cmd/gist/list/list_test.go index 2507f3e6b..26fa46114 100644 --- a/pkg/cmd/gist/list/list_test.go +++ b/pkg/cmd/gist/list/list_test.go @@ -108,12 +108,12 @@ func Test_listRun(t *testing.T) { { name: "default behavior", opts: &ListOptions{}, - wantOut: "1234567890 1 file public about 6 hours ago\n2345678901 tea leaves thwart... 2 files secret about 6 hours ago\n3456789012 short desc 11 files secret about 6 hours ago\n", + wantOut: "1234567890 cool.txt 1 file public about 6 hours ago\n4567890123 1 file public about 6 hours ago\n2345678901 tea leaves thwart... 2 files secret about 6 hours ago\n3456789012 short desc 11 files secret about 6 hours ago\n", }, { name: "with public filter", opts: &ListOptions{Visibility: "public"}, - wantOut: "1234567890 1 file public about 6 hours ago\n", + wantOut: "1234567890 cool.txt 1 file public about 6 hours ago\n4567890123 1 file public about 6 hours ago\n", }, { name: "with secret filter", @@ -123,12 +123,12 @@ func Test_listRun(t *testing.T) { { name: "with limit", opts: &ListOptions{Limit: 1}, - wantOut: "1234567890 1 file public about 6 hours ago\n", + wantOut: "1234567890 cool.txt 1 file public about 6 hours ago\n", }, { name: "nontty output", opts: &ListOptions{}, - wantOut: "1234567890\t\t1 file\tpublic\t0001-01-01 00:00:00 +0000 UTC\n2345678901\ttea leaves thwart those who court catastrophe\t2 files\tsecret\t0001-01-01 00:00:00 +0000 UTC\n3456789012\tshort desc\t11 files\tsecret\t0001-01-01 00:00:00 +0000 UTC\n", + wantOut: "1234567890\tcool.txt\t1 file\tpublic\t0001-01-01 00:00:00 +0000 UTC\n4567890123\t\t1 file\tpublic\t0001-01-01 00:00:00 +0000 UTC\n2345678901\ttea leaves thwart those who court catastrophe\t2 files\tsecret\t0001-01-01 00:00:00 +0000 UTC\n3456789012\tshort desc\t11 files\tsecret\t0001-01-01 00:00:00 +0000 UTC\n", nontty: true, }, } @@ -142,9 +142,15 @@ func Test_listRun(t *testing.T) { ID: "1234567890", Description: "", Files: map[string]*shared.GistFile{ - "cool.txt": { - Content: "lol", - }, + "cool.txt": {}, + }, + Public: true, + }, + { + ID: "4567890123", + Description: "", + Files: map[string]*shared.GistFile{ + "gistfile0.txt": {}, }, Public: true, }, @@ -152,12 +158,8 @@ func Test_listRun(t *testing.T) { ID: "2345678901", Description: "tea leaves thwart those who court catastrophe", Files: map[string]*shared.GistFile{ - "gistfile0.txt": { - Content: "lolol", - }, - "gistfile1.txt": { - Content: "lololol", - }, + "gistfile0.txt": {}, + "gistfile1.txt": {}, }, Public: false, }, From f124b426fe36a7ab11b7da0596c0e9d055ed77c9 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Tue, 15 Sep 2020 14:56:13 -0500 Subject: [PATCH 24/32] tweak gist view; support --filename --- pkg/cmd/gist/view/view.go | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/gist/view/view.go b/pkg/cmd/gist/view/view.go index 5e4da1e69..cbe735675 100644 --- a/pkg/cmd/gist/view/view.go +++ b/pkg/cmd/gist/view/view.go @@ -19,6 +19,7 @@ type ViewOptions struct { HttpClient func() (*http.Client, error) Selector string + Filename string Raw bool Web bool } @@ -49,6 +50,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman cmd.Flags().BoolVarP(&opts.Raw, "raw", "r", false, "do not try and render markdown") cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "open gist in browser") + cmd.Flags().StringVarP(&opts.Filename, "filename", "f", "", "display a single file of the gist") return cmd } @@ -87,12 +89,27 @@ func viewRun(opts *ViewOptions) error { cs := opts.IO.ColorScheme() if gist.Description != "" { - fmt.Fprintf(opts.IO.ErrOut, "%s\n", cs.Bold(gist.Description)) + fmt.Fprintf(opts.IO.Out, "%s\n", cs.Bold(gist.Description)) } + if opts.Filename != "" { + gistFile, ok := gist.Files[opts.Filename] + if !ok { + return fmt.Errorf("gist has no such file %q", opts.Filename) + } + + gist.Files = map[string]*shared.GistFile{ + opts.Filename: gistFile, + } + } + + showFilenames := len(gist.Files) > 1 + for filename, gistFile := range gist.Files { - fmt.Fprintf(opts.IO.ErrOut, "%s\n", cs.Gray(filename)) - fmt.Fprintln(opts.IO.ErrOut) + if showFilenames { + fmt.Fprintf(opts.IO.Out, "%s\n", cs.Gray(filename)) + fmt.Fprintln(opts.IO.Out) + } content := gistFile.Content if strings.Contains(gistFile.Type, "markdown") && !opts.Raw { rendered, err := utils.RenderMarkdown(gistFile.Content) @@ -104,6 +121,5 @@ func viewRun(opts *ViewOptions) error { fmt.Fprintln(opts.IO.Out) } - // TODO print gist files, possibly with rendered markdown return nil } From 4a467864d544fc5aec06acdbb641ee58fa0c8c37 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Tue, 15 Sep 2020 15:22:28 -0500 Subject: [PATCH 25/32] linter appeasement --- pkg/cmd/gist/list/list.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/gist/list/list.go b/pkg/cmd/gist/list/list.go index 1040d5f87..5e22e267b 100644 --- a/pkg/cmd/gist/list/list.go +++ b/pkg/cmd/gist/list/list.go @@ -96,7 +96,7 @@ func listRun(opts *ListOptions) error { description := gist.Description if description == "" { - for filename, _ := range gist.Files { + for filename := range gist.Files { if !strings.HasPrefix(filename, "gistfile") { description = filename break From ada2c566067b64db942c1de32578e74c6ea3c005 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Tue, 15 Sep 2020 16:05:30 -0500 Subject: [PATCH 26/32] test gist view --- pkg/cmd/gist/view/view.go | 18 +++- pkg/cmd/gist/view/view_test.go | 149 ++++++++++++++++++++++++++++++++- 2 files changed, 162 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/gist/view/view.go b/pkg/cmd/gist/view/view.go index cbe735675..fecb69adb 100644 --- a/pkg/cmd/gist/view/view.go +++ b/pkg/cmd/gist/view/view.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "net/url" + "sort" "strings" "github.com/cli/cli/internal/ghinstance" @@ -105,10 +106,12 @@ func viewRun(opts *ViewOptions) error { showFilenames := len(gist.Files) > 1 + outs := []string{} // to ensure consistent ordering + for filename, gistFile := range gist.Files { + out := "" if showFilenames { - fmt.Fprintf(opts.IO.Out, "%s\n", cs.Gray(filename)) - fmt.Fprintln(opts.IO.Out) + out += fmt.Sprintf("%s\n\n", cs.Gray(filename)) } content := gistFile.Content if strings.Contains(gistFile.Type, "markdown") && !opts.Raw { @@ -117,8 +120,15 @@ func viewRun(opts *ViewOptions) error { content = rendered } } - fmt.Fprintf(opts.IO.Out, "%s\n", content) - fmt.Fprintln(opts.IO.Out) + out += fmt.Sprintf("%s\n\n", content) + + outs = append(outs, out) + } + + sort.Strings(outs) + + for _, out := range outs { + fmt.Fprint(opts.IO.Out, out) } return nil diff --git a/pkg/cmd/gist/view/view_test.go b/pkg/cmd/gist/view/view_test.go index d77ab946d..0ddc33181 100644 --- a/pkg/cmd/gist/view/view_test.go +++ b/pkg/cmd/gist/view/view_test.go @@ -2,9 +2,12 @@ package view import ( "bytes" + "net/http" "testing" + "github.com/cli/cli/pkg/cmd/gist/shared" "github.com/cli/cli/pkg/cmdutil" + "github.com/cli/cli/pkg/httpmock" "github.com/cli/cli/pkg/iostreams" "github.com/google/shlex" "github.com/stretchr/testify/assert" @@ -34,6 +37,16 @@ func TestNewCmdView(t *testing.T) { Selector: "123", }, }, + { + name: "filename passed", + cli: "-fcool.txt 123", + tty: true, + wants: ViewOptions{ + Raw: false, + Selector: "123", + Filename: "cool.txt", + }, + }, } for _, tt := range tests { @@ -63,8 +76,142 @@ func TestNewCmdView(t *testing.T) { assert.Equal(t, tt.wants.Raw, gotOpts.Raw) assert.Equal(t, tt.wants.Selector, gotOpts.Selector) + assert.Equal(t, tt.wants.Filename, gotOpts.Filename) }) } } -// TODO execution tests +func Test_viewRun(t *testing.T) { + tests := []struct { + name string + opts *ViewOptions + wantOut string + gist *shared.Gist + wantErr bool + }{ + { + name: "no such gist", + wantErr: true, + }, + { + name: "one file", + gist: &shared.Gist{ + Files: map[string]*shared.GistFile{ + "cicada.txt": { + Content: "bwhiizzzbwhuiiizzzz", + Type: "text/plain", + }, + }, + }, + wantOut: "bwhiizzzbwhuiiizzzz\n\n", + }, + { + name: "filename selected", + opts: &ViewOptions{ + Filename: "cicada.txt", + }, + gist: &shared.Gist{ + Files: map[string]*shared.GistFile{ + "cicada.txt": { + Content: "bwhiizzzbwhuiiizzzz", + Type: "text/plain", + }, + "foo.md": { + Content: "# foo", + Type: "application/markdown", + }, + }, + }, + wantOut: "bwhiizzzbwhuiiizzzz\n\n", + }, + { + name: "multiple files, no description", + gist: &shared.Gist{ + Files: map[string]*shared.GistFile{ + "cicada.txt": { + Content: "bwhiizzzbwhuiiizzzz", + Type: "text/plain", + }, + "foo.md": { + Content: "# foo", + Type: "application/markdown", + }, + }, + }, + wantOut: "cicada.txt\n\nbwhiizzzbwhuiiizzzz\n\nfoo.md\n\n\n # foo \n\n\n\n", + }, + { + name: "multiple files, description", + gist: &shared.Gist{ + Description: "some files", + Files: map[string]*shared.GistFile{ + "cicada.txt": { + Content: "bwhiizzzbwhuiiizzzz", + Type: "text/plain", + }, + "foo.md": { + Content: "- foo", + Type: "application/markdown", + }, + }, + }, + wantOut: "some files\ncicada.txt\n\nbwhiizzzbwhuiiizzzz\n\nfoo.md\n\n\n \n • foo \n\n\n\n", + }, + { + name: "raw", + opts: &ViewOptions{ + Raw: true, + }, + gist: &shared.Gist{ + Description: "some files", + Files: map[string]*shared.GistFile{ + "cicada.txt": { + Content: "bwhiizzzbwhuiiizzzz", + Type: "text/plain", + }, + "foo.md": { + Content: "- foo", + Type: "application/markdown", + }, + }, + }, + wantOut: "some files\ncicada.txt\n\nbwhiizzzbwhuiiizzzz\n\nfoo.md\n\n- foo\n\n", + }, + } + + for _, tt := range tests { + reg := &httpmock.Registry{} + if tt.gist == nil { + reg.Register(httpmock.REST("GET", "gists/1234"), + httpmock.StatusStringResponse(404, "Not Found")) + } else { + reg.Register(httpmock.REST("GET", "gists/1234"), + httpmock.JSONResponse(tt.gist)) + } + + if tt.opts == nil { + tt.opts = &ViewOptions{} + } + + tt.opts.HttpClient = func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + } + io, _, stdout, _ := iostreams.Test() + io.SetStdoutTTY(true) + tt.opts.IO = io + + tt.opts.Selector = "1234" + + t.Run(tt.name, func(t *testing.T) { + err := viewRun(tt.opts) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + assert.Equal(t, tt.wantOut, stdout.String()) + reg.Verify(t) + }) + } +} From 62f54f0f029b546c34bc50de1e866966c183079c Mon Sep 17 00:00:00 2001 From: vilmibm Date: Tue, 15 Sep 2020 16:18:26 -0500 Subject: [PATCH 27/32] start on edit tests --- pkg/cmd/gist/edit/edit_test.go | 65 ++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 pkg/cmd/gist/edit/edit_test.go diff --git a/pkg/cmd/gist/edit/edit_test.go b/pkg/cmd/gist/edit/edit_test.go new file mode 100644 index 000000000..eab8c214e --- /dev/null +++ b/pkg/cmd/gist/edit/edit_test.go @@ -0,0 +1,65 @@ +package edit + +import ( + "bytes" + "testing" + + "github.com/cli/cli/pkg/cmdutil" + "github.com/google/shlex" + "github.com/stretchr/testify/assert" +) + +func TestNewCmdEdit(t *testing.T) { + tests := []struct { + name string + cli string + wants EditOptions + }{ + { + name: "no flags", + cli: "123", + wants: EditOptions{ + Selector: "123", + }, + }, + { + name: "filename", + cli: "123 --filename cool.md", + wants: EditOptions{ + Selector: "123", + Filename: "cool.md", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := &cmdutil.Factory{} + + argv, err := shlex.Split(tt.cli) + assert.NoError(t, err) + + var gotOpts *EditOptions + cmd := NewCmdEdit(f, func(opts *EditOptions) error { + gotOpts = opts + return nil + }) + cmd.SetArgs(argv) + cmd.SetIn(&bytes.Buffer{}) + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + + _, err = cmd.ExecuteC() + assert.NoError(t, err) + + assert.Equal(t, tt.wants.Filename, gotOpts.Filename) + assert.Equal(t, tt.wants.Selector, gotOpts.Selector) + }) + } +} + +// TODO execution tests +// TODO no such gist +// TODO one files +// TODO multiple files, submit +// TODO multiple files, cancel From a9ab2a98fca42ee99fa74a44e4e7b8a2c47608c2 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Tue, 15 Sep 2020 17:04:43 -0500 Subject: [PATCH 28/32] move Edit to opts for testing --- pkg/cmd/gist/edit/edit.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/gist/edit/edit.go b/pkg/cmd/gist/edit/edit.go index 3caf69b34..554e9363d 100644 --- a/pkg/cmd/gist/edit/edit.go +++ b/pkg/cmd/gist/edit/edit.go @@ -7,7 +7,6 @@ import ( "fmt" "net/http" "net/url" - "os" "sort" "strings" @@ -28,6 +27,8 @@ type EditOptions struct { HttpClient func() (*http.Client, error) Config func() (config.Config, error) + Edit func(string, string, string, *iostreams.IOStreams) (string, error) + Selector string Filename string } @@ -37,6 +38,13 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman IO: f.IOStreams, HttpClient: f.HttpClient, Config: f.Config, + Edit: func(editorCmd, filename, defaultContent string, io *iostreams.IOStreams) (string, error) { + return surveyext.Edit( + editorCmd, + "*."+filename, + defaultContent, + io.In, io.Out, io.ErrOut, nil) + }, } cmd := &cobra.Command{ @@ -115,12 +123,7 @@ func editRun(opts *EditOptions) error { if err != nil { return err } - text, err := surveyext.Edit( - editorCommand, - "*."+filename, - gist.Files[filename].Content, - // TODO: consider using iostreams here - os.Stdin, os.Stdout, os.Stderr, nil) + text, err := opts.Edit(editorCommand, filename, gist.Files[filename].Content, opts.IO) if err != nil { return err From 15cf786c5a253601ace15885b8380e0273931a0e Mon Sep 17 00:00:00 2001 From: vilmibm Date: Tue, 15 Sep 2020 17:20:36 -0500 Subject: [PATCH 29/32] wip tests --- pkg/cmd/gist/edit/edit_test.go | 126 +++++++++++++++++++++++++++++++-- 1 file changed, 121 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/gist/edit/edit_test.go b/pkg/cmd/gist/edit/edit_test.go index eab8c214e..8b39e95cd 100644 --- a/pkg/cmd/gist/edit/edit_test.go +++ b/pkg/cmd/gist/edit/edit_test.go @@ -2,9 +2,17 @@ package edit import ( "bytes" + "encoding/json" + "io/ioutil" + "net/http" "testing" + "github.com/cli/cli/internal/config" + "github.com/cli/cli/pkg/cmd/gist/shared" "github.com/cli/cli/pkg/cmdutil" + "github.com/cli/cli/pkg/httpmock" + "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/pkg/prompt" "github.com/google/shlex" "github.com/stretchr/testify/assert" ) @@ -58,8 +66,116 @@ func TestNewCmdEdit(t *testing.T) { } } -// TODO execution tests -// TODO no such gist -// TODO one files -// TODO multiple files, submit -// TODO multiple files, cancel +func Test_editRun(t *testing.T) { + tests := []struct { + name string + opts *EditOptions + gist *shared.Gist + httpStubs func(*httpmock.Registry) + askStubs func(*prompt.AskStubber) + nontty bool + wantErr bool + wantParams map[string]interface{} + }{ + { + name: "no such gist", + wantErr: true, + }, + { + name: "one file", + gist: &shared.Gist{ + ID: "1234", + Files: map[string]*shared.GistFile{ + "cicada.txt": { + Filename: "cicada.txt", + Content: "bwhiizzzbwhuiiizzzz", + Type: "text/plain", + }, + }, + }, + //askStubs: func(as *prompt.AskStubber) { + // as.StubOne("new file content") + //}, + httpStubs: func(reg *httpmock.Registry) { + reg.Register(httpmock.REST("POST", "gists/1234"), + httpmock.StatusStringResponse(201, "{}")) + }, + wantParams: map[string]interface{}{ + "description": "", + "updated_at": "0001-01-01T00:00:00Z", + "public": false, + "files": map[string]interface{}{ + "cicada.txt": map[string]interface{}{ + "content": "new file content", + "filename": "cicada.txt", + "type": "text/plain", + }, + }, + }, + }, + // TODO multiple files, submit + // TODO multiple files, cancel + } + + for _, tt := range tests { + reg := &httpmock.Registry{} + if tt.gist == nil { + reg.Register(httpmock.REST("GET", "gists/1234"), + httpmock.StatusStringResponse(404, "Not Found")) + } else { + reg.Register(httpmock.REST("GET", "gists/1234"), + httpmock.JSONResponse(tt.gist)) + } + + if tt.httpStubs != nil { + tt.httpStubs(reg) + } + + as, teardown := prompt.InitAskStubber() + defer teardown() + if tt.askStubs != nil { + tt.askStubs(as) + } + + if tt.opts == nil { + tt.opts = &EditOptions{} + } + + tt.opts.Edit = func(_, _, _ string, _ *iostreams.IOStreams) (string, error) { + return "new file content", nil + } + + tt.opts.HttpClient = func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + } + io, _, _, _ := iostreams.Test() + io.SetStdoutTTY(!tt.nontty) + io.SetStdinTTY(!tt.nontty) + tt.opts.IO = io + + tt.opts.Selector = "1234" + + tt.opts.Config = func() (config.Config, error) { + return config.NewBlankConfig(), nil + } + + t.Run(tt.name, func(t *testing.T) { + err := editRun(tt.opts) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + reg.Verify(t) + + bodyBytes, _ := ioutil.ReadAll(reg.Requests[1].Body) + reqBody := make(map[string]interface{}) + err = json.Unmarshal(bodyBytes, &reqBody) + if err != nil { + t.Fatalf("error decoding JSON: %v", err) + } + assert.Equal(t, tt.wantParams, reqBody) + }) + } +} From 0d45dd82f347e30f560ddadfd49f3f14fc3ef464 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Tue, 15 Sep 2020 17:29:54 -0500 Subject: [PATCH 30/32] finish edit tests --- pkg/cmd/gist/edit/edit_test.go | 89 +++++++++++++++++++++++++++++----- 1 file changed, 76 insertions(+), 13 deletions(-) diff --git a/pkg/cmd/gist/edit/edit_test.go b/pkg/cmd/gist/edit/edit_test.go index 8b39e95cd..d7fffd958 100644 --- a/pkg/cmd/gist/edit/edit_test.go +++ b/pkg/cmd/gist/edit/edit_test.go @@ -93,9 +93,6 @@ func Test_editRun(t *testing.T) { }, }, }, - //askStubs: func(as *prompt.AskStubber) { - // as.StubOne("new file content") - //}, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("POST", "gists/1234"), httpmock.StatusStringResponse(201, "{}")) @@ -113,8 +110,73 @@ func Test_editRun(t *testing.T) { }, }, }, - // TODO multiple files, submit - // TODO multiple files, cancel + { + name: "multiple files, submit", + askStubs: func(as *prompt.AskStubber) { + as.StubOne("unix.md") + as.StubOne("Submit") + }, + gist: &shared.Gist{ + ID: "1234", + Description: "catbug", + Files: map[string]*shared.GistFile{ + "cicada.txt": { + Filename: "cicada.txt", + Content: "bwhiizzzbwhuiiizzzz", + Type: "text/plain", + }, + "unix.md": { + Filename: "unix.md", + Content: "meow", + Type: "application/markdown", + }, + }, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register(httpmock.REST("POST", "gists/1234"), + httpmock.StatusStringResponse(201, "{}")) + }, + wantParams: map[string]interface{}{ + "description": "catbug", + "updated_at": "0001-01-01T00:00:00Z", + "public": false, + "files": map[string]interface{}{ + "cicada.txt": map[string]interface{}{ + "content": "bwhiizzzbwhuiiizzzz", + "filename": "cicada.txt", + "type": "text/plain", + }, + "unix.md": map[string]interface{}{ + "content": "new file content", + "filename": "unix.md", + "type": "application/markdown", + }, + }, + }, + }, + { + name: "multiple files, cancel", + askStubs: func(as *prompt.AskStubber) { + as.StubOne("unix.md") + as.StubOne("Cancel") + }, + wantErr: true, + gist: &shared.Gist{ + ID: "1234", + Files: map[string]*shared.GistFile{ + "cicada.txt": { + Filename: "cicada.txt", + Content: "bwhiizzzbwhuiiizzzz", + Type: "text/plain", + }, + "unix.md": { + Filename: "unix.md", + Content: "meow", + Type: "application/markdown", + }, + }, + }, + }, } for _, tt := range tests { @@ -161,21 +223,22 @@ func Test_editRun(t *testing.T) { t.Run(tt.name, func(t *testing.T) { err := editRun(tt.opts) + reg.Verify(t) if tt.wantErr { assert.Error(t, err) return } assert.NoError(t, err) - reg.Verify(t) - - bodyBytes, _ := ioutil.ReadAll(reg.Requests[1].Body) - reqBody := make(map[string]interface{}) - err = json.Unmarshal(bodyBytes, &reqBody) - if err != nil { - t.Fatalf("error decoding JSON: %v", err) + if tt.wantParams != nil { + bodyBytes, _ := ioutil.ReadAll(reg.Requests[1].Body) + reqBody := make(map[string]interface{}) + err = json.Unmarshal(bodyBytes, &reqBody) + if err != nil { + t.Fatalf("error decoding JSON: %v", err) + } + assert.Equal(t, tt.wantParams, reqBody) } - assert.Equal(t, tt.wantParams, reqBody) }) } } From 2b70e8266a7d20e3e2f14db24bd4203ad06b43b8 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Wed, 16 Sep 2020 10:57:20 -0500 Subject: [PATCH 31/32] better time stub --- pkg/cmd/gist/list/list.go | 7 +------ pkg/cmd/gist/list/list_test.go | 35 ++++++++++++++++++++-------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/pkg/cmd/gist/list/list.go b/pkg/cmd/gist/list/list.go index 5e22e267b..e1142bdb7 100644 --- a/pkg/cmd/gist/list/list.go +++ b/pkg/cmd/gist/list/list.go @@ -17,8 +17,6 @@ type ListOptions struct { IO *iostreams.IOStreams HttpClient func() (*http.Client, error) - Since func(t time.Time) time.Duration - Limit int Visibility string // all, secret, public } @@ -27,9 +25,6 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman opts := &ListOptions{ IO: f.IOStreams, HttpClient: f.HttpClient, - Since: func(t time.Time) time.Duration { - return time.Since(t) - }, } cmd := &cobra.Command{ @@ -109,7 +104,7 @@ func listRun(opts *ListOptions) error { tp.AddField(utils.Pluralize(fileCount, "file"), nil, nil) tp.AddField(visibility, nil, visColor) if tp.IsTTY() { - updatedAt := utils.FuzzyAgo(opts.Since(gist.UpdatedAt)) + updatedAt := utils.FuzzyAgo(time.Since(gist.UpdatedAt)) tp.AddField(updatedAt, nil, cs.Gray) } else { tp.AddField(gist.UpdatedAt.String(), nil, nil) diff --git a/pkg/cmd/gist/list/list_test.go b/pkg/cmd/gist/list/list_test.go index 26fa46114..497f68edc 100644 --- a/pkg/cmd/gist/list/list_test.go +++ b/pkg/cmd/gist/list/list_test.go @@ -89,11 +89,12 @@ func TestNewCmdList(t *testing.T) { func Test_listRun(t *testing.T) { tests := []struct { - name string - opts *ListOptions - wantOut string - stubs func(*httpmock.Registry) - nontty bool + name string + opts *ListOptions + wantOut string + stubs func(*httpmock.Registry) + nontty bool + updatedAt *time.Time }{ { name: "no gists", @@ -126,20 +127,28 @@ func Test_listRun(t *testing.T) { wantOut: "1234567890 cool.txt 1 file public about 6 hours ago\n", }, { - name: "nontty output", - opts: &ListOptions{}, - wantOut: "1234567890\tcool.txt\t1 file\tpublic\t0001-01-01 00:00:00 +0000 UTC\n4567890123\t\t1 file\tpublic\t0001-01-01 00:00:00 +0000 UTC\n2345678901\ttea leaves thwart those who court catastrophe\t2 files\tsecret\t0001-01-01 00:00:00 +0000 UTC\n3456789012\tshort desc\t11 files\tsecret\t0001-01-01 00:00:00 +0000 UTC\n", - nontty: true, + name: "nontty output", + opts: &ListOptions{}, + updatedAt: &time.Time{}, + wantOut: "1234567890\tcool.txt\t1 file\tpublic\t0001-01-01 00:00:00 +0000 UTC\n4567890123\t\t1 file\tpublic\t0001-01-01 00:00:00 +0000 UTC\n2345678901\ttea leaves thwart those who court catastrophe\t2 files\tsecret\t0001-01-01 00:00:00 +0000 UTC\n3456789012\tshort desc\t11 files\tsecret\t0001-01-01 00:00:00 +0000 UTC\n", + nontty: true, }, } for _, tt := range tests { + sixHoursAgo, _ := time.ParseDuration("-6h") + updatedAt := time.Now().Add(sixHoursAgo) + if tt.updatedAt != nil { + updatedAt = *tt.updatedAt + } + reg := &httpmock.Registry{} if tt.stubs == nil { reg.Register(httpmock.REST("GET", "gists"), httpmock.JSONResponse([]shared.Gist{ { ID: "1234567890", + UpdatedAt: updatedAt, Description: "", Files: map[string]*shared.GistFile{ "cool.txt": {}, @@ -148,6 +157,7 @@ func Test_listRun(t *testing.T) { }, { ID: "4567890123", + UpdatedAt: updatedAt, Description: "", Files: map[string]*shared.GistFile{ "gistfile0.txt": {}, @@ -156,6 +166,7 @@ func Test_listRun(t *testing.T) { }, { ID: "2345678901", + UpdatedAt: updatedAt, Description: "tea leaves thwart those who court catastrophe", Files: map[string]*shared.GistFile{ "gistfile0.txt": {}, @@ -165,6 +176,7 @@ func Test_listRun(t *testing.T) { }, { ID: "3456789012", + UpdatedAt: updatedAt, Description: "short desc", Files: map[string]*shared.GistFile{ "gistfile0.txt": {}, @@ -190,11 +202,6 @@ func Test_listRun(t *testing.T) { return &http.Client{Transport: reg}, nil } - tt.opts.Since = func(t time.Time) time.Duration { - d, _ := time.ParseDuration("6h") - return d - } - io, _, stdout, _ := iostreams.Test() io.SetStdoutTTY(!tt.nontty) tt.opts.IO = io From 7c986c0454690616b207313f7c2f58fd8343bad9 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Wed, 16 Sep 2020 11:23:14 -0500 Subject: [PATCH 32/32] help typo --- pkg/cmd/gist/gist.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/gist/gist.go b/pkg/cmd/gist/gist.go index 6c86cc431..029fab588 100644 --- a/pkg/cmd/gist/gist.go +++ b/pkg/cmd/gist/gist.go @@ -18,9 +18,9 @@ func NewCmdGist(f *cmdutil.Factory) *cobra.Command { Annotations: map[string]string{ "IsCore": "true", "help:arguments": heredoc.Doc(` - A gist can be supplied as argument in any of the following formats: + A gist can be supplied as argument in either of the following formats: - by ID, e.g. 5b0e0062eb8e9654adad7bb1d81cc75f - - by URL, e.g. "https://gist.github.com/OWNER/5b0e0062eb8e9654adad7bb1d81cc75f"; or + - by URL, e.g. "https://gist.github.com/OWNER/5b0e0062eb8e9654adad7bb1d81cc75f" `), }, }