github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/api/queries_pr.go (about)

     1  package api
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/cli/cli/internal/ghinstance"
    13  	"github.com/cli/cli/internal/ghrepo"
    14  	"github.com/cli/cli/pkg/set"
    15  	"github.com/shurcooL/githubv4"
    16  	"golang.org/x/sync/errgroup"
    17  )
    18  
    19  type PullRequestsPayload struct {
    20  	ViewerCreated   PullRequestAndTotalCount
    21  	ReviewRequested PullRequestAndTotalCount
    22  	CurrentPR       *PullRequest
    23  	DefaultBranch   string
    24  }
    25  
    26  type PullRequestAndTotalCount struct {
    27  	TotalCount   int
    28  	PullRequests []PullRequest
    29  }
    30  
    31  type PullRequest struct {
    32  	ID               string
    33  	Number           int
    34  	Title            string
    35  	State            string
    36  	Closed           bool
    37  	URL              string
    38  	BaseRefName      string
    39  	HeadRefName      string
    40  	Body             string
    41  	Mergeable        string
    42  	Additions        int
    43  	Deletions        int
    44  	ChangedFiles     int
    45  	MergeStateStatus string
    46  	CreatedAt        time.Time
    47  	UpdatedAt        time.Time
    48  	ClosedAt         *time.Time
    49  	MergedAt         *time.Time
    50  
    51  	MergeCommit          *Commit
    52  	PotentialMergeCommit *Commit
    53  
    54  	Files struct {
    55  		Nodes []PullRequestFile
    56  	}
    57  
    58  	Author              Author
    59  	MergedBy            *Author
    60  	HeadRepositoryOwner Owner
    61  	HeadRepository      *PRRepository
    62  	IsCrossRepository   bool
    63  	IsDraft             bool
    64  	MaintainerCanModify bool
    65  
    66  	BaseRef struct {
    67  		BranchProtectionRule struct {
    68  			RequiresStrictStatusChecks bool
    69  		}
    70  	}
    71  
    72  	ReviewDecision string
    73  
    74  	Commits struct {
    75  		TotalCount int
    76  		Nodes      []PullRequestCommit
    77  	}
    78  	StatusCheckRollup struct {
    79  		Nodes []struct {
    80  			Commit struct {
    81  				StatusCheckRollup struct {
    82  					Contexts struct {
    83  						Nodes []struct {
    84  							TypeName    string    `json:"__typename"`
    85  							Name        string    `json:"name"`
    86  							Context     string    `json:"context,omitempty"`
    87  							State       string    `json:"state,omitempty"`
    88  							Status      string    `json:"status"`
    89  							Conclusion  string    `json:"conclusion"`
    90  							StartedAt   time.Time `json:"startedAt"`
    91  							CompletedAt time.Time `json:"completedAt"`
    92  							DetailsURL  string    `json:"detailsUrl"`
    93  							TargetURL   string    `json:"targetUrl,omitempty"`
    94  						}
    95  						PageInfo struct {
    96  							HasNextPage bool
    97  							EndCursor   string
    98  						}
    99  					}
   100  				}
   101  			}
   102  		}
   103  	}
   104  
   105  	Assignees      Assignees
   106  	Labels         Labels
   107  	ProjectCards   ProjectCards
   108  	Milestone      *Milestone
   109  	Comments       Comments
   110  	ReactionGroups ReactionGroups
   111  	Reviews        PullRequestReviews
   112  	ReviewRequests ReviewRequests
   113  }
   114  
   115  type PRRepository struct {
   116  	ID   string `json:"id"`
   117  	Name string `json:"name"`
   118  }
   119  
   120  // Commit loads just the commit SHA and nothing else
   121  type Commit struct {
   122  	OID string `json:"oid"`
   123  }
   124  
   125  type PullRequestCommit struct {
   126  	Commit PullRequestCommitCommit
   127  }
   128  
   129  // PullRequestCommitCommit contains full information about a commit
   130  type PullRequestCommitCommit struct {
   131  	OID     string `json:"oid"`
   132  	Authors struct {
   133  		Nodes []struct {
   134  			Name  string
   135  			Email string
   136  			User  GitHubUser
   137  		}
   138  	}
   139  	MessageHeadline string
   140  	MessageBody     string
   141  	CommittedDate   time.Time
   142  	AuthoredDate    time.Time
   143  }
   144  
   145  type PullRequestFile struct {
   146  	Path      string `json:"path"`
   147  	Additions int    `json:"additions"`
   148  	Deletions int    `json:"deletions"`
   149  }
   150  
   151  type ReviewRequests struct {
   152  	Nodes []struct {
   153  		RequestedReviewer RequestedReviewer
   154  	}
   155  }
   156  
   157  type RequestedReviewer struct {
   158  	TypeName     string `json:"__typename"`
   159  	Login        string `json:"login"`
   160  	Name         string `json:"name"`
   161  	Slug         string `json:"slug"`
   162  	Organization struct {
   163  		Login string `json:"login"`
   164  	} `json:"organization"`
   165  }
   166  
   167  func (r RequestedReviewer) LoginOrSlug() string {
   168  	if r.TypeName == teamTypeName {
   169  		return fmt.Sprintf("%s/%s", r.Organization.Login, r.Slug)
   170  	}
   171  	return r.Login
   172  }
   173  
   174  const teamTypeName = "Team"
   175  
   176  func (r ReviewRequests) Logins() []string {
   177  	logins := make([]string, len(r.Nodes))
   178  	for i, r := range r.Nodes {
   179  		logins[i] = r.RequestedReviewer.LoginOrSlug()
   180  	}
   181  	return logins
   182  }
   183  
   184  func (pr PullRequest) HeadLabel() string {
   185  	if pr.IsCrossRepository {
   186  		return fmt.Sprintf("%s:%s", pr.HeadRepositoryOwner.Login, pr.HeadRefName)
   187  	}
   188  	return pr.HeadRefName
   189  }
   190  
   191  func (pr PullRequest) Link() string {
   192  	return pr.URL
   193  }
   194  
   195  func (pr PullRequest) Identifier() string {
   196  	return pr.ID
   197  }
   198  
   199  func (pr PullRequest) IsOpen() bool {
   200  	return pr.State == "OPEN"
   201  }
   202  
   203  type PullRequestReviewStatus struct {
   204  	ChangesRequested bool
   205  	Approved         bool
   206  	ReviewRequired   bool
   207  }
   208  
   209  func (pr *PullRequest) ReviewStatus() PullRequestReviewStatus {
   210  	var status PullRequestReviewStatus
   211  	switch pr.ReviewDecision {
   212  	case "CHANGES_REQUESTED":
   213  		status.ChangesRequested = true
   214  	case "APPROVED":
   215  		status.Approved = true
   216  	case "REVIEW_REQUIRED":
   217  		status.ReviewRequired = true
   218  	}
   219  	return status
   220  }
   221  
   222  type PullRequestChecksStatus struct {
   223  	Pending int
   224  	Failing int
   225  	Passing int
   226  	Total   int
   227  }
   228  
   229  func (pr *PullRequest) ChecksStatus() (summary PullRequestChecksStatus) {
   230  	if len(pr.StatusCheckRollup.Nodes) == 0 {
   231  		return
   232  	}
   233  	commit := pr.StatusCheckRollup.Nodes[0].Commit
   234  	for _, c := range commit.StatusCheckRollup.Contexts.Nodes {
   235  		state := c.State // StatusContext
   236  		if state == "" {
   237  			// CheckRun
   238  			if c.Status == "COMPLETED" {
   239  				state = c.Conclusion
   240  			} else {
   241  				state = c.Status
   242  			}
   243  		}
   244  		switch state {
   245  		case "SUCCESS", "NEUTRAL", "SKIPPED":
   246  			summary.Passing++
   247  		case "ERROR", "FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED":
   248  			summary.Failing++
   249  		default: // "EXPECTED", "REQUESTED", "WAITING", "QUEUED", "PENDING", "IN_PROGRESS", "STALE"
   250  			summary.Pending++
   251  		}
   252  		summary.Total++
   253  	}
   254  	return
   255  }
   256  
   257  func (pr *PullRequest) DisplayableReviews() PullRequestReviews {
   258  	published := []PullRequestReview{}
   259  	for _, prr := range pr.Reviews.Nodes {
   260  		//Dont display pending reviews
   261  		//Dont display commenting reviews without top level comment body
   262  		if prr.State != "PENDING" && !(prr.State == "COMMENTED" && prr.Body == "") {
   263  			published = append(published, prr)
   264  		}
   265  	}
   266  	return PullRequestReviews{Nodes: published, TotalCount: len(published)}
   267  }
   268  
   269  func (c Client) PullRequestDiff(baseRepo ghrepo.Interface, prNumber int) (io.ReadCloser, error) {
   270  	url := fmt.Sprintf("%srepos/%s/pulls/%d",
   271  		ghinstance.RESTPrefix(baseRepo.RepoHost()), ghrepo.FullName(baseRepo), prNumber)
   272  	req, err := http.NewRequest("GET", url, nil)
   273  	if err != nil {
   274  		return nil, err
   275  	}
   276  
   277  	req.Header.Set("Accept", "application/vnd.github.v3.diff; charset=utf-8")
   278  
   279  	resp, err := c.http.Do(req)
   280  	if err != nil {
   281  		return nil, err
   282  	}
   283  
   284  	if resp.StatusCode == 404 {
   285  		return nil, errors.New("pull request not found")
   286  	} else if resp.StatusCode != 200 {
   287  		return nil, HandleHTTPError(resp)
   288  	}
   289  
   290  	return resp.Body, nil
   291  }
   292  
   293  type pullRequestFeature struct {
   294  	HasReviewDecision       bool
   295  	HasStatusCheckRollup    bool
   296  	HasBranchProtectionRule bool
   297  }
   298  
   299  func determinePullRequestFeatures(httpClient *http.Client, hostname string) (prFeatures pullRequestFeature, err error) {
   300  	if !ghinstance.IsEnterprise(hostname) {
   301  		prFeatures.HasReviewDecision = true
   302  		prFeatures.HasStatusCheckRollup = true
   303  		prFeatures.HasBranchProtectionRule = true
   304  		return
   305  	}
   306  
   307  	var featureDetection struct {
   308  		PullRequest struct {
   309  			Fields []struct {
   310  				Name string
   311  			} `graphql:"fields(includeDeprecated: true)"`
   312  		} `graphql:"PullRequest: __type(name: \"PullRequest\")"`
   313  		Commit struct {
   314  			Fields []struct {
   315  				Name string
   316  			} `graphql:"fields(includeDeprecated: true)"`
   317  		} `graphql:"Commit: __type(name: \"Commit\")"`
   318  	}
   319  
   320  	// needs to be a separate query because the backend only supports 2 `__type` expressions in one query
   321  	var featureDetection2 struct {
   322  		Ref struct {
   323  			Fields []struct {
   324  				Name string
   325  			} `graphql:"fields(includeDeprecated: true)"`
   326  		} `graphql:"Ref: __type(name: \"Ref\")"`
   327  	}
   328  
   329  	v4 := graphQLClient(httpClient, hostname)
   330  
   331  	g := new(errgroup.Group)
   332  	g.Go(func() error {
   333  		return v4.QueryNamed(context.Background(), "PullRequest_fields", &featureDetection, nil)
   334  	})
   335  	g.Go(func() error {
   336  		return v4.QueryNamed(context.Background(), "PullRequest_fields2", &featureDetection2, nil)
   337  	})
   338  
   339  	err = g.Wait()
   340  	if err != nil {
   341  		return
   342  	}
   343  
   344  	for _, field := range featureDetection.PullRequest.Fields {
   345  		switch field.Name {
   346  		case "reviewDecision":
   347  			prFeatures.HasReviewDecision = true
   348  		}
   349  	}
   350  	for _, field := range featureDetection.Commit.Fields {
   351  		switch field.Name {
   352  		case "statusCheckRollup":
   353  			prFeatures.HasStatusCheckRollup = true
   354  		}
   355  	}
   356  	for _, field := range featureDetection2.Ref.Fields {
   357  		switch field.Name {
   358  		case "branchProtectionRule":
   359  			prFeatures.HasBranchProtectionRule = true
   360  		}
   361  	}
   362  	return
   363  }
   364  
   365  type StatusOptions struct {
   366  	CurrentPR int
   367  	HeadRef   string
   368  	Username  string
   369  	Fields    []string
   370  }
   371  
   372  func PullRequestStatus(client *Client, repo ghrepo.Interface, options StatusOptions) (*PullRequestsPayload, error) {
   373  	type edges struct {
   374  		TotalCount int
   375  		Edges      []struct {
   376  			Node PullRequest
   377  		}
   378  	}
   379  
   380  	type response struct {
   381  		Repository struct {
   382  			DefaultBranchRef struct {
   383  				Name string
   384  			}
   385  			PullRequests edges
   386  			PullRequest  *PullRequest
   387  		}
   388  		ViewerCreated   edges
   389  		ReviewRequested edges
   390  	}
   391  
   392  	var fragments string
   393  	if len(options.Fields) > 0 {
   394  		fields := set.NewStringSet()
   395  		fields.AddValues(options.Fields)
   396  		// these are always necessary to find the PR for the current branch
   397  		fields.AddValues([]string{"isCrossRepository", "headRepositoryOwner", "headRefName"})
   398  		gr := PullRequestGraphQL(fields.ToSlice())
   399  		fragments = fmt.Sprintf("fragment pr on PullRequest{%s}fragment prWithReviews on PullRequest{...pr}", gr)
   400  	} else {
   401  		var err error
   402  		fragments, err = pullRequestFragment(client.http, repo.RepoHost())
   403  		if err != nil {
   404  			return nil, err
   405  		}
   406  	}
   407  
   408  	queryPrefix := `
   409  	query PullRequestStatus($owner: String!, $repo: String!, $headRefName: String!, $viewerQuery: String!, $reviewerQuery: String!, $per_page: Int = 10) {
   410  		repository(owner: $owner, name: $repo) {
   411  			defaultBranchRef {
   412  				name
   413  			}
   414  			pullRequests(headRefName: $headRefName, first: $per_page, orderBy: { field: CREATED_AT, direction: DESC }) {
   415  				totalCount
   416  				edges {
   417  					node {
   418  						...prWithReviews
   419  					}
   420  				}
   421  			}
   422  		}
   423  	`
   424  	if options.CurrentPR > 0 {
   425  		queryPrefix = `
   426  		query PullRequestStatus($owner: String!, $repo: String!, $number: Int!, $viewerQuery: String!, $reviewerQuery: String!, $per_page: Int = 10) {
   427  			repository(owner: $owner, name: $repo) {
   428  				defaultBranchRef {
   429  					name
   430  				}
   431  				pullRequest(number: $number) {
   432  					...prWithReviews
   433  				}
   434  			}
   435  		`
   436  	}
   437  
   438  	query := fragments + queryPrefix + `
   439        viewerCreated: search(query: $viewerQuery, type: ISSUE, first: $per_page) {
   440         totalCount: issueCount
   441          edges {
   442            node {
   443              ...prWithReviews
   444            }
   445          }
   446        }
   447        reviewRequested: search(query: $reviewerQuery, type: ISSUE, first: $per_page) {
   448          totalCount: issueCount
   449          edges {
   450            node {
   451              ...pr
   452            }
   453          }
   454        }
   455      }
   456  	`
   457  
   458  	currentUsername := options.Username
   459  	if currentUsername == "@me" && ghinstance.IsEnterprise(repo.RepoHost()) {
   460  		var err error
   461  		currentUsername, err = CurrentLoginName(client, repo.RepoHost())
   462  		if err != nil {
   463  			return nil, err
   464  		}
   465  	}
   466  
   467  	viewerQuery := fmt.Sprintf("repo:%s state:open is:pr author:%s", ghrepo.FullName(repo), currentUsername)
   468  	reviewerQuery := fmt.Sprintf("repo:%s state:open review-requested:%s", ghrepo.FullName(repo), currentUsername)
   469  
   470  	currentPRHeadRef := options.HeadRef
   471  	branchWithoutOwner := currentPRHeadRef
   472  	if idx := strings.Index(currentPRHeadRef, ":"); idx >= 0 {
   473  		branchWithoutOwner = currentPRHeadRef[idx+1:]
   474  	}
   475  
   476  	variables := map[string]interface{}{
   477  		"viewerQuery":   viewerQuery,
   478  		"reviewerQuery": reviewerQuery,
   479  		"owner":         repo.RepoOwner(),
   480  		"repo":          repo.RepoName(),
   481  		"headRefName":   branchWithoutOwner,
   482  		"number":        options.CurrentPR,
   483  	}
   484  
   485  	var resp response
   486  	err := client.GraphQL(repo.RepoHost(), query, variables, &resp)
   487  	if err != nil {
   488  		return nil, err
   489  	}
   490  
   491  	var viewerCreated []PullRequest
   492  	for _, edge := range resp.ViewerCreated.Edges {
   493  		viewerCreated = append(viewerCreated, edge.Node)
   494  	}
   495  
   496  	var reviewRequested []PullRequest
   497  	for _, edge := range resp.ReviewRequested.Edges {
   498  		reviewRequested = append(reviewRequested, edge.Node)
   499  	}
   500  
   501  	var currentPR = resp.Repository.PullRequest
   502  	if currentPR == nil {
   503  		for _, edge := range resp.Repository.PullRequests.Edges {
   504  			if edge.Node.HeadLabel() == currentPRHeadRef {
   505  				currentPR = &edge.Node
   506  				break // Take the most recent PR for the current branch
   507  			}
   508  		}
   509  	}
   510  
   511  	payload := PullRequestsPayload{
   512  		ViewerCreated: PullRequestAndTotalCount{
   513  			PullRequests: viewerCreated,
   514  			TotalCount:   resp.ViewerCreated.TotalCount,
   515  		},
   516  		ReviewRequested: PullRequestAndTotalCount{
   517  			PullRequests: reviewRequested,
   518  			TotalCount:   resp.ReviewRequested.TotalCount,
   519  		},
   520  		CurrentPR:     currentPR,
   521  		DefaultBranch: resp.Repository.DefaultBranchRef.Name,
   522  	}
   523  
   524  	return &payload, nil
   525  }
   526  
   527  func pullRequestFragment(httpClient *http.Client, hostname string) (string, error) {
   528  	cachedClient := NewCachedClient(httpClient, time.Hour*24)
   529  	prFeatures, err := determinePullRequestFeatures(cachedClient, hostname)
   530  	if err != nil {
   531  		return "", err
   532  	}
   533  
   534  	fields := []string{
   535  		"number", "title", "state", "url", "isDraft", "isCrossRepository",
   536  		"headRefName", "headRepositoryOwner", "mergeStateStatus",
   537  	}
   538  	if prFeatures.HasStatusCheckRollup {
   539  		fields = append(fields, "statusCheckRollup")
   540  	}
   541  	if prFeatures.HasBranchProtectionRule {
   542  		fields = append(fields, "requiresStrictStatusChecks")
   543  	}
   544  
   545  	var reviewFields []string
   546  	if prFeatures.HasReviewDecision {
   547  		reviewFields = append(reviewFields, "reviewDecision")
   548  	}
   549  
   550  	fragments := fmt.Sprintf(`
   551  	fragment pr on PullRequest {%s}
   552  	fragment prWithReviews on PullRequest {...pr,%s}
   553  	`, PullRequestGraphQL(fields), PullRequestGraphQL(reviewFields))
   554  	return fragments, nil
   555  }
   556  
   557  // CreatePullRequest creates a pull request in a GitHub repository
   558  func CreatePullRequest(client *Client, repo *Repository, params map[string]interface{}) (*PullRequest, error) {
   559  	query := `
   560  		mutation PullRequestCreate($input: CreatePullRequestInput!) {
   561  			createPullRequest(input: $input) {
   562  				pullRequest {
   563  					id
   564  					url
   565  				}
   566  			}
   567  	}`
   568  
   569  	inputParams := map[string]interface{}{
   570  		"repositoryId": repo.ID,
   571  	}
   572  	for key, val := range params {
   573  		switch key {
   574  		case "title", "body", "draft", "baseRefName", "headRefName", "maintainerCanModify":
   575  			inputParams[key] = val
   576  		}
   577  	}
   578  	variables := map[string]interface{}{
   579  		"input": inputParams,
   580  	}
   581  
   582  	result := struct {
   583  		CreatePullRequest struct {
   584  			PullRequest PullRequest
   585  		}
   586  	}{}
   587  
   588  	err := client.GraphQL(repo.RepoHost(), query, variables, &result)
   589  	if err != nil {
   590  		return nil, err
   591  	}
   592  	pr := &result.CreatePullRequest.PullRequest
   593  
   594  	// metadata parameters aren't currently available in `createPullRequest`,
   595  	// but they are in `updatePullRequest`
   596  	updateParams := make(map[string]interface{})
   597  	for key, val := range params {
   598  		switch key {
   599  		case "assigneeIds", "labelIds", "projectIds", "milestoneId":
   600  			if !isBlank(val) {
   601  				updateParams[key] = val
   602  			}
   603  		}
   604  	}
   605  	if len(updateParams) > 0 {
   606  		updateQuery := `
   607  		mutation PullRequestCreateMetadata($input: UpdatePullRequestInput!) {
   608  			updatePullRequest(input: $input) { clientMutationId }
   609  		}`
   610  		updateParams["pullRequestId"] = pr.ID
   611  		variables := map[string]interface{}{
   612  			"input": updateParams,
   613  		}
   614  		err := client.GraphQL(repo.RepoHost(), updateQuery, variables, &result)
   615  		if err != nil {
   616  			return pr, err
   617  		}
   618  	}
   619  
   620  	// reviewers are requested in yet another additional mutation
   621  	reviewParams := make(map[string]interface{})
   622  	if ids, ok := params["userReviewerIds"]; ok && !isBlank(ids) {
   623  		reviewParams["userIds"] = ids
   624  	}
   625  	if ids, ok := params["teamReviewerIds"]; ok && !isBlank(ids) {
   626  		reviewParams["teamIds"] = ids
   627  	}
   628  
   629  	//TODO: How much work to extract this into own method and use for create and edit?
   630  	if len(reviewParams) > 0 {
   631  		reviewQuery := `
   632  		mutation PullRequestCreateRequestReviews($input: RequestReviewsInput!) {
   633  			requestReviews(input: $input) { clientMutationId }
   634  		}`
   635  		reviewParams["pullRequestId"] = pr.ID
   636  		reviewParams["union"] = true
   637  		variables := map[string]interface{}{
   638  			"input": reviewParams,
   639  		}
   640  		err := client.GraphQL(repo.RepoHost(), reviewQuery, variables, &result)
   641  		if err != nil {
   642  			return pr, err
   643  		}
   644  	}
   645  
   646  	return pr, nil
   647  }
   648  
   649  func UpdatePullRequest(client *Client, repo ghrepo.Interface, params githubv4.UpdatePullRequestInput) error {
   650  	var mutation struct {
   651  		UpdatePullRequest struct {
   652  			PullRequest struct {
   653  				ID string
   654  			}
   655  		} `graphql:"updatePullRequest(input: $input)"`
   656  	}
   657  	variables := map[string]interface{}{"input": params}
   658  	gql := graphQLClient(client.http, repo.RepoHost())
   659  	err := gql.MutateNamed(context.Background(), "PullRequestUpdate", &mutation, variables)
   660  	return err
   661  }
   662  
   663  func UpdatePullRequestReviews(client *Client, repo ghrepo.Interface, params githubv4.RequestReviewsInput) error {
   664  	var mutation struct {
   665  		RequestReviews struct {
   666  			PullRequest struct {
   667  				ID string
   668  			}
   669  		} `graphql:"requestReviews(input: $input)"`
   670  	}
   671  	variables := map[string]interface{}{"input": params}
   672  	gql := graphQLClient(client.http, repo.RepoHost())
   673  	err := gql.MutateNamed(context.Background(), "PullRequestUpdateRequestReviews", &mutation, variables)
   674  	return err
   675  }
   676  
   677  func isBlank(v interface{}) bool {
   678  	switch vv := v.(type) {
   679  	case string:
   680  		return vv == ""
   681  	case []string:
   682  		return len(vv) == 0
   683  	default:
   684  		return true
   685  	}
   686  }
   687  
   688  func PullRequestClose(client *Client, repo ghrepo.Interface, pr *PullRequest) error {
   689  	var mutation struct {
   690  		ClosePullRequest struct {
   691  			PullRequest struct {
   692  				ID githubv4.ID
   693  			}
   694  		} `graphql:"closePullRequest(input: $input)"`
   695  	}
   696  
   697  	variables := map[string]interface{}{
   698  		"input": githubv4.ClosePullRequestInput{
   699  			PullRequestID: pr.ID,
   700  		},
   701  	}
   702  
   703  	gql := graphQLClient(client.http, repo.RepoHost())
   704  	err := gql.MutateNamed(context.Background(), "PullRequestClose", &mutation, variables)
   705  
   706  	return err
   707  }
   708  
   709  func PullRequestReopen(client *Client, repo ghrepo.Interface, pr *PullRequest) error {
   710  	var mutation struct {
   711  		ReopenPullRequest struct {
   712  			PullRequest struct {
   713  				ID githubv4.ID
   714  			}
   715  		} `graphql:"reopenPullRequest(input: $input)"`
   716  	}
   717  
   718  	variables := map[string]interface{}{
   719  		"input": githubv4.ReopenPullRequestInput{
   720  			PullRequestID: pr.ID,
   721  		},
   722  	}
   723  
   724  	gql := graphQLClient(client.http, repo.RepoHost())
   725  	err := gql.MutateNamed(context.Background(), "PullRequestReopen", &mutation, variables)
   726  
   727  	return err
   728  }
   729  
   730  func PullRequestReady(client *Client, repo ghrepo.Interface, pr *PullRequest) error {
   731  	var mutation struct {
   732  		MarkPullRequestReadyForReview struct {
   733  			PullRequest struct {
   734  				ID githubv4.ID
   735  			}
   736  		} `graphql:"markPullRequestReadyForReview(input: $input)"`
   737  	}
   738  
   739  	variables := map[string]interface{}{
   740  		"input": githubv4.MarkPullRequestReadyForReviewInput{
   741  			PullRequestID: pr.ID,
   742  		},
   743  	}
   744  
   745  	gql := graphQLClient(client.http, repo.RepoHost())
   746  	return gql.MutateNamed(context.Background(), "PullRequestReadyForReview", &mutation, variables)
   747  }
   748  
   749  func BranchDeleteRemote(client *Client, repo ghrepo.Interface, branch string) error {
   750  	path := fmt.Sprintf("repos/%s/%s/git/refs/heads/%s", repo.RepoOwner(), repo.RepoName(), branch)
   751  	return client.REST(repo.RepoHost(), "DELETE", path, nil, nil)
   752  }