Add gh issue list and gh issue view ISSUE_NUMBER
This commit is contained in:
parent
d658a8f407
commit
aea7ae8efd
8 changed files with 303 additions and 39 deletions
117
command/issue.go
Normal file
117
command/issue.go
Normal file
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
61
command/issue_test.go
Normal file
61
command/issue_test.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -129,7 +129,7 @@ func prView(cmd *cobra.Command, args []string) error {
|
||||||
|
|
||||||
func printPrs(prs ...api.PullRequest) {
|
func printPrs(prs ...api.PullRequest) {
|
||||||
for _, pr := range prs {
|
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))
|
fmt.Println(utils.Gray(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
func truncateTitle(title string) string {
|
func truncateTitle(title string, maxLength int) string {
|
||||||
const maxLength = 50
|
|
||||||
|
|
||||||
if len(title) > maxLength {
|
if len(title) > maxLength {
|
||||||
return title[0:maxLength-3] + "..."
|
return title[0:maxLength-3] + "..."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,29 +5,9 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/github/gh-cli/api"
|
|
||||||
"github.com/github/gh-cli/context"
|
|
||||||
"github.com/github/gh-cli/test"
|
"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) {
|
func TestPRList(t *testing.T) {
|
||||||
initBlankContext("OWNER/REPO", "master")
|
initBlankContext("OWNER/REPO", "master")
|
||||||
http := initFakeHTTP()
|
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)
|
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
39
command/testing.go
Normal 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/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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
36
test/fixtures/issueView.json
vendored
Normal file
36
test/fixtures/issueView.json
vendored
Normal 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -71,6 +71,7 @@ func RunCommand(root *cobra.Command, s string) (string, error) {
|
||||||
root.SetArgs(strings.Split(s, " "))
|
root.SetArgs(strings.Split(s, " "))
|
||||||
_, err = root.ExecuteC()
|
_, err = root.ExecuteC()
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue