Skip to content

Commit 528ea6e

Browse files
authored
Merge pull request cli#53 from github/issue-status-view
Add `issue status`, `issue create`
2 parents 262976b + 25142d4 commit 528ea6e

File tree

7 files changed

+257
-18
lines changed

7 files changed

+257
-18
lines changed

api/queries.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type IssuesPayload struct {
3333
type Issue struct {
3434
Number int
3535
Title string
36+
URL string
3637
}
3738

3839
func Issues(client *Client, ghRepo Repo, currentUsername string) (*IssuesPayload, error) {

api/queries_issue.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package api
2+
3+
func IssueCreate(client *Client, ghRepo Repo, params map[string]interface{}) (*Issue, error) {
4+
repoID, err := GitHubRepoId(client, ghRepo)
5+
if err != nil {
6+
return nil, err
7+
}
8+
9+
query := `
10+
mutation CreateIssue($input: CreateIssueInput!) {
11+
createIssue(input: $input) {
12+
issue {
13+
url
14+
}
15+
}
16+
}`
17+
18+
inputParams := map[string]interface{}{
19+
"repositoryId": repoID,
20+
}
21+
for key, val := range params {
22+
inputParams[key] = val
23+
}
24+
variables := map[string]interface{}{
25+
"input": inputParams,
26+
}
27+
28+
result := struct {
29+
CreateIssue struct {
30+
Issue Issue
31+
}
32+
}{}
33+
34+
err = client.GraphQL(query, variables, &result)
35+
if err != nil {
36+
return nil, err
37+
}
38+
39+
return &result.CreateIssue.Issue, nil
40+
}

api/queries_repo.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package api
2+
3+
import "fmt"
4+
5+
func GitHubRepoId(client *Client, ghRepo Repo) (string, error) {
6+
owner := ghRepo.RepoOwner()
7+
repo := ghRepo.RepoName()
8+
9+
query := `
10+
query FindRepoID($owner:String!, $name:String!) {
11+
repository(owner:$owner, name:$name) {
12+
id
13+
}
14+
}`
15+
variables := map[string]interface{}{
16+
"owner": owner,
17+
"name": repo,
18+
}
19+
20+
result := struct {
21+
Repository struct {
22+
Id string
23+
}
24+
}{}
25+
err := client.GraphQL(query, variables, &result)
26+
if err != nil || result.Repository.Id == "" {
27+
return "", fmt.Errorf("failed to determine GH repo ID: %s", err)
28+
}
29+
30+
return result.Repository.Id, nil
31+
}

command/issue.go

Lines changed: 92 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,46 @@ package command
22

33
import (
44
"fmt"
5+
"io/ioutil"
6+
"os"
57
"strconv"
8+
"strings"
69

710
"github.com/github/gh-cli/api"
811
"github.com/github/gh-cli/utils"
912
"github.com/spf13/cobra"
13+
"golang.org/x/crypto/ssh/terminal"
1014
)
1115

1216
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-
17+
RootCmd.AddCommand(issueCmd)
2318
issueCmd.AddCommand(
2419
&cobra.Command{
2520
Use: "status",
26-
Short: "Display issue status",
21+
Short: "Show status of relevant issues",
2722
RunE: issueList,
2823
},
2924
&cobra.Command{
30-
Use: "view [issue-number]",
25+
Use: "view <issue-number>",
3126
Args: cobra.MinimumNArgs(1),
32-
Short: "Open a issue in the browser",
27+
Short: "Open an issue in the browser",
3328
RunE: issueView,
3429
},
3530
)
31+
issueCmd.AddCommand(issueCreateCmd)
32+
issueCreateCmd.Flags().StringArrayP("message", "m", nil, "set title and body")
33+
issueCreateCmd.Flags().BoolP("web", "w", false, "open the web browser to create an issue")
34+
}
3635

37-
RootCmd.AddCommand(issueCmd)
36+
var issueCmd = &cobra.Command{
37+
Use: "issue",
38+
Short: "Work with GitHub issues",
39+
Long: `Helps you work with issues.`,
40+
}
41+
var issueCreateCmd = &cobra.Command{
42+
Use: "create",
43+
Short: "Create a new issue",
44+
RunE: issueCreate,
3845
}
3946

4047
func issueList(cmd *cobra.Command, args []string) error {
@@ -107,6 +114,77 @@ func issueView(cmd *cobra.Command, args []string) error {
107114
return utils.OpenInBrowser(openURL)
108115
}
109116

117+
func issueCreate(cmd *cobra.Command, args []string) error {
118+
ctx := contextForCommand(cmd)
119+
120+
baseRepo, err := ctx.BaseRepo()
121+
if err != nil {
122+
return err
123+
}
124+
125+
if isWeb, err := cmd.Flags().GetBool("web"); err == nil && isWeb {
126+
// TODO: move URL generation into GitHubRepository
127+
openURL := fmt.Sprintf("https://github.com/%s/%s/issues/new", baseRepo.RepoOwner(), baseRepo.RepoName())
128+
// TODO: figure out how to stub this in tests
129+
if stat, err := os.Stat(".github/ISSUE_TEMPLATE"); err == nil && stat.IsDir() {
130+
openURL += "/choose"
131+
}
132+
return utils.OpenInBrowser(openURL)
133+
}
134+
135+
var title string
136+
var body string
137+
138+
message, err := cmd.Flags().GetStringArray("message")
139+
if err != nil {
140+
return err
141+
}
142+
143+
apiClient, err := apiClientForContext(ctx)
144+
if err != nil {
145+
return err
146+
}
147+
148+
if len(message) > 0 {
149+
title = message[0]
150+
body = strings.Join(message[1:], "\n\n")
151+
} else {
152+
// TODO: open the text editor for issue title & body
153+
input := os.Stdin
154+
if terminal.IsTerminal(int(input.Fd())) {
155+
cmd.Println("Enter the issue title and body; press Enter + Ctrl-D when done:")
156+
}
157+
inputBytes, err := ioutil.ReadAll(input)
158+
if err != nil {
159+
return err
160+
}
161+
162+
parts := strings.SplitN(string(inputBytes), "\n\n", 2)
163+
if len(parts) > 0 {
164+
title = parts[0]
165+
}
166+
if len(parts) > 1 {
167+
body = parts[1]
168+
}
169+
}
170+
171+
if title == "" {
172+
return fmt.Errorf("aborting due to empty title")
173+
}
174+
params := map[string]interface{}{
175+
"title": title,
176+
"body": body,
177+
}
178+
179+
newIssue, err := api.IssueCreate(apiClient, baseRepo, params)
180+
if err != nil {
181+
return err
182+
}
183+
184+
fmt.Fprintln(cmd.OutOrStdout(), newIssue.URL)
185+
return nil
186+
}
187+
110188
func printIssues(issues ...api.Issue) {
111189
for _, issue := range issues {
112190
fmt.Printf(" #%d %s\n", issue.Number, truncate(70, issue.Title))

command/issue_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package command
22

33
import (
4+
"bytes"
5+
"encoding/json"
6+
"io/ioutil"
47
"os"
58
"os/exec"
69
"regexp"
@@ -69,3 +72,46 @@ func TestIssueView(t *testing.T) {
6972
t.Errorf("got: %q", url)
7073
}
7174
}
75+
76+
func TestIssueCreate(t *testing.T) {
77+
initBlankContext("OWNER/REPO", "master")
78+
http := initFakeHTTP()
79+
80+
http.StubResponse(200, bytes.NewBufferString(`
81+
{ "data": { "repository": {
82+
"id": "REPOID"
83+
} } }
84+
`))
85+
http.StubResponse(200, bytes.NewBufferString(`
86+
{ "data": { "createIssue": { "issue": {
87+
"URL": "https://github.com/OWNER/REPO/issues/12"
88+
} } } }
89+
`))
90+
91+
out := bytes.Buffer{}
92+
issueCreateCmd.SetOut(&out)
93+
94+
RootCmd.SetArgs([]string{"issue", "create", "-m", "hello", "-m", "ab", "-m", "cd"})
95+
_, err := RootCmd.ExecuteC()
96+
if err != nil {
97+
t.Errorf("error running command `issue create`: %v", err)
98+
}
99+
100+
bodyBytes, _ := ioutil.ReadAll(http.Requests[1].Body)
101+
reqBody := struct {
102+
Variables struct {
103+
Input struct {
104+
RepositoryID string
105+
Title string
106+
Body string
107+
}
108+
}
109+
}{}
110+
json.Unmarshal(bodyBytes, &reqBody)
111+
112+
eq(t, reqBody.Variables.Input.RepositoryID, "REPOID")
113+
eq(t, reqBody.Variables.Input.Title, "hello")
114+
eq(t, reqBody.Variables.Input.Body, "ab\n\ncd")
115+
116+
eq(t, out.String(), "https://github.com/OWNER/REPO/issues/12\n")
117+
}

go.sum

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc
55
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
66
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
77
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
8-
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
98
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
109
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1110
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -37,10 +36,7 @@ github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb6
3736
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
3837
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
3938
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
40-
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
4139
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
42-
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
43-
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
4440
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
4541
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
4642
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=

test/fixtures/issueList.json

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"data": {
3+
"assigned": {
4+
"issues": {
5+
"edges": [
6+
{
7+
"node": {
8+
"number": 9,
9+
"title": "corey thinks squash tastes bad"
10+
}
11+
},
12+
{
13+
"node": {
14+
"number": 10,
15+
"title": "broccoli is a superfood"
16+
}
17+
}
18+
]
19+
}
20+
},
21+
"mentioned": {
22+
"issues": {
23+
"edges": [
24+
{
25+
"node": {
26+
"number": 8,
27+
"title": "rabbits eat carrots"
28+
}
29+
},
30+
{
31+
"node": {
32+
"number": 11,
33+
"title": "swiss chard is neutral"
34+
}
35+
}
36+
]
37+
}
38+
},
39+
"recent": {
40+
"issues": {
41+
"edges": []
42+
}
43+
},
44+
45+
"pageInfo": { "hasNextPage": false }
46+
}
47+
}

0 commit comments

Comments
 (0)