diff --git a/api/client.go b/api/client.go new file mode 100644 index 000000000..0e45c31f1 --- /dev/null +++ b/api/client.go @@ -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 +} diff --git a/api/queries.go b/api/queries.go new file mode 100644 index 000000000..a4c023375 --- /dev/null +++ b/api/queries.go @@ -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 +} diff --git a/command/pr.go b/command/pr.go index e21d66e74..3dc8821c8 100644 --- a/command/pr.go +++ b/command/pr.go @@ -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) }