github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/api/queries_pr.go (about) 1 package api 2 3 import ( 4 "fmt" 5 "net/http" 6 "net/url" 7 "time" 8 9 "github.com/ungtb10d/cli/v2/internal/ghrepo" 10 "github.com/shurcooL/githubv4" 11 ) 12 13 type PullRequestAndTotalCount struct { 14 TotalCount int 15 PullRequests []PullRequest 16 SearchCapped bool 17 } 18 19 type PullRequest struct { 20 ID string 21 Number int 22 Title string 23 State string 24 Closed bool 25 URL string 26 BaseRefName string 27 HeadRefName string 28 HeadRefOid string 29 Body string 30 Mergeable string 31 Additions int 32 Deletions int 33 ChangedFiles int 34 MergeStateStatus string 35 IsInMergeQueue bool 36 IsMergeQueueEnabled bool // Indicates whether the pull request's base ref has a merge queue enabled. 37 CreatedAt time.Time 38 UpdatedAt time.Time 39 ClosedAt *time.Time 40 MergedAt *time.Time 41 42 MergeCommit *Commit 43 PotentialMergeCommit *Commit 44 45 Files struct { 46 Nodes []PullRequestFile 47 } 48 49 Author Author 50 MergedBy *Author 51 HeadRepositoryOwner Owner 52 HeadRepository *PRRepository 53 IsCrossRepository bool 54 IsDraft bool 55 MaintainerCanModify bool 56 57 BaseRef struct { 58 BranchProtectionRule struct { 59 RequiresStrictStatusChecks bool 60 RequiredApprovingReviewCount int 61 } 62 } 63 64 ReviewDecision string 65 66 Commits struct { 67 TotalCount int 68 Nodes []PullRequestCommit 69 } 70 StatusCheckRollup struct { 71 Nodes []StatusCheckRollupNode 72 } 73 74 Assignees Assignees 75 Labels Labels 76 ProjectCards ProjectCards 77 Milestone *Milestone 78 Comments Comments 79 ReactionGroups ReactionGroups 80 Reviews PullRequestReviews 81 LatestReviews PullRequestReviews 82 ReviewRequests ReviewRequests 83 } 84 85 type StatusCheckRollupNode struct { 86 Commit StatusCheckRollupCommit 87 } 88 89 type StatusCheckRollupCommit struct { 90 StatusCheckRollup CommitStatusCheckRollup 91 } 92 93 type CommitStatusCheckRollup struct { 94 Contexts CheckContexts 95 } 96 97 type CheckContexts struct { 98 Nodes []CheckContext 99 PageInfo struct { 100 HasNextPage bool 101 EndCursor string 102 } 103 } 104 105 type CheckContext struct { 106 TypeName string `json:"__typename"` 107 Name string `json:"name"` 108 IsRequired bool `json:"isRequired"` 109 CheckSuite struct { 110 WorkflowRun struct { 111 Workflow struct { 112 Name string `json:"name"` 113 } `json:"workflow"` 114 } `json:"workflowRun"` 115 } `json:"checkSuite"` 116 // QUEUED IN_PROGRESS COMPLETED WAITING PENDING REQUESTED 117 Status string `json:"status"` 118 // ACTION_REQUIRED TIMED_OUT CANCELLED FAILURE SUCCESS NEUTRAL SKIPPED STARTUP_FAILURE STALE 119 Conclusion string `json:"conclusion"` 120 StartedAt time.Time `json:"startedAt"` 121 CompletedAt time.Time `json:"completedAt"` 122 DetailsURL string `json:"detailsUrl"` 123 124 /* StatusContext fields */ 125 126 Context string `json:"context"` 127 // EXPECTED ERROR FAILURE PENDING SUCCESS 128 State string `json:"state"` 129 TargetURL string `json:"targetUrl"` 130 CreatedAt time.Time `json:"createdAt"` 131 } 132 133 type PRRepository struct { 134 ID string `json:"id"` 135 Name string `json:"name"` 136 } 137 138 // Commit loads just the commit SHA and nothing else 139 type Commit struct { 140 OID string `json:"oid"` 141 } 142 143 type PullRequestCommit struct { 144 Commit PullRequestCommitCommit 145 } 146 147 // PullRequestCommitCommit contains full information about a commit 148 type PullRequestCommitCommit struct { 149 OID string `json:"oid"` 150 Authors struct { 151 Nodes []struct { 152 Name string 153 Email string 154 User GitHubUser 155 } 156 } 157 MessageHeadline string 158 MessageBody string 159 CommittedDate time.Time 160 AuthoredDate time.Time 161 } 162 163 type PullRequestFile struct { 164 Path string `json:"path"` 165 Additions int `json:"additions"` 166 Deletions int `json:"deletions"` 167 } 168 169 type ReviewRequests struct { 170 Nodes []struct { 171 RequestedReviewer RequestedReviewer 172 } 173 } 174 175 type RequestedReviewer struct { 176 TypeName string `json:"__typename"` 177 Login string `json:"login"` 178 Name string `json:"name"` 179 Slug string `json:"slug"` 180 Organization struct { 181 Login string `json:"login"` 182 } `json:"organization"` 183 } 184 185 func (r RequestedReviewer) LoginOrSlug() string { 186 if r.TypeName == teamTypeName { 187 return fmt.Sprintf("%s/%s", r.Organization.Login, r.Slug) 188 } 189 return r.Login 190 } 191 192 const teamTypeName = "Team" 193 194 func (r ReviewRequests) Logins() []string { 195 logins := make([]string, len(r.Nodes)) 196 for i, r := range r.Nodes { 197 logins[i] = r.RequestedReviewer.LoginOrSlug() 198 } 199 return logins 200 } 201 202 func (pr PullRequest) HeadLabel() string { 203 if pr.IsCrossRepository { 204 return fmt.Sprintf("%s:%s", pr.HeadRepositoryOwner.Login, pr.HeadRefName) 205 } 206 return pr.HeadRefName 207 } 208 209 func (pr PullRequest) Link() string { 210 return pr.URL 211 } 212 213 func (pr PullRequest) Identifier() string { 214 return pr.ID 215 } 216 217 func (pr PullRequest) CurrentUserComments() []Comment { 218 return pr.Comments.CurrentUserComments() 219 } 220 221 func (pr PullRequest) IsOpen() bool { 222 return pr.State == "OPEN" 223 } 224 225 type PullRequestReviewStatus struct { 226 ChangesRequested bool 227 Approved bool 228 ReviewRequired bool 229 } 230 231 func (pr *PullRequest) ReviewStatus() PullRequestReviewStatus { 232 var status PullRequestReviewStatus 233 switch pr.ReviewDecision { 234 case "CHANGES_REQUESTED": 235 status.ChangesRequested = true 236 case "APPROVED": 237 status.Approved = true 238 case "REVIEW_REQUIRED": 239 status.ReviewRequired = true 240 } 241 return status 242 } 243 244 type PullRequestChecksStatus struct { 245 Pending int 246 Failing int 247 Passing int 248 Total int 249 } 250 251 func (pr *PullRequest) ChecksStatus() (summary PullRequestChecksStatus) { 252 if len(pr.StatusCheckRollup.Nodes) == 0 { 253 return 254 } 255 commit := pr.StatusCheckRollup.Nodes[0].Commit 256 for _, c := range commit.StatusCheckRollup.Contexts.Nodes { 257 state := c.State // StatusContext 258 if state == "" { 259 // CheckRun 260 if c.Status == "COMPLETED" { 261 state = c.Conclusion 262 } else { 263 state = c.Status 264 } 265 } 266 switch state { 267 case "SUCCESS", "NEUTRAL", "SKIPPED": 268 summary.Passing++ 269 case "ERROR", "FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED": 270 summary.Failing++ 271 default: // "EXPECTED", "REQUESTED", "WAITING", "QUEUED", "PENDING", "IN_PROGRESS", "STALE" 272 summary.Pending++ 273 } 274 summary.Total++ 275 } 276 277 return 278 } 279 280 func (pr *PullRequest) DisplayableReviews() PullRequestReviews { 281 published := []PullRequestReview{} 282 for _, prr := range pr.Reviews.Nodes { 283 //Dont display pending reviews 284 //Dont display commenting reviews without top level comment body 285 if prr.State != "PENDING" && !(prr.State == "COMMENTED" && prr.Body == "") { 286 published = append(published, prr) 287 } 288 } 289 return PullRequestReviews{Nodes: published, TotalCount: len(published)} 290 } 291 292 // CreatePullRequest creates a pull request in a GitHub repository 293 func CreatePullRequest(client *Client, repo *Repository, params map[string]interface{}) (*PullRequest, error) { 294 query := ` 295 mutation PullRequestCreate($input: CreatePullRequestInput!) { 296 createPullRequest(input: $input) { 297 pullRequest { 298 id 299 url 300 } 301 } 302 }` 303 304 inputParams := map[string]interface{}{ 305 "repositoryId": repo.ID, 306 } 307 for key, val := range params { 308 switch key { 309 case "title", "body", "draft", "baseRefName", "headRefName", "maintainerCanModify": 310 inputParams[key] = val 311 } 312 } 313 variables := map[string]interface{}{ 314 "input": inputParams, 315 } 316 317 result := struct { 318 CreatePullRequest struct { 319 PullRequest PullRequest 320 } 321 }{} 322 323 err := client.GraphQL(repo.RepoHost(), query, variables, &result) 324 if err != nil { 325 return nil, err 326 } 327 pr := &result.CreatePullRequest.PullRequest 328 329 // metadata parameters aren't currently available in `createPullRequest`, 330 // but they are in `updatePullRequest` 331 updateParams := make(map[string]interface{}) 332 for key, val := range params { 333 switch key { 334 case "assigneeIds", "labelIds", "projectIds", "milestoneId": 335 if !isBlank(val) { 336 updateParams[key] = val 337 } 338 } 339 } 340 if len(updateParams) > 0 { 341 updateQuery := ` 342 mutation PullRequestCreateMetadata($input: UpdatePullRequestInput!) { 343 updatePullRequest(input: $input) { clientMutationId } 344 }` 345 updateParams["pullRequestId"] = pr.ID 346 variables := map[string]interface{}{ 347 "input": updateParams, 348 } 349 err := client.GraphQL(repo.RepoHost(), updateQuery, variables, &result) 350 if err != nil { 351 return pr, err 352 } 353 } 354 355 // reviewers are requested in yet another additional mutation 356 reviewParams := make(map[string]interface{}) 357 if ids, ok := params["userReviewerIds"]; ok && !isBlank(ids) { 358 reviewParams["userIds"] = ids 359 } 360 if ids, ok := params["teamReviewerIds"]; ok && !isBlank(ids) { 361 reviewParams["teamIds"] = ids 362 } 363 364 //TODO: How much work to extract this into own method and use for create and edit? 365 if len(reviewParams) > 0 { 366 reviewQuery := ` 367 mutation PullRequestCreateRequestReviews($input: RequestReviewsInput!) { 368 requestReviews(input: $input) { clientMutationId } 369 }` 370 reviewParams["pullRequestId"] = pr.ID 371 reviewParams["union"] = true 372 variables := map[string]interface{}{ 373 "input": reviewParams, 374 } 375 err := client.GraphQL(repo.RepoHost(), reviewQuery, variables, &result) 376 if err != nil { 377 return pr, err 378 } 379 } 380 381 return pr, nil 382 } 383 384 func UpdatePullRequestReviews(client *Client, repo ghrepo.Interface, params githubv4.RequestReviewsInput) error { 385 var mutation struct { 386 RequestReviews struct { 387 PullRequest struct { 388 ID string 389 } 390 } `graphql:"requestReviews(input: $input)"` 391 } 392 variables := map[string]interface{}{"input": params} 393 err := client.Mutate(repo.RepoHost(), "PullRequestUpdateRequestReviews", &mutation, variables) 394 return err 395 } 396 397 func isBlank(v interface{}) bool { 398 switch vv := v.(type) { 399 case string: 400 return vv == "" 401 case []string: 402 return len(vv) == 0 403 default: 404 return true 405 } 406 } 407 408 func PullRequestClose(httpClient *http.Client, repo ghrepo.Interface, prID string) error { 409 var mutation struct { 410 ClosePullRequest struct { 411 PullRequest struct { 412 ID githubv4.ID 413 } 414 } `graphql:"closePullRequest(input: $input)"` 415 } 416 417 variables := map[string]interface{}{ 418 "input": githubv4.ClosePullRequestInput{ 419 PullRequestID: prID, 420 }, 421 } 422 423 client := NewClientFromHTTP(httpClient) 424 return client.Mutate(repo.RepoHost(), "PullRequestClose", &mutation, variables) 425 } 426 427 func PullRequestReopen(httpClient *http.Client, repo ghrepo.Interface, prID string) error { 428 var mutation struct { 429 ReopenPullRequest struct { 430 PullRequest struct { 431 ID githubv4.ID 432 } 433 } `graphql:"reopenPullRequest(input: $input)"` 434 } 435 436 variables := map[string]interface{}{ 437 "input": githubv4.ReopenPullRequestInput{ 438 PullRequestID: prID, 439 }, 440 } 441 442 client := NewClientFromHTTP(httpClient) 443 return client.Mutate(repo.RepoHost(), "PullRequestReopen", &mutation, variables) 444 } 445 446 func PullRequestReady(client *Client, repo ghrepo.Interface, pr *PullRequest) error { 447 var mutation struct { 448 MarkPullRequestReadyForReview struct { 449 PullRequest struct { 450 ID githubv4.ID 451 } 452 } `graphql:"markPullRequestReadyForReview(input: $input)"` 453 } 454 455 variables := map[string]interface{}{ 456 "input": githubv4.MarkPullRequestReadyForReviewInput{ 457 PullRequestID: pr.ID, 458 }, 459 } 460 461 return client.Mutate(repo.RepoHost(), "PullRequestReadyForReview", &mutation, variables) 462 } 463 464 func ConvertPullRequestToDraft(client *Client, repo ghrepo.Interface, pr *PullRequest) error { 465 var mutation struct { 466 ConvertPullRequestToDraft struct { 467 PullRequest struct { 468 ID githubv4.ID 469 } 470 } `graphql:"convertPullRequestToDraft(input: $input)"` 471 } 472 473 variables := map[string]interface{}{ 474 "input": githubv4.ConvertPullRequestToDraftInput{ 475 PullRequestID: pr.ID, 476 }, 477 } 478 479 return client.Mutate(repo.RepoHost(), "ConvertPullRequestToDraft", &mutation, variables) 480 } 481 482 func BranchDeleteRemote(client *Client, repo ghrepo.Interface, branch string) error { 483 path := fmt.Sprintf("repos/%s/%s/git/refs/heads/%s", repo.RepoOwner(), repo.RepoName(), url.PathEscape(branch)) 484 return client.REST(repo.RepoHost(), "DELETE", path, nil, nil) 485 }