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 24KB

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