Merge pull request #8 from github/graphql

Pull in the GraphQL API functionality
This commit is contained in:
Corey Johnson 2019-10-11 14:45:39 -07:00 committed by GitHub
commit 1009ad5598
3 changed files with 341 additions and 85 deletions

157
api/client.go Normal file
View file

@ -0,0 +1,157 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"os/user"
"regexp"
"github.com/github/gh-cli/version"
)
type graphQLResponse struct {
Data interface{}
Errors []struct {
Message string
}
}
/*
graphQL usage
type repoResponse struct {
Repository struct {
CreatedAt string
}
}
query := `query {
repository(owner: "golang", name: "go") {
createdAt
}
}`
variables := map[string]string{}
var resp repoResponse
err := graphql(query, map[string]string{}, &resp)
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", resp)
*/
func graphQL(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 {
return err
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(reqBody))
if err != nil {
return err
}
token, err := getToken()
if err != nil {
return err
}
req.Header.Set("Authorization", "token "+token)
req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.Header.Set("User-Agent", "GitHub CLI "+version.Version)
debugRequest(req, string(reqBody))
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
debugResponse(resp, string(body))
return handleResponse(resp, body, data)
}
func handleResponse(resp *http.Response, body []byte, data interface{}) error {
success := resp.StatusCode >= 200 && resp.StatusCode < 300
if !success {
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 {
var message string
var parsedBody struct {
Message string `json:"message"`
}
err := json.Unmarshal(body, &parsedBody)
if err != nil {
message = string(body)
} else {
message = parsedBody.Message
}
return fmt.Errorf("http error, '%s' failed (%d): '%s'", resp.Request.URL, resp.StatusCode, message)
}
func debugRequest(req *http.Request, body string) {
if _, ok := os.LookupEnv("DEBUG"); !ok {
return
}
fmt.Printf("DEBUG: GraphQL request to %s:\n %s\n\n", req.URL, body)
}
func debugResponse(resp *http.Response, body string) {
if _, ok := os.LookupEnv("DEBUG"); !ok {
return
}
fmt.Printf("DEBUG: GraphQL response:\n%+v\n\n%s\n\n", resp, body)
}
// TODO: Everything below this line will be removed when Nate's context work is complete
func getToken() (string, error) {
usr, err := user.Current()
if err != nil {
return "", err
}
content, err := ioutil.ReadFile(usr.HomeDir + "/.config/hub")
if err != nil {
return "", err
}
r := regexp.MustCompile(`oauth_token: (\w+)`)
token := r.FindStringSubmatch(string(content))
return token[1], nil
}

161
api/queries.go Normal file
View file

@ -0,0 +1,161 @@
package api
import (
"fmt"
"strings"
"github.com/github/gh-cli/git"
"github.com/github/gh-cli/github"
)
type PullRequestsPayload struct {
ViewerCreated []PullRequest
ReviewRequested []PullRequest
CurrentPR *PullRequest
}
type PullRequest struct {
Number int
Title string
URL string
HeadRefName string
}
func PullRequests() (PullRequestsPayload, error) {
type edges struct {
Edges []struct {
Node PullRequest
}
PageInfo struct {
HasNextPage bool
EndCursor string
}
}
type response struct {
Repository struct {
PullRequests edges
}
ViewerCreated edges
ReviewRequested edges
}
query := `
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
}
}
}
`
project := project()
owner := project.Owner
repo := project.Name
currentBranch := currentBranch()
viewerQuery := fmt.Sprintf("repo:%s/%s state:open is:pr author:%s", owner, repo, currentUsername())
reviewerQuery := fmt.Sprintf("repo:%s/%s state:open review-requested:%s", owner, repo, currentUsername())
variables := map[string]string{
"viewerQuery": viewerQuery,
"reviewerQuery": reviewerQuery,
"owner": owner,
"repo": repo,
"headRefName": currentBranch,
}
var resp response
err := graphQL(query, variables, &resp)
if err != nil {
return PullRequestsPayload{}, err
}
var viewerCreated []PullRequest
for _, edge := range resp.ViewerCreated.Edges {
viewerCreated = append(viewerCreated, edge.Node)
}
var reviewRequested []PullRequest
for _, edge := range resp.ReviewRequested.Edges {
reviewRequested = append(reviewRequested, edge.Node)
}
var currentPR *PullRequest
for _, edge := range resp.Repository.PullRequests.Edges {
currentPR = &edge.Node
}
payload := PullRequestsPayload{
viewerCreated,
reviewRequested,
currentPR,
}
return payload, nil
}
// TODO: Everything below this line will be removed when Nate's context work is complete
func project() github.Project {
remotes, error := github.Remotes()
if error != nil {
panic(error)
}
for _, remote := range remotes {
if project, error := remote.Project(); error == 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?")
}
func currentBranch() string {
currentBranch, err := git.Head()
if err != nil {
panic(err)
}
return strings.Replace(currentBranch, "refs/heads/", "", 1)
}
func currentUsername() string {
host, err := github.CurrentConfig().DefaultHost()
if err != nil {
panic(err)
}
return host.User
}

View file

@ -3,8 +3,7 @@ package command
import (
"fmt"
"github.com/github/gh-cli/git"
"github.com/github/gh-cli/github"
"github.com/github/gh-cli/api"
"github.com/spf13/cobra"
)
@ -27,94 +26,33 @@ work with pull requests.`,
var prListCmd = &cobra.Command{
Use: "list",
Short: "List pull requests",
Run: func(cmd *cobra.Command, args []string) {
ExecutePr()
RunE: func(cmd *cobra.Command, args []string) error {
return ExecutePr()
},
}
type prFilter int
func ExecutePr() error {
prPayload, err := api.PullRequests()
if err != nil {
return err
}
const (
createdByViewer prFilter = iota
reviewRequested
)
fmt.Printf("Current Pr\n")
if prPayload.CurrentPR != nil {
printPr(*prPayload.CurrentPR)
}
fmt.Printf("Your Prs\n")
for _, pr := range prPayload.ViewerCreated {
printPr(pr)
}
fmt.Printf("Prs you need to review\n")
for _, pr := range prPayload.ReviewRequested {
printPr(pr)
}
func ExecutePr() {
// prsForCurrentBranch := pullRequestsForCurrentBranch()
prsCreatedByViewer := pullRequests(createdByViewer)
// prsRequestingReview := pullRequests(reviewRequested)
fmt.Printf("🌭 count! %d\n", len(prsCreatedByViewer))
return nil
}
type searchBody struct {
Items []github.PullRequest `json:"items"`
}
func pullRequestsForCurrentBranch() []github.PullRequest {
project := project()
client := github.NewClient(project.Host)
currentBranch, error := git.Head()
if error != nil {
panic(error)
}
headWithOwner := fmt.Sprintf("%s:%s", project.Owner, currentBranch)
filterParams := map[string]interface{}{"headWithOwner": headWithOwner}
prs, error := client.FetchPullRequests(&project, filterParams, 10, nil)
if error != nil {
panic(error)
}
return prs
}
func pullRequests(filter prFilter) []github.PullRequest {
project := project()
client := github.NewClient(project.Host)
owner := project.Owner
name := project.Name
user, error := client.CurrentUser()
if error != nil {
panic(error)
}
var headers map[string]string
var q string
if filter == createdByViewer {
q = fmt.Sprintf("user:%s repo:%s state:open is:pr author:%s", owner, name, user.Login)
} else if filter == reviewRequested {
q = fmt.Sprintf("user:%s repo:%s state:open review-requested:%s", owner, name, user.Login)
} else {
panic("This is not a fitler")
}
data := map[string]interface{}{"q": q}
response, error := client.GenericAPIRequest("GET", "search/issues", data, headers, 60)
if error != nil {
panic(fmt.Sprintf("GenericAPIRequest failed %+v", error))
}
searchBody := searchBody{}
error = response.Unmarshal(&searchBody)
if error != nil {
panic(fmt.Sprintf("Unmarshal failed %+v", error))
}
return searchBody.Items
}
func project() github.Project {
remotes, error := github.Remotes()
if error != nil {
panic(error)
}
for _, remote := range remotes {
if project, error := remote.Project(); error == 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?")
func printPr(pr api.PullRequest) {
fmt.Printf(" #%d %s [%s]\n", pr.Number, pr.Title, pr.HeadRefName)
}