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  }