From f87d9f98f1ec6570b2f787cd813e310bb3bda0f0 Mon Sep 17 00:00:00 2001 From: danochoa Date: Fri, 3 Jan 2025 12:51:09 -0600 Subject: [PATCH] gist delete prompt for confirmation --- pkg/cmd/gist/delete/delete.go | 41 ++++++++++++++-- pkg/cmd/gist/delete/delete_test.go | 79 +++++++++++++++++++++++------- 2 files changed, 97 insertions(+), 23 deletions(-) diff --git a/pkg/cmd/gist/delete/delete.go b/pkg/cmd/gist/delete/delete.go index a6be7053a..85021979b 100644 --- a/pkg/cmd/gist/delete/delete.go +++ b/pkg/cmd/gist/delete/delete.go @@ -6,6 +6,7 @@ import ( "net/http" "strings" + "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/prompter" @@ -21,7 +22,8 @@ type DeleteOptions struct { HttpClient func() (*http.Client, error) Prompter prompter.Prompter - Selector string + Selector string + Confirmed bool } func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Command { @@ -33,9 +35,23 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co } cmd := &cobra.Command{ - Use: "delete { | }", + Use: "delete [ | ]", Short: "Delete a gist", - Args: cobra.MaximumNArgs(1), + Long: heredoc.Docf(` + Delete a GitHub gist. + + To delete a gist interactively, use %[1]sgh gist delete%[1]s with no arguments. + + To delete a gist non-interactively, supply the gist id or url. + `, "`"), + Example: heredoc.Doc(` + # delete a gist interactively + gh gist delete + + # delete a gist non-interactively + gh gist delete 1234 + `), + Args: cobra.MaximumNArgs(1), RunE: func(c *cobra.Command, args []string) error { if len(args) == 1 { opts.Selector = args[0] @@ -46,6 +62,7 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co return deleteRun(&opts) }, } + cmd.Flags().BoolVar(&opts.Confirmed, "yes", false, "confirm deletion without prompting") return cmd } @@ -83,9 +100,16 @@ func deleteRun(opts *DeleteOptions) error { return nil } } + if !opts.Confirmed { + err = opts.Prompter.ConfirmDeletion("delete") + + if err != nil { + return cmdutil.CancelError + } + } apiClient := api.NewClientFromHTTP(client) - if err := deleteGist(apiClient, host, gistID); err != nil { + if err := deleteGist(apiClient, host, gistID, opts); err != nil { if errors.Is(err, shared.NotFoundErr) { return fmt.Errorf("unable to delete gist %s: either the gist is not found or it is not owned by you", gistID) } @@ -95,7 +119,7 @@ func deleteRun(opts *DeleteOptions) error { return nil } -func deleteGist(apiClient *api.Client, hostname string, gistID string) error { +func deleteGist(apiClient *api.Client, hostname string, gistID string, opts *DeleteOptions) error { path := "gists/" + gistID err := apiClient.REST(hostname, "DELETE", path, nil, nil) if err != nil { @@ -105,5 +129,12 @@ func deleteGist(apiClient *api.Client, hostname string, gistID string) error { } return err } + if opts.IO.IsStdoutTTY() { + cs := opts.IO.ColorScheme() + fmt.Fprintf(opts.IO.Out, + "%s Deleted gist %s\n", + cs.SuccessIcon(), + gistID) + } return nil } diff --git a/pkg/cmd/gist/delete/delete_test.go b/pkg/cmd/gist/delete/delete_test.go index 3e346da51..c3db5f940 100644 --- a/pkg/cmd/gist/delete/delete_test.go +++ b/pkg/cmd/gist/delete/delete_test.go @@ -37,6 +37,13 @@ func TestNewCmdDelete(t *testing.T) { Selector: "", }, }, + { + name: "yes flag", + cli: "123 --yes", + wants: DeleteOptions{ + Selector: "123", + }, + }, } for _, tt := range tests { @@ -67,7 +74,7 @@ func TestNewCmdDelete(t *testing.T) { func Test_deleteRun(t *testing.T) { tests := []struct { name string - opts DeleteOptions + opts *DeleteOptions httpStubs func(*httpmock.Registry) mockGistList bool wantErr bool @@ -76,21 +83,23 @@ func Test_deleteRun(t *testing.T) { }{ { name: "successfully delete", - opts: DeleteOptions{ - Selector: "1234", + opts: &DeleteOptions{ + Selector: "1234", + Confirmed: false, }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("DELETE", "gists/1234"), httpmock.StatusStringResponse(200, "{}")) }, wantErr: false, - wantStdout: "", + wantStdout: "✓ Deleted gist 1234\n", wantStderr: "", }, { name: "successfully delete with prompt", - opts: DeleteOptions{ - Selector: "", + opts: &DeleteOptions{ + Selector: "", + Confirmed: false, }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("DELETE", "gists/1234"), @@ -98,13 +107,43 @@ func Test_deleteRun(t *testing.T) { }, mockGistList: true, wantErr: false, - wantStdout: "", + wantStdout: "✓ Deleted gist 1234\n", + wantStderr: "", + }, + { + name: "successfully delete with --yes", + opts: &DeleteOptions{ + Selector: "1234", + Confirmed: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register(httpmock.REST("DELETE", "gists/1234"), + httpmock.StatusStringResponse(200, "{}")) + }, + wantErr: false, + wantStdout: "✓ Deleted gist 1234\n", + wantStderr: "", + }, + { + name: "successfully delete with prompt and --yes", + opts: &DeleteOptions{ + Selector: "", + Confirmed: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register(httpmock.REST("DELETE", "gists/1234"), + httpmock.StatusStringResponse(200, "{}")) + }, + mockGistList: true, + wantErr: false, + wantStdout: "✓ Deleted gist 1234\n", wantStderr: "", }, { name: "not found", - opts: DeleteOptions{ - Selector: "1234", + opts: &DeleteOptions{ + Selector: "1234", + Confirmed: false, }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("DELETE", "gists/1234"), @@ -112,14 +151,18 @@ func Test_deleteRun(t *testing.T) { }, wantErr: true, wantStdout: "", - wantStderr: "", + wantStderr: "unable to delete gist 1234: either the gist is not found or it is not owned by you", }, } for _, tt := range tests { reg := &httpmock.Registry{} - if tt.httpStubs != nil { - tt.httpStubs(reg) + tt.httpStubs(reg) + pm := prompter.NewMockPrompter(t) + if !tt.opts.Confirmed { + pm.RegisterConfirmDeletion("Type delete to confirm deletion:", func(_ string) error { + return nil + }) } if tt.mockGistList { @@ -141,12 +184,11 @@ func Test_deleteRun(t *testing.T) { )), ) - pm := prompter.NewMockPrompter(t) pm.RegisterSelect("Select a gist", []string{"cool.txt about 6 hours ago"}, func(_, _ string, opts []string) (int, error) { return 0, nil }) - tt.opts.Prompter = pm } + tt.opts.Prompter = pm tt.opts.HttpClient = func() (*http.Client, error) { return &http.Client{Transport: reg}, nil @@ -154,13 +196,13 @@ func Test_deleteRun(t *testing.T) { tt.opts.Config = func() (gh.Config, error) { return config.NewBlankConfig(), nil } - ios, _, _, _ := iostreams.Test() - ios.SetStdoutTTY(false) + ios, _, stdout, stderr := iostreams.Test() + ios.SetStdoutTTY(true) ios.SetStdinTTY(false) tt.opts.IO = ios t.Run(tt.name, func(t *testing.T) { - err := deleteRun(&tt.opts) + err := deleteRun(tt.opts) reg.Verify(t) if tt.wantErr { assert.Error(t, err) @@ -170,7 +212,8 @@ func Test_deleteRun(t *testing.T) { return } assert.NoError(t, err) - + assert.Equal(t, tt.wantStdout, stdout.String()) + assert.Equal(t, tt.wantStderr, stderr.String()) }) } }