diff --git a/api/client.go b/api/client.go index 0e45c31f1..1dcf7c0ce 100644 --- a/api/client.go +++ b/api/client.go @@ -21,7 +21,7 @@ type graphQLResponse struct { } /* -graphQL usage +GraphQL: Declared as an external variable so it can be mocked in tests type repoResponse struct { Repository struct { @@ -45,7 +45,7 @@ if err != nil { fmt.Printf("%+v\n", resp) */ -func graphQL(query string, variables map[string]string, data interface{}) error { +var GraphQL = func(query string, variables map[string]string, data interface{}) error { url := "https://api.github.com/graphql" reqBody, err := json.Marshal(map[string]interface{}{"query": query, "variables": variables}) if err != nil { diff --git a/api/queries.go b/api/queries.go index a4c023375..cc7a5e512 100644 --- a/api/queries.go +++ b/api/queries.go @@ -98,7 +98,7 @@ func PullRequests() (PullRequestsPayload, error) { } var resp response - err := graphQL(query, variables, &resp) + err := GraphQL(query, variables, &resp) if err != nil { return PullRequestsPayload{}, err } @@ -133,7 +133,6 @@ func project() github.Project { if error != nil { panic(error) } - for _, remote := range remotes { if project, error := remote.Project(); error == nil { return *project diff --git a/command/pr.go b/command/pr.go index 035eb3646..596d9e988 100644 --- a/command/pr.go +++ b/command/pr.go @@ -12,7 +12,6 @@ import ( "github.com/github/gh-cli/git" "github.com/github/gh-cli/github" "github.com/github/gh-cli/utils" - "github.com/logrusorgru/aurora" "github.com/spf13/cobra" ) @@ -53,7 +52,7 @@ func prList(cmd *cobra.Command, args []string) error { if prPayload.CurrentPR != nil { printPrs(*prPayload.CurrentPR) } else { - message := fmt.Sprintf(" There is no pull request associated with %s", aurora.Cyan("["+currentBranch()+"]")) + message := fmt.Sprintf(" There is no pull request associated with %s", utils.Cyan("["+currentBranch()+"]")) printMessage(message) } fmt.Println() @@ -102,16 +101,16 @@ 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), aurora.Cyan(pr.HeadRefName)) + fmt.Printf(" #%d %s %s\n", pr.Number, truncateTitle(pr.Title), utils.Cyan("["+pr.HeadRefName+"]")) } } func printHeader(s string) { - fmt.Println(aurora.Bold(s)) + fmt.Println(utils.Bold(s)) } func printMessage(s string) { - fmt.Println(aurora.Gray(8, s)) + fmt.Println(utils.Gray(s)) } func truncateTitle(title string) string { diff --git a/command/pr_test.go b/command/pr_test.go new file mode 100644 index 000000000..ced88b3b8 --- /dev/null +++ b/command/pr_test.go @@ -0,0 +1,34 @@ +package command + +import ( + "regexp" + "testing" + + "github.com/github/gh-cli/test" +) + +func TestPRList(t *testing.T) { + teardown := test.MockGraphQLResponse("test/fixtures/pr.json") + defer teardown() + + gitRepo := test.UseTempGitRepo() + defer gitRepo.TearDown() + + output, err := test.RunCommand(RootCmd, "pr list") + if err != nil { + t.Errorf("error running command `pr list`: %v", err) + } + + expectedPrs := []*regexp.Regexp{ + regexp.MustCompile(`#8.*\[strawberries\]`), + regexp.MustCompile(`#9.*\[apples\]`), + regexp.MustCompile(`#10.*\[blueberries\]`), + regexp.MustCompile(`#11.*\[figs\]`), + } + + for _, r := range expectedPrs { + if !r.MatchString(output) { + t.Errorf("output did not match regexp /%s/", r) + } + } +} diff --git a/go.mod b/go.mod index a9718f209..dca837862 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,8 @@ go 1.13 require ( github.com/BurntSushi/toml v0.3.1 + github.com/gookit/color v1.2.0 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 - github.com/logrusorgru/aurora v0.0.0-20190803045625-94edacc10f9b github.com/mattn/go-colorable v0.1.2 github.com/mattn/go-isatty v0.0.9 github.com/mitchellh/go-homedir v1.1.0 diff --git a/go.sum b/go.sum index 1798ef0ec..4086387eb 100644 --- a/go.sum +++ b/go.sum @@ -5,15 +5,17 @@ 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= +github.com/gookit/color v1.2.0 h1:lHA77Kuyi5JpBnA9ESvwkY+nanLjRZ0mHbWQXRYk2Lk= +github.com/gookit/color v1.2.0/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/logrusorgru/aurora v0.0.0-20190803045625-94edacc10f9b h1:PMbSa9CgaiQR9NLlUTwKi+7aeLl3GG5JX5ERJxfQ3IE= -github.com/logrusorgru/aurora v0.0.0-20190803045625-94edacc10f9b/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -24,6 +26,7 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= @@ -34,7 +37,10 @@ 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/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= diff --git a/test/fixtures/pr.json b/test/fixtures/pr.json new file mode 100644 index 000000000..d3a7e89f2 --- /dev/null +++ b/test/fixtures/pr.json @@ -0,0 +1,50 @@ +{ + "repository": { + "pullRequests": { + "edges": [ + { + "node": { + "number": 10, + "title": "Blueberries are a good fruit", + "url": "https://github.com/github/gh-cli/pull/10", + "headRefName": "[blueberries]" + } + } + ] + } + }, + "viewerCreated": { + "edges": [ + { + "node": { + "number": 8, + "title": "Strawberries are not actually berries", + "url": "https://github.com/github/gh-cli/pull/8", + "headRefName": "[strawberries]" + } + } + ], + "pageInfo": { "hasNextPage": false } + }, + "reviewRequested": { + "edges": [ + { + "node": { + "number": 9, + "title": "Apples are tasty", + "url": "https://github.com/github/gh-cli/pull/9", + "headRefName": "[apples]" + } + }, + { + "node": { + "number": 11, + "title": "Figs are my favorite", + "url": "https://github.com/github/gh-cli/pull/1", + "headRefName": "[figs]" + } + } + ], + "pageInfo": { "hasNextPage": false } + } +} diff --git a/test/fixtures/test.git/HEAD b/test/fixtures/test.git/HEAD new file mode 100644 index 000000000..cb089cd89 --- /dev/null +++ b/test/fixtures/test.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/test/fixtures/test.git/config b/test/fixtures/test.git/config new file mode 100644 index 000000000..4f7d452b3 --- /dev/null +++ b/test/fixtures/test.git/config @@ -0,0 +1,6 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = true + ignorecase = true + precomposeunicode = false diff --git a/test/fixtures/test.git/info/exclude b/test/fixtures/test.git/info/exclude new file mode 100644 index 000000000..a5196d1be --- /dev/null +++ b/test/fixtures/test.git/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/test/fixtures/test.git/objects/08/f4b7b6513dffc6245857e497cfd6101dc47818 b/test/fixtures/test.git/objects/08/f4b7b6513dffc6245857e497cfd6101dc47818 new file mode 100644 index 000000000..37162913a --- /dev/null +++ b/test/fixtures/test.git/objects/08/f4b7b6513dffc6245857e497cfd6101dc47818 @@ -0,0 +1,3 @@ +xM +0F]seb~nxcMi)^ߠ7p>xb9U7ub{bgٴă#h8>4o +'yȜ],ڄFk-jUW*zI); \ No newline at end of file diff --git a/test/fixtures/test.git/objects/8a/1cdac440b4a3c44b988e300758a903a9866905 b/test/fixtures/test.git/objects/8a/1cdac440b4a3c44b988e300758a903a9866905 new file mode 100644 index 000000000..27427c268 Binary files /dev/null and b/test/fixtures/test.git/objects/8a/1cdac440b4a3c44b988e300758a903a9866905 differ diff --git a/test/fixtures/test.git/objects/9b/5a719a3d76ac9dc2fa635d9b1f34fd73994c06 b/test/fixtures/test.git/objects/9b/5a719a3d76ac9dc2fa635d9b1f34fd73994c06 new file mode 100644 index 000000000..7a03f1760 --- /dev/null +++ b/test/fixtures/test.git/objects/9b/5a719a3d76ac9dc2fa635d9b1f34fd73994c06 @@ -0,0 +1,3 @@ +xK +1D]}3$qBϐdq&#^s7UPūXqh/ZM w*cbcG3YIk g +6-Us4i61F#Y,K0iG|=~b7 $b KdD13eKnZC՟{Nm \ No newline at end of file diff --git a/test/fixtures/test.git/objects/9d/aeafb9864cf43055ae93beb0afd6c7d144bfa4 b/test/fixtures/test.git/objects/9d/aeafb9864cf43055ae93beb0afd6c7d144bfa4 new file mode 100644 index 000000000..4667dcf6f Binary files /dev/null and b/test/fixtures/test.git/objects/9d/aeafb9864cf43055ae93beb0afd6c7d144bfa4 differ diff --git a/test/fixtures/test.git/objects/ca/93b49848670d03b3968c8a481eca55f5fb2150 b/test/fixtures/test.git/objects/ca/93b49848670d03b3968c8a481eca55f5fb2150 new file mode 100644 index 000000000..4d0f210ec Binary files /dev/null and b/test/fixtures/test.git/objects/ca/93b49848670d03b3968c8a481eca55f5fb2150 differ diff --git a/test/fixtures/test.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 b/test/fixtures/test.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 new file mode 100644 index 000000000..711223894 Binary files /dev/null and b/test/fixtures/test.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 differ diff --git a/test/fixtures/test.git/refs/heads/master b/test/fixtures/test.git/refs/heads/master new file mode 100644 index 000000000..fc54b05cb --- /dev/null +++ b/test/fixtures/test.git/refs/heads/master @@ -0,0 +1 @@ +9b5a719a3d76ac9dc2fa635d9b1f34fd73994c06 diff --git a/test/helpers.go b/test/helpers.go new file mode 100644 index 000000000..ce2de88df --- /dev/null +++ b/test/helpers.go @@ -0,0 +1,130 @@ +package test + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/github/gh-cli/api" + "github.com/github/gh-cli/github" + "github.com/spf13/cobra" +) + +type TempGitRepo struct { + Remote string + TearDown func() +} + +func UseTempGitRepo() *TempGitRepo { + github.CreateTestConfigs("mario", "i-love-peach") + + pwd, _ := os.Getwd() + oldEnv := make(map[string]string) + overrideEnv := func(name, value string) { + oldEnv[name] = os.Getenv(name) + os.Setenv(name, value) + } + + remotePath := filepath.Join(pwd, "..", "test", "fixtures", "test.git") + home, err := ioutil.TempDir("", "test-repo") + if err != nil { + panic(err) + } + + overrideEnv("HOME", home) + overrideEnv("XDG_CONFIG_HOME", "") + overrideEnv("XDG_CONFIG_DIRS", "") + + targetPath := filepath.Join(home, "test.git") + cmd := exec.Command("git", "clone", remotePath, targetPath) + if output, err := cmd.Output(); err != nil { + panic(fmt.Errorf("error running %s\n%s\n%s", cmd, err, output)) + } + + if err = os.Chdir(targetPath); err != nil { + panic(err) + } + + // Our libs expect the origin to be a github url + cmd = exec.Command("git", "remote", "set-url", "origin", "https://github.com/github/FAKE-GITHUB-REPO-NAME") + if output, err := cmd.Output(); err != nil { + panic(fmt.Errorf("error running %s\n%s\n%s", cmd, err, output)) + } + + tearDown := func() { + if err := os.Chdir(pwd); err != nil { + panic(err) + } + for name, value := range oldEnv { + os.Setenv(name, value) + } + if err = os.RemoveAll(home); err != nil { + panic(err) + } + } + + return &TempGitRepo{Remote: remotePath, TearDown: tearDown} +} + +func MockGraphQLResponse(fixturePath string) (teardown func()) { + pwd, _ := os.Getwd() + fixturePath = filepath.Join(pwd, "..", fixturePath) + + originalGraphQL := api.GraphQL + api.GraphQL = func(query string, variables map[string]string, v interface{}) error { + contents, err := ioutil.ReadFile(fixturePath) + if err != nil { + return err + } + + json.Unmarshal(contents, &v) + if err != nil { + return err + } + + return nil + } + + return func() { + api.GraphQL = originalGraphQL + } +} + +func RunCommand(root *cobra.Command, s string) (string, error) { + var err error + output := captureOutput(func() { + root.SetArgs(strings.Split(s, " ")) + _, err = root.ExecuteC() + }) + if err != nil { + return "", err + } + return output, nil +} + +func captureOutput(f func()) string { + originalStdout := os.Stdout + defer func() { + os.Stdout = originalStdout + }() + + r, w, err := os.Pipe() + if err != nil { + panic("failed to pipe stdout") + } + os.Stdout = w + + f() + + w.Close() + out, err := ioutil.ReadAll(r) + if err != nil { + panic("failed to read captured input from stdout") + } + + return string(out) +} diff --git a/utils/color.go b/utils/color.go new file mode 100644 index 000000000..9b70f603d --- /dev/null +++ b/utils/color.go @@ -0,0 +1,43 @@ +package utils + +import "github.com/gookit/color" + +func Black(a ...interface{}) string { + return color.Black.Render(a...) +} + +func White(a ...interface{}) string { + return color.White.Render(a...) +} + +func Gray(a ...interface{}) string { + return color.Gray.Render(a...) +} + +func Red(a ...interface{}) string { + return color.Red.Render(a...) +} + +func Green(a ...interface{}) string { + return color.Green.Render(a...) +} + +func Yellow(a ...interface{}) string { + return color.Yellow.Render(a...) +} + +func Blue(a ...interface{}) string { + return color.Blue.Render(a...) +} + +func Magenta(a ...interface{}) string { + return color.Magenta.Render(a...) +} + +func Cyan(a ...interface{}) string { + return color.Cyan.Render(a...) +} + +func Bold(a ...interface{}) string { + return color.Bold.Render(a...) +}