code.gitea.io/gitea@v1.21.7/routers/api/v1/repo/pull_review.go (about) 1 // Copyright 2020 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package repo 5 6 import ( 7 "fmt" 8 "net/http" 9 "strings" 10 11 issues_model "code.gitea.io/gitea/models/issues" 12 "code.gitea.io/gitea/models/organization" 13 access_model "code.gitea.io/gitea/models/perm/access" 14 user_model "code.gitea.io/gitea/models/user" 15 "code.gitea.io/gitea/modules/context" 16 "code.gitea.io/gitea/modules/git" 17 api "code.gitea.io/gitea/modules/structs" 18 "code.gitea.io/gitea/modules/web" 19 "code.gitea.io/gitea/routers/api/v1/utils" 20 "code.gitea.io/gitea/services/convert" 21 issue_service "code.gitea.io/gitea/services/issue" 22 pull_service "code.gitea.io/gitea/services/pull" 23 ) 24 25 // ListPullReviews lists all reviews of a pull request 26 func ListPullReviews(ctx *context.APIContext) { 27 // swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/reviews repository repoListPullReviews 28 // --- 29 // summary: List all reviews for a pull request 30 // produces: 31 // - application/json 32 // parameters: 33 // - name: owner 34 // in: path 35 // description: owner of the repo 36 // type: string 37 // required: true 38 // - name: repo 39 // in: path 40 // description: name of the repo 41 // type: string 42 // required: true 43 // - name: index 44 // in: path 45 // description: index of the pull request 46 // type: integer 47 // format: int64 48 // required: true 49 // - name: page 50 // in: query 51 // description: page number of results to return (1-based) 52 // type: integer 53 // - name: limit 54 // in: query 55 // description: page size of results 56 // type: integer 57 // responses: 58 // "200": 59 // "$ref": "#/responses/PullReviewList" 60 // "404": 61 // "$ref": "#/responses/notFound" 62 63 pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 64 if err != nil { 65 if issues_model.IsErrPullRequestNotExist(err) { 66 ctx.NotFound("GetPullRequestByIndex", err) 67 } else { 68 ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) 69 } 70 return 71 } 72 73 if err = pr.LoadIssue(ctx); err != nil { 74 ctx.Error(http.StatusInternalServerError, "LoadIssue", err) 75 return 76 } 77 78 if err = pr.Issue.LoadRepo(ctx); err != nil { 79 ctx.Error(http.StatusInternalServerError, "LoadRepo", err) 80 return 81 } 82 83 opts := issues_model.FindReviewOptions{ 84 ListOptions: utils.GetListOptions(ctx), 85 Type: issues_model.ReviewTypeUnknown, 86 IssueID: pr.IssueID, 87 } 88 89 allReviews, err := issues_model.FindReviews(ctx, opts) 90 if err != nil { 91 ctx.InternalServerError(err) 92 return 93 } 94 95 count, err := issues_model.CountReviews(ctx, opts) 96 if err != nil { 97 ctx.InternalServerError(err) 98 return 99 } 100 101 apiReviews, err := convert.ToPullReviewList(ctx, allReviews, ctx.Doer) 102 if err != nil { 103 ctx.Error(http.StatusInternalServerError, "convertToPullReviewList", err) 104 return 105 } 106 107 ctx.SetTotalCountHeader(count) 108 ctx.JSON(http.StatusOK, &apiReviews) 109 } 110 111 // GetPullReview gets a specific review of a pull request 112 func GetPullReview(ctx *context.APIContext) { 113 // swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/reviews/{id} repository repoGetPullReview 114 // --- 115 // summary: Get a specific review for a pull request 116 // produces: 117 // - application/json 118 // parameters: 119 // - name: owner 120 // in: path 121 // description: owner of the repo 122 // type: string 123 // required: true 124 // - name: repo 125 // in: path 126 // description: name of the repo 127 // type: string 128 // required: true 129 // - name: index 130 // in: path 131 // description: index of the pull request 132 // type: integer 133 // format: int64 134 // required: true 135 // - name: id 136 // in: path 137 // description: id of the review 138 // type: integer 139 // format: int64 140 // required: true 141 // responses: 142 // "200": 143 // "$ref": "#/responses/PullReview" 144 // "404": 145 // "$ref": "#/responses/notFound" 146 147 review, _, statusSet := prepareSingleReview(ctx) 148 if statusSet { 149 return 150 } 151 152 apiReview, err := convert.ToPullReview(ctx, review, ctx.Doer) 153 if err != nil { 154 ctx.Error(http.StatusInternalServerError, "convertToPullReview", err) 155 return 156 } 157 158 ctx.JSON(http.StatusOK, apiReview) 159 } 160 161 // GetPullReviewComments lists all comments of a pull request review 162 func GetPullReviewComments(ctx *context.APIContext) { 163 // swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments repository repoGetPullReviewComments 164 // --- 165 // summary: Get a specific review for a pull request 166 // produces: 167 // - application/json 168 // parameters: 169 // - name: owner 170 // in: path 171 // description: owner of the repo 172 // type: string 173 // required: true 174 // - name: repo 175 // in: path 176 // description: name of the repo 177 // type: string 178 // required: true 179 // - name: index 180 // in: path 181 // description: index of the pull request 182 // type: integer 183 // format: int64 184 // required: true 185 // - name: id 186 // in: path 187 // description: id of the review 188 // type: integer 189 // format: int64 190 // required: true 191 // responses: 192 // "200": 193 // "$ref": "#/responses/PullReviewCommentList" 194 // "404": 195 // "$ref": "#/responses/notFound" 196 197 review, _, statusSet := prepareSingleReview(ctx) 198 if statusSet { 199 return 200 } 201 202 apiComments, err := convert.ToPullReviewCommentList(ctx, review, ctx.Doer) 203 if err != nil { 204 ctx.Error(http.StatusInternalServerError, "convertToPullReviewCommentList", err) 205 return 206 } 207 208 ctx.JSON(http.StatusOK, apiComments) 209 } 210 211 // DeletePullReview delete a specific review from a pull request 212 func DeletePullReview(ctx *context.APIContext) { 213 // swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/reviews/{id} repository repoDeletePullReview 214 // --- 215 // summary: Delete a specific review from a pull request 216 // produces: 217 // - application/json 218 // parameters: 219 // - name: owner 220 // in: path 221 // description: owner of the repo 222 // type: string 223 // required: true 224 // - name: repo 225 // in: path 226 // description: name of the repo 227 // type: string 228 // required: true 229 // - name: index 230 // in: path 231 // description: index of the pull request 232 // type: integer 233 // format: int64 234 // required: true 235 // - name: id 236 // in: path 237 // description: id of the review 238 // type: integer 239 // format: int64 240 // required: true 241 // responses: 242 // "204": 243 // "$ref": "#/responses/empty" 244 // "403": 245 // "$ref": "#/responses/forbidden" 246 // "404": 247 // "$ref": "#/responses/notFound" 248 249 review, _, statusSet := prepareSingleReview(ctx) 250 if statusSet { 251 return 252 } 253 254 if ctx.Doer == nil { 255 ctx.NotFound() 256 return 257 } 258 if !ctx.Doer.IsAdmin && ctx.Doer.ID != review.ReviewerID { 259 ctx.Error(http.StatusForbidden, "only admin and user itself can delete a review", nil) 260 return 261 } 262 263 if err := issues_model.DeleteReview(ctx, review); err != nil { 264 ctx.Error(http.StatusInternalServerError, "DeleteReview", fmt.Errorf("can not delete ReviewID: %d", review.ID)) 265 return 266 } 267 268 ctx.Status(http.StatusNoContent) 269 } 270 271 // CreatePullReview create a review to a pull request 272 func CreatePullReview(ctx *context.APIContext) { 273 // swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews repository repoCreatePullReview 274 // --- 275 // summary: Create a review to an pull request 276 // produces: 277 // - application/json 278 // parameters: 279 // - name: owner 280 // in: path 281 // description: owner of the repo 282 // type: string 283 // required: true 284 // - name: repo 285 // in: path 286 // description: name of the repo 287 // type: string 288 // required: true 289 // - name: index 290 // in: path 291 // description: index of the pull request 292 // type: integer 293 // format: int64 294 // required: true 295 // - name: body 296 // in: body 297 // required: true 298 // schema: 299 // "$ref": "#/definitions/CreatePullReviewOptions" 300 // responses: 301 // "200": 302 // "$ref": "#/responses/PullReview" 303 // "404": 304 // "$ref": "#/responses/notFound" 305 // "422": 306 // "$ref": "#/responses/validationError" 307 308 opts := web.GetForm(ctx).(*api.CreatePullReviewOptions) 309 pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 310 if err != nil { 311 if issues_model.IsErrPullRequestNotExist(err) { 312 ctx.NotFound("GetPullRequestByIndex", err) 313 } else { 314 ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) 315 } 316 return 317 } 318 319 // determine review type 320 reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body, len(opts.Comments) > 0) 321 if isWrong { 322 return 323 } 324 325 if err := pr.Issue.LoadRepo(ctx); err != nil { 326 ctx.Error(http.StatusInternalServerError, "pr.Issue.LoadRepo", err) 327 return 328 } 329 330 // if CommitID is empty, set it as lastCommitID 331 if opts.CommitID == "" { 332 333 gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, pr.Issue.Repo.RepoPath()) 334 if err != nil { 335 ctx.Error(http.StatusInternalServerError, "git.OpenRepository", err) 336 return 337 } 338 defer closer.Close() 339 340 headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName()) 341 if err != nil { 342 ctx.Error(http.StatusInternalServerError, "GetRefCommitID", err) 343 return 344 } 345 346 opts.CommitID = headCommitID 347 } 348 349 // create review comments 350 for _, c := range opts.Comments { 351 line := c.NewLineNum 352 if c.OldLineNum > 0 { 353 line = c.OldLineNum * -1 354 } 355 356 if _, err := pull_service.CreateCodeComment(ctx, 357 ctx.Doer, 358 ctx.Repo.GitRepo, 359 pr.Issue, 360 line, 361 c.Body, 362 c.Path, 363 true, // pending review 364 0, // no reply 365 opts.CommitID, 366 ); err != nil { 367 ctx.Error(http.StatusInternalServerError, "CreateCodeComment", err) 368 return 369 } 370 } 371 372 // create review and associate all pending review comments 373 review, _, err := pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, opts.CommitID, nil) 374 if err != nil { 375 ctx.Error(http.StatusInternalServerError, "SubmitReview", err) 376 return 377 } 378 379 // convert response 380 apiReview, err := convert.ToPullReview(ctx, review, ctx.Doer) 381 if err != nil { 382 ctx.Error(http.StatusInternalServerError, "convertToPullReview", err) 383 return 384 } 385 ctx.JSON(http.StatusOK, apiReview) 386 } 387 388 // SubmitPullReview submit a pending review to an pull request 389 func SubmitPullReview(ctx *context.APIContext) { 390 // swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id} repository repoSubmitPullReview 391 // --- 392 // summary: Submit a pending review to an pull request 393 // produces: 394 // - application/json 395 // parameters: 396 // - name: owner 397 // in: path 398 // description: owner of the repo 399 // type: string 400 // required: true 401 // - name: repo 402 // in: path 403 // description: name of the repo 404 // type: string 405 // required: true 406 // - name: index 407 // in: path 408 // description: index of the pull request 409 // type: integer 410 // format: int64 411 // required: true 412 // - name: id 413 // in: path 414 // description: id of the review 415 // type: integer 416 // format: int64 417 // required: true 418 // - name: body 419 // in: body 420 // required: true 421 // schema: 422 // "$ref": "#/definitions/SubmitPullReviewOptions" 423 // responses: 424 // "200": 425 // "$ref": "#/responses/PullReview" 426 // "404": 427 // "$ref": "#/responses/notFound" 428 // "422": 429 // "$ref": "#/responses/validationError" 430 431 opts := web.GetForm(ctx).(*api.SubmitPullReviewOptions) 432 review, pr, isWrong := prepareSingleReview(ctx) 433 if isWrong { 434 return 435 } 436 437 if review.Type != issues_model.ReviewTypePending { 438 ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("only a pending review can be submitted")) 439 return 440 } 441 442 // determine review type 443 reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body, len(review.Comments) > 0) 444 if isWrong { 445 return 446 } 447 448 // if review stay pending return 449 if reviewType == issues_model.ReviewTypePending { 450 ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review stay pending")) 451 return 452 } 453 454 headCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(pr.GetGitRefName()) 455 if err != nil { 456 ctx.Error(http.StatusInternalServerError, "GitRepo: GetRefCommitID", err) 457 return 458 } 459 460 // create review and associate all pending review comments 461 review, _, err = pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, headCommitID, nil) 462 if err != nil { 463 ctx.Error(http.StatusInternalServerError, "SubmitReview", err) 464 return 465 } 466 467 // convert response 468 apiReview, err := convert.ToPullReview(ctx, review, ctx.Doer) 469 if err != nil { 470 ctx.Error(http.StatusInternalServerError, "convertToPullReview", err) 471 return 472 } 473 ctx.JSON(http.StatusOK, apiReview) 474 } 475 476 // preparePullReviewType return ReviewType and false or nil and true if an error happen 477 func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest, event api.ReviewStateType, body string, hasComments bool) (issues_model.ReviewType, bool) { 478 if err := pr.LoadIssue(ctx); err != nil { 479 ctx.Error(http.StatusInternalServerError, "LoadIssue", err) 480 return -1, true 481 } 482 483 needsBody := true 484 hasBody := len(strings.TrimSpace(body)) > 0 485 486 var reviewType issues_model.ReviewType 487 switch event { 488 case api.ReviewStateApproved: 489 // can not approve your own PR 490 if pr.Issue.IsPoster(ctx.Doer.ID) { 491 ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("approve your own pull is not allowed")) 492 return -1, true 493 } 494 reviewType = issues_model.ReviewTypeApprove 495 needsBody = false 496 497 case api.ReviewStateRequestChanges: 498 // can not reject your own PR 499 if pr.Issue.IsPoster(ctx.Doer.ID) { 500 ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("reject your own pull is not allowed")) 501 return -1, true 502 } 503 reviewType = issues_model.ReviewTypeReject 504 505 case api.ReviewStateComment: 506 reviewType = issues_model.ReviewTypeComment 507 needsBody = false 508 // if there is no body we need to ensure that there are comments 509 if !hasBody && !hasComments { 510 ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review event %s requires a body or a comment", event)) 511 return -1, true 512 } 513 default: 514 reviewType = issues_model.ReviewTypePending 515 } 516 517 // reject reviews with empty body if a body is required for this call 518 if needsBody && !hasBody { 519 ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review event %s requires a body", event)) 520 return -1, true 521 } 522 523 return reviewType, false 524 } 525 526 // prepareSingleReview return review, related pull and false or nil, nil and true if an error happen 527 func prepareSingleReview(ctx *context.APIContext) (*issues_model.Review, *issues_model.PullRequest, bool) { 528 pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 529 if err != nil { 530 if issues_model.IsErrPullRequestNotExist(err) { 531 ctx.NotFound("GetPullRequestByIndex", err) 532 } else { 533 ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) 534 } 535 return nil, nil, true 536 } 537 538 review, err := issues_model.GetReviewByID(ctx, ctx.ParamsInt64(":id")) 539 if err != nil { 540 if issues_model.IsErrReviewNotExist(err) { 541 ctx.NotFound("GetReviewByID", err) 542 } else { 543 ctx.Error(http.StatusInternalServerError, "GetReviewByID", err) 544 } 545 return nil, nil, true 546 } 547 548 // validate the the review is for the given PR 549 if review.IssueID != pr.IssueID { 550 ctx.NotFound("ReviewNotInPR") 551 return nil, nil, true 552 } 553 554 // make sure that the user has access to this review if it is pending 555 if review.Type == issues_model.ReviewTypePending && review.ReviewerID != ctx.Doer.ID && !ctx.Doer.IsAdmin { 556 ctx.NotFound("GetReviewByID") 557 return nil, nil, true 558 } 559 560 if err := review.LoadAttributes(ctx); err != nil && !user_model.IsErrUserNotExist(err) { 561 ctx.Error(http.StatusInternalServerError, "ReviewLoadAttributes", err) 562 return nil, nil, true 563 } 564 565 return review, pr, false 566 } 567 568 // CreateReviewRequests create review requests to an pull request 569 func CreateReviewRequests(ctx *context.APIContext) { 570 // swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/requested_reviewers repository repoCreatePullReviewRequests 571 // --- 572 // summary: create review requests for a pull request 573 // produces: 574 // - application/json 575 // parameters: 576 // - name: owner 577 // in: path 578 // description: owner of the repo 579 // type: string 580 // required: true 581 // - name: repo 582 // in: path 583 // description: name of the repo 584 // type: string 585 // required: true 586 // - name: index 587 // in: path 588 // description: index of the pull request 589 // type: integer 590 // format: int64 591 // required: true 592 // - name: body 593 // in: body 594 // required: true 595 // schema: 596 // "$ref": "#/definitions/PullReviewRequestOptions" 597 // responses: 598 // "201": 599 // "$ref": "#/responses/PullReviewList" 600 // "422": 601 // "$ref": "#/responses/validationError" 602 // "404": 603 // "$ref": "#/responses/notFound" 604 605 opts := web.GetForm(ctx).(*api.PullReviewRequestOptions) 606 apiReviewRequest(ctx, *opts, true) 607 } 608 609 // DeleteReviewRequests delete review requests to an pull request 610 func DeleteReviewRequests(ctx *context.APIContext) { 611 // swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/requested_reviewers repository repoDeletePullReviewRequests 612 // --- 613 // summary: cancel review requests for a pull request 614 // produces: 615 // - application/json 616 // parameters: 617 // - name: owner 618 // in: path 619 // description: owner of the repo 620 // type: string 621 // required: true 622 // - name: repo 623 // in: path 624 // description: name of the repo 625 // type: string 626 // required: true 627 // - name: index 628 // in: path 629 // description: index of the pull request 630 // type: integer 631 // format: int64 632 // required: true 633 // - name: body 634 // in: body 635 // required: true 636 // schema: 637 // "$ref": "#/definitions/PullReviewRequestOptions" 638 // responses: 639 // "204": 640 // "$ref": "#/responses/empty" 641 // "422": 642 // "$ref": "#/responses/validationError" 643 // "404": 644 // "$ref": "#/responses/notFound" 645 opts := web.GetForm(ctx).(*api.PullReviewRequestOptions) 646 apiReviewRequest(ctx, *opts, false) 647 } 648 649 func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions, isAdd bool) { 650 pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 651 if err != nil { 652 if issues_model.IsErrPullRequestNotExist(err) { 653 ctx.NotFound("GetPullRequestByIndex", err) 654 } else { 655 ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) 656 } 657 return 658 } 659 660 if err := pr.Issue.LoadRepo(ctx); err != nil { 661 ctx.Error(http.StatusInternalServerError, "pr.Issue.LoadRepo", err) 662 return 663 } 664 665 reviewers := make([]*user_model.User, 0, len(opts.Reviewers)) 666 667 permDoer, err := access_model.GetUserRepoPermission(ctx, pr.Issue.Repo, ctx.Doer) 668 if err != nil { 669 ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) 670 return 671 } 672 673 for _, r := range opts.Reviewers { 674 var reviewer *user_model.User 675 if strings.Contains(r, "@") { 676 reviewer, err = user_model.GetUserByEmail(ctx, r) 677 } else { 678 reviewer, err = user_model.GetUserByName(ctx, r) 679 } 680 681 if err != nil { 682 if user_model.IsErrUserNotExist(err) { 683 ctx.NotFound("UserNotExist", fmt.Sprintf("User '%s' not exist", r)) 684 return 685 } 686 ctx.Error(http.StatusInternalServerError, "GetUser", err) 687 return 688 } 689 690 err = issue_service.IsValidReviewRequest(ctx, reviewer, ctx.Doer, isAdd, pr.Issue, &permDoer) 691 if err != nil { 692 if issues_model.IsErrNotValidReviewRequest(err) { 693 ctx.Error(http.StatusUnprocessableEntity, "NotValidReviewRequest", err) 694 return 695 } 696 ctx.Error(http.StatusInternalServerError, "IsValidReviewRequest", err) 697 return 698 } 699 700 reviewers = append(reviewers, reviewer) 701 } 702 703 var reviews []*issues_model.Review 704 if isAdd { 705 reviews = make([]*issues_model.Review, 0, len(reviewers)) 706 } 707 708 for _, reviewer := range reviewers { 709 comment, err := issue_service.ReviewRequest(ctx, pr.Issue, ctx.Doer, reviewer, isAdd) 710 if err != nil { 711 ctx.Error(http.StatusInternalServerError, "ReviewRequest", err) 712 return 713 } 714 715 if comment != nil && isAdd { 716 if err = comment.LoadReview(ctx); err != nil { 717 ctx.ServerError("ReviewRequest", err) 718 return 719 } 720 reviews = append(reviews, comment.Review) 721 } 722 } 723 724 if ctx.Repo.Repository.Owner.IsOrganization() && len(opts.TeamReviewers) > 0 { 725 726 teamReviewers := make([]*organization.Team, 0, len(opts.TeamReviewers)) 727 for _, t := range opts.TeamReviewers { 728 var teamReviewer *organization.Team 729 teamReviewer, err = organization.GetTeam(ctx, ctx.Repo.Owner.ID, t) 730 if err != nil { 731 if organization.IsErrTeamNotExist(err) { 732 ctx.NotFound("TeamNotExist", fmt.Sprintf("Team '%s' not exist", t)) 733 return 734 } 735 ctx.Error(http.StatusInternalServerError, "ReviewRequest", err) 736 return 737 } 738 739 err = issue_service.IsValidTeamReviewRequest(ctx, teamReviewer, ctx.Doer, isAdd, pr.Issue) 740 if err != nil { 741 if issues_model.IsErrNotValidReviewRequest(err) { 742 ctx.Error(http.StatusUnprocessableEntity, "NotValidReviewRequest", err) 743 return 744 } 745 ctx.Error(http.StatusInternalServerError, "IsValidTeamReviewRequest", err) 746 return 747 } 748 749 teamReviewers = append(teamReviewers, teamReviewer) 750 } 751 752 for _, teamReviewer := range teamReviewers { 753 comment, err := issue_service.TeamReviewRequest(ctx, pr.Issue, ctx.Doer, teamReviewer, isAdd) 754 if err != nil { 755 ctx.ServerError("TeamReviewRequest", err) 756 return 757 } 758 759 if comment != nil && isAdd { 760 if err = comment.LoadReview(ctx); err != nil { 761 ctx.ServerError("ReviewRequest", err) 762 return 763 } 764 reviews = append(reviews, comment.Review) 765 } 766 } 767 } 768 769 if isAdd { 770 apiReviews, err := convert.ToPullReviewList(ctx, reviews, ctx.Doer) 771 if err != nil { 772 ctx.Error(http.StatusInternalServerError, "convertToPullReviewList", err) 773 return 774 } 775 ctx.JSON(http.StatusCreated, apiReviews) 776 } else { 777 ctx.Status(http.StatusNoContent) 778 return 779 } 780 } 781 782 // DismissPullReview dismiss a review for a pull request 783 func DismissPullReview(ctx *context.APIContext) { 784 // swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/dismissals repository repoDismissPullReview 785 // --- 786 // summary: Dismiss a review for a pull request 787 // produces: 788 // - application/json 789 // parameters: 790 // - name: owner 791 // in: path 792 // description: owner of the repo 793 // type: string 794 // required: true 795 // - name: repo 796 // in: path 797 // description: name of the repo 798 // type: string 799 // required: true 800 // - name: index 801 // in: path 802 // description: index of the pull request 803 // type: integer 804 // format: int64 805 // required: true 806 // - name: id 807 // in: path 808 // description: id of the review 809 // type: integer 810 // format: int64 811 // required: true 812 // - name: body 813 // in: body 814 // required: true 815 // schema: 816 // "$ref": "#/definitions/DismissPullReviewOptions" 817 // responses: 818 // "200": 819 // "$ref": "#/responses/PullReview" 820 // "403": 821 // "$ref": "#/responses/forbidden" 822 // "404": 823 // "$ref": "#/responses/notFound" 824 // "422": 825 // "$ref": "#/responses/validationError" 826 opts := web.GetForm(ctx).(*api.DismissPullReviewOptions) 827 dismissReview(ctx, opts.Message, true, opts.Priors) 828 } 829 830 // UnDismissPullReview cancel to dismiss a review for a pull request 831 func UnDismissPullReview(ctx *context.APIContext) { 832 // swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/undismissals repository repoUnDismissPullReview 833 // --- 834 // summary: Cancel to dismiss a review for a pull request 835 // produces: 836 // - application/json 837 // parameters: 838 // - name: owner 839 // in: path 840 // description: owner of the repo 841 // type: string 842 // required: true 843 // - name: repo 844 // in: path 845 // description: name of the repo 846 // type: string 847 // required: true 848 // - name: index 849 // in: path 850 // description: index of the pull request 851 // type: integer 852 // format: int64 853 // required: true 854 // - name: id 855 // in: path 856 // description: id of the review 857 // type: integer 858 // format: int64 859 // required: true 860 // responses: 861 // "200": 862 // "$ref": "#/responses/PullReview" 863 // "403": 864 // "$ref": "#/responses/forbidden" 865 // "404": 866 // "$ref": "#/responses/notFound" 867 // "422": 868 // "$ref": "#/responses/validationError" 869 dismissReview(ctx, "", false, false) 870 } 871 872 func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors bool) { 873 if !ctx.Repo.IsAdmin() { 874 ctx.Error(http.StatusForbidden, "", "Must be repo admin") 875 return 876 } 877 review, pr, isWrong := prepareSingleReview(ctx) 878 if isWrong { 879 return 880 } 881 882 if review.Type != issues_model.ReviewTypeApprove && review.Type != issues_model.ReviewTypeReject { 883 ctx.Error(http.StatusForbidden, "", "not need to dismiss this review because it's type is not Approve or change request") 884 return 885 } 886 887 if pr.Issue.IsClosed { 888 ctx.Error(http.StatusForbidden, "", "not need to dismiss this review because this pr is closed") 889 return 890 } 891 892 _, err := pull_service.DismissReview(ctx, review.ID, ctx.Repo.Repository.ID, msg, ctx.Doer, isDismiss, dismissPriors) 893 if err != nil { 894 ctx.Error(http.StatusInternalServerError, "pull_service.DismissReview", err) 895 return 896 } 897 898 if review, err = issues_model.GetReviewByID(ctx, review.ID); err != nil { 899 ctx.Error(http.StatusInternalServerError, "GetReviewByID", err) 900 return 901 } 902 903 // convert response 904 apiReview, err := convert.ToPullReview(ctx, review, ctx.Doer) 905 if err != nil { 906 ctx.Error(http.StatusInternalServerError, "convertToPullReview", err) 907 return 908 } 909 ctx.JSON(http.StatusOK, apiReview) 910 }