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

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