Merge pull request #43 from github/gh-issue

Add `gh issue list` and `gh issue view`
This commit is contained in:
Corey Johnson 2019-11-06 10:09:11 -08:00 committed by GitHub
commit da334e283b
9 changed files with 403 additions and 39 deletions

View file

@ -2,6 +2,7 @@ package api
import (
"fmt"
"time"
)
type PullRequestsPayload struct {
@ -22,6 +23,108 @@ type Repo interface {
RepoOwner() string
}
type IssuesPayload struct {
Assigned []Issue
Mentioned []Issue
Recent []Issue
}
type Issue struct {
Number int
Title string
}
func Issues(client *Client, ghRepo Repo, currentUsername string) (*IssuesPayload, error) {
type issues struct {
Issues struct {
Edges []struct {
Node Issue
}
}
}
type response struct {
Assigned issues
Mentioned issues
Recent issues
}
query := `
fragment issue on Issue {
number
title
}
query($owner: String!, $repo: String!, $since: DateTime!, $viewer: String!, $per_page: Int = 10) {
assigned: repository(owner: $owner, name: $repo) {
issues(filterBy: {assignee: $viewer}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
edges {
node {
...issue
}
}
}
}
mentioned: repository(owner: $owner, name: $repo) {
issues(filterBy: {mentioned: $viewer}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
edges {
node {
...issue
}
}
}
}
recent: repository(owner: $owner, name: $repo) {
issues(filterBy: {since: $since}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
edges {
node {
...issue
}
}
}
}
}
`
owner := ghRepo.RepoOwner()
repo := ghRepo.RepoName()
since := time.Now().UTC().Add(time.Hour * -24).Format("2006-01-02T15:04:05-0700")
variables := map[string]interface{}{
"owner": owner,
"repo": repo,
"viewer": currentUsername,
"since": since,
}
var resp response
err := client.GraphQL(query, variables, &resp)
if err != nil {
return nil, err
}
var assigned []Issue
for _, edge := range resp.Assigned.Issues.Edges {
assigned = append(assigned, edge.Node)
}
var mentioned []Issue
for _, edge := range resp.Mentioned.Issues.Edges {
mentioned = append(mentioned, edge.Node)
}
var recent []Issue
for _, edge := range resp.Recent.Issues.Edges {
recent = append(recent, edge.Node)
}
payload := IssuesPayload{
assigned,
mentioned,
recent,
}
return &payload, nil
}
func PullRequests(client *Client, ghRepo Repo, currentBranch, currentUsername string) (*PullRequestsPayload, error) {
type edges struct {
Edges []struct {

114
command/issue.go Normal file
View file

@ -0,0 +1,114 @@
package command
import (
"fmt"
"strconv"
"github.com/github/gh-cli/api"
"github.com/github/gh-cli/utils"
"github.com/spf13/cobra"
)
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)
},
}
issueCmd.AddCommand(
&cobra.Command{
Use: "status",
Short: "Display issue status",
RunE: issueList,
},
&cobra.Command{
Use: "view [issue-number]",
Args: cobra.MinimumNArgs(1),
Short: "Open a issue in the browser",
RunE: issueView,
},
)
RootCmd.AddCommand(issueCmd)
}
func issueList(cmd *cobra.Command, args []string) error {
ctx := contextForCommand(cmd)
apiClient, err := apiClientForContext(ctx)
if err != nil {
return err
}
baseRepo, err := ctx.BaseRepo()
if err != nil {
return err
}
currentUser, err := ctx.AuthLogin()
if err != nil {
return err
}
issuePayload, err := api.Issues(apiClient, baseRepo, currentUser)
if err != nil {
return err
}
printHeader("Issues assigned to you")
if issuePayload.Assigned != nil {
printIssues(issuePayload.Assigned...)
} else {
message := fmt.Sprintf(" There are no issues assgined to you")
printMessage(message)
}
fmt.Println()
printHeader("Issues mentioning you")
if len(issuePayload.Mentioned) > 0 {
printIssues(issuePayload.Mentioned...)
} else {
printMessage(" There are no issues mentioning you")
}
fmt.Println()
printHeader("Recent issues")
if len(issuePayload.Recent) > 0 {
printIssues(issuePayload.Recent...)
} else {
printMessage(" There are no recent issues")
}
fmt.Println()
return nil
}
func issueView(cmd *cobra.Command, args []string) error {
ctx := contextForCommand(cmd)
baseRepo, err := ctx.BaseRepo()
if err != nil {
return err
}
var openURL string
if number, err := strconv.Atoi(args[0]); err == nil {
// TODO: move URL generation into GitHubRepository
openURL = fmt.Sprintf("https://github.com/%s/%s/issues/%d", baseRepo.RepoOwner(), baseRepo.RepoName(), number)
} else {
return fmt.Errorf("invalid issue number: '%s'", args[0])
}
fmt.Printf("Opening %s in your browser.\n", openURL)
return utils.OpenInBrowser(openURL)
}
func printIssues(issues ...api.Issue) {
for _, issue := range issues {
fmt.Printf(" #%d %s\n", issue.Number, truncateTitle(issue.Title, 70))
}
}

61
command/issue_test.go Normal file
View file

@ -0,0 +1,61 @@
package command
import (
"os"
"regexp"
"testing"
"github.com/github/gh-cli/test"
)
func TestIssueStatus(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
jsonFile, _ := os.Open("../test/fixtures/issueStatus.json")
defer jsonFile.Close()
http.StubResponse(200, jsonFile)
output, err := test.RunCommand(RootCmd, "issue status")
if err != nil {
t.Errorf("error running command `issue status`: %v", err)
}
expectedIssues := []*regexp.Regexp{
regexp.MustCompile(`#8.*carrots`),
regexp.MustCompile(`#9.*squash`),
regexp.MustCompile(`#10.*broccoli`),
regexp.MustCompile(`#11.*swiss chard`),
}
for _, r := range expectedIssues {
if !r.MatchString(output) {
t.Errorf("output did not match regexp /%s/", r)
}
}
}
func TestIssueView(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
jsonFile, _ := os.Open("../test/fixtures/issueView.json")
defer jsonFile.Close()
http.StubResponse(200, jsonFile)
teardown, callCount := mockOpenInBrowser()
defer teardown()
output, err := test.RunCommand(RootCmd, "issue view 8")
if err != nil {
t.Errorf("error running command `issue view`: %v", err)
}
if output == "" {
t.Errorf("command output expected got an empty string")
}
if *callCount != 1 {
t.Errorf("OpenInBrowser should be called 1 time but was called %d time(s)", *callCount)
}
}

View file

@ -129,7 +129,7 @@ func prView(cmd *cobra.Command, args []string) error {
func printPrs(prs ...api.PullRequest) {
for _, pr := range prs {
fmt.Printf(" #%d %s %s\n", pr.Number, truncateTitle(pr.Title), utils.Cyan("["+pr.HeadRefName+"]"))
fmt.Printf(" #%d %s %s\n", pr.Number, truncateTitle(pr.Title, 50), utils.Cyan("["+pr.HeadRefName+"]"))
}
}
@ -141,9 +141,7 @@ func printMessage(s string) {
fmt.Println(utils.Gray(s))
}
func truncateTitle(title string) string {
const maxLength = 50
func truncateTitle(title string, maxLength int) string {
if len(title) > maxLength {
return title[0:maxLength-3] + "..."
}

View file

@ -5,29 +5,9 @@ import (
"regexp"
"testing"
"github.com/github/gh-cli/api"
"github.com/github/gh-cli/context"
"github.com/github/gh-cli/test"
"github.com/github/gh-cli/utils"
)
func initBlankContext(repo, branch string) {
initContext = func() context.Context {
ctx := context.NewBlank()
ctx.SetBaseRepo(repo)
ctx.SetBranch(branch)
return ctx
}
}
func initFakeHTTP() *api.FakeHTTP {
http := &api.FakeHTTP{}
apiClientForContext = func(context.Context) (*api.Client, error) {
return api.NewClient(api.ReplaceTripper(http)), nil
}
return http
}
func TestPRList(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
@ -114,18 +94,3 @@ func TestPRView_NoActiveBranch(t *testing.T) {
t.Errorf("OpenInBrowser should be called once but was called %d time(s)", *callCount)
}
}
func mockOpenInBrowser() (func(), *int) {
callCount := 0
originalOpenInBrowser := utils.OpenInBrowser
teardown := func() {
utils.OpenInBrowser = originalOpenInBrowser
}
utils.OpenInBrowser = func(_ string) error {
callCount++
return nil
}
return teardown, &callCount
}

39
command/testing.go Normal file
View file

@ -0,0 +1,39 @@
package command
import (
"github.com/github/gh-cli/api"
"github.com/github/gh-cli/context"
"github.com/github/gh-cli/utils"
)
func initBlankContext(repo, branch string) {
initContext = func() context.Context {
ctx := context.NewBlank()
ctx.SetBaseRepo(repo)
ctx.SetBranch(branch)
return ctx
}
}
func initFakeHTTP() *api.FakeHTTP {
http := &api.FakeHTTP{}
apiClientForContext = func(context.Context) (*api.Client, error) {
return api.NewClient(api.ReplaceTripper(http)), nil
}
return http
}
func mockOpenInBrowser() (func(), *int) {
callCount := 0
originalOpenInBrowser := utils.OpenInBrowser
teardown := func() {
utils.OpenInBrowser = originalOpenInBrowser
}
utils.OpenInBrowser = func(_ string) error {
callCount++
return nil
}
return teardown, &callCount
}

47
test/fixtures/issueStatus.json vendored Normal file
View 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 }
}
}

36
test/fixtures/issueView.json vendored Normal file
View file

@ -0,0 +1,36 @@
{
"data": {
"repository": {
"issues": {
"edges": [
{
"node": {
"number": 8,
"title": "rabbits eat carrots",
"url": "https://github.com/github/gh-cli/pull/10"
}
},
{
"node": {
"number": 9,
"title": "corey thinks squash tastes bad"
}
},
{
"node": {
"number": 10,
"title": "broccoli is a superfood"
}
},
{
"node": {
"number": 11,
"title": "swiss chard is neutral"
}
}
]
}
},
"pageInfo": { "hasNextPage": false }
}
}

View file

@ -71,6 +71,7 @@ func RunCommand(root *cobra.Command, s string) (string, error) {
root.SetArgs(strings.Split(s, " "))
_, err = root.ExecuteC()
})
if err != nil {
return "", err
}