Add issue delete command
Similar to `issue close`, but for deleting an issue rather than just closing it. Resolves cli/cli#2820.
This commit is contained in:
parent
6e2c1b33b0
commit
57d5470df9
4 changed files with 224 additions and 0 deletions
|
|
@ -465,6 +465,27 @@ func IssueReopen(client *Client, repo ghrepo.Interface, issue Issue) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func IssueDelete(client *Client, repo ghrepo.Interface, issue Issue) error {
|
||||
var mutation struct {
|
||||
DeleteIssue struct {
|
||||
Repository struct {
|
||||
ID githubv4.ID
|
||||
}
|
||||
} `graphql:"deleteIssue(input: $input)"`
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"input": githubv4.DeleteIssueInput{
|
||||
IssueID: issue.ID,
|
||||
},
|
||||
}
|
||||
|
||||
gql := graphQLClient(client.http, repo.RepoHost())
|
||||
err := gql.MutateNamed(context.Background(), "IssueDelete", &mutation, variables)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// milestoneNodeIdToDatabaseId extracts the REST Database ID from the GraphQL Node ID
|
||||
// This conversion is necessary since the GraphQL API requires the use of the milestone's database ID
|
||||
// for querying the related issues.
|
||||
|
|
|
|||
76
pkg/cmd/issue/delete/delete.go
Normal file
76
pkg/cmd/issue/delete/delete.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package delete
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/cmd/issue/shared"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type DeleteOptions struct {
|
||||
HttpClient func() (*http.Client, error)
|
||||
Config func() (config.Config, error)
|
||||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
|
||||
SelectorArg string
|
||||
}
|
||||
|
||||
func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Command {
|
||||
opts := &DeleteOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
Config: f.Config,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete {<number> | <url>}",
|
||||
Short: "Delete issue",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
|
||||
if len(args) > 0 {
|
||||
opts.SelectorArg = args[0]
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
return deleteRun(opts)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func deleteRun(opts *DeleteOptions) error {
|
||||
cs := opts.IO.ColorScheme()
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
issue, baseRepo, err := shared.IssueFromArg(apiClient, opts.BaseRepo, opts.SelectorArg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = api.IssueDelete(apiClient, baseRepo, *issue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Deleted issue #%d (%s)\n", cs.Red("✔"), issue.Number, issue.Title)
|
||||
|
||||
return nil
|
||||
}
|
||||
125
pkg/cmd/issue/delete/delete_test.go
Normal file
125
pkg/cmd/issue/delete/delete_test.go
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
package delete
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/test"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, error) {
|
||||
io, _, stdout, stderr := iostreams.Test()
|
||||
io.SetStdoutTTY(isTTY)
|
||||
io.SetStdinTTY(isTTY)
|
||||
io.SetStderrTTY(isTTY)
|
||||
|
||||
factory := &cmdutil.Factory{
|
||||
IOStreams: io,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: rt}, nil
|
||||
},
|
||||
Config: func() (config.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd := NewCmdDelete(factory, nil)
|
||||
|
||||
argv, err := shlex.Split(cli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmd.SetArgs(argv)
|
||||
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(ioutil.Discard)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
return &test.CmdOut{
|
||||
OutBuf: stdout,
|
||||
ErrBuf: stderr,
|
||||
}, err
|
||||
}
|
||||
|
||||
func TestIssueDelete(t *testing.T) {
|
||||
httpRegistry := &httpmock.Registry{}
|
||||
defer httpRegistry.Verify(t)
|
||||
|
||||
httpRegistry.Register(
|
||||
httpmock.GraphQL(`query IssueByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"hasIssuesEnabled": true,
|
||||
"issue": { "id": "THE-ID", "number": 13, "title": "The title of the issue"}
|
||||
} } }`),
|
||||
)
|
||||
httpRegistry.Register(
|
||||
httpmock.GraphQL(`mutation IssueDelete\b`),
|
||||
httpmock.GraphQLMutation(`{"id": "THE-ID"}`,
|
||||
func(inputs map[string]interface{}) {
|
||||
assert.Equal(t, inputs["issueId"], "THE-ID")
|
||||
}),
|
||||
)
|
||||
|
||||
output, err := runCommand(httpRegistry, true, "13")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `issue delete`: %v", err)
|
||||
}
|
||||
|
||||
r := regexp.MustCompile(`Deleted issue #13 \(The title of the issue\)`)
|
||||
|
||||
if !r.MatchString(output.Stderr()) {
|
||||
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueDelete_doesNotExist(t *testing.T) {
|
||||
httpRegistry := &httpmock.Registry{}
|
||||
defer httpRegistry.Verify(t)
|
||||
|
||||
httpRegistry.Register(
|
||||
httpmock.GraphQL(`query IssueByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "errors": [
|
||||
{ "message": "Could not resolve to an Issue with the number of 13." }
|
||||
] }
|
||||
`),
|
||||
)
|
||||
|
||||
_, err := runCommand(httpRegistry, true, "13")
|
||||
if err == nil || err.Error() != "GraphQL error: Could not resolve to an Issue with the number of 13." {
|
||||
t.Errorf("error running command `issue delete`: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueDelete_issuesDisabled(t *testing.T) {
|
||||
httpRegistry := &httpmock.Registry{}
|
||||
defer httpRegistry.Verify(t)
|
||||
|
||||
httpRegistry.Register(
|
||||
httpmock.GraphQL(`query IssueByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"hasIssuesEnabled": false
|
||||
} } }`),
|
||||
)
|
||||
|
||||
_, err := runCommand(httpRegistry, true, "13")
|
||||
if err == nil || err.Error() != "the 'OWNER/REPO' repository has disabled issues" {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import (
|
|||
cmdClose "github.com/cli/cli/pkg/cmd/issue/close"
|
||||
cmdComment "github.com/cli/cli/pkg/cmd/issue/comment"
|
||||
cmdCreate "github.com/cli/cli/pkg/cmd/issue/create"
|
||||
cmdDelete "github.com/cli/cli/pkg/cmd/issue/delete"
|
||||
cmdList "github.com/cli/cli/pkg/cmd/issue/list"
|
||||
cmdReopen "github.com/cli/cli/pkg/cmd/issue/reopen"
|
||||
cmdStatus "github.com/cli/cli/pkg/cmd/issue/status"
|
||||
|
|
@ -42,6 +43,7 @@ func NewCmdIssue(f *cmdutil.Factory) *cobra.Command {
|
|||
cmd.AddCommand(cmdStatus.NewCmdStatus(f, nil))
|
||||
cmd.AddCommand(cmdView.NewCmdView(f, nil))
|
||||
cmd.AddCommand(cmdComment.NewCmdComment(f, nil))
|
||||
cmd.AddCommand(cmdDelete.NewCmdDelete(f, nil))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue