From aea7ae8efd7b3c07c01e41896c26557385477947 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Thu, 31 Oct 2019 11:02:27 -0700 Subject: [PATCH] Add `gh issue list` and `gh issue view ISSUE_NUMBER` --- command/issue.go | 117 +++++++++++++++++++++++++++++++++++ command/issue_test.go | 61 ++++++++++++++++++ command/pr.go | 6 +- command/pr_test.go | 35 ----------- command/testing.go | 39 ++++++++++++ test/fixtures/issueList.json | 47 ++++++++++++++ test/fixtures/issueView.json | 36 +++++++++++ test/helpers.go | 1 + 8 files changed, 303 insertions(+), 39 deletions(-) create mode 100644 command/issue.go create mode 100644 command/issue_test.go create mode 100644 command/testing.go create mode 100644 test/fixtures/issueList.json create mode 100644 test/fixtures/issueView.json diff --git a/command/issue.go b/command/issue.go new file mode 100644 index 000000000..e64d8d806 --- /dev/null +++ b/command/issue.go @@ -0,0 +1,117 @@ +package command + +import ( + "fmt" + "strconv" + + "github.com/github/gh-cli/api" + "github.com/github/gh-cli/utils" + "github.com/spf13/cobra" +) + +func init() { + RootCmd.AddCommand(issueCmd) + issueCmd.AddCommand( + &cobra.Command{ + Use: "list", + Short: "List issues", + RunE: issueList, + }, + &cobra.Command{ + Use: "view issue-number", + Args: cobra.MinimumNArgs(1), + Short: "Open a issue in the browser", + RunE: issueView, + }, + ) +} + +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) + }, +} + +func issueList(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true + + 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 { + cmd.SilenceUsage = true + + 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)) + } +} diff --git a/command/issue_test.go b/command/issue_test.go new file mode 100644 index 000000000..69312cac7 --- /dev/null +++ b/command/issue_test.go @@ -0,0 +1,61 @@ +package command + +import ( + "os" + "regexp" + "testing" + + "github.com/github/gh-cli/test" +) + +func TestIssueList(t *testing.T) { + initBlankContext("OWNER/REPO", "master") + http := initFakeHTTP() + + jsonFile, _ := os.Open("../test/fixtures/issueList.json") + defer jsonFile.Close() + http.StubResponse(200, jsonFile) + + output, err := test.RunCommand(RootCmd, "issue list") + if err != nil { + t.Errorf("error running command `issue list`: %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) + } +} diff --git a/command/pr.go b/command/pr.go index f1dfe8fb0..182522f29 100644 --- a/command/pr.go +++ b/command/pr.go @@ -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] + "..." } diff --git a/command/pr_test.go b/command/pr_test.go index e4a5122d7..5ac21b9bd 100644 --- a/command/pr_test.go +++ b/command/pr_test.go @@ -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 -} diff --git a/command/testing.go b/command/testing.go new file mode 100644 index 000000000..a6ccf4807 --- /dev/null +++ b/command/testing.go @@ -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 +} diff --git a/test/fixtures/issueList.json b/test/fixtures/issueList.json new file mode 100644 index 000000000..784c0cc8f --- /dev/null +++ b/test/fixtures/issueList.json @@ -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 } + } +} diff --git a/test/fixtures/issueView.json b/test/fixtures/issueView.json new file mode 100644 index 000000000..c9b0cb1b6 --- /dev/null +++ b/test/fixtures/issueView.json @@ -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 } + } +} diff --git a/test/helpers.go b/test/helpers.go index d9276565c..162befa56 100644 --- a/test/helpers.go +++ b/test/helpers.go @@ -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 }