package api import ( "fmt" "net/http" "net/url" "time" "github.com/cli/cli/v2/internal/ghrepo" "github.com/shurcooL/githubv4" ) type PullRequestAndTotalCount struct { TotalCount int PullRequests []PullRequest SearchCapped bool } type PullRequest struct { ID string Number int Title string State string Closed bool URL string BaseRefName string HeadRefName string HeadRefOid string Body string Mergeable string Additions int Deletions int ChangedFiles int MergeStateStatus string IsInMergeQueue bool IsMergeQueueEnabled bool // Indicates whether the pull request's base ref has a merge queue enabled. CreatedAt time.Time UpdatedAt time.Time ClosedAt *time.Time MergedAt *time.Time MergeCommit *Commit PotentialMergeCommit *Commit Files struct { Nodes []PullRequestFile } Author Author MergedBy *Author HeadRepositoryOwner Owner HeadRepository *PRRepository IsCrossRepository bool IsDraft bool MaintainerCanModify bool BaseRef struct { BranchProtectionRule struct { RequiresStrictStatusChecks bool RequiredApprovingReviewCount int } } ReviewDecision string Commits struct { TotalCount int Nodes []PullRequestCommit } StatusCheckRollup struct { Nodes []StatusCheckRollupNode } Assignees Assignees Labels Labels ProjectCards ProjectCards Milestone *Milestone Comments Comments ReactionGroups ReactionGroups Reviews PullRequestReviews LatestReviews PullRequestReviews ReviewRequests ReviewRequests } type StatusCheckRollupNode struct { Commit StatusCheckRollupCommit } type StatusCheckRollupCommit struct { StatusCheckRollup CommitStatusCheckRollup } type CommitStatusCheckRollup struct { Contexts CheckContexts } type CheckContexts struct { Nodes []CheckContext PageInfo struct { HasNextPage bool EndCursor string } } type CheckContext struct { TypeName string `json:"__typename"` Name string `json:"name"` IsRequired bool `json:"isRequired"` CheckSuite struct { WorkflowRun struct { Workflow struct { Name string `json:"name"` } `json:"workflow"` } `json:"workflowRun"` } `json:"checkSuite"` // QUEUED IN_PROGRESS COMPLETED WAITING PENDING REQUESTED Status string `json:"status"` // ACTION_REQUIRED TIMED_OUT CANCELLED FAILURE SUCCESS NEUTRAL SKIPPED STARTUP_FAILURE STALE Conclusion string `json:"conclusion"` StartedAt time.Time `json:"startedAt"` CompletedAt time.Time `json:"completedAt"` DetailsURL string `json:"detailsUrl"` /* StatusContext fields */ Context string `json:"context"` // EXPECTED ERROR FAILURE PENDING SUCCESS State string `json:"state"` TargetURL string `json:"targetUrl"` CreatedAt time.Time `json:"createdAt"` } type PRRepository struct { ID string `json:"id"` Name string `json:"name"` } // Commit loads just the commit SHA and nothing else type Commit struct { OID string `json:"oid"` } type PullRequestCommit struct { Commit PullRequestCommitCommit } // PullRequestCommitCommit contains full information about a commit type PullRequestCommitCommit struct { OID string `json:"oid"` Authors struct { Nodes []struct { Name string Email string User GitHubUser } } MessageHeadline string MessageBody string CommittedDate time.Time AuthoredDate time.Time } type PullRequestFile struct { Path string `json:"path"` Additions int `json:"additions"` Deletions int `json:"deletions"` } type ReviewRequests struct { Nodes []struct { RequestedReviewer RequestedReviewer } } type RequestedReviewer struct { TypeName string `json:"__typename"` Login string `json:"login"` Name string `json:"name"` Slug string `json:"slug"` Organization struct { Login string `json:"login"` } `json:"organization"` } func (r RequestedReviewer) LoginOrSlug() string { if r.TypeName == teamTypeName { return fmt.Sprintf("%s/%s", r.Organization.Login, r.Slug) } return r.Login } const teamTypeName = "Team" func (r ReviewRequests) Logins() []string { logins := make([]string, len(r.Nodes)) for i, r := range r.Nodes { logins[i] = r.RequestedReviewer.LoginOrSlug() } return logins } func (pr PullRequest) HeadLabel() string { if pr.IsCrossRepository { return fmt.Sprintf("%s:%s", pr.HeadRepositoryOwner.Login, pr.HeadRefName) } return pr.HeadRefName } func (pr PullRequest) Link() string { return pr.URL } func (pr PullRequest) Identifier() string { return pr.ID } func (pr PullRequest) IsOpen() bool { return pr.State == "OPEN" } type PullRequestReviewStatus struct { ChangesRequested bool Approved bool ReviewRequired bool } func (pr *PullRequest) ReviewStatus() PullRequestReviewStatus { var status PullRequestReviewStatus switch pr.ReviewDecision { case "CHANGES_REQUESTED": status.ChangesRequested = true case "APPROVED": status.Approved = true case "REVIEW_REQUIRED": status.ReviewRequired = true } return status } type PullRequestChecksStatus struct { Pending int Failing int Passing int Total int } func (pr *PullRequest) ChecksStatus() (summary PullRequestChecksStatus) { if len(pr.StatusCheckRollup.Nodes) == 0 { return } commit := pr.StatusCheckRollup.Nodes[0].Commit for _, c := range commit.StatusCheckRollup.Contexts.Nodes { state := c.State // StatusContext if state == "" { // CheckRun if c.Status == "COMPLETED" { state = c.Conclusion } else { state = c.Status } } switch state { case "SUCCESS", "NEUTRAL", "SKIPPED": summary.Passing++ case "ERROR", "FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED": summary.Failing++ default: // "EXPECTED", "REQUESTED", "WAITING", "QUEUED", "PENDING", "IN_PROGRESS", "STALE" summary.Pending++ } summary.Total++ } return } func (pr *PullRequest) DisplayableReviews() PullRequestReviews { published := []PullRequestReview{} for _, prr := range pr.Reviews.Nodes { //Dont display pending reviews //Dont display commenting reviews without top level comment body if prr.State != "PENDING" && !(prr.State == "COMMENTED" && prr.Body == "") { published = append(published, prr) } } return PullRequestReviews{Nodes: published, TotalCount: len(published)} } // CreatePullRequest creates a pull request in a GitHub repository func CreatePullRequest(client *Client, repo *Repository, params map[string]interface{}) (*PullRequest, error) { query := ` mutation PullRequestCreate($input: CreatePullRequestInput!) { createPullRequest(input: $input) { pullRequest { id url } } }` inputParams := map[string]interface{}{ "repositoryId": repo.ID, } for key, val := range params { switch key { case "title", "body", "draft", "baseRefName", "headRefName", "maintainerCanModify": inputParams[key] = val } } variables := map[string]interface{}{ "input": inputParams, } result := struct { CreatePullRequest struct { PullRequest PullRequest } }{} err := client.GraphQL(repo.RepoHost(), query, variables, &result) if err != nil { return nil, err } pr := &result.CreatePullRequest.PullRequest // metadata parameters aren't currently available in `createPullRequest`, // but they are in `updatePullRequest` updateParams := make(map[string]interface{}) for key, val := range params { switch key { case "assigneeIds", "labelIds", "projectIds", "milestoneId": if !isBlank(val) { updateParams[key] = val } } } if len(updateParams) > 0 { updateQuery := ` mutation PullRequestCreateMetadata($input: UpdatePullRequestInput!) { updatePullRequest(input: $input) { clientMutationId } }` updateParams["pullRequestId"] = pr.ID variables := map[string]interface{}{ "input": updateParams, } err := client.GraphQL(repo.RepoHost(), updateQuery, variables, &result) if err != nil { return pr, err } } // reviewers are requested in yet another additional mutation reviewParams := make(map[string]interface{}) if ids, ok := params["userReviewerIds"]; ok && !isBlank(ids) { reviewParams["userIds"] = ids } if ids, ok := params["teamReviewerIds"]; ok && !isBlank(ids) { reviewParams["teamIds"] = ids } //TODO: How much work to extract this into own method and use for create and edit? if len(reviewParams) > 0 { reviewQuery := ` mutation PullRequestCreateRequestReviews($input: RequestReviewsInput!) { requestReviews(input: $input) { clientMutationId } }` reviewParams["pullRequestId"] = pr.ID reviewParams["union"] = true variables := map[string]interface{}{ "input": reviewParams, } err := client.GraphQL(repo.RepoHost(), reviewQuery, variables, &result) if err != nil { return pr, err } } return pr, nil } func UpdatePullRequestReviews(client *Client, repo ghrepo.Interface, params githubv4.RequestReviewsInput) error { var mutation struct { RequestReviews struct { PullRequest struct { ID string } } `graphql:"requestReviews(input: $input)"` } variables := map[string]interface{}{"input": params} err := client.Mutate(repo.RepoHost(), "PullRequestUpdateRequestReviews", &mutation, variables) return err } func isBlank(v interface{}) bool { switch vv := v.(type) { case string: return vv == "" case []string: return len(vv) == 0 default: return true } } func PullRequestClose(httpClient *http.Client, repo ghrepo.Interface, prID string) error { var mutation struct { ClosePullRequest struct { PullRequest struct { ID githubv4.ID } } `graphql:"closePullRequest(input: $input)"` } variables := map[string]interface{}{ "input": githubv4.ClosePullRequestInput{ PullRequestID: prID, }, } client := NewClientFromHTTP(httpClient) return client.Mutate(repo.RepoHost(), "PullRequestClose", &mutation, variables) } func PullRequestReopen(httpClient *http.Client, repo ghrepo.Interface, prID string) error { var mutation struct { ReopenPullRequest struct { PullRequest struct { ID githubv4.ID } } `graphql:"reopenPullRequest(input: $input)"` } variables := map[string]interface{}{ "input": githubv4.ReopenPullRequestInput{ PullRequestID: prID, }, } client := NewClientFromHTTP(httpClient) return client.Mutate(repo.RepoHost(), "PullRequestReopen", &mutation, variables) } func PullRequestReady(client *Client, repo ghrepo.Interface, pr *PullRequest) error { var mutation struct { MarkPullRequestReadyForReview struct { PullRequest struct { ID githubv4.ID } } `graphql:"markPullRequestReadyForReview(input: $input)"` } variables := map[string]interface{}{ "input": githubv4.MarkPullRequestReadyForReviewInput{ PullRequestID: pr.ID, }, } return client.Mutate(repo.RepoHost(), "PullRequestReadyForReview", &mutation, variables) } func BranchDeleteRemote(client *Client, repo ghrepo.Interface, branch string) error { path := fmt.Sprintf("repos/%s/%s/git/refs/heads/%s", repo.RepoOwner(), repo.RepoName(), url.PathEscape(branch)) return client.REST(repo.RepoHost(), "DELETE", path, nil, nil) }