You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

pull_review.go 25KB

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