Skip to content

Commit bb3070a

Browse files
m4ver1ksamcoe
andauthored
cli#4258 Add sub command to delete asset from release (cli#4416)
* cli#4258 Add sub command to delete asset from release * Add just a bit of polish Co-authored-by: Sam Coe <samcoe@users.noreply.github.com>
1 parent 4340c65 commit bb3070a

File tree

3 files changed

+302
-0
lines changed

3 files changed

+302
-0
lines changed
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package deleteasset
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
7+
"github.com/AlecAivazis/survey/v2"
8+
"github.com/cli/cli/v2/api"
9+
"github.com/cli/cli/v2/internal/ghrepo"
10+
"github.com/cli/cli/v2/pkg/cmd/release/shared"
11+
"github.com/cli/cli/v2/pkg/cmdutil"
12+
"github.com/cli/cli/v2/pkg/iostreams"
13+
"github.com/cli/cli/v2/pkg/prompt"
14+
"github.com/spf13/cobra"
15+
)
16+
17+
type DeleteAssetOptions struct {
18+
HttpClient func() (*http.Client, error)
19+
IO *iostreams.IOStreams
20+
BaseRepo func() (ghrepo.Interface, error)
21+
22+
TagName string
23+
SkipConfirm bool
24+
AssetName string
25+
}
26+
27+
func NewCmdDeleteAsset(f *cmdutil.Factory, runF func(*DeleteAssetOptions) error) *cobra.Command {
28+
opts := &DeleteAssetOptions{
29+
IO: f.IOStreams,
30+
HttpClient: f.HttpClient,
31+
}
32+
33+
cmd := &cobra.Command{
34+
Use: "delete-asset <tag> <asset-name>",
35+
Short: "Delete an asset from a release",
36+
Args: cobra.ExactArgs(2),
37+
RunE: func(cmd *cobra.Command, args []string) error {
38+
// support `-R, --repo` override
39+
opts.BaseRepo = f.BaseRepo
40+
opts.TagName = args[0]
41+
opts.AssetName = args[1]
42+
if runF != nil {
43+
return runF(opts)
44+
}
45+
return deleteAssetRun(opts)
46+
},
47+
}
48+
49+
cmd.Flags().BoolVarP(&opts.SkipConfirm, "yes", "y", false, "Skip the confirmation prompt")
50+
51+
return cmd
52+
}
53+
54+
func deleteAssetRun(opts *DeleteAssetOptions) error {
55+
httpClient, err := opts.HttpClient()
56+
if err != nil {
57+
return err
58+
}
59+
60+
baseRepo, err := opts.BaseRepo()
61+
if err != nil {
62+
return err
63+
}
64+
65+
release, err := shared.FetchRelease(httpClient, baseRepo, opts.TagName)
66+
if err != nil {
67+
return err
68+
}
69+
70+
if !opts.SkipConfirm && opts.IO.CanPrompt() {
71+
var confirmed bool
72+
err := prompt.SurveyAskOne(&survey.Confirm{
73+
Message: fmt.Sprintf("Delete asset %s in release %s in %s?", opts.AssetName, release.TagName, ghrepo.FullName(baseRepo)),
74+
Default: true,
75+
}, &confirmed)
76+
if err != nil {
77+
return err
78+
}
79+
80+
if !confirmed {
81+
return cmdutil.CancelError
82+
}
83+
}
84+
85+
var assetURL string
86+
for _, a := range release.Assets {
87+
if a.Name == opts.AssetName {
88+
assetURL = a.APIURL
89+
break
90+
}
91+
}
92+
if assetURL == "" {
93+
return fmt.Errorf("asset %s not found in release %s", opts.AssetName, release.TagName)
94+
}
95+
96+
err = deleteAsset(httpClient, assetURL)
97+
if err != nil {
98+
return err
99+
}
100+
101+
if !opts.IO.IsStdoutTTY() || !opts.IO.IsStderrTTY() {
102+
return nil
103+
}
104+
105+
cs := opts.IO.ColorScheme()
106+
fmt.Fprintf(opts.IO.ErrOut, "%s Deleted asset %s from release %s\n", cs.SuccessIconWithColor(cs.Red), opts.AssetName, release.TagName)
107+
108+
return nil
109+
}
110+
111+
func deleteAsset(httpClient *http.Client, assetURL string) error {
112+
req, err := http.NewRequest("DELETE", assetURL, nil)
113+
if err != nil {
114+
return err
115+
}
116+
117+
resp, err := httpClient.Do(req)
118+
if err != nil {
119+
return err
120+
}
121+
defer resp.Body.Close()
122+
123+
if resp.StatusCode > 299 {
124+
return api.HandleHTTPError(resp)
125+
}
126+
return nil
127+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package deleteasset
2+
3+
import (
4+
"bytes"
5+
"io/ioutil"
6+
"net/http"
7+
"testing"
8+
9+
"github.com/cli/cli/v2/internal/ghrepo"
10+
"github.com/cli/cli/v2/pkg/cmdutil"
11+
"github.com/cli/cli/v2/pkg/httpmock"
12+
"github.com/cli/cli/v2/pkg/iostreams"
13+
"github.com/google/shlex"
14+
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
func Test_NewCmdDeleteAsset(t *testing.T) {
19+
tests := []struct {
20+
name string
21+
args string
22+
isTTY bool
23+
want DeleteAssetOptions
24+
wantErr string
25+
}{
26+
{
27+
name: "tag and asset arguments",
28+
args: "v1.2.3 test-asset",
29+
isTTY: true,
30+
want: DeleteAssetOptions{
31+
TagName: "v1.2.3",
32+
SkipConfirm: false,
33+
AssetName: "test-asset",
34+
},
35+
},
36+
{
37+
name: "skip confirm",
38+
args: "v1.2.3 test-asset -y",
39+
isTTY: true,
40+
want: DeleteAssetOptions{
41+
TagName: "v1.2.3",
42+
SkipConfirm: true,
43+
AssetName: "test-asset",
44+
},
45+
},
46+
{
47+
name: "no arguments",
48+
args: "",
49+
isTTY: true,
50+
wantErr: "accepts 2 arg(s), received 0",
51+
},
52+
{
53+
name: "one arguments",
54+
args: "v1.2.3",
55+
isTTY: true,
56+
wantErr: "accepts 2 arg(s), received 1",
57+
},
58+
}
59+
for _, tt := range tests {
60+
t.Run(tt.name, func(t *testing.T) {
61+
io, _, _, _ := iostreams.Test()
62+
io.SetStdoutTTY(tt.isTTY)
63+
io.SetStdinTTY(tt.isTTY)
64+
io.SetStderrTTY(tt.isTTY)
65+
66+
f := &cmdutil.Factory{
67+
IOStreams: io,
68+
}
69+
70+
var opts *DeleteAssetOptions
71+
cmd := NewCmdDeleteAsset(f, func(o *DeleteAssetOptions) error {
72+
opts = o
73+
return nil
74+
})
75+
76+
argv, err := shlex.Split(tt.args)
77+
require.NoError(t, err)
78+
cmd.SetArgs(argv)
79+
80+
cmd.SetIn(&bytes.Buffer{})
81+
cmd.SetOut(ioutil.Discard)
82+
cmd.SetErr(ioutil.Discard)
83+
84+
_, err = cmd.ExecuteC()
85+
if tt.wantErr != "" {
86+
require.EqualError(t, err, tt.wantErr)
87+
return
88+
} else {
89+
require.NoError(t, err)
90+
}
91+
92+
assert.Equal(t, tt.want.TagName, opts.TagName)
93+
assert.Equal(t, tt.want.SkipConfirm, opts.SkipConfirm)
94+
assert.Equal(t, tt.want.AssetName, opts.AssetName)
95+
})
96+
}
97+
}
98+
99+
func Test_deleteAssetRun(t *testing.T) {
100+
tests := []struct {
101+
name string
102+
isTTY bool
103+
opts DeleteAssetOptions
104+
wantErr string
105+
wantStdout string
106+
wantStderr string
107+
}{
108+
{
109+
name: "skipping confirmation",
110+
isTTY: true,
111+
opts: DeleteAssetOptions{
112+
TagName: "v1.2.3",
113+
SkipConfirm: true,
114+
AssetName: "test-asset",
115+
},
116+
wantStdout: ``,
117+
wantStderr: "✓ Deleted asset test-asset from release v1.2.3\n",
118+
},
119+
{
120+
name: "non-interactive",
121+
isTTY: false,
122+
opts: DeleteAssetOptions{
123+
TagName: "v1.2.3",
124+
SkipConfirm: false,
125+
AssetName: "test-asset",
126+
},
127+
wantStdout: ``,
128+
wantStderr: ``,
129+
},
130+
}
131+
for _, tt := range tests {
132+
t.Run(tt.name, func(t *testing.T) {
133+
io, _, stdout, stderr := iostreams.Test()
134+
io.SetStdoutTTY(tt.isTTY)
135+
io.SetStdinTTY(tt.isTTY)
136+
io.SetStderrTTY(tt.isTTY)
137+
138+
fakeHTTP := &httpmock.Registry{}
139+
fakeHTTP.Register(httpmock.REST("GET", "repos/OWNER/REPO/releases/tags/v1.2.3"), httpmock.StringResponse(`{
140+
"tag_name": "v1.2.3",
141+
"draft": false,
142+
"url": "https://api.github.com/repos/OWNER/REPO/releases/23456",
143+
"assets": [
144+
{
145+
"url": "https://api.github.com/repos/OWNER/REPO/releases/assets/1",
146+
"id": 1,
147+
"name": "test-asset"
148+
}
149+
]
150+
}`))
151+
fakeHTTP.Register(httpmock.REST("DELETE", "repos/OWNER/REPO/releases/assets/1"), httpmock.StatusStringResponse(204, ""))
152+
153+
tt.opts.IO = io
154+
tt.opts.HttpClient = func() (*http.Client, error) {
155+
return &http.Client{Transport: fakeHTTP}, nil
156+
}
157+
tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
158+
return ghrepo.FromFullName("OWNER/REPO")
159+
}
160+
161+
err := deleteAssetRun(&tt.opts)
162+
if tt.wantErr != "" {
163+
require.EqualError(t, err, tt.wantErr)
164+
return
165+
} else {
166+
require.NoError(t, err)
167+
}
168+
169+
assert.Equal(t, tt.wantStdout, stdout.String())
170+
assert.Equal(t, tt.wantStderr, stderr.String())
171+
})
172+
}
173+
}

pkg/cmd/release/release.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package release
33
import (
44
cmdCreate "github.com/cli/cli/v2/pkg/cmd/release/create"
55
cmdDelete "github.com/cli/cli/v2/pkg/cmd/release/delete"
6+
cmdDeleteAsset "github.com/cli/cli/v2/pkg/cmd/release/delete-asset"
67
cmdDownload "github.com/cli/cli/v2/pkg/cmd/release/download"
78
cmdList "github.com/cli/cli/v2/pkg/cmd/release/list"
89
cmdUpload "github.com/cli/cli/v2/pkg/cmd/release/upload"
@@ -24,6 +25,7 @@ func NewCmdRelease(f *cmdutil.Factory) *cobra.Command {
2425

2526
cmd.AddCommand(cmdCreate.NewCmdCreate(f, nil))
2627
cmd.AddCommand(cmdDelete.NewCmdDelete(f, nil))
28+
cmd.AddCommand(cmdDeleteAsset.NewCmdDeleteAsset(f, nil))
2729
cmd.AddCommand(cmdDownload.NewCmdDownload(f, nil))
2830
cmd.AddCommand(cmdList.NewCmdList(f, nil))
2931
cmd.AddCommand(cmdView.NewCmdView(f, nil))

0 commit comments

Comments
 (0)