Skip to content

Commit 1009ad5

Browse files
Merge pull request cli#8 from github/graphql
Pull in the GraphQL API functionality
2 parents 7a839c6 + 90b0a6c commit 1009ad5

File tree

3 files changed

+337
-81
lines changed

3 files changed

+337
-81
lines changed

api/client.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package api
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io/ioutil"
8+
"net/http"
9+
"os"
10+
"os/user"
11+
"regexp"
12+
13+
"github.com/github/gh-cli/version"
14+
)
15+
16+
type graphQLResponse struct {
17+
Data interface{}
18+
Errors []struct {
19+
Message string
20+
}
21+
}
22+
23+
/*
24+
graphQL usage
25+
26+
type repoResponse struct {
27+
Repository struct {
28+
CreatedAt string
29+
}
30+
}
31+
32+
query := `query {
33+
repository(owner: "golang", name: "go") {
34+
createdAt
35+
}
36+
}`
37+
38+
variables := map[string]string{}
39+
40+
var resp repoResponse
41+
err := graphql(query, map[string]string{}, &resp)
42+
if err != nil {
43+
panic(err)
44+
}
45+
46+
fmt.Printf("%+v\n", resp)
47+
*/
48+
func graphQL(query string, variables map[string]string, data interface{}) error {
49+
url := "https://api.github.com/graphql"
50+
reqBody, err := json.Marshal(map[string]interface{}{"query": query, "variables": variables})
51+
if err != nil {
52+
return err
53+
}
54+
55+
req, err := http.NewRequest("POST", url, bytes.NewBuffer(reqBody))
56+
if err != nil {
57+
return err
58+
}
59+
60+
token, err := getToken()
61+
if err != nil {
62+
return err
63+
}
64+
65+
req.Header.Set("Authorization", "token "+token)
66+
req.Header.Set("Content-Type", "application/json; charset=utf-8")
67+
req.Header.Set("User-Agent", "GitHub CLI "+version.Version)
68+
69+
debugRequest(req, string(reqBody))
70+
71+
client := &http.Client{}
72+
resp, err := client.Do(req)
73+
if err != nil {
74+
return err
75+
}
76+
defer resp.Body.Close()
77+
78+
body, err := ioutil.ReadAll(resp.Body)
79+
if err != nil {
80+
return err
81+
}
82+
83+
debugResponse(resp, string(body))
84+
return handleResponse(resp, body, data)
85+
}
86+
87+
func handleResponse(resp *http.Response, body []byte, data interface{}) error {
88+
success := resp.StatusCode >= 200 && resp.StatusCode < 300
89+
90+
if !success {
91+
return handleHTTPError(resp, body)
92+
}
93+
94+
gr := &graphQLResponse{Data: data}
95+
err := json.Unmarshal(body, &gr)
96+
if err != nil {
97+
return err
98+
}
99+
100+
if len(gr.Errors) > 0 {
101+
errorMessages := gr.Errors[0].Message
102+
for _, e := range gr.Errors[1:] {
103+
errorMessages += ", " + e.Message
104+
}
105+
return fmt.Errorf("graphql error: '%s'", errorMessages)
106+
}
107+
return nil
108+
109+
}
110+
111+
func handleHTTPError(resp *http.Response, body []byte) error {
112+
var message string
113+
var parsedBody struct {
114+
Message string `json:"message"`
115+
}
116+
err := json.Unmarshal(body, &parsedBody)
117+
if err != nil {
118+
message = string(body)
119+
} else {
120+
message = parsedBody.Message
121+
}
122+
123+
return fmt.Errorf("http error, '%s' failed (%d): '%s'", resp.Request.URL, resp.StatusCode, message)
124+
}
125+
126+
func debugRequest(req *http.Request, body string) {
127+
if _, ok := os.LookupEnv("DEBUG"); !ok {
128+
return
129+
}
130+
131+
fmt.Printf("DEBUG: GraphQL request to %s:\n %s\n\n", req.URL, body)
132+
}
133+
134+
func debugResponse(resp *http.Response, body string) {
135+
if _, ok := os.LookupEnv("DEBUG"); !ok {
136+
return
137+
}
138+
139+
fmt.Printf("DEBUG: GraphQL response:\n%+v\n\n%s\n\n", resp, body)
140+
}
141+
142+
// TODO: Everything below this line will be removed when Nate's context work is complete
143+
func getToken() (string, error) {
144+
usr, err := user.Current()
145+
if err != nil {
146+
return "", err
147+
}
148+
149+
content, err := ioutil.ReadFile(usr.HomeDir + "/.config/hub")
150+
if err != nil {
151+
return "", err
152+
}
153+
154+
r := regexp.MustCompile(`oauth_token: (\w+)`)
155+
token := r.FindStringSubmatch(string(content))
156+
return token[1], nil
157+
}

api/queries.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package api
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/github/gh-cli/git"
8+
"github.com/github/gh-cli/github"
9+
)
10+
11+
type PullRequestsPayload struct {
12+
ViewerCreated []PullRequest
13+
ReviewRequested []PullRequest
14+
CurrentPR *PullRequest
15+
}
16+
17+
type PullRequest struct {
18+
Number int
19+
Title string
20+
URL string
21+
HeadRefName string
22+
}
23+
24+
func PullRequests() (PullRequestsPayload, error) {
25+
type edges struct {
26+
Edges []struct {
27+
Node PullRequest
28+
}
29+
PageInfo struct {
30+
HasNextPage bool
31+
EndCursor string
32+
}
33+
}
34+
35+
type response struct {
36+
Repository struct {
37+
PullRequests edges
38+
}
39+
ViewerCreated edges
40+
ReviewRequested edges
41+
}
42+
43+
query := `
44+
fragment pr on PullRequest {
45+
number
46+
title
47+
url
48+
headRefName
49+
}
50+
51+
query($owner: String!, $repo: String!, $headRefName: String!, $viewerQuery: String!, $reviewerQuery: String!, $per_page: Int = 10) {
52+
repository(owner: $owner, name: $repo) {
53+
pullRequests(headRefName: $headRefName, first: 1) {
54+
edges {
55+
node {
56+
...pr
57+
}
58+
}
59+
}
60+
}
61+
viewerCreated: search(query: $viewerQuery, type: ISSUE, first: $per_page) {
62+
edges {
63+
node {
64+
...pr
65+
}
66+
}
67+
pageInfo {
68+
hasNextPage
69+
}
70+
}
71+
reviewRequested: search(query: $reviewerQuery, type: ISSUE, first: $per_page) {
72+
edges {
73+
node {
74+
...pr
75+
}
76+
}
77+
pageInfo {
78+
hasNextPage
79+
}
80+
}
81+
}
82+
`
83+
84+
project := project()
85+
owner := project.Owner
86+
repo := project.Name
87+
currentBranch := currentBranch()
88+
89+
viewerQuery := fmt.Sprintf("repo:%s/%s state:open is:pr author:%s", owner, repo, currentUsername())
90+
reviewerQuery := fmt.Sprintf("repo:%s/%s state:open review-requested:%s", owner, repo, currentUsername())
91+
92+
variables := map[string]string{
93+
"viewerQuery": viewerQuery,
94+
"reviewerQuery": reviewerQuery,
95+
"owner": owner,
96+
"repo": repo,
97+
"headRefName": currentBranch,
98+
}
99+
100+
var resp response
101+
err := graphQL(query, variables, &resp)
102+
if err != nil {
103+
return PullRequestsPayload{}, err
104+
}
105+
106+
var viewerCreated []PullRequest
107+
for _, edge := range resp.ViewerCreated.Edges {
108+
viewerCreated = append(viewerCreated, edge.Node)
109+
}
110+
111+
var reviewRequested []PullRequest
112+
for _, edge := range resp.ReviewRequested.Edges {
113+
reviewRequested = append(reviewRequested, edge.Node)
114+
}
115+
116+
var currentPR *PullRequest
117+
for _, edge := range resp.Repository.PullRequests.Edges {
118+
currentPR = &edge.Node
119+
}
120+
121+
payload := PullRequestsPayload{
122+
viewerCreated,
123+
reviewRequested,
124+
currentPR,
125+
}
126+
127+
return payload, nil
128+
}
129+
130+
// TODO: Everything below this line will be removed when Nate's context work is complete
131+
func project() github.Project {
132+
remotes, error := github.Remotes()
133+
if error != nil {
134+
panic(error)
135+
}
136+
137+
for _, remote := range remotes {
138+
if project, error := remote.Project(); error == nil {
139+
return *project
140+
}
141+
}
142+
143+
panic("Could not get the project. What is a project? I don't know, it's kind of like a git repository I think?")
144+
}
145+
146+
func currentBranch() string {
147+
currentBranch, err := git.Head()
148+
if err != nil {
149+
panic(err)
150+
}
151+
152+
return strings.Replace(currentBranch, "refs/heads/", "", 1)
153+
}
154+
155+
func currentUsername() string {
156+
host, err := github.CurrentConfig().DefaultHost()
157+
if err != nil {
158+
panic(err)
159+
}
160+
return host.User
161+
}

0 commit comments

Comments
 (0)