github.com/secman-team/gh-api@v1.8.2/api/queries_pr.go (about) 1 package api 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "net/http" 9 "sort" 10 "strings" 11 "time" 12 13 "github.com/secman-team/gh-api/core/ghinstance" 14 "github.com/secman-team/gh-api/core/ghrepo" 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 MergeStateStatus string 45 46 Author struct { 47 Login string 48 } 49 HeadRepositoryOwner struct { 50 Login string 51 } 52 HeadRepository struct { 53 Name string 54 DefaultBranchRef struct { 55 Name string 56 } 57 } 58 IsCrossRepository bool 59 IsDraft bool 60 MaintainerCanModify bool 61 62 BaseRef struct { 63 BranchProtectionRule struct { 64 RequiresStrictStatusChecks bool 65 } 66 } 67 68 ReviewDecision string 69 70 Commits struct { 71 TotalCount int 72 Nodes []struct { 73 Commit struct { 74 Oid string 75 StatusCheckRollup struct { 76 Contexts struct { 77 Nodes []struct { 78 Name string 79 Context string 80 State string 81 Status string 82 Conclusion string 83 StartedAt time.Time 84 CompletedAt time.Time 85 DetailsURL string 86 TargetURL string 87 } 88 } 89 } 90 } 91 } 92 } 93 Assignees Assignees 94 Labels Labels 95 ProjectCards ProjectCards 96 Milestone Milestone 97 Comments Comments 98 ReactionGroups ReactionGroups 99 Reviews PullRequestReviews 100 ReviewRequests ReviewRequests 101 } 102 103 type ReviewRequests struct { 104 Nodes []struct { 105 RequestedReviewer struct { 106 TypeName string `json:"__typename"` 107 Login string 108 Name string 109 } 110 } 111 TotalCount int 112 } 113 114 func (r ReviewRequests) Logins() []string { 115 logins := make([]string, len(r.Nodes)) 116 for i, a := range r.Nodes { 117 logins[i] = a.RequestedReviewer.Login 118 } 119 return logins 120 } 121 122 type NotFoundError struct { 123 error 124 } 125 126 func (err *NotFoundError) Unwrap() error { 127 return err.error 128 } 129 130 func (pr PullRequest) HeadLabel() string { 131 if pr.IsCrossRepository { 132 return fmt.Sprintf("%s:%s", pr.HeadRepositoryOwner.Login, pr.HeadRefName) 133 } 134 return pr.HeadRefName 135 } 136 137 func (pr PullRequest) Link() string { 138 return pr.URL 139 } 140 141 func (pr PullRequest) Identifier() string { 142 return pr.ID 143 } 144 145 type PullRequestReviewStatus struct { 146 ChangesRequested bool 147 Approved bool 148 ReviewRequired bool 149 } 150 151 func (pr *PullRequest) ReviewStatus() PullRequestReviewStatus { 152 var status PullRequestReviewStatus 153 switch pr.ReviewDecision { 154 case "CHANGES_REQUESTED": 155 status.ChangesRequested = true 156 case "APPROVED": 157 status.Approved = true 158 case "REVIEW_REQUIRED": 159 status.ReviewRequired = true 160 } 161 return status 162 } 163 164 type PullRequestChecksStatus struct { 165 Pending int 166 Failing int 167 Passing int 168 Total int 169 } 170 171 func (pr *PullRequest) ChecksStatus() (summary PullRequestChecksStatus) { 172 if len(pr.Commits.Nodes) == 0 { 173 return 174 } 175 commit := pr.Commits.Nodes[0].Commit 176 for _, c := range commit.StatusCheckRollup.Contexts.Nodes { 177 state := c.State // StatusContext 178 if state == "" { 179 // CheckRun 180 if c.Status == "COMPLETED" { 181 state = c.Conclusion 182 } else { 183 state = c.Status 184 } 185 } 186 switch state { 187 case "SUCCESS", "NEUTRAL", "SKIPPED": 188 summary.Passing++ 189 case "ERROR", "FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED": 190 summary.Failing++ 191 default: // "EXPECTED", "REQUESTED", "WAITING", "QUEUED", "PENDING", "IN_PROGRESS", "STALE" 192 summary.Pending++ 193 } 194 summary.Total++ 195 } 196 return 197 } 198 199 func (pr *PullRequest) DisplayableReviews() PullRequestReviews { 200 published := []PullRequestReview{} 201 for _, prr := range pr.Reviews.Nodes { 202 //Dont display pending reviews 203 //Dont display commenting reviews without top level comment body 204 if prr.State != "PENDING" && !(prr.State == "COMMENTED" && prr.Body == "") { 205 published = append(published, prr) 206 } 207 } 208 return PullRequestReviews{Nodes: published, TotalCount: len(published)} 209 } 210 211 func (c Client) PullRequestDiff(baseRepo ghrepo.Interface, prNumber int) (io.ReadCloser, error) { 212 url := fmt.Sprintf("%srepos/%s/pulls/%d", 213 ghinstance.RESTPrefix(baseRepo.RepoHost()), ghrepo.FullName(baseRepo), prNumber) 214 req, err := http.NewRequest("GET", url, nil) 215 if err != nil { 216 return nil, err 217 } 218 219 req.Header.Set("Accept", "application/vnd.github.v3.diff; charset=utf-8") 220 221 resp, err := c.http.Do(req) 222 if err != nil { 223 return nil, err 224 } 225 226 if resp.StatusCode == 404 { 227 return nil, &NotFoundError{errors.New("pull request not found")} 228 } else if resp.StatusCode != 200 { 229 return nil, HandleHTTPError(resp) 230 } 231 232 return resp.Body, nil 233 } 234 235 type pullRequestFeature struct { 236 HasReviewDecision bool 237 HasStatusCheckRollup bool 238 HasBranchProtectionRule bool 239 } 240 241 func determinePullRequestFeatures(httpClient *http.Client, hostname string) (prFeatures pullRequestFeature, err error) { 242 if !ghinstance.IsEnterprise(hostname) { 243 prFeatures.HasReviewDecision = true 244 prFeatures.HasStatusCheckRollup = true 245 prFeatures.HasBranchProtectionRule = true 246 return 247 } 248 249 var featureDetection struct { 250 PullRequest struct { 251 Fields []struct { 252 Name string 253 } `graphql:"fields(includeDeprecated: true)"` 254 } `graphql:"PullRequest: __type(name: \"PullRequest\")"` 255 Commit struct { 256 Fields []struct { 257 Name string 258 } `graphql:"fields(includeDeprecated: true)"` 259 } `graphql:"Commit: __type(name: \"Commit\")"` 260 } 261 262 // needs to be a separate query because the backend only supports 2 `__type` expressions in one query 263 var featureDetection2 struct { 264 Ref struct { 265 Fields []struct { 266 Name string 267 } `graphql:"fields(includeDeprecated: true)"` 268 } `graphql:"Ref: __type(name: \"Ref\")"` 269 } 270 271 v4 := graphQLClient(httpClient, hostname) 272 273 g := new(errgroup.Group) 274 g.Go(func() error { 275 return v4.QueryNamed(context.Background(), "PullRequest_fields", &featureDetection, nil) 276 }) 277 g.Go(func() error { 278 return v4.QueryNamed(context.Background(), "PullRequest_fields2", &featureDetection2, nil) 279 }) 280 281 err = g.Wait() 282 if err != nil { 283 return 284 } 285 286 for _, field := range featureDetection.PullRequest.Fields { 287 switch field.Name { 288 case "reviewDecision": 289 prFeatures.HasReviewDecision = true 290 } 291 } 292 for _, field := range featureDetection.Commit.Fields { 293 switch field.Name { 294 case "statusCheckRollup": 295 prFeatures.HasStatusCheckRollup = true 296 } 297 } 298 for _, field := range featureDetection2.Ref.Fields { 299 switch field.Name { 300 case "branchProtectionRule": 301 prFeatures.HasBranchProtectionRule = true 302 } 303 } 304 return 305 } 306 307 func PullRequests(client *Client, repo ghrepo.Interface, currentPRNumber int, currentPRHeadRef, currentUsername string) (*PullRequestsPayload, error) { 308 type edges struct { 309 TotalCount int 310 Edges []struct { 311 Node PullRequest 312 } 313 } 314 315 type response struct { 316 Repository struct { 317 DefaultBranchRef struct { 318 Name string 319 } 320 PullRequests edges 321 PullRequest *PullRequest 322 } 323 ViewerCreated edges 324 ReviewRequested edges 325 } 326 327 cachedClient := NewCachedClient(client.http, time.Hour*24) 328 prFeatures, err := determinePullRequestFeatures(cachedClient, repo.RepoHost()) 329 if err != nil { 330 return nil, err 331 } 332 333 var reviewsFragment string 334 if prFeatures.HasReviewDecision { 335 reviewsFragment = "reviewDecision" 336 } 337 338 var statusesFragment string 339 if prFeatures.HasStatusCheckRollup { 340 statusesFragment = ` 341 commits(last: 1) { 342 nodes { 343 commit { 344 statusCheckRollup { 345 contexts(last: 100) { 346 nodes { 347 ...on StatusContext { 348 state 349 } 350 ...on CheckRun { 351 conclusion 352 status 353 } 354 } 355 } 356 } 357 } 358 } 359 } 360 ` 361 } 362 363 var requiresStrictStatusChecks string 364 if prFeatures.HasBranchProtectionRule { 365 requiresStrictStatusChecks = ` 366 baseRef { 367 branchProtectionRule { 368 requiresStrictStatusChecks 369 } 370 }` 371 } 372 373 fragments := fmt.Sprintf(` 374 fragment pr on PullRequest { 375 number 376 title 377 state 378 url 379 headRefName 380 mergeStateStatus 381 headRepositoryOwner { 382 login 383 } 384 %s 385 isCrossRepository 386 isDraft 387 %s 388 } 389 fragment prWithReviews on PullRequest { 390 ...pr 391 %s 392 } 393 `, requiresStrictStatusChecks, statusesFragment, reviewsFragment) 394 395 queryPrefix := ` 396 query PullRequestStatus($owner: String!, $repo: String!, $headRefName: String!, $viewerQuery: String!, $reviewerQuery: String!, $per_page: Int = 10) { 397 repository(owner: $owner, name: $repo) { 398 defaultBranchRef { 399 name 400 } 401 pullRequests(headRefName: $headRefName, first: $per_page, orderBy: { field: CREATED_AT, direction: DESC }) { 402 totalCount 403 edges { 404 node { 405 ...prWithReviews 406 } 407 } 408 } 409 } 410 ` 411 if currentPRNumber > 0 { 412 queryPrefix = ` 413 query PullRequestStatus($owner: String!, $repo: String!, $number: Int!, $viewerQuery: String!, $reviewerQuery: String!, $per_page: Int = 10) { 414 repository(owner: $owner, name: $repo) { 415 defaultBranchRef { 416 name 417 } 418 pullRequest(number: $number) { 419 ...prWithReviews 420 } 421 } 422 ` 423 } 424 425 query := fragments + queryPrefix + ` 426 viewerCreated: search(query: $viewerQuery, type: ISSUE, first: $per_page) { 427 totalCount: issueCount 428 edges { 429 node { 430 ...prWithReviews 431 } 432 } 433 } 434 reviewRequested: search(query: $reviewerQuery, type: ISSUE, first: $per_page) { 435 totalCount: issueCount 436 edges { 437 node { 438 ...pr 439 } 440 } 441 } 442 } 443 ` 444 445 if currentUsername == "@me" && ghinstance.IsEnterprise(repo.RepoHost()) { 446 currentUsername, err = CurrentLoginName(client, repo.RepoHost()) 447 if err != nil { 448 return nil, err 449 } 450 } 451 452 viewerQuery := fmt.Sprintf("repo:%s state:open is:pr author:%s", ghrepo.FullName(repo), currentUsername) 453 reviewerQuery := fmt.Sprintf("repo:%s state:open review-requested:%s", ghrepo.FullName(repo), currentUsername) 454 455 branchWithoutOwner := currentPRHeadRef 456 if idx := strings.Index(currentPRHeadRef, ":"); idx >= 0 { 457 branchWithoutOwner = currentPRHeadRef[idx+1:] 458 } 459 460 variables := map[string]interface{}{ 461 "viewerQuery": viewerQuery, 462 "reviewerQuery": reviewerQuery, 463 "owner": repo.RepoOwner(), 464 "repo": repo.RepoName(), 465 "headRefName": branchWithoutOwner, 466 "number": currentPRNumber, 467 } 468 469 var resp response 470 err = client.GraphQL(repo.RepoHost(), query, variables, &resp) 471 if err != nil { 472 return nil, err 473 } 474 475 var viewerCreated []PullRequest 476 for _, edge := range resp.ViewerCreated.Edges { 477 viewerCreated = append(viewerCreated, edge.Node) 478 } 479 480 var reviewRequested []PullRequest 481 for _, edge := range resp.ReviewRequested.Edges { 482 reviewRequested = append(reviewRequested, edge.Node) 483 } 484 485 var currentPR = resp.Repository.PullRequest 486 if currentPR == nil { 487 for _, edge := range resp.Repository.PullRequests.Edges { 488 if edge.Node.HeadLabel() == currentPRHeadRef { 489 currentPR = &edge.Node 490 break // Take the most recent PR for the current branch 491 } 492 } 493 } 494 495 payload := PullRequestsPayload{ 496 ViewerCreated: PullRequestAndTotalCount{ 497 PullRequests: viewerCreated, 498 TotalCount: resp.ViewerCreated.TotalCount, 499 }, 500 ReviewRequested: PullRequestAndTotalCount{ 501 PullRequests: reviewRequested, 502 TotalCount: resp.ReviewRequested.TotalCount, 503 }, 504 CurrentPR: currentPR, 505 DefaultBranch: resp.Repository.DefaultBranchRef.Name, 506 } 507 508 return &payload, nil 509 } 510 511 func prCommitsFragment(httpClient *http.Client, hostname string) (string, error) { 512 cachedClient := NewCachedClient(httpClient, time.Hour*24) 513 if prFeatures, err := determinePullRequestFeatures(cachedClient, hostname); err != nil { 514 return "", err 515 } else if !prFeatures.HasStatusCheckRollup { 516 return "", nil 517 } 518 519 return ` 520 commits(last: 1) { 521 totalCount 522 nodes { 523 commit { 524 oid 525 statusCheckRollup { 526 contexts(last: 100) { 527 nodes { 528 ...on StatusContext { 529 context 530 state 531 targetUrl 532 } 533 ...on CheckRun { 534 name 535 status 536 conclusion 537 startedAt 538 completedAt 539 detailsUrl 540 } 541 } 542 } 543 } 544 } 545 } 546 } 547 `, nil 548 } 549 550 func PullRequestByNumber(client *Client, repo ghrepo.Interface, number int) (*PullRequest, error) { 551 type response struct { 552 Repository struct { 553 PullRequest PullRequest 554 } 555 } 556 557 statusesFragment, err := prCommitsFragment(client.http, repo.RepoHost()) 558 if err != nil { 559 return nil, err 560 } 561 562 query := ` 563 query PullRequestByNumber($owner: String!, $repo: String!, $pr_number: Int!) { 564 repository(owner: $owner, name: $repo) { 565 pullRequest(number: $pr_number) { 566 id 567 url 568 number 569 title 570 state 571 closed 572 body 573 mergeable 574 additions 575 deletions 576 author { 577 login 578 } 579 ` + statusesFragment + ` 580 baseRefName 581 headRefName 582 headRepositoryOwner { 583 login 584 } 585 headRepository { 586 name 587 } 588 isCrossRepository 589 isDraft 590 maintainerCanModify 591 reviewRequests(first: 100) { 592 nodes { 593 requestedReviewer { 594 __typename 595 ...on User { 596 login 597 } 598 ...on Team { 599 name 600 } 601 } 602 } 603 totalCount 604 } 605 assignees(first: 100) { 606 nodes { 607 login 608 } 609 totalCount 610 } 611 labels(first: 100) { 612 nodes { 613 name 614 } 615 totalCount 616 } 617 projectCards(first: 100) { 618 nodes { 619 project { 620 name 621 } 622 column { 623 name 624 } 625 } 626 totalCount 627 } 628 milestone{ 629 title 630 } 631 ` + commentsFragment() + ` 632 ` + reactionGroupsFragment() + ` 633 } 634 } 635 }` 636 637 variables := map[string]interface{}{ 638 "owner": repo.RepoOwner(), 639 "repo": repo.RepoName(), 640 "pr_number": number, 641 } 642 643 var resp response 644 err = client.GraphQL(repo.RepoHost(), query, variables, &resp) 645 if err != nil { 646 return nil, err 647 } 648 649 return &resp.Repository.PullRequest, nil 650 } 651 652 func PullRequestForBranch(client *Client, repo ghrepo.Interface, baseBranch, headBranch string, stateFilters []string) (*PullRequest, error) { 653 type response struct { 654 Repository struct { 655 PullRequests struct { 656 Nodes []PullRequest 657 } 658 } 659 } 660 661 statusesFragment, err := prCommitsFragment(client.http, repo.RepoHost()) 662 if err != nil { 663 return nil, err 664 } 665 666 query := ` 667 query PullRequestForBranch($owner: String!, $repo: String!, $headRefName: String!, $states: [PullRequestState!]) { 668 repository(owner: $owner, name: $repo) { 669 pullRequests(headRefName: $headRefName, states: $states, first: 30, orderBy: { field: CREATED_AT, direction: DESC }) { 670 nodes { 671 id 672 number 673 title 674 state 675 body 676 mergeable 677 additions 678 deletions 679 author { 680 login 681 } 682 ` + statusesFragment + ` 683 url 684 baseRefName 685 headRefName 686 headRepositoryOwner { 687 login 688 } 689 headRepository { 690 name 691 } 692 isCrossRepository 693 isDraft 694 maintainerCanModify 695 reviewRequests(first: 100) { 696 nodes { 697 requestedReviewer { 698 __typename 699 ...on User { 700 login 701 } 702 ...on Team { 703 name 704 } 705 } 706 } 707 totalCount 708 } 709 assignees(first: 100) { 710 nodes { 711 login 712 } 713 totalCount 714 } 715 labels(first: 100) { 716 nodes { 717 name 718 } 719 totalCount 720 } 721 projectCards(first: 100) { 722 nodes { 723 project { 724 name 725 } 726 column { 727 name 728 } 729 } 730 totalCount 731 } 732 milestone{ 733 title 734 } 735 ` + commentsFragment() + ` 736 ` + reactionGroupsFragment() + ` 737 } 738 } 739 } 740 }` 741 742 branchWithoutOwner := headBranch 743 if idx := strings.Index(headBranch, ":"); idx >= 0 { 744 branchWithoutOwner = headBranch[idx+1:] 745 } 746 747 variables := map[string]interface{}{ 748 "owner": repo.RepoOwner(), 749 "repo": repo.RepoName(), 750 "headRefName": branchWithoutOwner, 751 "states": stateFilters, 752 } 753 754 var resp response 755 err = client.GraphQL(repo.RepoHost(), query, variables, &resp) 756 if err != nil { 757 return nil, err 758 } 759 760 prs := resp.Repository.PullRequests.Nodes 761 sortPullRequestsByState(prs) 762 763 for _, pr := range prs { 764 if pr.HeadLabel() == headBranch && (baseBranch == "" || pr.BaseRefName == baseBranch) { 765 return &pr, nil 766 } 767 } 768 769 return nil, &NotFoundError{fmt.Errorf("no pull requests found for branch %q", headBranch)} 770 } 771 772 // sortPullRequestsByState sorts a PullRequest slice by open-first 773 func sortPullRequestsByState(prs []PullRequest) { 774 sort.SliceStable(prs, func(a, b int) bool { 775 return prs[a].State == "OPEN" 776 }) 777 } 778 779 // CreatePullRequest creates a pull request in a GitHub repository 780 func CreatePullRequest(client *Client, repo *Repository, params map[string]interface{}) (*PullRequest, error) { 781 query := ` 782 mutation PullRequestCreate($input: CreatePullRequestInput!) { 783 createPullRequest(input: $input) { 784 pullRequest { 785 id 786 url 787 } 788 } 789 }` 790 791 inputParams := map[string]interface{}{ 792 "repositoryId": repo.ID, 793 } 794 for key, val := range params { 795 switch key { 796 case "title", "body", "draft", "baseRefName", "headRefName", "maintainerCanModify": 797 inputParams[key] = val 798 } 799 } 800 variables := map[string]interface{}{ 801 "input": inputParams, 802 } 803 804 result := struct { 805 CreatePullRequest struct { 806 PullRequest PullRequest 807 } 808 }{} 809 810 err := client.GraphQL(repo.RepoHost(), query, variables, &result) 811 if err != nil { 812 return nil, err 813 } 814 pr := &result.CreatePullRequest.PullRequest 815 816 // metadata parameters aren't currently available in `createPullRequest`, 817 // but they are in `updatePullRequest` 818 updateParams := make(map[string]interface{}) 819 for key, val := range params { 820 switch key { 821 case "assigneeIds", "labelIds", "projectIds", "milestoneId": 822 if !isBlank(val) { 823 updateParams[key] = val 824 } 825 } 826 } 827 if len(updateParams) > 0 { 828 updateQuery := ` 829 mutation PullRequestCreateMetadata($input: UpdatePullRequestInput!) { 830 updatePullRequest(input: $input) { clientMutationId } 831 }` 832 updateParams["pullRequestId"] = pr.ID 833 variables := map[string]interface{}{ 834 "input": updateParams, 835 } 836 err := client.GraphQL(repo.RepoHost(), updateQuery, variables, &result) 837 if err != nil { 838 return pr, err 839 } 840 } 841 842 // reviewers are requested in yet another additional mutation 843 reviewParams := make(map[string]interface{}) 844 if ids, ok := params["userReviewerIds"]; ok && !isBlank(ids) { 845 reviewParams["userIds"] = ids 846 } 847 if ids, ok := params["teamReviewerIds"]; ok && !isBlank(ids) { 848 reviewParams["teamIds"] = ids 849 } 850 851 //TODO: How much work to extract this into own method and use for create and edit? 852 if len(reviewParams) > 0 { 853 reviewQuery := ` 854 mutation PullRequestCreateRequestReviews($input: RequestReviewsInput!) { 855 requestReviews(input: $input) { clientMutationId } 856 }` 857 reviewParams["pullRequestId"] = pr.ID 858 reviewParams["union"] = true 859 variables := map[string]interface{}{ 860 "input": reviewParams, 861 } 862 err := client.GraphQL(repo.RepoHost(), reviewQuery, variables, &result) 863 if err != nil { 864 return pr, err 865 } 866 } 867 868 return pr, nil 869 } 870 871 func UpdatePullRequest(client *Client, repo ghrepo.Interface, params githubv4.UpdatePullRequestInput) error { 872 var mutation struct { 873 UpdatePullRequest struct { 874 PullRequest struct { 875 ID string 876 } 877 } `graphql:"updatePullRequest(input: $input)"` 878 } 879 variables := map[string]interface{}{"input": params} 880 gql := graphQLClient(client.http, repo.RepoHost()) 881 err := gql.MutateNamed(context.Background(), "PullRequestUpdate", &mutation, variables) 882 return err 883 } 884 885 func UpdatePullRequestReviews(client *Client, repo ghrepo.Interface, params githubv4.RequestReviewsInput) error { 886 var mutation struct { 887 RequestReviews struct { 888 PullRequest struct { 889 ID string 890 } 891 } `graphql:"requestReviews(input: $input)"` 892 } 893 variables := map[string]interface{}{"input": params} 894 gql := graphQLClient(client.http, repo.RepoHost()) 895 err := gql.MutateNamed(context.Background(), "PullRequestUpdateRequestReviews", &mutation, variables) 896 return err 897 } 898 899 func isBlank(v interface{}) bool { 900 switch vv := v.(type) { 901 case string: 902 return vv == "" 903 case []string: 904 return len(vv) == 0 905 default: 906 return true 907 } 908 } 909 910 func PullRequestClose(client *Client, repo ghrepo.Interface, pr *PullRequest) error { 911 var mutation struct { 912 ClosePullRequest struct { 913 PullRequest struct { 914 ID githubv4.ID 915 } 916 } `graphql:"closePullRequest(input: $input)"` 917 } 918 919 variables := map[string]interface{}{ 920 "input": githubv4.ClosePullRequestInput{ 921 PullRequestID: pr.ID, 922 }, 923 } 924 925 gql := graphQLClient(client.http, repo.RepoHost()) 926 err := gql.MutateNamed(context.Background(), "PullRequestClose", &mutation, variables) 927 928 return err 929 } 930 931 func PullRequestReopen(client *Client, repo ghrepo.Interface, pr *PullRequest) error { 932 var mutation struct { 933 ReopenPullRequest struct { 934 PullRequest struct { 935 ID githubv4.ID 936 } 937 } `graphql:"reopenPullRequest(input: $input)"` 938 } 939 940 variables := map[string]interface{}{ 941 "input": githubv4.ReopenPullRequestInput{ 942 PullRequestID: pr.ID, 943 }, 944 } 945 946 gql := graphQLClient(client.http, repo.RepoHost()) 947 err := gql.MutateNamed(context.Background(), "PullRequestReopen", &mutation, variables) 948 949 return err 950 } 951 952 func PullRequestReady(client *Client, repo ghrepo.Interface, pr *PullRequest) error { 953 var mutation struct { 954 MarkPullRequestReadyForReview struct { 955 PullRequest struct { 956 ID githubv4.ID 957 } 958 } `graphql:"markPullRequestReadyForReview(input: $input)"` 959 } 960 961 variables := map[string]interface{}{ 962 "input": githubv4.MarkPullRequestReadyForReviewInput{ 963 PullRequestID: pr.ID, 964 }, 965 } 966 967 gql := graphQLClient(client.http, repo.RepoHost()) 968 return gql.MutateNamed(context.Background(), "PullRequestReadyForReview", &mutation, variables) 969 } 970 971 func BranchDeleteRemote(client *Client, repo ghrepo.Interface, branch string) error { 972 path := fmt.Sprintf("repos/%s/%s/git/refs/heads/%s", repo.RepoOwner(), repo.RepoName(), branch) 973 return client.REST(repo.RepoHost(), "DELETE", path, nil, nil) 974 } 975 976 func min(a, b int) int { 977 if a < b { 978 return a 979 } 980 return b 981 }