diff --git a/api/client.go b/api/client.go index a7ea0636c..a351f564a 100644 --- a/api/client.go +++ b/api/client.go @@ -20,7 +20,7 @@ type graphQLResponse struct { } /* -graphQL usage +GraphQL: Declared as an external variable so it can be mocked in tests type repoResponse struct { Repository struct { @@ -44,7 +44,7 @@ if err != nil { fmt.Printf("%+v\n", resp) */ -func graphQL(query string, variables map[string]string, v 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 { @@ -81,29 +81,31 @@ func graphQL(query string, variables map[string]string, v interface{}) error { } debugResponse(resp, string(body)) - return handleResponse(resp, body, v) + return handleResponse(resp, body, data) } -func handleResponse(resp *http.Response, body []byte, v interface{}) error { +func handleResponse(resp *http.Response, body []byte, data interface{}) error { success := resp.StatusCode >= 200 && resp.StatusCode < 300 - if success { - gr := &graphQLResponse{Data: v} - err := json.Unmarshal(body, &gr) - if err != nil { - return err - } - if len(gr.Errors) > 0 { - errorMessages := gr.Errors[0].Message - for _, e := range gr.Errors[1:] { - errorMessages += ", " + e.Message - } - return fmt.Errorf("graphql error: '%s'", errorMessages) - } - return nil + if !success { + return handleHTTPError(resp, body) } - return handleHTTPError(resp, body) + gr := &graphQLResponse{Data: data} + err := json.Unmarshal(body, &gr) + if err != nil { + return err + } + + if len(gr.Errors) > 0 { + errorMessages := gr.Errors[0].Message + for _, e := range gr.Errors[1:] { + errorMessages += ", " + e.Message + } + return fmt.Errorf("graphql error: '%s'", errorMessages) + } + return nil + } func handleHTTPError(resp *http.Response, body []byte) error { diff --git a/api/queries.go b/api/queries.go index a72b75189..187d2c860 100644 --- a/api/queries.go +++ b/api/queries.go @@ -39,45 +39,45 @@ func PullRequests() (PullRequestsPayload, error) { } query := ` - fragment pr on PullRequest { - number - title - url - headRefName - } + fragment pr on PullRequest { + number + title + url + headRefName + } - query($owner: String!, $repo: String!, $headRefName: String!, $viewerQuery: String!, $reviewerQuery: String!, $per_page: Int = 10) { - repository(owner: $owner, name: $repo) { - pullRequests(headRefName: $headRefName, first: 1) { - edges { - node { - ...pr - } - } - } - } - viewerCreated: search(query: $viewerQuery, type: ISSUE, first: $per_page) { - edges { - node { - ...pr - } - } - pageInfo { - hasNextPage - } - } - reviewRequested: search(query: $reviewerQuery, type: ISSUE, first: $per_page) { - edges { - node { - ...pr - } - } - pageInfo { - hasNextPage - } - } - } - ` + query($owner: String!, $repo: String!, $headRefName: String!, $viewerQuery: String!, $reviewerQuery: String!, $per_page: Int = 10) { + repository(owner: $owner, name: $repo) { + pullRequests(headRefName: $headRefName, first: 1) { + edges { + node { + ...pr + } + } + } + } + viewerCreated: search(query: $viewerQuery, type: ISSUE, first: $per_page) { + edges { + node { + ...pr + } + } + pageInfo { + hasNextPage + } + } + reviewRequested: search(query: $reviewerQuery, type: ISSUE, first: $per_page) { + edges { + node { + ...pr + } + } + pageInfo { + hasNextPage + } + } + } + ` ctx, err := context.GetContext() if err != nil { @@ -103,7 +103,7 @@ func PullRequests() (PullRequestsPayload, error) { } var resp response - err = graphQL(query, variables, &resp) + err = GraphQL(query, variables, &resp) if err != nil { return PullRequestsPayload{}, err } diff --git a/command/pr.go b/command/pr.go index d1caadeae..596d9e988 100644 --- a/command/pr.go +++ b/command/pr.go @@ -2,14 +2,33 @@ package command import ( "fmt" + "net/url" + "os" + "os/exec" + "strconv" + "strings" "github.com/github/gh-cli/api" + "github.com/github/gh-cli/git" + "github.com/github/gh-cli/github" + "github.com/github/gh-cli/utils" "github.com/spf13/cobra" ) func init() { RootCmd.AddCommand(prCmd) - prCmd.AddCommand(prListCmd) + prCmd.AddCommand( + &cobra.Command{ + Use: "list", + Short: "List pull requests", + RunE: prList, + }, + &cobra.Command{ + Use: "view [pr-number]", + Short: "Open a pull request in the browser", + RunE: prView, + }, + ) } var prCmd = &cobra.Command{ @@ -18,44 +37,134 @@ var prCmd = &cobra.Command{ Long: `This command allows you to work with pull requests.`, Args: cobra.MinimumNArgs(1), - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("pr") + RunE: func(cmd *cobra.Command, args []string) error { + return fmt.Errorf("%+v is not a valid PR command", args) }, } -var prListCmd = &cobra.Command{ - Use: "list", - Short: "List pull requests", - Run: func(cmd *cobra.Command, args []string) { - err := ExecutePr() - if err != nil { - panic(err) // In the future this should handle the error better, but for now panic seems like a valid reaction - } - }, -} - -func ExecutePr() error { +func prList(cmd *cobra.Command, args []string) error { prPayload, err := api.PullRequests() if err != nil { return err } - fmt.Printf("Current Pr\n") + printHeader("Current branch") if prPayload.CurrentPR != nil { - printPr(*prPayload.CurrentPR) + printPrs(*prPayload.CurrentPR) + } else { + message := fmt.Sprintf(" There is no pull request associated with %s", utils.Cyan("["+currentBranch()+"]")) + printMessage(message) } - fmt.Printf("Your Prs\n") - for _, pr := range prPayload.ViewerCreated { - printPr(pr) + fmt.Println() + + printHeader("Created by you") + if len(prPayload.ViewerCreated) > 0 { + printPrs(prPayload.ViewerCreated...) + } else { + printMessage(" You have no open pull requests") } - fmt.Printf("Prs you need to review\n") - for _, pr := range prPayload.ReviewRequested { - printPr(pr) + fmt.Println() + + printHeader("Requesting a code review from you") + if len(prPayload.ReviewRequested) > 0 { + printPrs(prPayload.ReviewRequested...) + } else { + printMessage(" You have no pull requests to review") } + fmt.Println() return nil } -func printPr(pr api.PullRequest) { - fmt.Printf(" #%d %s [%s]\n", pr.Number, pr.Title, pr.HeadRefName) +func prView(cmd *cobra.Command, args []string) error { + project := project() + + var openURL string + if len(args) > 0 { + if prNumber, err := strconv.Atoi(args[0]); err == nil { + openURL = project.WebURL("", "", fmt.Sprintf("pull/%d", prNumber)) + } else { + return fmt.Errorf("invalid pull request number: '%s'", args[0]) + } + } else { + prPayload, err := api.PullRequests() + if err != nil || prPayload.CurrentPR == nil { + branch := currentBranch() + return fmt.Errorf("The [%s] branch has no open PRs", branch) + } + openURL = prPayload.CurrentPR.URL + } + + fmt.Printf("Opening %s in your browser.\n", openURL) + return openInBrowser(openURL) +} + +func printPrs(prs ...api.PullRequest) { + for _, pr := range prs { + fmt.Printf(" #%d %s %s\n", pr.Number, truncateTitle(pr.Title), utils.Cyan("["+pr.HeadRefName+"]")) + } +} + +func printHeader(s string) { + fmt.Println(utils.Bold(s)) +} + +func printMessage(s string) { + fmt.Println(utils.Gray(s)) +} + +func truncateTitle(title string) string { + const maxLength = 50 + + if len(title) > maxLength { + return title[0:maxLength-3] + "..." + } + return title +} + +func openInBrowser(url string) error { + launcher, err := utils.BrowserLauncher() + if err != nil { + return err + } + endingArgs := append(launcher[1:], url) + return exec.Command(launcher[0], endingArgs...).Run() +} + +// The functions below should be replaced at some point by the context package +// 🧨🧨🧨🧨🧨🧨🧨🧨🧨🧨🧨🧨🧨🧨🧨🧨🧨🧨🧨🧨🧨🧨🧨🧨🧨🧨🧨🧨🧨🧨🧨🧨🧨🧨 +func currentBranch() string { + currentBranch, err := git.Head() + if err != nil { + panic(err) + } + + return strings.Replace(currentBranch, "refs/heads/", "", 1) +} + +func project() github.Project { + if repoFromEnv := os.Getenv("GH_REPO"); repoFromEnv != "" { + repoURL, err := url.Parse(fmt.Sprintf("https://github.com/%s.git", repoFromEnv)) + if err != nil { + panic(err) + } + project, err := github.NewProjectFromURL(repoURL) + if err != nil { + panic(err) + } + return *project + } + + remotes, err := github.Remotes() + if err != nil { + panic(err) + } + + for _, remote := range remotes { + if project, err := remote.Project(); err == nil { + return *project + } + } + + panic("Could not get the project. What is a project? I don't know, it's kind of like a git repository I think?") } 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 6b1d3c4c9..dca837862 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ 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/mattn/go-colorable v0.1.2 github.com/mattn/go-isatty v0.0.9 diff --git a/go.sum b/go.sum index 24567cbbf..4086387eb 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,12 @@ 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= @@ -22,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= @@ -32,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...) +}