Skip to content

Commit f786802

Browse files
committed
Customizable API client
1 parent 8370602 commit f786802

File tree

12 files changed

+269
-175
lines changed

12 files changed

+269
-175
lines changed

api/client.go

Lines changed: 72 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,47 +4,78 @@ import (
44
"bytes"
55
"encoding/json"
66
"fmt"
7+
"io"
78
"io/ioutil"
89
"net/http"
9-
"os"
10-
11-
"github.com/github/gh-cli/context"
12-
"github.com/github/gh-cli/version"
1310
)
1411

15-
type graphQLResponse struct {
16-
Data interface{}
17-
Errors []struct {
18-
Message string
12+
// ClientOption represents an argument to NewClient
13+
type ClientOption = func(http.RoundTripper) http.RoundTripper
14+
15+
// NewClient initializes a Client
16+
func NewClient(opts ...ClientOption) *Client {
17+
tr := http.DefaultTransport
18+
for _, opt := range opts {
19+
tr = opt(tr)
1920
}
21+
http := &http.Client{Transport: tr}
22+
client := &Client{http: http}
23+
return client
2024
}
2125

22-
/*
23-
GraphQL: Declared as an external variable so it can be mocked in tests
26+
// AddHeader turns a RoundTripper into one that adds a request header
27+
func AddHeader(name, value string) ClientOption {
28+
return func(tr http.RoundTripper) http.RoundTripper {
29+
return &funcTripper{roundTrip: func(req *http.Request) (*http.Response, error) {
30+
req.Header.Add(name, value)
31+
return tr.RoundTrip(req)
32+
}}
33+
}
34+
}
2435

25-
type repoResponse struct {
26-
Repository struct {
27-
CreatedAt string
36+
// VerboseLog enables request/response logging within a RoundTripper
37+
func VerboseLog(out io.Writer) ClientOption {
38+
return func(tr http.RoundTripper) http.RoundTripper {
39+
return &funcTripper{roundTrip: func(req *http.Request) (*http.Response, error) {
40+
fmt.Fprintf(out, "> %s %s\n", req.Method, req.URL.RequestURI())
41+
res, err := tr.RoundTrip(req)
42+
if err == nil {
43+
fmt.Fprintf(out, "< HTTP %s\n", res.Status)
44+
}
45+
return res, err
46+
}}
2847
}
2948
}
3049

31-
query := `query {
32-
repository(owner: "golang", name: "go") {
33-
createdAt
50+
// ReplaceTripper substitutes the underlying RoundTripper with a custom one
51+
func ReplaceTripper(tr http.RoundTripper) ClientOption {
52+
return func(http.RoundTripper) http.RoundTripper {
53+
return tr
3454
}
35-
}`
55+
}
3656

37-
variables := map[string]string{}
57+
type funcTripper struct {
58+
roundTrip func(*http.Request) (*http.Response, error)
59+
}
60+
61+
func (tr funcTripper) RoundTrip(req *http.Request) (*http.Response, error) {
62+
return tr.roundTrip(req)
63+
}
3864

39-
var resp repoResponse
40-
err := graphql(query, map[string]string{}, &resp)
41-
if err != nil {
42-
panic(err)
65+
// Client facilitates making HTTP requests to the GitHub API
66+
type Client struct {
67+
http *http.Client
4368
}
4469

45-
fmt.Printf("%+v\n", resp)
46-
*/
47-
var GraphQL = func(query string, variables map[string]string, data interface{}) error {
70+
type graphQLResponse struct {
71+
Data interface{}
72+
Errors []struct {
73+
Message string
74+
}
75+
}
76+
77+
// GraphQL performs a GraphQL request and parses the response
78+
func (c Client) GraphQL(query string, variables map[string]interface{}, data interface{}) error {
4879
url := "https://api.github.com/graphql"
4980
reqBody, err := json.Marshal(map[string]interface{}{"query": query, "variables": variables})
5081
if err != nil {
@@ -56,42 +87,31 @@ var GraphQL = func(query string, variables map[string]string, data interface{})
5687
return err
5788
}
5889

59-
token, err := context.Current().AuthToken()
60-
if err != nil {
61-
return err
62-
}
63-
64-
req.Header.Set("Authorization", "token "+token)
6590
req.Header.Set("Content-Type", "application/json; charset=utf-8")
66-
req.Header.Set("User-Agent", "GitHub CLI "+version.Version)
6791

68-
debugRequest(req, string(reqBody))
69-
70-
client := &http.Client{}
71-
resp, err := client.Do(req)
92+
resp, err := c.http.Do(req)
7293
if err != nil {
7394
return err
7495
}
7596
defer resp.Body.Close()
7697

77-
body, err := ioutil.ReadAll(resp.Body)
78-
if err != nil {
79-
return err
80-
}
81-
82-
debugResponse(resp, string(body))
83-
return handleResponse(resp, body, data)
98+
return handleResponse(resp, data)
8499
}
85100

86-
func handleResponse(resp *http.Response, body []byte, data interface{}) error {
101+
func handleResponse(resp *http.Response, data interface{}) error {
87102
success := resp.StatusCode >= 200 && resp.StatusCode < 300
88103

89104
if !success {
90-
return handleHTTPError(resp, body)
105+
return handleHTTPError(resp)
106+
}
107+
108+
body, err := ioutil.ReadAll(resp.Body)
109+
if err != nil {
110+
return err
91111
}
92112

93113
gr := &graphQLResponse{Data: data}
94-
err := json.Unmarshal(body, &gr)
114+
err = json.Unmarshal(body, &gr)
95115
if err != nil {
96116
return err
97117
}
@@ -107,12 +127,16 @@ func handleResponse(resp *http.Response, body []byte, data interface{}) error {
107127

108128
}
109129

110-
func handleHTTPError(resp *http.Response, body []byte) error {
130+
func handleHTTPError(resp *http.Response) error {
111131
var message string
112132
var parsedBody struct {
113133
Message string `json:"message"`
114134
}
115-
err := json.Unmarshal(body, &parsedBody)
135+
body, err := ioutil.ReadAll(resp.Body)
136+
if err != nil {
137+
return err
138+
}
139+
err = json.Unmarshal(body, &parsedBody)
116140
if err != nil {
117141
message = string(body)
118142
} else {
@@ -121,19 +145,3 @@ func handleHTTPError(resp *http.Response, body []byte) error {
121145

122146
return fmt.Errorf("http error, '%s' failed (%d): '%s'", resp.Request.URL, resp.StatusCode, message)
123147
}
124-
125-
func debugRequest(req *http.Request, body string) {
126-
if _, ok := os.LookupEnv("DEBUG"); !ok {
127-
return
128-
}
129-
130-
fmt.Printf("DEBUG: GraphQL request to %s:\n %s\n\n", req.URL, body)
131-
}
132-
133-
func debugResponse(resp *http.Response, body string) {
134-
if _, ok := os.LookupEnv("DEBUG"); !ok {
135-
return
136-
}
137-
138-
fmt.Printf("DEBUG: GraphQL response:\n%+v\n\n%s\n\n", resp, body)
139-
}

api/queries.go

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package api
22

33
import (
44
"fmt"
5-
6-
"github.com/github/gh-cli/context"
75
)
86

97
type PullRequestsPayload struct {
@@ -19,7 +17,12 @@ type PullRequest struct {
1917
HeadRefName string
2018
}
2119

22-
func PullRequests() (*PullRequestsPayload, error) {
20+
type Repo interface {
21+
RepoName() string
22+
RepoOwner() string
23+
}
24+
25+
func PullRequests(client *Client, ghRepo Repo, currentBranch, currentUsername string) (*PullRequestsPayload, error) {
2326
type edges struct {
2427
Edges []struct {
2528
Node PullRequest
@@ -48,7 +51,7 @@ func PullRequests() (*PullRequestsPayload, error) {
4851
4952
query($owner: String!, $repo: String!, $headRefName: String!, $viewerQuery: String!, $reviewerQuery: String!, $per_page: Int = 10) {
5053
repository(owner: $owner, name: $repo) {
51-
pullRequests(headRefName: $headRefName, first: 1) {
54+
pullRequests(headRefName: $headRefName, states: OPEN, first: 1) {
5255
edges {
5356
node {
5457
...pr
@@ -79,26 +82,13 @@ func PullRequests() (*PullRequestsPayload, error) {
7982
}
8083
`
8184

82-
ghRepo, err := context.Current().BaseRepo()
83-
if err != nil {
84-
return nil, err
85-
}
86-
currentBranch, err := context.Current().Branch()
87-
if err != nil {
88-
return nil, err
89-
}
90-
currentUsername, err := context.Current().AuthLogin()
91-
if err != nil {
92-
return nil, err
93-
}
94-
95-
owner := ghRepo.Owner
96-
repo := ghRepo.Name
85+
owner := ghRepo.RepoOwner()
86+
repo := ghRepo.RepoName()
9787

9888
viewerQuery := fmt.Sprintf("repo:%s/%s state:open is:pr author:%s", owner, repo, currentUsername)
9989
reviewerQuery := fmt.Sprintf("repo:%s/%s state:open review-requested:%s", owner, repo, currentUsername)
10090

101-
variables := map[string]string{
91+
variables := map[string]interface{}{
10292
"viewerQuery": viewerQuery,
10393
"reviewerQuery": reviewerQuery,
10494
"owner": owner,
@@ -107,7 +97,7 @@ func PullRequests() (*PullRequestsPayload, error) {
10797
}
10898

10999
var resp response
110-
err = GraphQL(query, variables, &resp)
100+
err := client.GraphQL(query, variables, &resp)
111101
if err != nil {
112102
return nil, err
113103
}
@@ -135,3 +125,49 @@ func PullRequests() (*PullRequestsPayload, error) {
135125

136126
return &payload, nil
137127
}
128+
129+
func PullRequestsForBranch(client *Client, ghRepo Repo, branch string) ([]PullRequest, error) {
130+
type response struct {
131+
Repository struct {
132+
PullRequests struct {
133+
Edges []struct {
134+
Node PullRequest
135+
}
136+
}
137+
}
138+
}
139+
140+
query := `
141+
query($owner: String!, $repo: String!, $headRefName: String!) {
142+
repository(owner: $owner, name: $repo) {
143+
pullRequests(headRefName: $headRefName, states: OPEN, first: 1) {
144+
edges {
145+
node {
146+
number
147+
title
148+
url
149+
}
150+
}
151+
}
152+
}
153+
}`
154+
155+
variables := map[string]interface{}{
156+
"owner": ghRepo.RepoOwner(),
157+
"repo": ghRepo.RepoName(),
158+
"headRefName": branch,
159+
}
160+
161+
var resp response
162+
err := client.GraphQL(query, variables, &resp)
163+
if err != nil {
164+
return nil, err
165+
}
166+
167+
prs := []PullRequest{}
168+
for _, edge := range resp.Repository.PullRequests.Edges {
169+
prs = append(prs, edge.Node)
170+
}
171+
172+
return prs, nil
173+
}

command/pr.go

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,25 @@ work with pull requests.`,
3838

3939
func prList(cmd *cobra.Command, args []string) error {
4040
ctx := contextForCommand(cmd)
41-
prPayload, err := api.PullRequests()
41+
apiClient, err := apiClientForContext(ctx)
42+
if err != nil {
43+
return err
44+
}
45+
46+
baseRepo, err := ctx.BaseRepo()
47+
if err != nil {
48+
return err
49+
}
50+
currentBranch, err := ctx.Branch()
51+
if err != nil {
52+
return err
53+
}
54+
currentUser, err := ctx.AuthLogin()
55+
if err != nil {
56+
return err
57+
}
58+
59+
prPayload, err := api.PullRequests(apiClient, baseRepo, currentBranch, currentUser)
4260
if err != nil {
4361
return err
4462
}
@@ -47,10 +65,6 @@ func prList(cmd *cobra.Command, args []string) error {
4765
if prPayload.CurrentPR != nil {
4866
printPrs(*prPayload.CurrentPR)
4967
} else {
50-
currentBranch, err := ctx.Branch()
51-
if err != nil {
52-
return err
53-
}
5468
message := fmt.Sprintf(" There is no pull request associated with %s", utils.Cyan("["+currentBranch+"]"))
5569
printMessage(message)
5670
}
@@ -86,23 +100,27 @@ func prView(cmd *cobra.Command, args []string) error {
86100
if len(args) > 0 {
87101
if prNumber, err := strconv.Atoi(args[0]); err == nil {
88102
// TODO: move URL generation into GitHubRepository
89-
openURL = fmt.Sprintf("https://github.com/%s/%s/pull/%d", baseRepo.Owner, baseRepo.Name, prNumber)
103+
openURL = fmt.Sprintf("https://github.com/%s/%s/pull/%d", baseRepo.RepoOwner(), baseRepo.RepoName(), prNumber)
90104
} else {
91105
return fmt.Errorf("invalid pull request number: '%s'", args[0])
92106
}
93107
} else {
94-
prPayload, err := api.PullRequests()
108+
apiClient, err := apiClientForContext(ctx)
109+
if err != nil {
110+
return err
111+
}
112+
currentBranch, err := ctx.Branch()
113+
if err != nil {
114+
return err
115+
}
116+
117+
prs, err := api.PullRequestsForBranch(apiClient, baseRepo, currentBranch)
95118
if err != nil {
96119
return err
97-
} else if prPayload.CurrentPR == nil {
98-
branch, err := ctx.Branch()
99-
if err != nil {
100-
return err
101-
}
102-
fmt.Printf("The [%s] branch has no open PRs", branch)
103-
return nil
120+
} else if len(prs) < 1 {
121+
return fmt.Errorf("the '%s' branch has no open pull requests", currentBranch)
104122
}
105-
openURL = prPayload.CurrentPR.URL
123+
openURL = prs[0].URL
106124
}
107125

108126
fmt.Printf("Opening %s in your browser.\n", openURL)

0 commit comments

Comments
 (0)