Merge pull request #53 from github/issue-status-view
Add `issue status`, `issue create`
This commit is contained in:
commit
528ea6eee0
7 changed files with 257 additions and 18 deletions
|
|
@ -33,6 +33,7 @@ type IssuesPayload struct {
|
|||
type Issue struct {
|
||||
Number int
|
||||
Title string
|
||||
URL string
|
||||
}
|
||||
|
||||
func Issues(client *Client, ghRepo Repo, currentUsername string) (*IssuesPayload, error) {
|
||||
|
|
|
|||
40
api/queries_issue.go
Normal file
40
api/queries_issue.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package api
|
||||
|
||||
func IssueCreate(client *Client, ghRepo Repo, params map[string]interface{}) (*Issue, error) {
|
||||
repoID, err := GitHubRepoId(client, ghRepo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := `
|
||||
mutation CreateIssue($input: CreateIssueInput!) {
|
||||
createIssue(input: $input) {
|
||||
issue {
|
||||
url
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
inputParams := map[string]interface{}{
|
||||
"repositoryId": repoID,
|
||||
}
|
||||
for key, val := range params {
|
||||
inputParams[key] = val
|
||||
}
|
||||
variables := map[string]interface{}{
|
||||
"input": inputParams,
|
||||
}
|
||||
|
||||
result := struct {
|
||||
CreateIssue struct {
|
||||
Issue Issue
|
||||
}
|
||||
}{}
|
||||
|
||||
err = client.GraphQL(query, variables, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result.CreateIssue.Issue, nil
|
||||
}
|
||||
31
api/queries_repo.go
Normal file
31
api/queries_repo.go
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
package api
|
||||
|
||||
import "fmt"
|
||||
|
||||
func GitHubRepoId(client *Client, ghRepo Repo) (string, error) {
|
||||
owner := ghRepo.RepoOwner()
|
||||
repo := ghRepo.RepoName()
|
||||
|
||||
query := `
|
||||
query FindRepoID($owner:String!, $name:String!) {
|
||||
repository(owner:$owner, name:$name) {
|
||||
id
|
||||
}
|
||||
}`
|
||||
variables := map[string]interface{}{
|
||||
"owner": owner,
|
||||
"name": repo,
|
||||
}
|
||||
|
||||
result := struct {
|
||||
Repository struct {
|
||||
Id string
|
||||
}
|
||||
}{}
|
||||
err := client.GraphQL(query, variables, &result)
|
||||
if err != nil || result.Repository.Id == "" {
|
||||
return "", fmt.Errorf("failed to determine GH repo ID: %s", err)
|
||||
}
|
||||
|
||||
return result.Repository.Id, nil
|
||||
}
|
||||
106
command/issue.go
106
command/issue.go
|
|
@ -2,39 +2,46 @@ package command
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/github/gh-cli/api"
|
||||
"github.com/github/gh-cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
func init() {
|
||||
var issueCmd = &cobra.Command{
|
||||
Use: "issue",
|
||||
Short: "Work with GitHub issues",
|
||||
Long: `This command allows you to work with issues.`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("%+v is not a valid issue command", args)
|
||||
},
|
||||
}
|
||||
|
||||
RootCmd.AddCommand(issueCmd)
|
||||
issueCmd.AddCommand(
|
||||
&cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Display issue status",
|
||||
Short: "Show status of relevant issues",
|
||||
RunE: issueList,
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "view [issue-number]",
|
||||
Use: "view <issue-number>",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Short: "Open a issue in the browser",
|
||||
Short: "Open an issue in the browser",
|
||||
RunE: issueView,
|
||||
},
|
||||
)
|
||||
issueCmd.AddCommand(issueCreateCmd)
|
||||
issueCreateCmd.Flags().StringArrayP("message", "m", nil, "set title and body")
|
||||
issueCreateCmd.Flags().BoolP("web", "w", false, "open the web browser to create an issue")
|
||||
}
|
||||
|
||||
RootCmd.AddCommand(issueCmd)
|
||||
var issueCmd = &cobra.Command{
|
||||
Use: "issue",
|
||||
Short: "Work with GitHub issues",
|
||||
Long: `Helps you work with issues.`,
|
||||
}
|
||||
var issueCreateCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create a new issue",
|
||||
RunE: issueCreate,
|
||||
}
|
||||
|
||||
func issueList(cmd *cobra.Command, args []string) error {
|
||||
|
|
@ -107,6 +114,77 @@ func issueView(cmd *cobra.Command, args []string) error {
|
|||
return utils.OpenInBrowser(openURL)
|
||||
}
|
||||
|
||||
func issueCreate(cmd *cobra.Command, args []string) error {
|
||||
ctx := contextForCommand(cmd)
|
||||
|
||||
baseRepo, err := ctx.BaseRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isWeb, err := cmd.Flags().GetBool("web"); err == nil && isWeb {
|
||||
// TODO: move URL generation into GitHubRepository
|
||||
openURL := fmt.Sprintf("https://github.com/%s/%s/issues/new", baseRepo.RepoOwner(), baseRepo.RepoName())
|
||||
// TODO: figure out how to stub this in tests
|
||||
if stat, err := os.Stat(".github/ISSUE_TEMPLATE"); err == nil && stat.IsDir() {
|
||||
openURL += "/choose"
|
||||
}
|
||||
return utils.OpenInBrowser(openURL)
|
||||
}
|
||||
|
||||
var title string
|
||||
var body string
|
||||
|
||||
message, err := cmd.Flags().GetStringArray("message")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiClient, err := apiClientForContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(message) > 0 {
|
||||
title = message[0]
|
||||
body = strings.Join(message[1:], "\n\n")
|
||||
} else {
|
||||
// TODO: open the text editor for issue title & body
|
||||
input := os.Stdin
|
||||
if terminal.IsTerminal(int(input.Fd())) {
|
||||
cmd.Println("Enter the issue title and body; press Enter + Ctrl-D when done:")
|
||||
}
|
||||
inputBytes, err := ioutil.ReadAll(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parts := strings.SplitN(string(inputBytes), "\n\n", 2)
|
||||
if len(parts) > 0 {
|
||||
title = parts[0]
|
||||
}
|
||||
if len(parts) > 1 {
|
||||
body = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
if title == "" {
|
||||
return fmt.Errorf("aborting due to empty title")
|
||||
}
|
||||
params := map[string]interface{}{
|
||||
"title": title,
|
||||
"body": body,
|
||||
}
|
||||
|
||||
newIssue, err := api.IssueCreate(apiClient, baseRepo, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(cmd.OutOrStdout(), newIssue.URL)
|
||||
return nil
|
||||
}
|
||||
|
||||
func printIssues(issues ...api.Issue) {
|
||||
for _, issue := range issues {
|
||||
fmt.Printf(" #%d %s\n", issue.Number, truncate(70, issue.Title))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
|
|
@ -69,3 +72,46 @@ func TestIssueView(t *testing.T) {
|
|||
t.Errorf("got: %q", url)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueCreate(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": {
|
||||
"id": "REPOID"
|
||||
} } }
|
||||
`))
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "createIssue": { "issue": {
|
||||
"URL": "https://github.com/OWNER/REPO/issues/12"
|
||||
} } } }
|
||||
`))
|
||||
|
||||
out := bytes.Buffer{}
|
||||
issueCreateCmd.SetOut(&out)
|
||||
|
||||
RootCmd.SetArgs([]string{"issue", "create", "-m", "hello", "-m", "ab", "-m", "cd"})
|
||||
_, err := RootCmd.ExecuteC()
|
||||
if err != nil {
|
||||
t.Errorf("error running command `issue create`: %v", err)
|
||||
}
|
||||
|
||||
bodyBytes, _ := ioutil.ReadAll(http.Requests[1].Body)
|
||||
reqBody := struct {
|
||||
Variables struct {
|
||||
Input struct {
|
||||
RepositoryID string
|
||||
Title string
|
||||
Body string
|
||||
}
|
||||
}
|
||||
}{}
|
||||
json.Unmarshal(bodyBytes, &reqBody)
|
||||
|
||||
eq(t, reqBody.Variables.Input.RepositoryID, "REPOID")
|
||||
eq(t, reqBody.Variables.Input.Title, "hello")
|
||||
eq(t, reqBody.Variables.Input.Body, "ab\n\ncd")
|
||||
|
||||
eq(t, out.String(), "https://github.com/OWNER/REPO/issues/12\n")
|
||||
}
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -5,7 +5,6 @@ github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc
|
|||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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
|
|||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
|
||||
|
|
|
|||
47
test/fixtures/issueList.json
vendored
Normal file
47
test/fixtures/issueList.json
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"data": {
|
||||
"assigned": {
|
||||
"issues": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"number": 9,
|
||||
"title": "corey thinks squash tastes bad"
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"number": 10,
|
||||
"title": "broccoli is a superfood"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"mentioned": {
|
||||
"issues": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"number": 8,
|
||||
"title": "rabbits eat carrots"
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"number": 11,
|
||||
"title": "swiss chard is neutral"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"recent": {
|
||||
"issues": {
|
||||
"edges": []
|
||||
}
|
||||
},
|
||||
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue