code.gitea.io/gitea@v1.22.3/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 "errors" 8 "fmt" 9 "net/http" 10 "strings" 11 12 issues_model "code.gitea.io/gitea/models/issues" 13 "code.gitea.io/gitea/models/organization" 14 access_model "code.gitea.io/gitea/models/perm/access" 15 user_model "code.gitea.io/gitea/models/user" 16 "code.gitea.io/gitea/modules/gitrepo" 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/context" 21 "code.gitea.io/gitea/services/convert" 22 issue_service "code.gitea.io/gitea/services/issue" 23 pull_service "code.gitea.io/gitea/services/pull" 24 ) 25 26 // ListPullReviews lists all reviews of a pull request 27 func ListPullReviews(ctx *context.APIContext) { 28 // swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/reviews repository repoListPullReviews 29 // --- 30 // summary: List all reviews for a pull request 31 // produces: 32 // - application/json 33 // parameters: 34 // - name: owner 35 // in: path 36 // description: owner of the repo 37 // type: string 38 // required: true 39 // - name: repo 40 // in: path 41 // description: name of the repo 42 // type: string 43 // required: true 44 // - name: index 45 // in: path 46 // description: index of the pull request 47 // type: integer 48 // format: int64 49 // required: true 50 // - name: page 51 // in: query 52 // description: page number of results to return (1-based) 53 // type: integer 54 // - name: limit 55 // in: query 56 // description: page size of results 57 // type: integer 58 // responses: 59 // "200": 60 // "$ref": "#/responses/PullReviewList" 61 // "404": 62 // "$ref": "#/responses/notFound" 63 64 pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 65 if err != nil { 66 if issues_model.IsErrPullRequestNotExist(err) { 67 ctx.NotFound("GetPullRequestByIndex", err) 68 } else { 69 ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) 70 } 71 return 72 } 73 74 if err = pr.LoadIssue(ctx); err != nil { 75 ctx.Error(http.StatusInternalServerError, "LoadIssue", err) 76 return 77 } 78 79 if err = pr.Issue.LoadRepo(ctx); err != nil { 80 ctx.Error(http.StatusInternalServerError, "LoadRepo", err) 81 return 82 } 83 84 opts := issues_model.FindReviewOptions{ 85 ListOptions: utils.GetListOptions(ctx), 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 gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.Issue.Repo) 333 if err != nil { 334 ctx.Error(http.StatusInternalServerError, "git.OpenRepository", err) 335 return 336 } 337 defer closer.Close() 338 339 headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName()) 340 if err != nil { 341 ctx.Error(http.StatusInternalServerError, "GetRefCommitID", err) 342 return 343 } 344 345 opts.CommitID = headCommitID 346 } 347 348 // create review comments 349 for _, c := range opts.Comments { 350 line := c.NewLineNum 351 if c.OldLineNum > 0 { 352 line = c.OldLineNum * -1 353 } 354 355 if _, err := pull_service.CreateCodeComment(ctx, 356 ctx.Doer, 357 ctx.Repo.GitRepo, 358 pr.Issue, 359 line, 360 c.Body, 361 c.Path, 362 true, // pending review 363 0, // no reply 364 opts.CommitID, 365 nil, 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 if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) { 376 ctx.Error(http.StatusUnprocessableEntity, "", err) 377 } else { 378 ctx.Error(http.StatusInternalServerError, "SubmitReview", err) 379 } 380 return 381 } 382 383 // convert response 384 apiReview, err := convert.ToPullReview(ctx, review, ctx.Doer) 385 if err != nil { 386 ctx.Error(http.StatusInternalServerError, "convertToPullReview", err) 387 return 388 } 389 ctx.JSON(http.StatusOK, apiReview) 390 } 391 392 // SubmitPullReview submit a pending review to an pull request 393 func SubmitPullReview(ctx *context.APIContext) { 394 // swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id} repository repoSubmitPullReview 395 // --- 396 // summary: Submit a pending review to an pull request 397 // produces: 398 // - application/json 399 // parameters: 400 // - name: owner 401 // in: path 402 // description: owner of the repo 403 // type: string 404 // required: true 405 // - name: repo 406 // in: path 407 // description: name of the repo 408 // type: string 409 // required: true 410 // - name: index 411 // in: path 412 // description: index of the pull request 413 // type: integer 414 // format: int64 415 // required: true 416 // - name: id 417 // in: path 418 // description: id of the review 419 // type: integer 420 // format: int64 421 // required: true 422 // - name: body 423 // in: body 424 // required: true 425 // schema: 426 // "$ref": "#/definitions/SubmitPullReviewOptions" 427 // responses: 428 // "200": 429 // "$ref": "#/responses/PullReview" 430 // "404": 431 // "$ref": "#/responses/notFound" 432 // "422": 433 // "$ref": "#/responses/validationError" 434 435 opts := web.GetForm(ctx).(*api.SubmitPullReviewOptions) 436 review, pr, isWrong := prepareSingleReview(ctx) 437 if isWrong { 438 return 439 } 440 441 if review.Type != issues_model.ReviewTypePending { 442 ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("only a pending review can be submitted")) 443 return 444 } 445 446 // determine review type 447 reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body, len(review.Comments) > 0) 448 if isWrong { 449 return 450 } 451 452 // if review stay pending return 453 if reviewType == issues_model.ReviewTypePending { 454 ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review stay pending")) 455 return 456 } 457 458 headCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(pr.GetGitRefName()) 459 if err != nil { 460 ctx.Error(http.StatusInternalServerError, "GitRepo: GetRefCommitID", err) 461 return 462 } 463 464 // create review and associate all pending review comments 465 review, _, err = pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, headCommitID, nil) 466 if err != nil { 467 if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) { 468 ctx.Error(http.StatusUnprocessableEntity, "", err) 469 } else { 470 ctx.Error(http.StatusInternalServerError, "SubmitReview", err) 471 } 472 return 473 } 474 475 // convert response 476 apiReview, err := convert.ToPullReview(ctx, review, ctx.Doer) 477 if err != nil { 478 ctx.Error(http.StatusInternalServerError, "convertToPullReview", err) 479 return 480 } 481 ctx.JSON(http.StatusOK, apiReview) 482 } 483 484 // preparePullReviewType return ReviewType and false or nil and true if an error happen 485 func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest, event api.ReviewStateType, body string, hasComments bool) (issues_model.ReviewType, bool) { 486 if err := pr.LoadIssue(ctx); err != nil { 487 ctx.Error(http.StatusInternalServerError, "LoadIssue", err) 488 return -1, true 489 } 490 491 needsBody := true 492 hasBody := len(strings.TrimSpace(body)) > 0 493 494 var reviewType issues_model.ReviewType 495 switch event { 496 case api.ReviewStateApproved: 497 // can not approve your own PR 498 if pr.Issue.IsPoster(ctx.Doer.ID) { 499 ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("approve your own pull is not allowed")) 500 return -1, true 501 } 502 reviewType = issues_model.ReviewTypeApprove 503 needsBody = false 504 505 case api.ReviewStateRequestChanges: 506 // can not reject your own PR 507 if pr.Issue.IsPoster(ctx.Doer.ID) { 508 ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("reject your own pull is not allowed")) 509 return -1, true 510 } 511 reviewType = issues_model.ReviewTypeReject 512 513 case api.ReviewStateComment: 514 reviewType = issues_model.ReviewTypeComment 515 needsBody = false 516 // if there is no body we need to ensure that there are comments 517 if !hasBody && !hasComments { 518 ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review event %s requires a body or a comment", event)) 519 return -1, true 520 } 521 default: 522 reviewType = issues_model.ReviewTypePending 523 } 524 525 // reject reviews with empty body if a body is required for this call 526 if needsBody && !hasBody { 527 ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review event %s requires a body", event)) 528 return -1, true 529 } 530 531 return reviewType, false 532 } 533 534 // prepareSingleReview return review, related pull and false or nil, nil and true if an error happen 535 func prepareSingleReview(ctx *context.APIContext) (*issues_model.Review, *issues_model.PullRequest, bool) { 536 pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 537 if err != nil { 538 if issues_model.IsErrPullRequestNotExist(err) { 539 ctx.NotFound("GetPullRequestByIndex", err) 540 } else { 541 ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) 542 } 543 return nil, nil, true 544 } 545 546 review, err := issues_model.GetReviewByID(ctx, ctx.ParamsInt64(":id")) 547 if err != nil { 548 if issues_model.IsErrReviewNotExist(err) { 549 ctx.NotFound("GetReviewByID", err) 550 } else { 551 ctx.Error(http.StatusInternalServerError, "GetReviewByID", err) 552 } 553 return nil, nil, true 554 } 555 556 // validate the review is for the given PR 557 if review.IssueID != pr.IssueID { 558 ctx.NotFound("ReviewNotInPR") 559 return nil, nil, true 560 } 561 562 // make sure that the user has access to this review if it is pending 563 if review.Type == issues_model.ReviewTypePending && review.ReviewerID != ctx.Doer.ID && !ctx.Doer.IsAdmin { 564 ctx.NotFound("GetReviewByID") 565 return nil, nil, true 566 } 567 568 if err := review.LoadAttributes(ctx); err != nil && !user_model.IsErrUserNotExist(err) { 569 ctx.Error(http.StatusInternalServerError, "ReviewLoadAttributes", err) 570 return nil, nil, true 571 } 572 573 return review, pr, false 574 } 575 576 // CreateReviewRequests create review requests to an pull request 577 func CreateReviewRequests(ctx *context.APIContext) { 578 // swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/requested_reviewers repository repoCreatePullReviewRequests 579 // --- 580 // summary: create review requests for a pull request 581 // produces: 582 // - application/json 583 // parameters: 584 // - name: owner 585 // in: path 586 // description: owner of the repo 587 // type: string 588 // required: true 589 // - name: repo 590 // in: path 591 // description: name of the repo 592 // type: string 593 // required: true 594 // - name: index 595 // in: path 596 // description: index of the pull request 597 // type: integer 598 // format: int64 599 // required: true 600 // - name: body 601 // in: body 602 // required: true 603 // schema: 604 // "$ref": "#/definitions/PullReviewRequestOptions" 605 // responses: 606 // "201": 607 // "$ref": "#/responses/PullReviewList" 608 // "422": 609 // "$ref": "#/responses/validationError" 610 // "404": 611 // "$ref": "#/responses/notFound" 612 613 opts := web.GetForm(ctx).(*api.PullReviewRequestOptions) 614 apiReviewRequest(ctx, *opts, true) 615 } 616 617 // DeleteReviewRequests delete review requests to an pull request 618 func DeleteReviewRequests(ctx *context.APIContext) { 619 // swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/requested_reviewers repository repoDeletePullReviewRequests 620 // --- 621 // summary: cancel review requests for a pull request 622 // produces: 623 // - application/json 624 // parameters: 625 // - name: owner 626 // in: path 627 // description: owner of the repo 628 // type: string 629 // required: true 630 // - name: repo 631 // in: path 632 // description: name of the repo 633 // type: string 634 // required: true 635 // - name: index 636 // in: path 637 // description: index of the pull request 638 // type: integer 639 // format: int64 640 // required: true 641 // - name: body 642 // in: body 643 // required: true 644 // schema: 645 // "$ref": "#/definitions/PullReviewRequestOptions" 646 // responses: 647 // "204": 648 // "$ref": "#/responses/empty" 649 // "422": 650 // "$ref": "#/responses/validationError" 651 // "403": 652 // "$ref": "#/responses/forbidden" 653 // "404": 654 // "$ref": "#/responses/notFound" 655 opts := web.GetForm(ctx).(*api.PullReviewRequestOptions) 656 apiReviewRequest(ctx, *opts, false) 657 } 658 659 func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions, isAdd bool) { 660 pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 661 if err != nil { 662 if issues_model.IsErrPullRequestNotExist(err) { 663 ctx.NotFound("GetPullRequestByIndex", err) 664 } else { 665 ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) 666 } 667 return 668 } 669 670 if err := pr.Issue.LoadRepo(ctx); err != nil { 671 ctx.Error(http.StatusInternalServerError, "pr.Issue.LoadRepo", err) 672 return 673 } 674 675 reviewers := make([]*user_model.User, 0, len(opts.Reviewers)) 676 677 permDoer, err := access_model.GetUserRepoPermission(ctx, pr.Issue.Repo, ctx.Doer) 678 if err != nil { 679 ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) 680 return 681 } 682 683 for _, r := range opts.Reviewers { 684 var reviewer *user_model.User 685 if strings.Contains(r, "@") { 686 reviewer, err = user_model.GetUserByEmail(ctx, r) 687 } else { 688 reviewer, err = user_model.GetUserByName(ctx, r) 689 } 690 691 if err != nil { 692 if user_model.IsErrUserNotExist(err) { 693 ctx.NotFound("UserNotExist", fmt.Sprintf("User '%s' not exist", r)) 694 return 695 } 696 ctx.Error(http.StatusInternalServerError, "GetUser", err) 697 return 698 } 699 700 err = issue_service.IsValidReviewRequest(ctx, reviewer, ctx.Doer, isAdd, pr.Issue, &permDoer) 701 if err != nil { 702 if issues_model.IsErrNotValidReviewRequest(err) { 703 ctx.Error(http.StatusUnprocessableEntity, "NotValidReviewRequest", err) 704 return 705 } 706 ctx.Error(http.StatusInternalServerError, "IsValidReviewRequest", err) 707 return 708 } 709 710 reviewers = append(reviewers, reviewer) 711 } 712 713 var reviews []*issues_model.Review 714 if isAdd { 715 reviews = make([]*issues_model.Review, 0, len(reviewers)) 716 } 717 718 for _, reviewer := range reviewers { 719 comment, err := issue_service.ReviewRequest(ctx, pr.Issue, ctx.Doer, reviewer, isAdd) 720 if err != nil { 721 if issues_model.IsErrReviewRequestOnClosedPR(err) { 722 ctx.Error(http.StatusForbidden, "", err) 723 return 724 } 725 ctx.Error(http.StatusInternalServerError, "ReviewRequest", err) 726 return 727 } 728 729 if comment != nil && isAdd { 730 if err = comment.LoadReview(ctx); err != nil { 731 ctx.ServerError("ReviewRequest", err) 732 return 733 } 734 reviews = append(reviews, comment.Review) 735 } 736 } 737 738 if ctx.Repo.Repository.Owner.IsOrganization() && len(opts.TeamReviewers) > 0 { 739 teamReviewers := make([]*organization.Team, 0, len(opts.TeamReviewers)) 740 for _, t := range opts.TeamReviewers { 741 var teamReviewer *organization.Team 742 teamReviewer, err = organization.GetTeam(ctx, ctx.Repo.Owner.ID, t) 743 if err != nil { 744 if organization.IsErrTeamNotExist(err) { 745 ctx.NotFound("TeamNotExist", fmt.Sprintf("Team '%s' not exist", t)) 746 return 747 } 748 ctx.Error(http.StatusInternalServerError, "ReviewRequest", err) 749 return 750 } 751 752 err = issue_service.IsValidTeamReviewRequest(ctx, teamReviewer, ctx.Doer, isAdd, pr.Issue) 753 if err != nil { 754 if issues_model.IsErrNotValidReviewRequest(err) { 755 ctx.Error(http.StatusUnprocessableEntity, "NotValidReviewRequest", err) 756 return 757 } 758 ctx.Error(http.StatusInternalServerError, "IsValidTeamReviewRequest", err) 759 return 760 } 761 762 teamReviewers = append(teamReviewers, teamReviewer) 763 } 764 765 for _, teamReviewer := range teamReviewers { 766 comment, err := issue_service.TeamReviewRequest(ctx, pr.Issue, ctx.Doer, teamReviewer, isAdd) 767 if err != nil { 768 ctx.ServerError("TeamReviewRequest", err) 769 return 770 } 771 772 if comment != nil && isAdd { 773 if err = comment.LoadReview(ctx); err != nil { 774 ctx.ServerError("ReviewRequest", err) 775 return 776 } 777 reviews = append(reviews, comment.Review) 778 } 779 } 780 } 781 782 if isAdd { 783 apiReviews, err := convert.ToPullReviewList(ctx, reviews, ctx.Doer) 784 if err != nil { 785 ctx.Error(http.StatusInternalServerError, "convertToPullReviewList", err) 786 return 787 } 788 ctx.JSON(http.StatusCreated, apiReviews) 789 } else { 790 ctx.Status(http.StatusNoContent) 791 return 792 } 793 } 794 795 // DismissPullReview dismiss a review for a pull request 796 func DismissPullReview(ctx *context.APIContext) { 797 // swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/dismissals repository repoDismissPullReview 798 // --- 799 // summary: Dismiss a review for a pull request 800 // produces: 801 // - application/json 802 // parameters: 803 // - name: owner 804 // in: path 805 // description: owner of the repo 806 // type: string 807 // required: true 808 // - name: repo 809 // in: path 810 // description: name of the repo 811 // type: string 812 // required: true 813 // - name: index 814 // in: path 815 // description: index of the pull request 816 // type: integer 817 // format: int64 818 // required: true 819 // - name: id 820 // in: path 821 // description: id of the review 822 // type: integer 823 // format: int64 824 // required: true 825 // - name: body 826 // in: body 827 // required: true 828 // schema: 829 // "$ref": "#/definitions/DismissPullReviewOptions" 830 // responses: 831 // "200": 832 // "$ref": "#/responses/PullReview" 833 // "403": 834 // "$ref": "#/responses/forbidden" 835 // "404": 836 // "$ref": "#/responses/notFound" 837 // "422": 838 // "$ref": "#/responses/validationError" 839 opts := web.GetForm(ctx).(*api.DismissPullReviewOptions) 840 dismissReview(ctx, opts.Message, true, opts.Priors) 841 } 842 843 // UnDismissPullReview cancel to dismiss a review for a pull request 844 func UnDismissPullReview(ctx *context.APIContext) { 845 // swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/undismissals repository repoUnDismissPullReview 846 // --- 847 // summary: Cancel to dismiss a review for a pull request 848 // produces: 849 // - application/json 850 // parameters: 851 // - name: owner 852 // in: path 853 // description: owner of the repo 854 // type: string 855 // required: true 856 // - name: repo 857 // in: path 858 // description: name of the repo 859 // type: string 860 // required: true 861 // - name: index 862 // in: path 863 // description: index of the pull request 864 // type: integer 865 // format: int64 866 // required: true 867 // - name: id 868 // in: path 869 // description: id of the review 870 // type: integer 871 // format: int64 872 // required: true 873 // responses: 874 // "200": 875 // "$ref": "#/responses/PullReview" 876 // "403": 877 // "$ref": "#/responses/forbidden" 878 // "404": 879 // "$ref": "#/responses/notFound" 880 // "422": 881 // "$ref": "#/responses/validationError" 882 dismissReview(ctx, "", false, false) 883 } 884 885 func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors bool) { 886 if !ctx.Repo.IsAdmin() { 887 ctx.Error(http.StatusForbidden, "", "Must be repo admin") 888 return 889 } 890 review, _, isWrong := prepareSingleReview(ctx) 891 if isWrong { 892 return 893 } 894 895 if review.Type != issues_model.ReviewTypeApprove && review.Type != issues_model.ReviewTypeReject { 896 ctx.Error(http.StatusForbidden, "", "not need to dismiss this review because it's type is not Approve or change request") 897 return 898 } 899 900 _, err := pull_service.DismissReview(ctx, review.ID, ctx.Repo.Repository.ID, msg, ctx.Doer, isDismiss, dismissPriors) 901 if err != nil { 902 if pull_service.IsErrDismissRequestOnClosedPR(err) { 903 ctx.Error(http.StatusForbidden, "", err) 904 return 905 } 906 ctx.Error(http.StatusInternalServerError, "pull_service.DismissReview", err) 907 return 908 } 909 910 if review, err = issues_model.GetReviewByID(ctx, review.ID); err != nil { 911 ctx.Error(http.StatusInternalServerError, "GetReviewByID", err) 912 return 913 } 914 915 // convert response 916 apiReview, err := convert.ToPullReview(ctx, review, ctx.Doer) 917 if err != nil { 918 ctx.Error(http.StatusInternalServerError, "convertToPullReview", err) 919 return 920 } 921 ctx.JSON(http.StatusOK, apiReview) 922 }