package github import ( "fmt" "github.com/cli/go-gh/v2/pkg/api" graphql "github.com/cli/shurcooL-graphql" ) // PullRequest represents a GitHub pull request. type PullRequest struct { ID string `graphql:"id"` Number int `graphql:"number"` Title string `graphql:"title"` State string `graphql:"state"` URL string `graphql:"url"` HeadRefName string `graphql:"headRefName"` BaseRefName string `graphql:"baseRefName"` IsDraft bool `graphql:"isDraft"` Merged bool `graphql:"merged"` } // Client wraps GitHub API operations. type Client struct { gql *api.GraphQLClient rest *api.RESTClient owner string repo string slug string } // NewClient creates a new GitHub API client for the given repository. func NewClient(owner, repo string) (*Client, error) { gql, err := api.DefaultGraphQLClient() if err != nil { return nil, fmt.Errorf("creating GraphQL client: %w", err) } rest, err := api.DefaultRESTClient() if err != nil { return nil, fmt.Errorf("creating REST client: %w", err) } return &Client{ gql: gql, rest: rest, owner: owner, repo: repo, slug: owner + "/" + repo, }, nil } // FindPRForBranch finds an open PR by head branch name. func (c *Client) FindPRForBranch(branch string) (*PullRequest, error) { var query struct { Repository struct { PullRequests struct { Nodes []PullRequest } `graphql:"pullRequests(headRefName: $head, states: [OPEN], first: 1)"` } `graphql:"repository(owner: $owner, name: $name)"` } variables := map[string]interface{}{ "owner": graphql.String(c.owner), "name": graphql.String(c.repo), "head": graphql.String(branch), } if err := c.gql.Query("FindPRForBranch", &query, variables); err != nil { return nil, fmt.Errorf("querying PRs: %w", err) } nodes := query.Repository.PullRequests.Nodes if len(nodes) == 0 { return nil, nil } n := nodes[0] return &PullRequest{ ID: n.ID, Number: n.Number, Title: n.Title, State: n.State, URL: n.URL, HeadRefName: n.HeadRefName, BaseRefName: n.BaseRefName, IsDraft: n.IsDraft, Merged: n.Merged, }, nil } // FindAnyPRForBranch finds the most recent PR by head branch name regardless of state. func (c *Client) FindAnyPRForBranch(branch string) (*PullRequest, error) { var query struct { Repository struct { PullRequests struct { Nodes []PullRequest } `graphql:"pullRequests(headRefName: $head, last: 1)"` } `graphql:"repository(owner: $owner, name: $name)"` } variables := map[string]interface{}{ "owner": graphql.String(c.owner), "name": graphql.String(c.repo), "head": graphql.String(branch), } if err := c.gql.Query("FindAnyPRForBranch", &query, variables); err != nil { return nil, fmt.Errorf("querying PRs: %w", err) } nodes := query.Repository.PullRequests.Nodes if len(nodes) == 0 { return nil, nil } n := nodes[0] return &PullRequest{ ID: n.ID, Number: n.Number, Title: n.Title, State: n.State, URL: n.URL, HeadRefName: n.HeadRefName, BaseRefName: n.BaseRefName, IsDraft: n.IsDraft, Merged: n.Merged, }, nil } // CreatePR creates a new pull request. func (c *Client) CreatePR(base, head, title, body string, draft bool) (*PullRequest, error) { var mutation struct { CreatePullRequest struct { PullRequest struct { ID string Number int Title string State string URL string `graphql:"url"` HeadRefName string BaseRefName string IsDraft bool } } `graphql:"createPullRequest(input: $input)"` } repoID, err := c.repositoryID() if err != nil { return nil, err } type CreatePullRequestInput struct { RepositoryID string `json:"repositoryId"` BaseRefName string `json:"baseRefName"` HeadRefName string `json:"headRefName"` Title string `json:"title"` Body string `json:"body,omitempty"` Draft bool `json:"draft"` } variables := map[string]interface{}{ "input": CreatePullRequestInput{ RepositoryID: repoID, BaseRefName: base, HeadRefName: head, Title: title, Body: body, Draft: draft, }, } if err := c.gql.Mutate("CreatePullRequest", &mutation, variables); err != nil { return nil, fmt.Errorf("creating PR: %w", err) } pr := mutation.CreatePullRequest.PullRequest return &PullRequest{ ID: pr.ID, Number: pr.Number, Title: pr.Title, State: pr.State, URL: pr.URL, HeadRefName: pr.HeadRefName, BaseRefName: pr.BaseRefName, IsDraft: pr.IsDraft, }, nil } // PRDetails holds enriched pull request data for display in the TUI. type PRDetails struct { Number int Title string State string // OPEN, CLOSED, MERGED URL string IsDraft bool Merged bool CommentsCount int } // FindPRDetailsForBranch fetches enriched PR data for display purposes. // Returns nil without error if no PR exists for the branch. func (c *Client) FindPRDetailsForBranch(branch string) (*PRDetails, error) { var query struct { Repository struct { PullRequests struct { Nodes []struct { ID string `graphql:"id"` Number int `graphql:"number"` Title string `graphql:"title"` State string `graphql:"state"` URL string `graphql:"url"` HeadRefName string `graphql:"headRefName"` BaseRefName string `graphql:"baseRefName"` IsDraft bool `graphql:"isDraft"` Merged bool `graphql:"merged"` Comments struct { TotalCount int `graphql:"totalCount"` } `graphql:"comments"` } } `graphql:"pullRequests(headRefName: $head, last: 1)"` } `graphql:"repository(owner: $owner, name: $name)"` } variables := map[string]interface{}{ "owner": graphql.String(c.owner), "name": graphql.String(c.repo), "head": graphql.String(branch), } if err := c.gql.Query("FindPRDetailsForBranch", &query, variables); err != nil { return nil, fmt.Errorf("querying PR details: %w", err) } nodes := query.Repository.PullRequests.Nodes if len(nodes) == 0 { return nil, nil } n := nodes[0] return &PRDetails{ Number: n.Number, Title: n.Title, State: n.State, URL: n.URL, IsDraft: n.IsDraft, Merged: n.Merged, CommentsCount: n.Comments.TotalCount, }, nil } // DeleteStack deletes a stack on GitHub. // TODO: Implement once the stack API is available. func (c *Client) DeleteStack() error { return fmt.Errorf("deleting a stack on GitHub is not yet supported by the API") } func (c *Client) repositoryID() (string, error) { var query struct { Repository struct { ID string } `graphql:"repository(owner: $owner, name: $name)"` } variables := map[string]interface{}{ "owner": graphql.String(c.owner), "name": graphql.String(c.repo), } if err := c.gql.Query("RepositoryID", &query, variables); err != nil { return "", fmt.Errorf("fetching repository ID: %w", err) } return query.Repository.ID, nil }