gitea源码

pull_review.go 24KB

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