Thanks to visit codestin.com
Credit goes to github.com

Skip to content
36 changes: 35 additions & 1 deletion api/queries_issue.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package api

import (
"context"
"fmt"
"time"

"github.com/cli/cli/internal/ghrepo"
"github.com/shurcooL/githubv4"
)

type IssuesPayload struct {
Expand All @@ -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
Expand Down Expand Up @@ -61,6 +65,10 @@ type Issue struct {
}
}

type IssuesDisabledError struct {
error
}

const fragments = `
fragment issue on Issue {
number
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
44 changes: 44 additions & 0 deletions command/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -514,6 +522,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 {
Expand Down
73 changes: 73 additions & 0 deletions command/issue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe a third test for the already closed state