Merge pull request #843 from cli/when-god-closes-an-issue-he-opens-a-pull-request
Add `gh issue close <urlOrNumber>`
This commit is contained in:
commit
28b35aa333
3 changed files with 152 additions and 1 deletions
|
|
@ -1,10 +1,12 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/shurcooL/githubv4"
|
||||
)
|
||||
|
||||
type IssuesPayload struct {
|
||||
|
|
@ -20,10 +22,12 @@ type IssuesAndTotalCount struct {
|
|||
|
||||
// Ref. https://developer.github.com/v4/object/issue/
|
||||
type Issue struct {
|
||||
ID string
|
||||
Number int
|
||||
Title string
|
||||
URL string
|
||||
State string
|
||||
Closed bool
|
||||
Body string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
|
|
@ -61,6 +65,10 @@ type Issue struct {
|
|||
}
|
||||
}
|
||||
|
||||
type IssuesDisabledError struct {
|
||||
error
|
||||
}
|
||||
|
||||
const fragments = `
|
||||
fragment issue on Issue {
|
||||
number
|
||||
|
|
@ -296,8 +304,10 @@ func IssueByNumber(client *Client, repo ghrepo.Interface, number int) (*Issue, e
|
|||
repository(owner: $owner, name: $repo) {
|
||||
hasIssuesEnabled
|
||||
issue(number: $issue_number) {
|
||||
id
|
||||
title
|
||||
state
|
||||
closed
|
||||
body
|
||||
author {
|
||||
login
|
||||
|
|
@ -351,8 +361,32 @@ func IssueByNumber(client *Client, repo ghrepo.Interface, number int) (*Issue, e
|
|||
}
|
||||
|
||||
if !resp.Repository.HasIssuesEnabled {
|
||||
return nil, fmt.Errorf("the '%s' repository has disabled issues", ghrepo.FullName(repo))
|
||||
|
||||
return nil, &IssuesDisabledError{fmt.Errorf("the '%s' repository has disabled issues", ghrepo.FullName(repo))}
|
||||
}
|
||||
|
||||
return &resp.Repository.Issue, nil
|
||||
}
|
||||
|
||||
func IssueClose(client *Client, repo ghrepo.Interface, issue Issue) error {
|
||||
var mutation struct {
|
||||
CloseIssue struct {
|
||||
Issue struct {
|
||||
ID githubv4.ID
|
||||
}
|
||||
} `graphql:"closeIssue(input: $input)"`
|
||||
}
|
||||
|
||||
input := githubv4.CloseIssueInput{
|
||||
IssueID: issue.ID,
|
||||
}
|
||||
|
||||
v4 := githubv4.NewClient(client.http)
|
||||
err := v4.Mutate(context.Background(), &mutation, input, nil)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ func init() {
|
|||
|
||||
issueCmd.AddCommand(issueViewCmd)
|
||||
issueViewCmd.Flags().BoolP("web", "w", false, "Open an issue in the browser")
|
||||
|
||||
issueCmd.AddCommand(issueCloseCmd)
|
||||
}
|
||||
|
||||
var issueCmd = &cobra.Command{
|
||||
|
|
@ -79,6 +81,12 @@ var issueViewCmd = &cobra.Command{
|
|||
With '--web', open the issue in a web browser instead.`,
|
||||
RunE: issueView,
|
||||
}
|
||||
var issueCloseCmd = &cobra.Command{
|
||||
Use: "close <number>",
|
||||
Short: "close and issue issues",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: issueClose,
|
||||
}
|
||||
|
||||
func issueList(cmd *cobra.Command, args []string) error {
|
||||
ctx := contextForCommand(cmd)
|
||||
|
|
@ -518,6 +526,42 @@ func issueProjectList(issue api.Issue) string {
|
|||
return list
|
||||
}
|
||||
|
||||
func issueClose(cmd *cobra.Command, args []string) error {
|
||||
ctx := contextForCommand(cmd)
|
||||
apiClient, err := apiClientForContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
baseRepo, err := determineBaseRepo(cmd, ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
issue, err := issueFromArg(apiClient, baseRepo, args[0])
|
||||
var idErr *api.IssuesDisabledError
|
||||
if errors.As(err, &idErr) {
|
||||
return fmt.Errorf("issues disabled for %s", ghrepo.FullName(baseRepo))
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to find issue #%d: %w", issue.Number, err)
|
||||
}
|
||||
|
||||
if issue.Closed {
|
||||
fmt.Fprintf(colorableErr(cmd), "%s Issue #%d is already closed\n", utils.Yellow("!"), issue.Number)
|
||||
return nil
|
||||
}
|
||||
|
||||
err = api.IssueClose(apiClient, baseRepo, *issue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("API call failed:%w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(colorableErr(cmd), "%s Closed issue #%d\n", utils.Red("✔"), issue.Number)
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func displayURL(urlStr string) string {
|
||||
u, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -680,3 +680,76 @@ func TestIssueStateTitleWithColor(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueClose(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": {
|
||||
"hasIssuesEnabled": true,
|
||||
"issue": { "number": 13}
|
||||
} } }
|
||||
`))
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`{"id": "THE-ID"}`))
|
||||
|
||||
output, err := RunCommand(issueCloseCmd, "issue close 13")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `issue close`: %v", err)
|
||||
}
|
||||
|
||||
r := regexp.MustCompile(`Closed issue #13`)
|
||||
|
||||
if !r.MatchString(output.Stderr()) {
|
||||
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueClose_alreadyClosed(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": {
|
||||
"hasIssuesEnabled": true,
|
||||
"issue": { "number": 13, "closed": true}
|
||||
} } }
|
||||
`))
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`{"id": "THE-ID"}`))
|
||||
|
||||
output, err := RunCommand(issueCloseCmd, "issue close 13")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `issue close`: %v", err)
|
||||
}
|
||||
|
||||
r := regexp.MustCompile(`#13 is already closed`)
|
||||
|
||||
if !r.MatchString(output.Stderr()) {
|
||||
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueClose_issuesDisabled(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": {
|
||||
"hasIssuesEnabled": false
|
||||
} } }
|
||||
`))
|
||||
|
||||
_, err := RunCommand(issueCloseCmd, "issue close 13")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error when issues are disabled")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "issues disabled") {
|
||||
t.Fatalf("got unexpected error: %s", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue