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  }