Merge branch 'trunk' into rerun-err-msg
This commit is contained in:
commit
143d628f1b
8 changed files with 421 additions and 91 deletions
4
go.mod
4
go.mod
|
|
@ -40,7 +40,7 @@ require (
|
|||
github.com/opentracing/opentracing-go v1.2.0
|
||||
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d
|
||||
github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc
|
||||
github.com/sigstore/protobuf-specs v0.3.2
|
||||
github.com/sigstore/protobuf-specs v0.3.3
|
||||
github.com/sigstore/sigstore-go v0.6.2
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
|
|
@ -51,7 +51,7 @@ require (
|
|||
golang.org/x/term v0.27.0
|
||||
golang.org/x/text v0.21.0
|
||||
google.golang.org/grpc v1.64.1
|
||||
google.golang.org/protobuf v1.34.2
|
||||
google.golang.org/protobuf v1.36.2
|
||||
gopkg.in/h2non/gock.v1 v1.1.2
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
|
|
|||
8
go.sum
8
go.sum
|
|
@ -393,8 +393,8 @@ github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc h1:vH0NQbIDk+mJL
|
|||
github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8=
|
||||
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0=
|
||||
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE=
|
||||
github.com/sigstore/protobuf-specs v0.3.2 h1:nCVARCN+fHjlNCk3ThNXwrZRqIommIeNKWwQvORuRQo=
|
||||
github.com/sigstore/protobuf-specs v0.3.2/go.mod h1:RZ0uOdJR4OB3tLQeAyWoJFbNCBFrPQdcokntde4zRBA=
|
||||
github.com/sigstore/protobuf-specs v0.3.3 h1:RMZQgXTD/pF7KW6b5NaRLYxFYZ/wzx44PQFXN2PEo5g=
|
||||
github.com/sigstore/protobuf-specs v0.3.3/go.mod h1:vIhZ6Uor1a38+wvRrKcqL2PtYNlgoIW9lhzYzkyy4EU=
|
||||
github.com/sigstore/rekor v1.3.6 h1:QvpMMJVWAp69a3CHzdrLelqEqpTM3ByQRt5B5Kspbi8=
|
||||
github.com/sigstore/rekor v1.3.6/go.mod h1:JDTSNNMdQ/PxdsS49DJkJ+pRJCO/83nbR5p3aZQteXc=
|
||||
github.com/sigstore/sigstore v1.8.9 h1:NiUZIVWywgYuVTxXmRoTT4O4QAGiTEKup4N1wdxFadk=
|
||||
|
|
@ -548,8 +548,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5 h1:
|
|||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
|
||||
google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
|
||||
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
|
||||
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ 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"
|
||||
"github.com/cli/cli/v2/pkg/cmd/gist/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
|
|
@ -18,8 +20,10 @@ type DeleteOptions struct {
|
|||
IO *iostreams.IOStreams
|
||||
Config func() (gh.Config, error)
|
||||
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 {
|
||||
|
|
@ -27,33 +31,51 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co
|
|||
IO: f.IOStreams,
|
||||
Config: f.Config,
|
||||
HttpClient: f.HttpClient,
|
||||
Prompter: f.Prompter,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete {<id> | <url>}",
|
||||
Short: "Delete a gist",
|
||||
Args: cmdutil.ExactArgs(1, "cannot delete: gist argument required"),
|
||||
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 {
|
||||
opts.Selector = args[0]
|
||||
if !opts.IO.CanPrompt() && !opts.Confirmed {
|
||||
return cmdutil.FlagErrorf("--yes required when not running interactively")
|
||||
}
|
||||
|
||||
if !opts.IO.CanPrompt() && len(args) == 0 {
|
||||
return cmdutil.FlagErrorf("id or url argument required in non-interactive mode")
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
opts.Selector = args[0]
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
return runF(&opts)
|
||||
}
|
||||
return deleteRun(&opts)
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&opts.Confirmed, "yes", false, "confirm deletion without prompting")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func deleteRun(opts *DeleteOptions) error {
|
||||
gistID := opts.Selector
|
||||
|
||||
if strings.Contains(gistID, "/") {
|
||||
id, err := shared.GistIDFromURL(gistID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gistID = id
|
||||
}
|
||||
client, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -66,14 +88,56 @@ func deleteRun(opts *DeleteOptions) error {
|
|||
|
||||
host, _ := cfg.Authentication().DefaultHost()
|
||||
|
||||
gistID := opts.Selector
|
||||
if strings.Contains(gistID, "/") {
|
||||
id, err := shared.GistIDFromURL(gistID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gistID = id
|
||||
}
|
||||
|
||||
cs := opts.IO.ColorScheme()
|
||||
var gist *shared.Gist
|
||||
if gistID == "" {
|
||||
gist, err = shared.PromptGists(opts.Prompter, client, host, cs)
|
||||
} else {
|
||||
gist, err = shared.GetGist(client, host, gistID)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if gist.ID == "" {
|
||||
fmt.Fprintln(opts.IO.Out, "No gists found.")
|
||||
return nil
|
||||
}
|
||||
|
||||
if !opts.Confirmed {
|
||||
confirmed, err := opts.Prompter.Confirm(fmt.Sprintf("Delete %q gist?", gist.Filename()), false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !confirmed {
|
||||
return cmdutil.CancelError
|
||||
}
|
||||
}
|
||||
|
||||
apiClient := api.NewClientFromHTTP(client)
|
||||
if err := deleteGist(apiClient, host, gistID); err != nil {
|
||||
if err := deleteGist(apiClient, host, gist.ID); 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)
|
||||
return fmt.Errorf("unable to delete gist %q: either the gist is not found or it is not owned by you", gist.Filename())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
cs := opts.IO.ColorScheme()
|
||||
fmt.Fprintf(opts.IO.Out,
|
||||
"%s Gist %q deleted\n",
|
||||
cs.SuccessIcon(),
|
||||
gist.Filename())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,36 +2,95 @@ package delete
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/gh"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/pkg/cmd/gist/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
ghAPI "github.com/cli/go-gh/v2/pkg/api"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewCmdDelete(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cli string
|
||||
wants DeleteOptions
|
||||
name string
|
||||
cli string
|
||||
tty bool
|
||||
want DeleteOptions
|
||||
wantErr bool
|
||||
wantErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "valid selector",
|
||||
cli: "123",
|
||||
wants: DeleteOptions{
|
||||
tty: true,
|
||||
want: DeleteOptions{
|
||||
Selector: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid selector, no ID supplied",
|
||||
cli: "",
|
||||
tty: true,
|
||||
want: DeleteOptions{
|
||||
Selector: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no ID supplied with --yes",
|
||||
cli: "--yes",
|
||||
tty: true,
|
||||
want: DeleteOptions{
|
||||
Selector: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "selector with --yes, no tty",
|
||||
cli: "123 --yes",
|
||||
tty: false,
|
||||
want: DeleteOptions{
|
||||
Selector: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ID arg without --yes, no tty",
|
||||
cli: "123",
|
||||
tty: false,
|
||||
want: DeleteOptions{
|
||||
Selector: "",
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "--yes required when not running interactively",
|
||||
},
|
||||
{
|
||||
name: "no ID supplied with --yes, no tty",
|
||||
cli: "--yes",
|
||||
tty: false,
|
||||
want: DeleteOptions{
|
||||
Selector: "",
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "id or url argument required in non-interactive mode",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f := &cmdutil.Factory{}
|
||||
io, _, _, _ := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: io,
|
||||
}
|
||||
io.SetStdinTTY(tt.tty)
|
||||
io.SetStdoutTTY(tt.tty)
|
||||
|
||||
argv, err := shlex.Split(tt.cli)
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -47,69 +106,210 @@ func TestNewCmdDelete(t *testing.T) {
|
|||
cmd.SetErr(&bytes.Buffer{})
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
assert.NoError(t, err)
|
||||
if tt.wantErr {
|
||||
assert.EqualError(t, err, tt.wantErrMsg)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.wants.Selector, gotOpts.Selector)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.want.Selector, gotOpts.Selector)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_deleteRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts DeleteOptions
|
||||
httpStubs func(*httpmock.Registry)
|
||||
wantErr bool
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
name string
|
||||
opts *DeleteOptions
|
||||
cancel bool
|
||||
httpStubs func(*httpmock.Registry)
|
||||
mockPromptGists bool
|
||||
noGists bool
|
||||
wantErr bool
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
}{
|
||||
{
|
||||
name: "successfully delete",
|
||||
opts: DeleteOptions{
|
||||
opts: &DeleteOptions{
|
||||
Selector: "1234",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "gists/1234"),
|
||||
httpmock.JSONResponse(shared.Gist{ID: "1234", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}}))
|
||||
reg.Register(httpmock.REST("DELETE", "gists/1234"),
|
||||
httpmock.StatusStringResponse(200, "{}"))
|
||||
},
|
||||
wantStdout: "✓ Gist \"cool.txt\" deleted\n",
|
||||
},
|
||||
{
|
||||
name: "successfully delete with prompt",
|
||||
opts: &DeleteOptions{
|
||||
Selector: "",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("DELETE", "gists/1234"),
|
||||
httpmock.StatusStringResponse(200, "{}"))
|
||||
},
|
||||
wantErr: false,
|
||||
wantStdout: "",
|
||||
wantStderr: "",
|
||||
mockPromptGists: true,
|
||||
wantStdout: "✓ Gist \"cool.txt\" deleted\n",
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
opts: DeleteOptions{
|
||||
name: "successfully delete with --yes",
|
||||
opts: &DeleteOptions{
|
||||
Selector: "1234",
|
||||
Confirmed: true,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "gists/1234"),
|
||||
httpmock.JSONResponse(shared.Gist{ID: "1234", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}}))
|
||||
reg.Register(httpmock.REST("DELETE", "gists/1234"),
|
||||
httpmock.StatusStringResponse(200, "{}"))
|
||||
},
|
||||
wantStdout: "✓ Gist \"cool.txt\" deleted\n",
|
||||
},
|
||||
{
|
||||
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, "{}"))
|
||||
},
|
||||
mockPromptGists: true,
|
||||
wantStdout: "✓ Gist \"cool.txt\" deleted\n",
|
||||
},
|
||||
{
|
||||
name: "cancel delete with id",
|
||||
opts: &DeleteOptions{
|
||||
Selector: "1234",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "gists/1234"),
|
||||
httpmock.JSONResponse(shared.Gist{ID: "1234", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}}))
|
||||
},
|
||||
cancel: true,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "cancel delete with url",
|
||||
opts: &DeleteOptions{
|
||||
Selector: "https://gist.github.com/myrepo/1234",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "gists/1234"),
|
||||
httpmock.JSONResponse(shared.Gist{ID: "1234", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}}))
|
||||
},
|
||||
cancel: true,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "cancel delete with prompt",
|
||||
opts: &DeleteOptions{
|
||||
Selector: "",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {},
|
||||
mockPromptGists: true,
|
||||
cancel: true,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "not owned by you",
|
||||
opts: &DeleteOptions{
|
||||
Selector: "1234",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "gists/1234"),
|
||||
httpmock.JSONResponse(shared.Gist{ID: "1234", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}}))
|
||||
reg.Register(httpmock.REST("DELETE", "gists/1234"),
|
||||
httpmock.StatusStringResponse(404, "{}"))
|
||||
},
|
||||
wantErr: true,
|
||||
wantStdout: "",
|
||||
wantStderr: "",
|
||||
wantStderr: "unable to delete gist \"cool.txt\": either the gist is not found or it is not owned by you",
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
opts: &DeleteOptions{
|
||||
Selector: "1234",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "gists/1234"),
|
||||
httpmock.StatusStringResponse(404, "{}"))
|
||||
},
|
||||
wantErr: true,
|
||||
wantStderr: "not found",
|
||||
},
|
||||
{
|
||||
name: "no gists",
|
||||
opts: &DeleteOptions{
|
||||
Selector: "",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {},
|
||||
mockPromptGists: true,
|
||||
noGists: true,
|
||||
wantStdout: "No gists found.\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
reg := &httpmock.Registry{}
|
||||
if tt.httpStubs != nil {
|
||||
tt.httpStubs(reg)
|
||||
pm := prompter.NewMockPrompter(t)
|
||||
if !tt.opts.Confirmed {
|
||||
pm.RegisterConfirm("Delete \"cool.txt\" gist?", func(_ string, _ bool) (bool, error) {
|
||||
return !tt.cancel, nil
|
||||
})
|
||||
}
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
tt.httpStubs(reg)
|
||||
if tt.mockPromptGists {
|
||||
if tt.noGists {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query GistList\b`),
|
||||
httpmock.StringResponse(
|
||||
`{ "data": { "viewer": { "gists": { "nodes": [] }} } }`),
|
||||
)
|
||||
} else {
|
||||
sixHours, _ := time.ParseDuration("6h")
|
||||
sixHoursAgo := time.Now().Add(-sixHours)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query GistList\b`),
|
||||
httpmock.StringResponse(fmt.Sprintf(
|
||||
`{ "data": { "viewer": { "gists": { "nodes": [
|
||||
{
|
||||
"name": "1234",
|
||||
"files": [{ "name": "cool.txt" }],
|
||||
"updatedAt": "%s",
|
||||
"isPublic": true
|
||||
}
|
||||
] } } } }`,
|
||||
sixHoursAgo.Format(time.RFC3339),
|
||||
)),
|
||||
)
|
||||
pm.RegisterSelect("Select a gist", []string{"cool.txt about 6 hours ago"}, func(_, _ string, _ []string) (int, error) {
|
||||
return 0, nil
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
tt.opts.Prompter = pm
|
||||
|
||||
tt.opts.HttpClient = func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
@ -119,6 +319,75 @@ 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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_gistDelete(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
httpStubs func(*httpmock.Registry)
|
||||
hostname string
|
||||
gistID string
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "successful delete",
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("DELETE", "gists/1234"),
|
||||
httpmock.StatusStringResponse(204, "{}"),
|
||||
)
|
||||
},
|
||||
hostname: "github.com",
|
||||
gistID: "1234",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "when an gist is not found, it returns a NotFoundError",
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("DELETE", "gists/1234"),
|
||||
httpmock.StatusStringResponse(404, "{}"),
|
||||
)
|
||||
},
|
||||
hostname: "github.com",
|
||||
gistID: "1234",
|
||||
wantErr: shared.NotFoundErr,
|
||||
},
|
||||
{
|
||||
name: "when there is a non-404 error deleting the gist, that error is returned",
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("DELETE", "gists/1234"),
|
||||
httpmock.StatusJSONResponse(500, `{"message": "arbitrary error"}`),
|
||||
)
|
||||
},
|
||||
hostname: "github.com",
|
||||
gistID: "1234",
|
||||
wantErr: api.HTTPError{
|
||||
HTTPError: &ghAPI.HTTPError{
|
||||
StatusCode: 500,
|
||||
Message: "arbitrary error",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
tt.httpStubs(reg)
|
||||
client := api.NewClientFromHTTP(&http.Client{Transport: reg})
|
||||
|
||||
err := deleteGist(client, tt.hostname, tt.gistID)
|
||||
if tt.wantErr != nil {
|
||||
assert.ErrorAs(t, err, &tt.wantErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,15 +108,16 @@ func editRun(opts *EditOptions) error {
|
|||
if gistID == "" {
|
||||
cs := opts.IO.ColorScheme()
|
||||
if gistID == "" {
|
||||
gistID, err = shared.PromptGists(opts.Prompter, client, host, cs)
|
||||
gist, err := shared.PromptGists(opts.Prompter, client, host, cs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gistID == "" {
|
||||
if gist.ID == "" {
|
||||
fmt.Fprintln(opts.IO.Out, "No gists found.")
|
||||
return nil
|
||||
}
|
||||
gistID = gist.ID
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,19 @@ type Gist struct {
|
|||
Owner *GistOwner `json:"owner,omitempty"`
|
||||
}
|
||||
|
||||
func (g Gist) Filename() string {
|
||||
filenames := make([]string, 0, len(g.Files))
|
||||
for fn := range g.Files {
|
||||
filenames = append(filenames, fn)
|
||||
}
|
||||
sort.Strings(filenames)
|
||||
return filenames[0]
|
||||
}
|
||||
|
||||
func (g Gist) TruncDescription() string {
|
||||
return text.Truncate(100, text.RemoveExcessiveWhitespace(g.Description))
|
||||
}
|
||||
|
||||
var NotFoundErr = errors.New("not found")
|
||||
|
||||
func GetGist(client *http.Client, hostname, gistID string) (*Gist, error) {
|
||||
|
|
@ -202,47 +215,29 @@ func IsBinaryContents(contents []byte) bool {
|
|||
return isBinary
|
||||
}
|
||||
|
||||
func PromptGists(prompter prompter.Prompter, client *http.Client, host string, cs *iostreams.ColorScheme) (gistID string, err error) {
|
||||
func PromptGists(prompter prompter.Prompter, client *http.Client, host string, cs *iostreams.ColorScheme) (gist *Gist, err error) {
|
||||
gists, err := ListGists(client, host, 10, nil, false, "all")
|
||||
if err != nil {
|
||||
return "", err
|
||||
return &Gist{}, err
|
||||
}
|
||||
|
||||
if len(gists) == 0 {
|
||||
return "", nil
|
||||
return &Gist{}, nil
|
||||
}
|
||||
|
||||
var opts []string
|
||||
var gistIDs = make([]string, len(gists))
|
||||
var opts = 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)
|
||||
opts[i] = fmt.Sprintf("%s %s %s", cs.Bold(gist.Filename()), gist.TruncDescription(), cs.Gray(gistTime))
|
||||
}
|
||||
|
||||
result, err := prompter.Select("Select a gist", "", opts)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
return &Gist{}, err
|
||||
}
|
||||
|
||||
return gistIDs[result], nil
|
||||
return &gists[result], nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,12 +93,15 @@ func TestIsBinaryContents(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPromptGists(t *testing.T) {
|
||||
sixHours, _ := time.ParseDuration("6h")
|
||||
sixHoursAgo := time.Now().Add(-sixHours)
|
||||
sixHoursAgoFormatted := sixHoursAgo.Format(time.RFC3339Nano)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
prompterStubs func(pm *prompter.MockPrompter)
|
||||
response string
|
||||
wantOut string
|
||||
gist *Gist
|
||||
wantOut Gist
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
|
|
@ -112,21 +115,21 @@ func TestPromptGists(t *testing.T) {
|
|||
},
|
||||
response: `{ "data": { "viewer": { "gists": { "nodes": [
|
||||
{
|
||||
"name": "gistid1",
|
||||
"name": "1234",
|
||||
"files": [{ "name": "cool.txt" }],
|
||||
"description": "",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": true
|
||||
},
|
||||
{
|
||||
"name": "gistid2",
|
||||
"name": "5678",
|
||||
"files": [{ "name": "gistfile0.txt" }],
|
||||
"description": "",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": true
|
||||
}
|
||||
] } } } }`,
|
||||
wantOut: "gistid1",
|
||||
wantOut: Gist{ID: "1234", Files: map[string]*GistFile{"cool.txt": {Filename: "cool.txt"}}, UpdatedAt: sixHoursAgo, Public: true},
|
||||
},
|
||||
{
|
||||
name: "multiple files, select second gist",
|
||||
|
|
@ -139,26 +142,26 @@ func TestPromptGists(t *testing.T) {
|
|||
},
|
||||
response: `{ "data": { "viewer": { "gists": { "nodes": [
|
||||
{
|
||||
"name": "gistid1",
|
||||
"name": "1234",
|
||||
"files": [{ "name": "cool.txt" }],
|
||||
"description": "",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": true
|
||||
},
|
||||
{
|
||||
"name": "gistid2",
|
||||
"name": "5678",
|
||||
"files": [{ "name": "gistfile0.txt" }],
|
||||
"description": "",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": true
|
||||
}
|
||||
] } } } }`,
|
||||
wantOut: "gistid2",
|
||||
wantOut: Gist{ID: "5678", Files: map[string]*GistFile{"gistfile0.txt": {Filename: "gistfile0.txt"}}, UpdatedAt: sixHoursAgo, Public: true},
|
||||
},
|
||||
{
|
||||
name: "no files",
|
||||
response: `{ "data": { "viewer": { "gists": { "nodes": [] } } } }`,
|
||||
wantOut: "",
|
||||
wantOut: Gist{},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -166,15 +169,12 @@ func TestPromptGists(t *testing.T) {
|
|||
|
||||
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),
|
||||
sixHoursAgoFormatted,
|
||||
)),
|
||||
)
|
||||
client := &http.Client{Transport: reg}
|
||||
|
|
@ -185,9 +185,9 @@ func TestPromptGists(t *testing.T) {
|
|||
tt.prompterStubs(mockPrompter)
|
||||
}
|
||||
|
||||
gistID, err := PromptGists(mockPrompter, client, "github.com", ios.ColorScheme())
|
||||
gist, err := PromptGists(mockPrompter, client, "github.com", ios.ColorScheme())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantOut, gistID)
|
||||
assert.Equal(t, tt.wantOut.ID, gist.ID)
|
||||
reg.Verify(t)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,15 +89,16 @@ func viewRun(opts *ViewOptions) error {
|
|||
|
||||
cs := opts.IO.ColorScheme()
|
||||
if gistID == "" {
|
||||
gistID, err = shared.PromptGists(opts.Prompter, client, hostname, cs)
|
||||
gist, err := shared.PromptGists(opts.Prompter, client, hostname, cs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gistID == "" {
|
||||
if gist.ID == "" {
|
||||
fmt.Fprintln(opts.IO.Out, "No gists found.")
|
||||
return nil
|
||||
}
|
||||
gistID = gist.ID
|
||||
}
|
||||
|
||||
if opts.Web {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue