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

pull_review.go 14 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  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. api "code.gitea.io/gitea/modules/structs"
  13. "code.gitea.io/gitea/routers/api/v1/utils"
  14. pull_service "code.gitea.io/gitea/services/pull"
  15. )
  16. // ListPullReviews lists all reviews of a pull request
  17. func ListPullReviews(ctx *context.APIContext) {
  18. // swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/reviews repository repoListPullReviews
  19. // ---
  20. // summary: List all reviews for a pull request
  21. // produces:
  22. // - application/json
  23. // parameters:
  24. // - name: owner
  25. // in: path
  26. // description: owner of the repo
  27. // type: string
  28. // required: true
  29. // - name: repo
  30. // in: path
  31. // description: name of the repo
  32. // type: string
  33. // required: true
  34. // - name: index
  35. // in: path
  36. // description: index of the pull request
  37. // type: integer
  38. // format: int64
  39. // required: true
  40. // - name: page
  41. // in: query
  42. // description: page number of results to return (1-based)
  43. // type: integer
  44. // - name: limit
  45. // in: query
  46. // description: page size of results, maximum page size is 50
  47. // type: integer
  48. // responses:
  49. // "200":
  50. // "$ref": "#/responses/PullReviewList"
  51. // "404":
  52. // "$ref": "#/responses/notFound"
  53. pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  54. if err != nil {
  55. if models.IsErrPullRequestNotExist(err) {
  56. ctx.NotFound("GetPullRequestByIndex", err)
  57. } else {
  58. ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
  59. }
  60. return
  61. }
  62. if err = pr.LoadIssue(); err != nil {
  63. ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
  64. return
  65. }
  66. if err = pr.Issue.LoadRepo(); err != nil {
  67. ctx.Error(http.StatusInternalServerError, "LoadRepo", err)
  68. return
  69. }
  70. allReviews, err := models.FindReviews(models.FindReviewOptions{
  71. ListOptions: utils.GetListOptions(ctx),
  72. Type: models.ReviewTypeUnknown,
  73. IssueID: pr.IssueID,
  74. })
  75. if err != nil {
  76. ctx.Error(http.StatusInternalServerError, "FindReviews", err)
  77. return
  78. }
  79. apiReviews, err := convert.ToPullReviewList(allReviews, ctx.User)
  80. if err != nil {
  81. ctx.Error(http.StatusInternalServerError, "convertToPullReviewList", err)
  82. return
  83. }
  84. ctx.JSON(http.StatusOK, &apiReviews)
  85. }
  86. // GetPullReview gets a specific review of a pull request
  87. func GetPullReview(ctx *context.APIContext) {
  88. // swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/reviews/{id} repository repoGetPullReview
  89. // ---
  90. // summary: Get a specific review for a pull request
  91. // produces:
  92. // - application/json
  93. // parameters:
  94. // - name: owner
  95. // in: path
  96. // description: owner of the repo
  97. // type: string
  98. // required: true
  99. // - name: repo
  100. // in: path
  101. // description: name of the repo
  102. // type: string
  103. // required: true
  104. // - name: index
  105. // in: path
  106. // description: index of the pull request
  107. // type: integer
  108. // format: int64
  109. // required: true
  110. // - name: id
  111. // in: path
  112. // description: id of the review
  113. // type: integer
  114. // format: int64
  115. // required: true
  116. // responses:
  117. // "200":
  118. // "$ref": "#/responses/PullReview"
  119. // "404":
  120. // "$ref": "#/responses/notFound"
  121. review, _, statusSet := prepareSingleReview(ctx)
  122. if statusSet {
  123. return
  124. }
  125. apiReview, err := convert.ToPullReview(review, ctx.User)
  126. if err != nil {
  127. ctx.Error(http.StatusInternalServerError, "convertToPullReview", err)
  128. return
  129. }
  130. ctx.JSON(http.StatusOK, apiReview)
  131. }
  132. // GetPullReviewComments lists all comments of a pull request review
  133. func GetPullReviewComments(ctx *context.APIContext) {
  134. // swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments repository repoGetPullReviewComments
  135. // ---
  136. // summary: Get a specific review for a pull request
  137. // produces:
  138. // - application/json
  139. // parameters:
  140. // - name: owner
  141. // in: path
  142. // description: owner of the repo
  143. // type: string
  144. // required: true
  145. // - name: repo
  146. // in: path
  147. // description: name of the repo
  148. // type: string
  149. // required: true
  150. // - name: index
  151. // in: path
  152. // description: index of the pull request
  153. // type: integer
  154. // format: int64
  155. // required: true
  156. // - name: id
  157. // in: path
  158. // description: id of the review
  159. // type: integer
  160. // format: int64
  161. // required: true
  162. // responses:
  163. // "200":
  164. // "$ref": "#/responses/PullReviewCommentList"
  165. // "404":
  166. // "$ref": "#/responses/notFound"
  167. review, _, statusSet := prepareSingleReview(ctx)
  168. if statusSet {
  169. return
  170. }
  171. apiComments, err := convert.ToPullReviewCommentList(review, ctx.User)
  172. if err != nil {
  173. ctx.Error(http.StatusInternalServerError, "convertToPullReviewCommentList", err)
  174. return
  175. }
  176. ctx.JSON(http.StatusOK, apiComments)
  177. }
  178. // DeletePullReview delete a specific review from a pull request
  179. func DeletePullReview(ctx *context.APIContext) {
  180. // swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/reviews/{id} repository repoDeletePullReview
  181. // ---
  182. // summary: Delete a specific review from a pull request
  183. // produces:
  184. // - application/json
  185. // parameters:
  186. // - name: owner
  187. // in: path
  188. // description: owner of the repo
  189. // type: string
  190. // required: true
  191. // - name: repo
  192. // in: path
  193. // description: name of the repo
  194. // type: string
  195. // required: true
  196. // - name: index
  197. // in: path
  198. // description: index of the pull request
  199. // type: integer
  200. // format: int64
  201. // required: true
  202. // - name: id
  203. // in: path
  204. // description: id of the review
  205. // type: integer
  206. // format: int64
  207. // required: true
  208. // responses:
  209. // "204":
  210. // "$ref": "#/responses/empty"
  211. // "403":
  212. // "$ref": "#/responses/forbidden"
  213. // "404":
  214. // "$ref": "#/responses/notFound"
  215. review, _, statusSet := prepareSingleReview(ctx)
  216. if statusSet {
  217. return
  218. }
  219. if ctx.User == nil {
  220. ctx.NotFound()
  221. return
  222. }
  223. if !ctx.User.IsAdmin && ctx.User.ID != review.ReviewerID {
  224. ctx.Error(http.StatusForbidden, "only admin and user itself can delete a review", nil)
  225. return
  226. }
  227. if err := models.DeleteReview(review); err != nil {
  228. ctx.Error(http.StatusInternalServerError, "DeleteReview", fmt.Errorf("can not delete ReviewID: %d", review.ID))
  229. return
  230. }
  231. ctx.Status(http.StatusNoContent)
  232. }
  233. // CreatePullReview create a review to an pull request
  234. func CreatePullReview(ctx *context.APIContext, opts api.CreatePullReviewOptions) {
  235. // swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews repository repoCreatePullReview
  236. // ---
  237. // summary: Create a review to an pull request
  238. // produces:
  239. // - application/json
  240. // parameters:
  241. // - name: owner
  242. // in: path
  243. // description: owner of the repo
  244. // type: string
  245. // required: true
  246. // - name: repo
  247. // in: path
  248. // description: name of the repo
  249. // type: string
  250. // required: true
  251. // - name: index
  252. // in: path
  253. // description: index of the pull request
  254. // type: integer
  255. // format: int64
  256. // required: true
  257. // - name: body
  258. // in: body
  259. // required: true
  260. // schema:
  261. // "$ref": "#/definitions/CreatePullReviewOptions"
  262. // responses:
  263. // "200":
  264. // "$ref": "#/responses/PullReview"
  265. // "404":
  266. // "$ref": "#/responses/notFound"
  267. // "422":
  268. // "$ref": "#/responses/validationError"
  269. pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  270. if err != nil {
  271. if models.IsErrPullRequestNotExist(err) {
  272. ctx.NotFound("GetPullRequestByIndex", err)
  273. } else {
  274. ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
  275. }
  276. return
  277. }
  278. // determine review type
  279. reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body)
  280. if isWrong {
  281. return
  282. }
  283. if err := pr.Issue.LoadRepo(); err != nil {
  284. ctx.Error(http.StatusInternalServerError, "pr.Issue.LoadRepo", err)
  285. return
  286. }
  287. // create review comments
  288. for _, c := range opts.Comments {
  289. line := c.NewLineNum
  290. if c.OldLineNum > 0 {
  291. line = c.OldLineNum * -1
  292. }
  293. if _, err := pull_service.CreateCodeComment(
  294. ctx.User,
  295. ctx.Repo.GitRepo,
  296. pr.Issue,
  297. line,
  298. c.Body,
  299. c.Path,
  300. true, // is review
  301. 0, // no reply
  302. opts.CommitID,
  303. ); err != nil {
  304. ctx.ServerError("CreateCodeComment", err)
  305. return
  306. }
  307. }
  308. // create review and associate all pending review comments
  309. review, _, err := pull_service.SubmitReview(ctx.User, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, opts.CommitID)
  310. if err != nil {
  311. ctx.Error(http.StatusInternalServerError, "SubmitReview", err)
  312. return
  313. }
  314. // convert response
  315. apiReview, err := convert.ToPullReview(review, ctx.User)
  316. if err != nil {
  317. ctx.Error(http.StatusInternalServerError, "convertToPullReview", err)
  318. return
  319. }
  320. ctx.JSON(http.StatusOK, apiReview)
  321. }
  322. // SubmitPullReview submit a pending review to an pull request
  323. func SubmitPullReview(ctx *context.APIContext, opts api.SubmitPullReviewOptions) {
  324. // swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id} repository repoSubmitPullReview
  325. // ---
  326. // summary: Submit a pending review to an pull request
  327. // produces:
  328. // - application/json
  329. // parameters:
  330. // - name: owner
  331. // in: path
  332. // description: owner of the repo
  333. // type: string
  334. // required: true
  335. // - name: repo
  336. // in: path
  337. // description: name of the repo
  338. // type: string
  339. // required: true
  340. // - name: index
  341. // in: path
  342. // description: index of the pull request
  343. // type: integer
  344. // format: int64
  345. // required: true
  346. // - name: id
  347. // in: path
  348. // description: id of the review
  349. // type: integer
  350. // format: int64
  351. // required: true
  352. // - name: body
  353. // in: body
  354. // required: true
  355. // schema:
  356. // "$ref": "#/definitions/SubmitPullReviewOptions"
  357. // responses:
  358. // "200":
  359. // "$ref": "#/responses/PullReview"
  360. // "404":
  361. // "$ref": "#/responses/notFound"
  362. // "422":
  363. // "$ref": "#/responses/validationError"
  364. review, pr, isWrong := prepareSingleReview(ctx)
  365. if isWrong {
  366. return
  367. }
  368. if review.Type != models.ReviewTypePending {
  369. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("only a pending review can be submitted"))
  370. return
  371. }
  372. // determine review type
  373. reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body)
  374. if isWrong {
  375. return
  376. }
  377. // if review stay pending return
  378. if reviewType == models.ReviewTypePending {
  379. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review stay pending"))
  380. return
  381. }
  382. headCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(pr.GetGitRefName())
  383. if err != nil {
  384. ctx.Error(http.StatusInternalServerError, "GitRepo: GetRefCommitID", err)
  385. return
  386. }
  387. // create review and associate all pending review comments
  388. review, _, err = pull_service.SubmitReview(ctx.User, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, headCommitID)
  389. if err != nil {
  390. ctx.Error(http.StatusInternalServerError, "SubmitReview", err)
  391. return
  392. }
  393. // convert response
  394. apiReview, err := convert.ToPullReview(review, ctx.User)
  395. if err != nil {
  396. ctx.Error(http.StatusInternalServerError, "convertToPullReview", err)
  397. return
  398. }
  399. ctx.JSON(http.StatusOK, apiReview)
  400. }
  401. // preparePullReviewType return ReviewType and false or nil and true if an error happen
  402. func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, event api.ReviewStateType, body string) (models.ReviewType, bool) {
  403. if err := pr.LoadIssue(); err != nil {
  404. ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
  405. return -1, true
  406. }
  407. var reviewType models.ReviewType
  408. switch event {
  409. case api.ReviewStateApproved:
  410. // can not approve your own PR
  411. if pr.Issue.IsPoster(ctx.User.ID) {
  412. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("approve your own pull is not allowed"))
  413. return -1, true
  414. }
  415. reviewType = models.ReviewTypeApprove
  416. case api.ReviewStateRequestChanges:
  417. // can not reject your own PR
  418. if pr.Issue.IsPoster(ctx.User.ID) {
  419. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("reject your own pull is not allowed"))
  420. return -1, true
  421. }
  422. reviewType = models.ReviewTypeReject
  423. case api.ReviewStateComment:
  424. reviewType = models.ReviewTypeComment
  425. default:
  426. reviewType = models.ReviewTypePending
  427. }
  428. // reject reviews with empty body if not approve type
  429. if reviewType != models.ReviewTypeApprove && len(strings.TrimSpace(body)) == 0 {
  430. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review event %s need body", event))
  431. return -1, true
  432. }
  433. return reviewType, false
  434. }
  435. // prepareSingleReview return review, related pull and false or nil, nil and true if an error happen
  436. func prepareSingleReview(ctx *context.APIContext) (*models.Review, *models.PullRequest, bool) {
  437. pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  438. if err != nil {
  439. if models.IsErrPullRequestNotExist(err) {
  440. ctx.NotFound("GetPullRequestByIndex", err)
  441. } else {
  442. ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
  443. }
  444. return nil, nil, true
  445. }
  446. review, err := models.GetReviewByID(ctx.ParamsInt64(":id"))
  447. if err != nil {
  448. if models.IsErrReviewNotExist(err) {
  449. ctx.NotFound("GetReviewByID", err)
  450. } else {
  451. ctx.Error(http.StatusInternalServerError, "GetReviewByID", err)
  452. }
  453. return nil, nil, true
  454. }
  455. // validate the the review is for the given PR
  456. if review.IssueID != pr.IssueID {
  457. ctx.NotFound("ReviewNotInPR")
  458. return nil, nil, true
  459. }
  460. // make sure that the user has access to this review if it is pending
  461. if review.Type == models.ReviewTypePending && review.ReviewerID != ctx.User.ID && !ctx.User.IsAdmin {
  462. ctx.NotFound("GetReviewByID")
  463. return nil, nil, true
  464. }
  465. if err := review.LoadAttributes(); err != nil && !models.IsErrUserNotExist(err) {
  466. ctx.Error(http.StatusInternalServerError, "ReviewLoadAttributes", err)
  467. return nil, nil, true
  468. }
  469. return review, pr, false
  470. }