X Tutup
Skip to content

Commit d91b312

Browse files
author
Nate Smith
authored
Merge pull request cli#2839 from kevinmbeaulieu/kb/delete-issue-cmd
Add `issue delete` command
2 parents 11d3972 + 4b036f6 commit d91b312

File tree

4 files changed

+277
-0
lines changed

4 files changed

+277
-0
lines changed

api/queries_issue.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,27 @@ func IssueReopen(client *Client, repo ghrepo.Interface, issue Issue) error {
467467
return err
468468
}
469469

470+
func IssueDelete(client *Client, repo ghrepo.Interface, issue Issue) error {
471+
var mutation struct {
472+
DeleteIssue struct {
473+
Repository struct {
474+
ID githubv4.ID
475+
}
476+
} `graphql:"deleteIssue(input: $input)"`
477+
}
478+
479+
variables := map[string]interface{}{
480+
"input": githubv4.DeleteIssueInput{
481+
IssueID: issue.ID,
482+
},
483+
}
484+
485+
gql := graphQLClient(client.http, repo.RepoHost())
486+
err := gql.MutateNamed(context.Background(), "IssueDelete", &mutation, variables)
487+
488+
return err
489+
}
490+
470491
// milestoneNodeIdToDatabaseId extracts the REST Database ID from the GraphQL Node ID
471492
// This conversion is necessary since the GraphQL API requires the use of the milestone's database ID
472493
// for querying the related issues.

pkg/cmd/issue/delete/delete.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package delete
2+
3+
import (
4+
"fmt"
5+
"github.com/AlecAivazis/survey/v2"
6+
"github.com/cli/cli/api"
7+
"github.com/cli/cli/internal/config"
8+
"github.com/cli/cli/internal/ghrepo"
9+
"github.com/cli/cli/pkg/cmd/issue/shared"
10+
"github.com/cli/cli/pkg/cmdutil"
11+
"github.com/cli/cli/pkg/iostreams"
12+
"github.com/cli/cli/pkg/prompt"
13+
"github.com/spf13/cobra"
14+
"net/http"
15+
"strconv"
16+
)
17+
18+
type DeleteOptions struct {
19+
HttpClient func() (*http.Client, error)
20+
Config func() (config.Config, error)
21+
IO *iostreams.IOStreams
22+
BaseRepo func() (ghrepo.Interface, error)
23+
24+
SelectorArg string
25+
}
26+
27+
func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Command {
28+
opts := &DeleteOptions{
29+
IO: f.IOStreams,
30+
HttpClient: f.HttpClient,
31+
Config: f.Config,
32+
}
33+
34+
cmd := &cobra.Command{
35+
Use: "delete {<number> | <url>}",
36+
Short: "Delete issue",
37+
Args: cobra.ExactArgs(1),
38+
RunE: func(cmd *cobra.Command, args []string) error {
39+
// support `-R, --repo` override
40+
opts.BaseRepo = f.BaseRepo
41+
42+
if len(args) > 0 {
43+
opts.SelectorArg = args[0]
44+
}
45+
46+
if runF != nil {
47+
return runF(opts)
48+
}
49+
return deleteRun(opts)
50+
},
51+
}
52+
53+
return cmd
54+
}
55+
56+
func deleteRun(opts *DeleteOptions) error {
57+
cs := opts.IO.ColorScheme()
58+
59+
httpClient, err := opts.HttpClient()
60+
if err != nil {
61+
return err
62+
}
63+
apiClient := api.NewClientFromHTTP(httpClient)
64+
65+
issue, baseRepo, err := shared.IssueFromArg(apiClient, opts.BaseRepo, opts.SelectorArg)
66+
if err != nil {
67+
return err
68+
}
69+
70+
// When executed in an interactive shell, require confirmation. Otherwise skip confirmation.
71+
if opts.IO.CanPrompt() {
72+
answer := ""
73+
err = prompt.SurveyAskOne(
74+
&survey.Input{
75+
Message: fmt.Sprintf("You're going to delete issue #%d. This action cannot be reversed. To confirm, type the issue number:", issue.Number),
76+
},
77+
&answer,
78+
)
79+
if err != nil {
80+
return err
81+
}
82+
answerInt, err := strconv.Atoi(answer)
83+
if err != nil || answerInt != issue.Number {
84+
fmt.Fprintf(opts.IO.Out, "Issue #%d was not deleted.\n", issue.Number)
85+
return nil
86+
}
87+
}
88+
89+
err = api.IssueDelete(apiClient, baseRepo, *issue)
90+
if err != nil {
91+
return err
92+
}
93+
94+
fmt.Fprintf(opts.IO.ErrOut, "%s Deleted issue #%d (%s).\n", cs.Red("✔"), issue.Number, issue.Title)
95+
96+
return nil
97+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package delete
2+
3+
import (
4+
"bytes"
5+
"github.com/cli/cli/pkg/prompt"
6+
"io/ioutil"
7+
"net/http"
8+
"regexp"
9+
"testing"
10+
11+
"github.com/cli/cli/internal/config"
12+
"github.com/cli/cli/internal/ghrepo"
13+
"github.com/cli/cli/pkg/cmdutil"
14+
"github.com/cli/cli/pkg/httpmock"
15+
"github.com/cli/cli/pkg/iostreams"
16+
"github.com/cli/cli/test"
17+
"github.com/google/shlex"
18+
"github.com/stretchr/testify/assert"
19+
)
20+
21+
func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, error) {
22+
io, _, stdout, stderr := iostreams.Test()
23+
io.SetStdoutTTY(isTTY)
24+
io.SetStdinTTY(isTTY)
25+
io.SetStderrTTY(isTTY)
26+
27+
factory := &cmdutil.Factory{
28+
IOStreams: io,
29+
HttpClient: func() (*http.Client, error) {
30+
return &http.Client{Transport: rt}, nil
31+
},
32+
Config: func() (config.Config, error) {
33+
return config.NewBlankConfig(), nil
34+
},
35+
BaseRepo: func() (ghrepo.Interface, error) {
36+
return ghrepo.New("OWNER", "REPO"), nil
37+
},
38+
}
39+
40+
cmd := NewCmdDelete(factory, nil)
41+
42+
argv, err := shlex.Split(cli)
43+
if err != nil {
44+
return nil, err
45+
}
46+
cmd.SetArgs(argv)
47+
48+
cmd.SetIn(&bytes.Buffer{})
49+
cmd.SetOut(ioutil.Discard)
50+
cmd.SetErr(ioutil.Discard)
51+
52+
_, err = cmd.ExecuteC()
53+
return &test.CmdOut{
54+
OutBuf: stdout,
55+
ErrBuf: stderr,
56+
}, err
57+
}
58+
59+
func TestIssueDelete(t *testing.T) {
60+
httpRegistry := &httpmock.Registry{}
61+
defer httpRegistry.Verify(t)
62+
63+
httpRegistry.Register(
64+
httpmock.GraphQL(`query IssueByNumber\b`),
65+
httpmock.StringResponse(`
66+
{ "data": { "repository": {
67+
"hasIssuesEnabled": true,
68+
"issue": { "id": "THE-ID", "number": 13, "title": "The title of the issue"}
69+
} } }`),
70+
)
71+
httpRegistry.Register(
72+
httpmock.GraphQL(`mutation IssueDelete\b`),
73+
httpmock.GraphQLMutation(`{"id": "THE-ID"}`,
74+
func(inputs map[string]interface{}) {
75+
assert.Equal(t, inputs["issueId"], "THE-ID")
76+
}),
77+
)
78+
as, teardown := prompt.InitAskStubber()
79+
defer teardown()
80+
as.StubOne("13")
81+
82+
output, err := runCommand(httpRegistry, true, "13")
83+
if err != nil {
84+
t.Fatalf("error running command `issue delete`: %v", err)
85+
}
86+
87+
r := regexp.MustCompile(`Deleted issue #13 \(The title of the issue\)`)
88+
89+
if !r.MatchString(output.Stderr()) {
90+
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
91+
}
92+
}
93+
94+
func TestIssueDelete_cancel(t *testing.T) {
95+
httpRegistry := &httpmock.Registry{}
96+
defer httpRegistry.Verify(t)
97+
98+
httpRegistry.Register(
99+
httpmock.GraphQL(`query IssueByNumber\b`),
100+
httpmock.StringResponse(`
101+
{ "data": { "repository": {
102+
"hasIssuesEnabled": true,
103+
"issue": { "id": "THE-ID", "number": 13, "title": "The title of the issue"}
104+
} } }`),
105+
)
106+
as, teardown := prompt.InitAskStubber()
107+
defer teardown()
108+
as.StubOne("14")
109+
110+
output, err := runCommand(httpRegistry, true, "13")
111+
if err != nil {
112+
t.Fatalf("error running command `issue delete`: %v", err)
113+
}
114+
115+
r := regexp.MustCompile(`Issue #13 was not deleted`)
116+
117+
if !r.MatchString(output.String()) {
118+
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.String())
119+
}
120+
}
121+
122+
func TestIssueDelete_doesNotExist(t *testing.T) {
123+
httpRegistry := &httpmock.Registry{}
124+
defer httpRegistry.Verify(t)
125+
126+
httpRegistry.Register(
127+
httpmock.GraphQL(`query IssueByNumber\b`),
128+
httpmock.StringResponse(`
129+
{ "errors": [
130+
{ "message": "Could not resolve to an Issue with the number of 13." }
131+
] }
132+
`),
133+
)
134+
135+
_, err := runCommand(httpRegistry, true, "13")
136+
if err == nil || err.Error() != "GraphQL error: Could not resolve to an Issue with the number of 13." {
137+
t.Errorf("error running command `issue delete`: %v", err)
138+
}
139+
}
140+
141+
func TestIssueDelete_issuesDisabled(t *testing.T) {
142+
httpRegistry := &httpmock.Registry{}
143+
defer httpRegistry.Verify(t)
144+
145+
httpRegistry.Register(
146+
httpmock.GraphQL(`query IssueByNumber\b`),
147+
httpmock.StringResponse(`
148+
{ "data": { "repository": {
149+
"hasIssuesEnabled": false
150+
} } }`),
151+
)
152+
153+
_, err := runCommand(httpRegistry, true, "13")
154+
if err == nil || err.Error() != "the 'OWNER/REPO' repository has disabled issues" {
155+
t.Fatalf("got error: %v", err)
156+
}
157+
}

pkg/cmd/issue/issue.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
cmdClose "github.com/cli/cli/pkg/cmd/issue/close"
66
cmdComment "github.com/cli/cli/pkg/cmd/issue/comment"
77
cmdCreate "github.com/cli/cli/pkg/cmd/issue/create"
8+
cmdDelete "github.com/cli/cli/pkg/cmd/issue/delete"
89
cmdList "github.com/cli/cli/pkg/cmd/issue/list"
910
cmdReopen "github.com/cli/cli/pkg/cmd/issue/reopen"
1011
cmdStatus "github.com/cli/cli/pkg/cmd/issue/status"
@@ -42,6 +43,7 @@ func NewCmdIssue(f *cmdutil.Factory) *cobra.Command {
4243
cmd.AddCommand(cmdStatus.NewCmdStatus(f, nil))
4344
cmd.AddCommand(cmdView.NewCmdView(f, nil))
4445
cmd.AddCommand(cmdComment.NewCmdComment(f, nil))
46+
cmd.AddCommand(cmdDelete.NewCmdDelete(f, nil))
4547

4648
return cmd
4749
}

0 commit comments

Comments
 (0)
X Tutup