Merge remote-tracking branch 'origin/master' into ghr-context

This commit is contained in:
Mislav Marohnić 2019-10-17 02:23:00 +02:00
commit 12e295e46b
19 changed files with 480 additions and 83 deletions

View file

@ -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 {

View file

@ -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
}

View file

@ -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?")
}

34
command/pr_test.go Normal file
View file

@ -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)
}
}
}

1
go.mod
View file

@ -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

8
go.sum
View file

@ -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=

50
test/fixtures/pr.json vendored Normal file
View file

@ -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 }
}
}

1
test/fixtures/test.git/HEAD vendored Normal file
View file

@ -0,0 +1 @@
ref: refs/heads/master

6
test/fixtures/test.git/config vendored Normal file
View file

@ -0,0 +1,6 @@
[core]
repositoryformatversion = 0
filemode = true
bare = true
ignorecase = true
precomposeunicode = false

6
test/fixtures/test.git/info/exclude vendored Normal file
View file

@ -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]
# *~

View file

@ -0,0 +1,3 @@
x<01><>M
Â0F]çseb~œ€ˆnÝx™¦cMi¨)^ß 7pó>xðñbÉ9UÐä7ub{ŽÖbgÙ´íă#h8<68>÷<01>âµ>Ë×4o™áöÅ
Çñ'ÊyÈœ¦],ùÚ„½Fkœ†-¢j¶U«üûW—¾‡*¯z¤IÔ)”;â

View file

@ -0,0 +1,3 @@
x<01><>K
1D]η}¥3ι$qεBΟ<>dq&#^ίρs7UPΕ«XΖqhΠ/ZM Άw*<2A>cbc±G”3ΩΛ½ΦYηΠI<CEA0>βζk<CEB6> g
6-U<>s4iΦ6³1χFΆμ#Y,ό£<CF8C>K…ύ0<CF8D><30>iγG°Ύ|ƒ²=<3D>~Έ®b7 •λ$b KdD1§3eK<65>φΕn¨χο™Z<>σΥ<CF83>{Nm

View file

@ -0,0 +1 @@
9b5a719a3d76ac9dc2fa635d9b1f34fd73994c06

130
test/helpers.go Normal file
View file

@ -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)
}

43
utils/color.go Normal file
View file

@ -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...)
}