Skip to content

Commit 9e9e994

Browse files
Merge pull request cli#853 from cli/reopen
Add `gh issue reopen <issue number or url>`
2 parents 876ca32 + e2aab30 commit 9e9e994

File tree

3 files changed

+135
-2
lines changed

3 files changed

+135
-2
lines changed

api/queries_issue.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,3 +390,22 @@ func IssueClose(client *Client, repo ghrepo.Interface, issue Issue) error {
390390

391391
return nil
392392
}
393+
394+
func IssueReopen(client *Client, repo ghrepo.Interface, issue Issue) error {
395+
var mutation struct {
396+
ReopenIssue struct {
397+
Issue struct {
398+
ID githubv4.ID
399+
}
400+
} `graphql:"reopenIssue(input: $input)"`
401+
}
402+
403+
input := githubv4.ReopenIssueInput{
404+
IssueID: issue.ID,
405+
}
406+
407+
v4 := githubv4.NewClient(client.http)
408+
err := v4.Mutate(context.Background(), &mutation, input, nil)
409+
410+
return err
411+
}

command/issue.go

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ func init() {
4141
issueViewCmd.Flags().BoolP("web", "w", false, "Open an issue in the browser")
4242

4343
issueCmd.AddCommand(issueCloseCmd)
44+
issueCmd.AddCommand(issueReopenCmd)
4445
}
4546

4647
var issueCmd = &cobra.Command{
@@ -82,11 +83,17 @@ With '--web', open the issue in a web browser instead.`,
8283
RunE: issueView,
8384
}
8485
var issueCloseCmd = &cobra.Command{
85-
Use: "close <number>",
86-
Short: "close and issue issues",
86+
Use: "close <numberOrURL>",
87+
Short: "close issue",
8788
Args: cobra.ExactArgs(1),
8889
RunE: issueClose,
8990
}
91+
var issueReopenCmd = &cobra.Command{
92+
Use: "reopen <numberOrURL>",
93+
Short: "reopen issue",
94+
Args: cobra.ExactArgs(1),
95+
RunE: issueReopen,
96+
}
9097

9198
func issueList(cmd *cobra.Command, args []string) error {
9299
ctx := contextForCommand(cmd)
@@ -559,7 +566,41 @@ func issueClose(cmd *cobra.Command, args []string) error {
559566
fmt.Fprintf(colorableErr(cmd), "%s Closed issue #%d\n", utils.Red("✔"), issue.Number)
560567

561568
return nil
569+
}
570+
571+
func issueReopen(cmd *cobra.Command, args []string) error {
572+
ctx := contextForCommand(cmd)
573+
apiClient, err := apiClientForContext(ctx)
574+
if err != nil {
575+
return err
576+
}
577+
578+
baseRepo, err := determineBaseRepo(cmd, ctx)
579+
if err != nil {
580+
return err
581+
}
562582

583+
issue, err := issueFromArg(apiClient, baseRepo, args[0])
584+
var idErr *api.IssuesDisabledError
585+
if errors.As(err, &idErr) {
586+
return fmt.Errorf("issues disabled for %s", ghrepo.FullName(baseRepo))
587+
} else if err != nil {
588+
return fmt.Errorf("failed to find issue #%d: %w", issue.Number, err)
589+
}
590+
591+
if !issue.Closed {
592+
fmt.Fprintf(colorableErr(cmd), "%s Issue #%d is already open\n", utils.Yellow("!"), issue.Number)
593+
return nil
594+
}
595+
596+
err = api.IssueReopen(apiClient, baseRepo, *issue)
597+
if err != nil {
598+
return fmt.Errorf("API call failed:%w", err)
599+
}
600+
601+
fmt.Fprintf(colorableErr(cmd), "%s Reopened issue #%d\n", utils.Green("✔"), issue.Number)
602+
603+
return nil
563604
}
564605

565606
func displayURL(urlStr string) string {

command/issue_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,3 +753,76 @@ func TestIssueClose_issuesDisabled(t *testing.T) {
753753
t.Fatalf("got unexpected error: %s", err)
754754
}
755755
}
756+
757+
func TestIssueReopen(t *testing.T) {
758+
initBlankContext("", "OWNER/REPO", "master")
759+
http := initFakeHTTP()
760+
http.StubRepoResponse("OWNER", "REPO")
761+
762+
http.StubResponse(200, bytes.NewBufferString(`
763+
{ "data": { "repository": {
764+
"hasIssuesEnabled": true,
765+
"issue": { "number": 2, "closed": true}
766+
} } }
767+
`))
768+
769+
http.StubResponse(200, bytes.NewBufferString(`{"id": "THE-ID"}`))
770+
771+
output, err := RunCommand(issueReopenCmd, "issue reopen 2")
772+
if err != nil {
773+
t.Fatalf("error running command `issue reopen`: %v", err)
774+
}
775+
776+
r := regexp.MustCompile(`Reopened issue #2`)
777+
778+
if !r.MatchString(output.Stderr()) {
779+
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
780+
}
781+
}
782+
783+
func TestIssueReopen_alreadyOpen(t *testing.T) {
784+
initBlankContext("", "OWNER/REPO", "master")
785+
http := initFakeHTTP()
786+
http.StubRepoResponse("OWNER", "REPO")
787+
788+
http.StubResponse(200, bytes.NewBufferString(`
789+
{ "data": { "repository": {
790+
"hasIssuesEnabled": true,
791+
"issue": { "number": 2, "closed": false}
792+
} } }
793+
`))
794+
795+
http.StubResponse(200, bytes.NewBufferString(`{"id": "THE-ID"}`))
796+
797+
output, err := RunCommand(issueReopenCmd, "issue reopen 2")
798+
if err != nil {
799+
t.Fatalf("error running command `issue reopen`: %v", err)
800+
}
801+
802+
r := regexp.MustCompile(`#2 is already open`)
803+
804+
if !r.MatchString(output.Stderr()) {
805+
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
806+
}
807+
}
808+
809+
func TestIssueReopen_issuesDisabled(t *testing.T) {
810+
initBlankContext("", "OWNER/REPO", "master")
811+
http := initFakeHTTP()
812+
http.StubRepoResponse("OWNER", "REPO")
813+
814+
http.StubResponse(200, bytes.NewBufferString(`
815+
{ "data": { "repository": {
816+
"hasIssuesEnabled": false
817+
} } }
818+
`))
819+
820+
_, err := RunCommand(issueReopenCmd, "issue reopen 2")
821+
if err == nil {
822+
t.Fatalf("expected error when issues are disabled")
823+
}
824+
825+
if !strings.Contains(err.Error(), "issues disabled") {
826+
t.Fatalf("got unexpected error: %s", err)
827+
}
828+
}

0 commit comments

Comments
 (0)