From 9d392ec376466d9969cd27f3f8529e079384f8a0 Mon Sep 17 00:00:00 2001 From: Adarsh K Kumar Date: Sat, 2 Oct 2021 17:55:43 +0530 Subject: [PATCH 1/2] #4258 Add sub command to delete asset from release --- pkg/cmd/release/delete-asset/delete_asset.go | 127 +++++++++++++ .../release/delete-asset/delete_asset_test.go | 167 ++++++++++++++++++ pkg/cmd/release/release.go | 2 + 3 files changed, 296 insertions(+) create mode 100644 pkg/cmd/release/delete-asset/delete_asset.go create mode 100644 pkg/cmd/release/delete-asset/delete_asset_test.go diff --git a/pkg/cmd/release/delete-asset/delete_asset.go b/pkg/cmd/release/delete-asset/delete_asset.go new file mode 100644 index 00000000000..bc0de2d51ca --- /dev/null +++ b/pkg/cmd/release/delete-asset/delete_asset.go @@ -0,0 +1,127 @@ +package deleteasset + +import ( + "fmt" + "net/http" + + "github.com/AlecAivazis/survey/v2" + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmd/release/shared" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/prompt" + "github.com/spf13/cobra" +) + +type DeleteAssetOptions struct { + HttpClient func() (*http.Client, error) + IO *iostreams.IOStreams + BaseRepo func() (ghrepo.Interface, error) + + TagName string + SkipConfirm bool + AssetName string +} + +func NewCmdDeleteAsset(f *cmdutil.Factory, runF func(*DeleteAssetOptions) error) *cobra.Command { + opts := &DeleteAssetOptions{ + IO: f.IOStreams, + HttpClient: f.HttpClient, + } + + cmd := &cobra.Command{ + Use: "delete-asset ", + Short: "Delete an asset from the release", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + opts.BaseRepo = f.BaseRepo + + opts.TagName = args[0] + opts.AssetName = args[1] + + if runF != nil { + return runF(opts) + } + return deleteAssetRun(opts) + }, + } + + cmd.Flags().BoolVarP(&opts.SkipConfirm, "yes", "y", false, "Skip the confirmation prompt") + + return cmd +} + +func deleteAssetRun(opts *DeleteAssetOptions) error { + httpClient, err := opts.HttpClient() + if err != nil { + return err + } + + baseRepo, err := opts.BaseRepo() + if err != nil { + return err + } + + release, err := shared.FetchRelease(httpClient, baseRepo, opts.TagName) + if err != nil { + return err + } + + if !opts.SkipConfirm && opts.IO.CanPrompt() { + var confirmed bool + err := prompt.SurveyAskOne(&survey.Confirm{ + Message: fmt.Sprintf("Delete asset %s in release %s in %s?", opts.AssetName, release.TagName, ghrepo.FullName(baseRepo)), + Default: true, + }, &confirmed) + if err != nil { + return err + } + + if !confirmed { + return cmdutil.CancelError + } + } + iofmt := opts.IO.ColorScheme() + + var assetURL string + for _, a := range release.Assets { + if a.Name == opts.AssetName { + assetURL = a.APIURL + } + } + if assetURL == "" { + return fmt.Errorf("asset %s not found in release %s", opts.AssetName, release.TagName) + } + + err = deleteAsset(httpClient, assetURL) + if err != nil { + return err + } + + if !opts.IO.IsStdoutTTY() || !opts.IO.IsStderrTTY() { + return nil + } + + fmt.Fprintf(opts.IO.ErrOut, "%s Deleted asset %s from release %s\n", iofmt.SuccessIconWithColor(iofmt.Red), opts.AssetName, release.TagName) + + return nil +} + +func deleteAsset(httpClient *http.Client, assetURL string) error { + req, err := http.NewRequest("DELETE", assetURL, nil) + if err != nil { + return err + } + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode > 299 { + return api.HandleHTTPError(resp) + } + return nil +} diff --git a/pkg/cmd/release/delete-asset/delete_asset_test.go b/pkg/cmd/release/delete-asset/delete_asset_test.go new file mode 100644 index 00000000000..218f17162a5 --- /dev/null +++ b/pkg/cmd/release/delete-asset/delete_asset_test.go @@ -0,0 +1,167 @@ +package deleteasset + +import ( + "bytes" + "io/ioutil" + "net/http" + "testing" + + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/google/shlex" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_NewCmdDelete(t *testing.T) { + tests := []struct { + name string + args string + isTTY bool + want DeleteAssetOptions + wantErr string + }{ + { + name: "version argument", + args: "v1.2.3 test-asset", + isTTY: true, + want: DeleteAssetOptions{ + TagName: "v1.2.3", + SkipConfirm: false, + AssetName: "test-asset", + }, + }, + { + name: "skip confirm", + args: "v1.2.3 test-asset -y", + isTTY: true, + want: DeleteAssetOptions{ + TagName: "v1.2.3", + SkipConfirm: true, + AssetName: "test-asset", + }, + }, + { + name: "no arguments", + args: "", + isTTY: true, + wantErr: "accepts 2 arg(s), received 0", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + io, _, _, _ := iostreams.Test() + io.SetStdoutTTY(tt.isTTY) + io.SetStdinTTY(tt.isTTY) + io.SetStderrTTY(tt.isTTY) + + f := &cmdutil.Factory{ + IOStreams: io, + } + + var opts *DeleteAssetOptions + cmd := NewCmdDeleteAsset(f, func(o *DeleteAssetOptions) error { + opts = o + return nil + }) + + argv, err := shlex.Split(tt.args) + require.NoError(t, err) + cmd.SetArgs(argv) + + cmd.SetIn(&bytes.Buffer{}) + cmd.SetOut(ioutil.Discard) + cmd.SetErr(ioutil.Discard) + + _, err = cmd.ExecuteC() + if tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + return + } else { + require.NoError(t, err) + } + + assert.Equal(t, tt.want.TagName, opts.TagName) + assert.Equal(t, tt.want.SkipConfirm, opts.SkipConfirm) + assert.Equal(t, tt.want.AssetName, opts.AssetName) + }) + } +} + +func Test_deleteRun(t *testing.T) { + tests := []struct { + name string + isTTY bool + opts DeleteAssetOptions + wantErr string + wantStdout string + wantStderr string + }{ + { + name: "skipping confirmation", + isTTY: true, + opts: DeleteAssetOptions{ + TagName: "v1.2.3", + SkipConfirm: true, + AssetName: "test-asset", + }, + wantStdout: ``, + wantStderr: "✓ Deleted asset test-asset from release v1.2.3\n", + }, + { + name: "non-interactive", + isTTY: false, + opts: DeleteAssetOptions{ + TagName: "v1.2.3", + SkipConfirm: false, + AssetName: "test-asset", + }, + wantStdout: ``, + wantStderr: ``, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + io, _, stdout, stderr := iostreams.Test() + io.SetStdoutTTY(tt.isTTY) + io.SetStdinTTY(tt.isTTY) + io.SetStderrTTY(tt.isTTY) + + fakeHTTP := &httpmock.Registry{} + fakeHTTP.Register(httpmock.REST("GET", "repos/OWNER/REPO/releases/tags/v1.2.3"), httpmock.StringResponse(`{ + "tag_name": "v1.2.3", + "draft": false, + "url": "https://api.github.com/repos/OWNER/REPO/releases/23456", + "assets": [ + { + "url": "https://api.github.com/repos/OWNER/REPO/releases/assets/1", + "id": 1, + "name": "test-asset" + } + ] + }`)) + fakeHTTP.Register(httpmock.REST("DELETE", "repos/OWNER/REPO/releases/assets/1"), httpmock.StatusStringResponse(204, "")) + + tt.opts.IO = io + tt.opts.HttpClient = func() (*http.Client, error) { + return &http.Client{Transport: fakeHTTP}, nil + } + tt.opts.BaseRepo = func() (ghrepo.Interface, error) { + return ghrepo.FromFullName("OWNER/REPO") + } + + err := deleteAssetRun(&tt.opts) + if tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + return + } else { + require.NoError(t, err) + } + + assert.Equal(t, tt.wantStdout, stdout.String()) + assert.Equal(t, tt.wantStderr, stderr.String()) + }) + } +} diff --git a/pkg/cmd/release/release.go b/pkg/cmd/release/release.go index 01132404e18..2d68418f0c4 100644 --- a/pkg/cmd/release/release.go +++ b/pkg/cmd/release/release.go @@ -3,6 +3,7 @@ package release import ( cmdCreate "github.com/cli/cli/v2/pkg/cmd/release/create" cmdDelete "github.com/cli/cli/v2/pkg/cmd/release/delete" + cmdDeleteAsset "github.com/cli/cli/v2/pkg/cmd/release/delete-asset" cmdDownload "github.com/cli/cli/v2/pkg/cmd/release/download" cmdList "github.com/cli/cli/v2/pkg/cmd/release/list" cmdUpload "github.com/cli/cli/v2/pkg/cmd/release/upload" @@ -24,6 +25,7 @@ func NewCmdRelease(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(cmdCreate.NewCmdCreate(f, nil)) cmd.AddCommand(cmdDelete.NewCmdDelete(f, nil)) + cmd.AddCommand(cmdDeleteAsset.NewCmdDeleteAsset(f, nil)) cmd.AddCommand(cmdDownload.NewCmdDownload(f, nil)) cmd.AddCommand(cmdList.NewCmdList(f, nil)) cmd.AddCommand(cmdView.NewCmdView(f, nil)) From f2a6d4a950a8310d0e01c958b35fe621c1d59a18 Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Thu, 20 Jan 2022 11:58:12 +0200 Subject: [PATCH 2/2] Add just a bit of polish --- pkg/cmd/release/delete-asset/delete_asset.go | 12 +++++----- .../release/delete-asset/delete_asset_test.go | 24 ++++++++++++------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/pkg/cmd/release/delete-asset/delete_asset.go b/pkg/cmd/release/delete-asset/delete_asset.go index bc0de2d51ca..f506590210a 100644 --- a/pkg/cmd/release/delete-asset/delete_asset.go +++ b/pkg/cmd/release/delete-asset/delete_asset.go @@ -31,15 +31,14 @@ func NewCmdDeleteAsset(f *cmdutil.Factory, runF func(*DeleteAssetOptions) error) } cmd := &cobra.Command{ - Use: "delete-asset ", - Short: "Delete an asset from the release", + Use: "delete-asset ", + Short: "Delete an asset from a release", Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { + // support `-R, --repo` override opts.BaseRepo = f.BaseRepo - opts.TagName = args[0] opts.AssetName = args[1] - if runF != nil { return runF(opts) } @@ -82,12 +81,12 @@ func deleteAssetRun(opts *DeleteAssetOptions) error { return cmdutil.CancelError } } - iofmt := opts.IO.ColorScheme() var assetURL string for _, a := range release.Assets { if a.Name == opts.AssetName { assetURL = a.APIURL + break } } if assetURL == "" { @@ -103,7 +102,8 @@ func deleteAssetRun(opts *DeleteAssetOptions) error { return nil } - fmt.Fprintf(opts.IO.ErrOut, "%s Deleted asset %s from release %s\n", iofmt.SuccessIconWithColor(iofmt.Red), opts.AssetName, release.TagName) + cs := opts.IO.ColorScheme() + fmt.Fprintf(opts.IO.ErrOut, "%s Deleted asset %s from release %s\n", cs.SuccessIconWithColor(cs.Red), opts.AssetName, release.TagName) return nil } diff --git a/pkg/cmd/release/delete-asset/delete_asset_test.go b/pkg/cmd/release/delete-asset/delete_asset_test.go index 218f17162a5..56aae3b359b 100644 --- a/pkg/cmd/release/delete-asset/delete_asset_test.go +++ b/pkg/cmd/release/delete-asset/delete_asset_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/require" ) -func Test_NewCmdDelete(t *testing.T) { +func Test_NewCmdDeleteAsset(t *testing.T) { tests := []struct { name string args string @@ -24,7 +24,7 @@ func Test_NewCmdDelete(t *testing.T) { wantErr string }{ { - name: "version argument", + name: "tag and asset arguments", args: "v1.2.3 test-asset", isTTY: true, want: DeleteAssetOptions{ @@ -49,6 +49,12 @@ func Test_NewCmdDelete(t *testing.T) { isTTY: true, wantErr: "accepts 2 arg(s), received 0", }, + { + name: "one arguments", + args: "v1.2.3", + isTTY: true, + wantErr: "accepts 2 arg(s), received 1", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -90,7 +96,7 @@ func Test_NewCmdDelete(t *testing.T) { } } -func Test_deleteRun(t *testing.T) { +func Test_deleteAssetRun(t *testing.T) { tests := []struct { name string isTTY bool @@ -135,12 +141,12 @@ func Test_deleteRun(t *testing.T) { "draft": false, "url": "https://api.github.com/repos/OWNER/REPO/releases/23456", "assets": [ - { - "url": "https://api.github.com/repos/OWNER/REPO/releases/assets/1", - "id": 1, - "name": "test-asset" - } - ] + { + "url": "https://api.github.com/repos/OWNER/REPO/releases/assets/1", + "id": 1, + "name": "test-asset" + } + ] }`)) fakeHTTP.Register(httpmock.REST("DELETE", "repos/OWNER/REPO/releases/assets/1"), httpmock.StatusStringResponse(204, ""))