package api
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"sort"
"strings"
"time"
"github.com/cli/cli/internal/ghinstance"
"github.com/cli/cli/internal/ghrepo"
"github.com/shurcooL/githubv4"
"golang.org/x/sync/errgroup"
)
type PullRequestsPayload struct {
ViewerCreated PullRequestAndTotalCount
ReviewRequested PullRequestAndTotalCount
CurrentPR *PullRequest
DefaultBranch string
}
type PullRequestAndTotalCount struct {
TotalCount int
PullRequests []PullRequest
}
type PullRequest struct {
ID string
Number int
Title string
State string
Closed bool
URL string
BaseRefName string
HeadRefName string
Body string
Mergeable string
MergeStateStatus string
Author struct {
Login string
}
HeadRepositoryOwner struct {
Login string
}
HeadRepository struct {
Name string
DefaultBranchRef struct {
Name string
}
}
IsCrossRepository bool
IsDraft bool
MaintainerCanModify bool
BaseRef struct {
BranchProtectionRule struct {
RequiresStrictStatusChecks bool
}
}
ReviewDecision string
Commits struct {
TotalCount int
Nodes []struct {
Commit struct {
Oid string
StatusCheckRollup struct {
Contexts struct {
Nodes []struct {
Name string
Context string
State string
Status string
Conclusion string
StartedAt time.Time
CompletedAt time.Time
DetailsURL string
TargetURL string
}
}
}
}
}
}
Assignees Assignees
Labels Labels
ProjectCards ProjectCards
Milestone Milestone
Comments Comments
ReactionGroups ReactionGroups
Reviews PullRequestReviews
ReviewRequests ReviewRequests
}
type ReviewRequests struct {
Nodes []struct {
RequestedReviewer struct {
TypeName string `json:"__typename"`
Login string
Name string
}
}
TotalCount int
}
func (r ReviewRequests) Logins() []string {
logins := make([]string, len(r.Nodes))
for i, a := range r.Nodes {
logins[i] = a.RequestedReviewer.Login
}
return logins
}
type NotFoundError struct {
error
}
func (err *NotFoundError) Unwrap() error {
return err.error
}
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
}
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.Commits.Nodes) == 0 {
return
}
commit := pr.Commits.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++
case "EXPECTED", "REQUESTED", "QUEUED", "PENDING", "IN_PROGRESS", "STALE":
summary.Pending++
default:
panic(fmt.Errorf("unsupported status: %q", state))
}
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)}
}
func (c Client) PullRequestDiff(baseRepo ghrepo.Interface, prNumber int) (io.ReadCloser, error) {
url := fmt.Sprintf("%srepos/%s/pulls/%d",
ghinstance.RESTPrefix(baseRepo.RepoHost()), ghrepo.FullName(baseRepo), prNumber)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/vnd.github.v3.diff; charset=utf-8")
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode == 404 {
return nil, &NotFoundError{errors.New("pull request not found")}
} else if resp.StatusCode != 200 {
return nil, HandleHTTPError(resp)
}
return resp.Body, nil
}
type pullRequestFeature struct {
HasReviewDecision bool
HasStatusCheckRollup bool
HasBranchProtectionRule bool
}
func determinePullRequestFeatures(httpClient *http.Client, hostname string) (prFeatures pullRequestFeature, err error) {
if !ghinstance.IsEnterprise(hostname) {
prFeatures.HasReviewDecision = true
prFeatures.HasStatusCheckRollup = true
prFeatures.HasBranchProtectionRule = true
return
}
var featureDetection struct {
PullRequest struct {
Fields []struct {
Name string
} `graphql:"fields(includeDeprecated: true)"`
} `graphql:"PullRequest: __type(name: \"PullRequest\")"`
Commit struct {
Fields []struct {
Name string
} `graphql:"fields(includeDeprecated: true)"`
} `graphql:"Commit: __type(name: \"Commit\")"`
}
// needs to be a separate query because the backend only supports 2 `__type` expressions in one query
var featureDetection2 struct {
Ref struct {
Fields []struct {
Name string
} `graphql:"fields(includeDeprecated: true)"`
} `graphql:"Ref: __type(name: \"Ref\")"`
}
v4 := graphQLClient(httpClient, hostname)
g := new(errgroup.Group)
g.Go(func() error {
return v4.QueryNamed(context.Background(), "PullRequest_fields", &featureDetection, nil)
})
g.Go(func() error {
return v4.QueryNamed(context.Background(), "PullRequest_fields2", &featureDetection2, nil)
})
err = g.Wait()
if err != nil {
return
}
for _, field := range featureDetection.PullRequest.Fields {
switch field.Name {
case "reviewDecision":
prFeatures.HasReviewDecision = true
}
}
for _, field := range featureDetection.Commit.Fields {
switch field.Name {
case "statusCheckRollup":
prFeatures.HasStatusCheckRollup = true
}
}
for _, field := range featureDetection2.Ref.Fields {
switch field.Name {
case "branchProtectionRule":
prFeatures.HasBranchProtectionRule = true
}
}
return
}
func PullRequests(client *Client, repo ghrepo.Interface, currentPRNumber int, currentPRHeadRef, currentUsername string) (*PullRequestsPayload, error) {
type edges struct {
TotalCount int
Edges []struct {
Node PullRequest
}
}
type response struct {
Repository struct {
DefaultBranchRef struct {
Name string
}
PullRequests edges
PullRequest *PullRequest
}
ViewerCreated edges
ReviewRequested edges
}
cachedClient := NewCachedClient(client.http, time.Hour*24)
prFeatures, err := determinePullRequestFeatures(cachedClient, repo.RepoHost())
if err != nil {
return nil, err
}
var reviewsFragment string
if prFeatures.HasReviewDecision {
reviewsFragment = "reviewDecision"
}
var statusesFragment string
if prFeatures.HasStatusCheckRollup {
statusesFragment = `
commits(last: 1) {
nodes {
commit {
statusCheckRollup {
contexts(last: 100) {
nodes {
...on StatusContext {
state
}
...on CheckRun {
conclusion
status
}
}
}
}
}
}
}
`
}
var requiresStrictStatusChecks string
if prFeatures.HasBranchProtectionRule {
requiresStrictStatusChecks = `
baseRef {
branchProtectionRule {
requiresStrictStatusChecks
}
}`
}
fragments := fmt.Sprintf(`
fragment pr on PullRequest {
number
title
state
url
headRefName
mergeStateStatus
headRepositoryOwner {
login
}
%s
isCrossRepository
isDraft
%s
}
fragment prWithReviews on PullRequest {
...pr
%s
}
`, requiresStrictStatusChecks, statusesFragment, reviewsFragment)
queryPrefix := `
query PullRequestStatus($owner: String!, $repo: String!, $headRefName: String!, $viewerQuery: String!, $reviewerQuery: String!, $per_page: Int = 10) {
repository(owner: $owner, name: $repo) {
defaultBranchRef {
name
}
pullRequests(headRefName: $headRefName, first: $per_page, orderBy: { field: CREATED_AT, direction: DESC }) {
totalCount
edges {
node {
...prWithReviews
}
}
}
}
`
if currentPRNumber > 0 {
queryPrefix = `
query PullRequestStatus($owner: String!, $repo: String!, $number: Int!, $viewerQuery: String!, $reviewerQuery: String!, $per_page: Int = 10) {
repository(owner: $owner, name: $repo) {
defaultBranchRef {
name
}
pullRequest(number: $number) {
...prWithReviews
}
}
`
}
query := fragments + queryPrefix + `
viewerCreated: search(query: $viewerQuery, type: ISSUE, first: $per_page) {
totalCount: issueCount
edges {
node {
...prWithReviews
}
}
}
reviewRequested: search(query: $reviewerQuery, type: ISSUE, first: $per_page) {
totalCount: issueCount
edges {
node {
...pr
}
}
}
}
`
if currentUsername == "@me" && ghinstance.IsEnterprise(repo.RepoHost()) {
currentUsername, err = CurrentLoginName(client, repo.RepoHost())
if err != nil {
return nil, err
}
}
viewerQuery := fmt.Sprintf("repo:%s state:open is:pr author:%s", ghrepo.FullName(repo), currentUsername)
reviewerQuery := fmt.Sprintf("repo:%s state:open review-requested:%s", ghrepo.FullName(repo), currentUsername)
branchWithoutOwner := currentPRHeadRef
if idx := strings.Index(currentPRHeadRef, ":"); idx >= 0 {
branchWithoutOwner = currentPRHeadRef[idx+1:]
}
variables := map[string]interface{}{
"viewerQuery": viewerQuery,
"reviewerQuery": reviewerQuery,
"owner": repo.RepoOwner(),
"repo": repo.RepoName(),
"headRefName": branchWithoutOwner,
"number": currentPRNumber,
}
var resp response
err = client.GraphQL(repo.RepoHost(), query, variables, &resp)
if err != nil {
return nil, 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 = resp.Repository.PullRequest
if currentPR == nil {
for _, edge := range resp.Repository.PullRequests.Edges {
if edge.Node.HeadLabel() == currentPRHeadRef {
currentPR = &edge.Node
break // Take the most recent PR for the current branch
}
}
}
payload := PullRequestsPayload{
ViewerCreated: PullRequestAndTotalCount{
PullRequests: viewerCreated,
TotalCount: resp.ViewerCreated.TotalCount,
},
ReviewRequested: PullRequestAndTotalCount{
PullRequests: reviewRequested,
TotalCount: resp.ReviewRequested.TotalCount,
},
CurrentPR: currentPR,
DefaultBranch: resp.Repository.DefaultBranchRef.Name,
}
return &payload, nil
}
func prCommitsFragment(httpClient *http.Client, hostname string) (string, error) {
cachedClient := NewCachedClient(httpClient, time.Hour*24)
if prFeatures, err := determinePullRequestFeatures(cachedClient, hostname); err != nil {
return "", err
} else if !prFeatures.HasStatusCheckRollup {
return "", nil
}
return `
commits(last: 1) {
totalCount
nodes {
commit {
oid
statusCheckRollup {
contexts(last: 100) {
nodes {
...on StatusContext {
context
state
targetUrl
}
...on CheckRun {
name
status
conclusion
startedAt
completedAt
detailsUrl
}
}
}
}
}
}
}
`, nil
}
func PullRequestByNumber(client *Client, repo ghrepo.Interface, number int) (*PullRequest, error) {
type response struct {
Repository struct {
PullRequest PullRequest
}
}
statusesFragment, err := prCommitsFragment(client.http, repo.RepoHost())
if err != nil {
return nil, err
}
query := `
query PullRequestByNumber($owner: String!, $repo: String!, $pr_number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $pr_number) {
id
url
number
title
state
closed
body
mergeable
author {
login
}
` + statusesFragment + `
baseRefName
headRefName
headRepositoryOwner {
login
}
headRepository {
name
}
isCrossRepository
isDraft
maintainerCanModify
reviewRequests(first: 100) {
nodes {
requestedReviewer {
__typename
...on User {
login
}
...on Team {
name
}
}
}
totalCount
}
assignees(first: 100) {
nodes {
login
}
totalCount
}
labels(first: 100) {
nodes {
name
}
totalCount
}
projectCards(first: 100) {
nodes {
project {
name
}
column {
name
}
}
totalCount
}
milestone{
title
}
` + commentsFragment() + `
` + reactionGroupsFragment() + `
}
}
}`
variables := map[string]interface{}{
"owner": repo.RepoOwner(),
"repo": repo.RepoName(),
"pr_number": number,
}
var resp response
err = client.GraphQL(repo.RepoHost(), query, variables, &resp)
if err != nil {
return nil, err
}
return &resp.Repository.PullRequest, nil
}
func PullRequestForBranch(client *Client, repo ghrepo.Interface, baseBranch, headBranch string, stateFilters []string) (*PullRequest, error) {
type response struct {
Repository struct {
PullRequests struct {
Nodes []PullRequest
}
}
}
statusesFragment, err := prCommitsFragment(client.http, repo.RepoHost())
if err != nil {
return nil, err
}
query := `
query PullRequestForBranch($owner: String!, $repo: String!, $headRefName: String!, $states: [PullRequestState!]) {
repository(owner: $owner, name: $repo) {
pullRequests(headRefName: $headRefName, states: $states, first: 30, orderBy: { field: CREATED_AT, direction: DESC }) {
nodes {
id
number
title
state
body
mergeable
author {
login
}
` + statusesFragment + `
url
baseRefName
headRefName
headRepositoryOwner {
login
}
headRepository {
name
}
isCrossRepository
isDraft
maintainerCanModify
reviewRequests(first: 100) {
nodes {
requestedReviewer {
__typename
...on User {
login
}
...on Team {
name
}
}
}
totalCount
}
assignees(first: 100) {
nodes {
login
}
totalCount
}
labels(first: 100) {
nodes {
name
}
totalCount
}
projectCards(first: 100) {
nodes {
project {
name
}
column {
name
}
}
totalCount
}
milestone{
title
}
` + commentsFragment() + `
` + reactionGroupsFragment() + `
}
}
}
}`
branchWithoutOwner := headBranch
if idx := strings.Index(headBranch, ":"); idx >= 0 {
branchWithoutOwner = headBranch[idx+1:]
}
variables := map[string]interface{}{
"owner": repo.RepoOwner(),
"repo": repo.RepoName(),
"headRefName": branchWithoutOwner,
"states": stateFilters,
}
var resp response
err = client.GraphQL(repo.RepoHost(), query, variables, &resp)
if err != nil {
return nil, err
}
prs := resp.Repository.PullRequests.Nodes
sortPullRequestsByState(prs)
for _, pr := range prs {
if pr.HeadLabel() == headBranch && (baseBranch == "" || pr.BaseRefName == baseBranch) {
return &pr, nil
}
}
return nil, &NotFoundError{fmt.Errorf("no pull requests found for branch %q", headBranch)}
}
// sortPullRequestsByState sorts a PullRequest slice by open-first
func sortPullRequestsByState(prs []PullRequest) {
sort.SliceStable(prs, func(a, b int) bool {
return prs[a].State == "OPEN"
})
}
// 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 UpdatePullRequest(client *Client, repo ghrepo.Interface, params githubv4.UpdatePullRequestInput) error {
var mutation struct {
UpdatePullRequest struct {
PullRequest struct {
ID string
}
} `graphql:"updatePullRequest(input: $input)"`
}
variables := map[string]interface{}{"input": params}
gql := graphQLClient(client.http, repo.RepoHost())
err := gql.MutateNamed(context.Background(), "PullRequestUpdate", &mutation, variables)
return err
}
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}
gql := graphQLClient(client.http, repo.RepoHost())
err := gql.MutateNamed(context.Background(), "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 PullRequestList(client *Client, repo ghrepo.Interface, vars map[string]interface{}, limit int) (*PullRequestAndTotalCount, error) {
type prBlock struct {
Edges []struct {
Node PullRequest
}
PageInfo struct {
HasNextPage bool
EndCursor string
}
TotalCount int
IssueCount int
}
type response struct {
Repository struct {
PullRequests prBlock
}
Search prBlock
}
fragment := `
fragment pr on PullRequest {
number
title
state
url
headRefName
headRepositoryOwner {
login
}
isCrossRepository
isDraft
}
`
// If assignee wasn't specified, use `Repository.pullRequest` for ability to
// query by multiple labels
query := fragment + `
query PullRequestList(
$owner: String!,
$repo: String!,
$limit: Int!,
$endCursor: String,
$baseBranch: String,
$labels: [String!],
$state: [PullRequestState!] = OPEN
) {
repository(owner: $owner, name: $repo) {
pullRequests(
states: $state,
baseRefName: $baseBranch,
labels: $labels,
first: $limit,
after: $endCursor,
orderBy: {field: CREATED_AT, direction: DESC}
) {
totalCount
edges {
node {
...pr
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}`
var check = make(map[int]struct{})
var prs []PullRequest
pageLimit := min(limit, 100)
variables := map[string]interface{}{}
res := PullRequestAndTotalCount{}
// If assignee was specified, use the `search` API rather than
// `Repository.pullRequests`, but this mode doesn't support multiple labels
if assignee, ok := vars["assignee"].(string); ok {
query = fragment + `
query PullRequestList(
$q: String!,
$limit: Int!,
$endCursor: String,
) {
search(query: $q, type: ISSUE, first: $limit, after: $endCursor) {
issueCount
edges {
node {
...pr
}
}
pageInfo {
hasNextPage
endCursor
}
}
}`
search := []string{
fmt.Sprintf("repo:%s/%s", repo.RepoOwner(), repo.RepoName()),
fmt.Sprintf("assignee:%s", assignee),
"is:pr",
"sort:created-desc",
}
if states, ok := vars["state"].([]string); ok && len(states) == 1 {
switch states[0] {
case "OPEN":
search = append(search, "state:open")
case "CLOSED":
search = append(search, "state:closed")
case "MERGED":
search = append(search, "is:merged")
}
}
if labels, ok := vars["labels"].([]string); ok && len(labels) > 0 {
if len(labels) > 1 {
return nil, fmt.Errorf("multiple labels with --assignee are not supported")
}
search = append(search, fmt.Sprintf(`label:"%s"`, labels[0]))
}
if baseBranch, ok := vars["baseBranch"].(string); ok {
search = append(search, fmt.Sprintf(`base:"%s"`, baseBranch))
}
variables["q"] = strings.Join(search, " ")
} else {
variables["owner"] = repo.RepoOwner()
variables["repo"] = repo.RepoName()
for name, val := range vars {
variables[name] = val
}
}
loop:
for {
variables["limit"] = pageLimit
var data response
err := client.GraphQL(repo.RepoHost(), query, variables, &data)
if err != nil {
return nil, err
}
prData := data.Repository.PullRequests
res.TotalCount = prData.TotalCount
if _, ok := variables["q"]; ok {
prData = data.Search
res.TotalCount = prData.IssueCount
}
for _, edge := range prData.Edges {
if _, exists := check[edge.Node.Number]; exists {
continue
}
prs = append(prs, edge.Node)
check[edge.Node.Number] = struct{}{}
if len(prs) == limit {
break loop
}
}
if prData.PageInfo.HasNextPage {
variables["endCursor"] = prData.PageInfo.EndCursor
pageLimit = min(pageLimit, limit-len(prs))
} else {
break
}
}
res.PullRequests = prs
return &res, nil
}
func PullRequestClose(client *Client, repo ghrepo.Interface, pr *PullRequest) error {
var mutation struct {
ClosePullRequest struct {
PullRequest struct {
ID githubv4.ID
}
} `graphql:"closePullRequest(input: $input)"`
}
variables := map[string]interface{}{
"input": githubv4.ClosePullRequestInput{
PullRequestID: pr.ID,
},
}
gql := graphQLClient(client.http, repo.RepoHost())
err := gql.MutateNamed(context.Background(), "PullRequestClose", &mutation, variables)
return err
}
func PullRequestReopen(client *Client, repo ghrepo.Interface, pr *PullRequest) error {
var mutation struct {
ReopenPullRequest struct {
PullRequest struct {
ID githubv4.ID
}
} `graphql:"reopenPullRequest(input: $input)"`
}
variables := map[string]interface{}{
"input": githubv4.ReopenPullRequestInput{
PullRequestID: pr.ID,
},
}
gql := graphQLClient(client.http, repo.RepoHost())
err := gql.MutateNamed(context.Background(), "PullRequestReopen", &mutation, variables)
return err
}
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,
},
}
gql := graphQLClient(client.http, repo.RepoHost())
return gql.MutateNamed(context.Background(), "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(), branch)
return client.REST(repo.RepoHost(), "DELETE", path, nil, nil)
}
func min(a, b int) int {
if a < b {
return a
}
return b
}