Skip to content

Commit da334e2

Browse files
Merge pull request cli#43 from github/gh-issue
Add `gh issue list` and `gh issue view`
2 parents 9ad41cf + 3782ed6 commit da334e2

File tree

9 files changed

+403
-39
lines changed

9 files changed

+403
-39
lines changed

api/queries.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package api
22

33
import (
44
"fmt"
5+
"time"
56
)
67

78
type PullRequestsPayload struct {
@@ -22,6 +23,108 @@ type Repo interface {
2223
RepoOwner() string
2324
}
2425

26+
type IssuesPayload struct {
27+
Assigned []Issue
28+
Mentioned []Issue
29+
Recent []Issue
30+
}
31+
32+
type Issue struct {
33+
Number int
34+
Title string
35+
}
36+
37+
func Issues(client *Client, ghRepo Repo, currentUsername string) (*IssuesPayload, error) {
38+
type issues struct {
39+
Issues struct {
40+
Edges []struct {
41+
Node Issue
42+
}
43+
}
44+
}
45+
46+
type response struct {
47+
Assigned issues
48+
Mentioned issues
49+
Recent issues
50+
}
51+
52+
query := `
53+
fragment issue on Issue {
54+
number
55+
title
56+
}
57+
query($owner: String!, $repo: String!, $since: DateTime!, $viewer: String!, $per_page: Int = 10) {
58+
assigned: repository(owner: $owner, name: $repo) {
59+
issues(filterBy: {assignee: $viewer}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
60+
edges {
61+
node {
62+
...issue
63+
}
64+
}
65+
}
66+
}
67+
mentioned: repository(owner: $owner, name: $repo) {
68+
issues(filterBy: {mentioned: $viewer}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
69+
edges {
70+
node {
71+
...issue
72+
}
73+
}
74+
}
75+
}
76+
recent: repository(owner: $owner, name: $repo) {
77+
issues(filterBy: {since: $since}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
78+
edges {
79+
node {
80+
...issue
81+
}
82+
}
83+
}
84+
}
85+
}
86+
`
87+
88+
owner := ghRepo.RepoOwner()
89+
repo := ghRepo.RepoName()
90+
since := time.Now().UTC().Add(time.Hour * -24).Format("2006-01-02T15:04:05-0700")
91+
variables := map[string]interface{}{
92+
"owner": owner,
93+
"repo": repo,
94+
"viewer": currentUsername,
95+
"since": since,
96+
}
97+
98+
var resp response
99+
err := client.GraphQL(query, variables, &resp)
100+
if err != nil {
101+
return nil, err
102+
}
103+
104+
var assigned []Issue
105+
for _, edge := range resp.Assigned.Issues.Edges {
106+
assigned = append(assigned, edge.Node)
107+
}
108+
109+
var mentioned []Issue
110+
for _, edge := range resp.Mentioned.Issues.Edges {
111+
mentioned = append(mentioned, edge.Node)
112+
}
113+
114+
var recent []Issue
115+
for _, edge := range resp.Recent.Issues.Edges {
116+
recent = append(recent, edge.Node)
117+
}
118+
119+
payload := IssuesPayload{
120+
assigned,
121+
mentioned,
122+
recent,
123+
}
124+
125+
return &payload, nil
126+
}
127+
25128
func PullRequests(client *Client, ghRepo Repo, currentBranch, currentUsername string) (*PullRequestsPayload, error) {
26129
type edges struct {
27130
Edges []struct {

command/issue.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package command
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
7+
"github.com/github/gh-cli/api"
8+
"github.com/github/gh-cli/utils"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
func init() {
13+
var issueCmd = &cobra.Command{
14+
Use: "issue",
15+
Short: "Work with GitHub issues",
16+
Long: `This command allows you to work with issues.`,
17+
Args: cobra.MinimumNArgs(1),
18+
RunE: func(cmd *cobra.Command, args []string) error {
19+
return fmt.Errorf("%+v is not a valid issue command", args)
20+
},
21+
}
22+
23+
issueCmd.AddCommand(
24+
&cobra.Command{
25+
Use: "status",
26+
Short: "Display issue status",
27+
RunE: issueList,
28+
},
29+
&cobra.Command{
30+
Use: "view [issue-number]",
31+
Args: cobra.MinimumNArgs(1),
32+
Short: "Open a issue in the browser",
33+
RunE: issueView,
34+
},
35+
)
36+
37+
RootCmd.AddCommand(issueCmd)
38+
}
39+
40+
func issueList(cmd *cobra.Command, args []string) error {
41+
ctx := contextForCommand(cmd)
42+
apiClient, err := apiClientForContext(ctx)
43+
if err != nil {
44+
return err
45+
}
46+
47+
baseRepo, err := ctx.BaseRepo()
48+
if err != nil {
49+
return err
50+
}
51+
52+
currentUser, err := ctx.AuthLogin()
53+
if err != nil {
54+
return err
55+
}
56+
57+
issuePayload, err := api.Issues(apiClient, baseRepo, currentUser)
58+
if err != nil {
59+
return err
60+
}
61+
62+
printHeader("Issues assigned to you")
63+
if issuePayload.Assigned != nil {
64+
printIssues(issuePayload.Assigned...)
65+
} else {
66+
message := fmt.Sprintf(" There are no issues assgined to you")
67+
printMessage(message)
68+
}
69+
fmt.Println()
70+
71+
printHeader("Issues mentioning you")
72+
if len(issuePayload.Mentioned) > 0 {
73+
printIssues(issuePayload.Mentioned...)
74+
} else {
75+
printMessage(" There are no issues mentioning you")
76+
}
77+
fmt.Println()
78+
79+
printHeader("Recent issues")
80+
if len(issuePayload.Recent) > 0 {
81+
printIssues(issuePayload.Recent...)
82+
} else {
83+
printMessage(" There are no recent issues")
84+
}
85+
fmt.Println()
86+
87+
return nil
88+
}
89+
90+
func issueView(cmd *cobra.Command, args []string) error {
91+
ctx := contextForCommand(cmd)
92+
93+
baseRepo, err := ctx.BaseRepo()
94+
if err != nil {
95+
return err
96+
}
97+
98+
var openURL string
99+
if number, err := strconv.Atoi(args[0]); err == nil {
100+
// TODO: move URL generation into GitHubRepository
101+
openURL = fmt.Sprintf("https://github.com/%s/%s/issues/%d", baseRepo.RepoOwner(), baseRepo.RepoName(), number)
102+
} else {
103+
return fmt.Errorf("invalid issue number: '%s'", args[0])
104+
}
105+
106+
fmt.Printf("Opening %s in your browser.\n", openURL)
107+
return utils.OpenInBrowser(openURL)
108+
}
109+
110+
func printIssues(issues ...api.Issue) {
111+
for _, issue := range issues {
112+
fmt.Printf(" #%d %s\n", issue.Number, truncateTitle(issue.Title, 70))
113+
}
114+
}

command/issue_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package command
2+
3+
import (
4+
"os"
5+
"regexp"
6+
"testing"
7+
8+
"github.com/github/gh-cli/test"
9+
)
10+
11+
func TestIssueStatus(t *testing.T) {
12+
initBlankContext("OWNER/REPO", "master")
13+
http := initFakeHTTP()
14+
15+
jsonFile, _ := os.Open("../test/fixtures/issueStatus.json")
16+
defer jsonFile.Close()
17+
http.StubResponse(200, jsonFile)
18+
19+
output, err := test.RunCommand(RootCmd, "issue status")
20+
if err != nil {
21+
t.Errorf("error running command `issue status`: %v", err)
22+
}
23+
24+
expectedIssues := []*regexp.Regexp{
25+
regexp.MustCompile(`#8.*carrots`),
26+
regexp.MustCompile(`#9.*squash`),
27+
regexp.MustCompile(`#10.*broccoli`),
28+
regexp.MustCompile(`#11.*swiss chard`),
29+
}
30+
31+
for _, r := range expectedIssues {
32+
if !r.MatchString(output) {
33+
t.Errorf("output did not match regexp /%s/", r)
34+
}
35+
}
36+
}
37+
38+
func TestIssueView(t *testing.T) {
39+
initBlankContext("OWNER/REPO", "master")
40+
http := initFakeHTTP()
41+
42+
jsonFile, _ := os.Open("../test/fixtures/issueView.json")
43+
defer jsonFile.Close()
44+
http.StubResponse(200, jsonFile)
45+
46+
teardown, callCount := mockOpenInBrowser()
47+
defer teardown()
48+
49+
output, err := test.RunCommand(RootCmd, "issue view 8")
50+
if err != nil {
51+
t.Errorf("error running command `issue view`: %v", err)
52+
}
53+
54+
if output == "" {
55+
t.Errorf("command output expected got an empty string")
56+
}
57+
58+
if *callCount != 1 {
59+
t.Errorf("OpenInBrowser should be called 1 time but was called %d time(s)", *callCount)
60+
}
61+
}

command/pr.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ func prView(cmd *cobra.Command, args []string) error {
129129

130130
func printPrs(prs ...api.PullRequest) {
131131
for _, pr := range prs {
132-
fmt.Printf(" #%d %s %s\n", pr.Number, truncateTitle(pr.Title), utils.Cyan("["+pr.HeadRefName+"]"))
132+
fmt.Printf(" #%d %s %s\n", pr.Number, truncateTitle(pr.Title, 50), utils.Cyan("["+pr.HeadRefName+"]"))
133133
}
134134
}
135135

@@ -141,9 +141,7 @@ func printMessage(s string) {
141141
fmt.Println(utils.Gray(s))
142142
}
143143

144-
func truncateTitle(title string) string {
145-
const maxLength = 50
146-
144+
func truncateTitle(title string, maxLength int) string {
147145
if len(title) > maxLength {
148146
return title[0:maxLength-3] + "..."
149147
}

command/pr_test.go

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,9 @@ import (
55
"regexp"
66
"testing"
77

8-
"github.com/github/gh-cli/api"
9-
"github.com/github/gh-cli/context"
108
"github.com/github/gh-cli/test"
11-
"github.com/github/gh-cli/utils"
129
)
1310

14-
func initBlankContext(repo, branch string) {
15-
initContext = func() context.Context {
16-
ctx := context.NewBlank()
17-
ctx.SetBaseRepo(repo)
18-
ctx.SetBranch(branch)
19-
return ctx
20-
}
21-
}
22-
23-
func initFakeHTTP() *api.FakeHTTP {
24-
http := &api.FakeHTTP{}
25-
apiClientForContext = func(context.Context) (*api.Client, error) {
26-
return api.NewClient(api.ReplaceTripper(http)), nil
27-
}
28-
return http
29-
}
30-
3111
func TestPRList(t *testing.T) {
3212
initBlankContext("OWNER/REPO", "master")
3313
http := initFakeHTTP()
@@ -114,18 +94,3 @@ func TestPRView_NoActiveBranch(t *testing.T) {
11494
t.Errorf("OpenInBrowser should be called once but was called %d time(s)", *callCount)
11595
}
11696
}
117-
118-
func mockOpenInBrowser() (func(), *int) {
119-
callCount := 0
120-
originalOpenInBrowser := utils.OpenInBrowser
121-
teardown := func() {
122-
utils.OpenInBrowser = originalOpenInBrowser
123-
}
124-
125-
utils.OpenInBrowser = func(_ string) error {
126-
callCount++
127-
return nil
128-
}
129-
130-
return teardown, &callCount
131-
}

0 commit comments

Comments
 (0)