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.

review.go 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658
  1. // Copyright 2018 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 models
  5. import (
  6. "fmt"
  7. "strings"
  8. "code.gitea.io/gitea/modules/timeutil"
  9. "xorm.io/builder"
  10. )
  11. // ReviewType defines the sort of feedback a review gives
  12. type ReviewType int
  13. // ReviewTypeUnknown unknown review type
  14. const ReviewTypeUnknown ReviewType = -1
  15. const (
  16. // ReviewTypePending is a review which is not published yet
  17. ReviewTypePending ReviewType = iota
  18. // ReviewTypeApprove approves changes
  19. ReviewTypeApprove
  20. // ReviewTypeComment gives general feedback
  21. ReviewTypeComment
  22. // ReviewTypeReject gives feedback blocking merge
  23. ReviewTypeReject
  24. // ReviewTypeRequest request review from others
  25. ReviewTypeRequest
  26. )
  27. // Icon returns the corresponding icon for the review type
  28. func (rt ReviewType) Icon() string {
  29. switch rt {
  30. case ReviewTypeApprove:
  31. return "check"
  32. case ReviewTypeReject:
  33. return "request-changes"
  34. case ReviewTypeComment:
  35. return "comment"
  36. case ReviewTypeRequest:
  37. return "primitive-dot"
  38. default:
  39. return "comment"
  40. }
  41. }
  42. // Review represents collection of code comments giving feedback for a PR
  43. type Review struct {
  44. ID int64 `xorm:"pk autoincr"`
  45. Type ReviewType
  46. Reviewer *User `xorm:"-"`
  47. ReviewerID int64 `xorm:"index"`
  48. OriginalAuthor string
  49. OriginalAuthorID int64
  50. Issue *Issue `xorm:"-"`
  51. IssueID int64 `xorm:"index"`
  52. Content string `xorm:"TEXT"`
  53. // Official is a review made by an assigned approver (counts towards approval)
  54. Official bool `xorm:"NOT NULL DEFAULT false"`
  55. CommitID string `xorm:"VARCHAR(40)"`
  56. Stale bool `xorm:"NOT NULL DEFAULT false"`
  57. CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
  58. UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
  59. // CodeComments are the initial code comments of the review
  60. CodeComments CodeComments `xorm:"-"`
  61. Comments []*Comment `xorm:"-"`
  62. }
  63. func (r *Review) loadCodeComments(e Engine) (err error) {
  64. if r.CodeComments == nil {
  65. r.CodeComments, err = fetchCodeCommentsByReview(e, r.Issue, nil, r)
  66. }
  67. return
  68. }
  69. // LoadCodeComments loads CodeComments
  70. func (r *Review) LoadCodeComments() error {
  71. return r.loadCodeComments(x)
  72. }
  73. func (r *Review) loadIssue(e Engine) (err error) {
  74. r.Issue, err = getIssueByID(e, r.IssueID)
  75. return
  76. }
  77. func (r *Review) loadReviewer(e Engine) (err error) {
  78. if r.ReviewerID == 0 {
  79. return nil
  80. }
  81. r.Reviewer, err = getUserByID(e, r.ReviewerID)
  82. return
  83. }
  84. // LoadReviewer loads reviewer
  85. func (r *Review) LoadReviewer() error {
  86. return r.loadReviewer(x)
  87. }
  88. func (r *Review) loadAttributes(e Engine) (err error) {
  89. if err = r.loadReviewer(e); err != nil {
  90. return
  91. }
  92. if err = r.loadIssue(e); err != nil {
  93. return
  94. }
  95. return
  96. }
  97. // LoadAttributes loads all attributes except CodeComments
  98. func (r *Review) LoadAttributes() error {
  99. return r.loadAttributes(x)
  100. }
  101. func getReviewByID(e Engine, id int64) (*Review, error) {
  102. review := new(Review)
  103. if has, err := e.ID(id).Get(review); err != nil {
  104. return nil, err
  105. } else if !has {
  106. return nil, ErrReviewNotExist{ID: id}
  107. } else {
  108. return review, nil
  109. }
  110. }
  111. // GetReviewByID returns the review by the given ID
  112. func GetReviewByID(id int64) (*Review, error) {
  113. return getReviewByID(x, id)
  114. }
  115. // FindReviewOptions represent possible filters to find reviews
  116. type FindReviewOptions struct {
  117. Type ReviewType
  118. IssueID int64
  119. ReviewerID int64
  120. OfficialOnly bool
  121. }
  122. func (opts *FindReviewOptions) toCond() builder.Cond {
  123. var cond = builder.NewCond()
  124. if opts.IssueID > 0 {
  125. cond = cond.And(builder.Eq{"issue_id": opts.IssueID})
  126. }
  127. if opts.ReviewerID > 0 {
  128. cond = cond.And(builder.Eq{"reviewer_id": opts.ReviewerID})
  129. }
  130. if opts.Type != ReviewTypeUnknown {
  131. cond = cond.And(builder.Eq{"type": opts.Type})
  132. }
  133. if opts.OfficialOnly {
  134. cond = cond.And(builder.Eq{"official": true})
  135. }
  136. return cond
  137. }
  138. func findReviews(e Engine, opts FindReviewOptions) ([]*Review, error) {
  139. reviews := make([]*Review, 0, 10)
  140. sess := e.Where(opts.toCond())
  141. return reviews, sess.
  142. Asc("created_unix").
  143. Asc("id").
  144. Find(&reviews)
  145. }
  146. // FindReviews returns reviews passing FindReviewOptions
  147. func FindReviews(opts FindReviewOptions) ([]*Review, error) {
  148. return findReviews(x, opts)
  149. }
  150. // CreateReviewOptions represent the options to create a review. Type, Issue and Reviewer are required.
  151. type CreateReviewOptions struct {
  152. Content string
  153. Type ReviewType
  154. Issue *Issue
  155. Reviewer *User
  156. Official bool
  157. CommitID string
  158. Stale bool
  159. }
  160. // IsOfficialReviewer check if reviewer can make official reviews in issue (counts towards required approvals)
  161. func IsOfficialReviewer(issue *Issue, reviewer *User) (bool, error) {
  162. return isOfficialReviewer(x, issue, reviewer)
  163. }
  164. func isOfficialReviewer(e Engine, issue *Issue, reviewer *User) (bool, error) {
  165. pr, err := getPullRequestByIssueID(e, issue.ID)
  166. if err != nil {
  167. return false, err
  168. }
  169. if err = pr.loadProtectedBranch(e); err != nil {
  170. return false, err
  171. }
  172. if pr.ProtectedBranch == nil {
  173. return false, nil
  174. }
  175. return pr.ProtectedBranch.isUserOfficialReviewer(e, reviewer)
  176. }
  177. func createReview(e Engine, opts CreateReviewOptions) (*Review, error) {
  178. review := &Review{
  179. Type: opts.Type,
  180. Issue: opts.Issue,
  181. IssueID: opts.Issue.ID,
  182. Reviewer: opts.Reviewer,
  183. ReviewerID: opts.Reviewer.ID,
  184. Content: opts.Content,
  185. Official: opts.Official,
  186. CommitID: opts.CommitID,
  187. Stale: opts.Stale,
  188. }
  189. if _, err := e.Insert(review); err != nil {
  190. return nil, err
  191. }
  192. return review, nil
  193. }
  194. // CreateReview creates a new review based on opts
  195. func CreateReview(opts CreateReviewOptions) (*Review, error) {
  196. return createReview(x, opts)
  197. }
  198. func getCurrentReview(e Engine, reviewer *User, issue *Issue) (*Review, error) {
  199. if reviewer == nil {
  200. return nil, nil
  201. }
  202. reviews, err := findReviews(e, FindReviewOptions{
  203. Type: ReviewTypePending,
  204. IssueID: issue.ID,
  205. ReviewerID: reviewer.ID,
  206. })
  207. if err != nil {
  208. return nil, err
  209. }
  210. if len(reviews) == 0 {
  211. return nil, ErrReviewNotExist{}
  212. }
  213. reviews[0].Reviewer = reviewer
  214. reviews[0].Issue = issue
  215. return reviews[0], nil
  216. }
  217. // ReviewExists returns whether a review exists for a particular line of code in the PR
  218. func ReviewExists(issue *Issue, treePath string, line int64) (bool, error) {
  219. return x.Cols("id").Exist(&Comment{IssueID: issue.ID, TreePath: treePath, Line: line, Type: CommentTypeCode})
  220. }
  221. // GetCurrentReview returns the current pending review of reviewer for given issue
  222. func GetCurrentReview(reviewer *User, issue *Issue) (*Review, error) {
  223. return getCurrentReview(x, reviewer, issue)
  224. }
  225. // ContentEmptyErr represents an content empty error
  226. type ContentEmptyErr struct {
  227. }
  228. func (ContentEmptyErr) Error() string {
  229. return "Review content is empty"
  230. }
  231. // IsContentEmptyErr returns true if err is a ContentEmptyErr
  232. func IsContentEmptyErr(err error) bool {
  233. _, ok := err.(ContentEmptyErr)
  234. return ok
  235. }
  236. // SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
  237. func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content, commitID string, stale bool) (*Review, *Comment, error) {
  238. sess := x.NewSession()
  239. defer sess.Close()
  240. if err := sess.Begin(); err != nil {
  241. return nil, nil, err
  242. }
  243. var official = false
  244. review, err := getCurrentReview(sess, doer, issue)
  245. if err != nil {
  246. if !IsErrReviewNotExist(err) {
  247. return nil, nil, err
  248. }
  249. if reviewType != ReviewTypeApprove && len(strings.TrimSpace(content)) == 0 {
  250. return nil, nil, ContentEmptyErr{}
  251. }
  252. if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject {
  253. // Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared
  254. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, doer.ID); err != nil {
  255. return nil, nil, err
  256. }
  257. official, err = isOfficialReviewer(sess, issue, doer)
  258. if err != nil {
  259. return nil, nil, err
  260. }
  261. }
  262. // No current review. Create a new one!
  263. review, err = createReview(sess, CreateReviewOptions{
  264. Type: reviewType,
  265. Issue: issue,
  266. Reviewer: doer,
  267. Content: content,
  268. Official: official,
  269. CommitID: commitID,
  270. Stale: stale,
  271. })
  272. if err != nil {
  273. return nil, nil, err
  274. }
  275. } else {
  276. if err := review.loadCodeComments(sess); err != nil {
  277. return nil, nil, err
  278. }
  279. if reviewType != ReviewTypeApprove && len(review.CodeComments) == 0 && len(strings.TrimSpace(content)) == 0 {
  280. return nil, nil, ContentEmptyErr{}
  281. }
  282. if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject {
  283. // Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared
  284. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, doer.ID); err != nil {
  285. return nil, nil, err
  286. }
  287. official, err = isOfficialReviewer(sess, issue, doer)
  288. if err != nil {
  289. return nil, nil, err
  290. }
  291. }
  292. review.Official = official
  293. review.Issue = issue
  294. review.Content = content
  295. review.Type = reviewType
  296. review.CommitID = commitID
  297. review.Stale = stale
  298. if _, err := sess.ID(review.ID).Cols("content, type, official, commit_id, stale").Update(review); err != nil {
  299. return nil, nil, err
  300. }
  301. }
  302. comm, err := createComment(sess, &CreateCommentOptions{
  303. Type: CommentTypeReview,
  304. Doer: doer,
  305. Content: review.Content,
  306. Issue: issue,
  307. Repo: issue.Repo,
  308. ReviewID: review.ID,
  309. })
  310. if err != nil || comm == nil {
  311. return nil, nil, err
  312. }
  313. comm.Review = review
  314. return review, comm, sess.Commit()
  315. }
  316. // GetReviewersByIssueID gets the latest review of each reviewer for a pull request
  317. func GetReviewersByIssueID(issueID int64) (reviews []*Review, err error) {
  318. reviewsUnfiltered := []*Review{}
  319. sess := x.NewSession()
  320. defer sess.Close()
  321. if err := sess.Begin(); err != nil {
  322. return nil, err
  323. }
  324. // Get latest review of each reviwer, sorted in order they were made
  325. if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND type in (?, ?, ?) GROUP BY issue_id, reviewer_id) ORDER BY review.updated_unix ASC",
  326. issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest).
  327. Find(&reviewsUnfiltered); err != nil {
  328. return nil, err
  329. }
  330. // Load reviewer and skip if user is deleted
  331. for _, review := range reviewsUnfiltered {
  332. if err = review.loadReviewer(sess); err != nil {
  333. if !IsErrUserNotExist(err) {
  334. return nil, err
  335. }
  336. } else {
  337. reviews = append(reviews, review)
  338. }
  339. }
  340. return reviews, nil
  341. }
  342. // GetReviewerByIssueIDAndUserID get the latest review of reviewer for a pull request
  343. func GetReviewerByIssueIDAndUserID(issueID, userID int64) (review *Review, err error) {
  344. return getReviewerByIssueIDAndUserID(x, issueID, userID)
  345. }
  346. func getReviewerByIssueIDAndUserID(e Engine, issueID, userID int64) (review *Review, err error) {
  347. review = new(Review)
  348. if _, err := e.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_id = ? AND type in (?, ?, ?))",
  349. issueID, userID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest).
  350. Get(review); err != nil {
  351. return nil, err
  352. }
  353. return
  354. }
  355. // MarkReviewsAsStale marks existing reviews as stale
  356. func MarkReviewsAsStale(issueID int64) (err error) {
  357. _, err = x.Exec("UPDATE `review` SET stale=? WHERE issue_id=?", true, issueID)
  358. return
  359. }
  360. // MarkReviewsAsNotStale marks existing reviews as not stale for a giving commit SHA
  361. func MarkReviewsAsNotStale(issueID int64, commitID string) (err error) {
  362. _, err = x.Exec("UPDATE `review` SET stale=? WHERE issue_id=? AND commit_id=?", false, issueID, commitID)
  363. return
  364. }
  365. // InsertReviews inserts review and review comments
  366. func InsertReviews(reviews []*Review) error {
  367. sess := x.NewSession()
  368. defer sess.Close()
  369. if err := sess.Begin(); err != nil {
  370. return err
  371. }
  372. for _, review := range reviews {
  373. if _, err := sess.NoAutoTime().Insert(review); err != nil {
  374. return err
  375. }
  376. if _, err := sess.NoAutoTime().Insert(&Comment{
  377. Type: CommentTypeReview,
  378. Content: review.Content,
  379. PosterID: review.ReviewerID,
  380. OriginalAuthor: review.OriginalAuthor,
  381. OriginalAuthorID: review.OriginalAuthorID,
  382. IssueID: review.IssueID,
  383. ReviewID: review.ID,
  384. CreatedUnix: review.CreatedUnix,
  385. UpdatedUnix: review.UpdatedUnix,
  386. }); err != nil {
  387. return err
  388. }
  389. for _, c := range review.Comments {
  390. c.ReviewID = review.ID
  391. }
  392. if len(review.Comments) > 0 {
  393. if _, err := sess.NoAutoTime().Insert(review.Comments); err != nil {
  394. return err
  395. }
  396. }
  397. }
  398. return sess.Commit()
  399. }
  400. // AddRewiewRequest add a review request from one reviewer
  401. func AddRewiewRequest(issue *Issue, reviewer *User, doer *User) (comment *Comment, err error) {
  402. review, err := GetReviewerByIssueIDAndUserID(issue.ID, reviewer.ID)
  403. if err != nil {
  404. return
  405. }
  406. // skip it when reviewer hase been request to review
  407. if review != nil && review.Type == ReviewTypeRequest {
  408. return nil, nil
  409. }
  410. sess := x.NewSession()
  411. defer sess.Close()
  412. if err := sess.Begin(); err != nil {
  413. return nil, err
  414. }
  415. var official bool
  416. official, err = isOfficialReviewer(sess, issue, reviewer)
  417. if err != nil {
  418. return nil, err
  419. }
  420. if !official {
  421. official, err = isOfficialReviewer(sess, issue, doer)
  422. if err != nil {
  423. return nil, err
  424. }
  425. }
  426. if official {
  427. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, reviewer.ID); err != nil {
  428. return nil, err
  429. }
  430. }
  431. _, err = createReview(sess, CreateReviewOptions{
  432. Type: ReviewTypeRequest,
  433. Issue: issue,
  434. Reviewer: reviewer,
  435. Official: official,
  436. Stale: false,
  437. })
  438. if err != nil {
  439. return
  440. }
  441. comment, err = createComment(sess, &CreateCommentOptions{
  442. Type: CommentTypeReviewRequest,
  443. Doer: doer,
  444. Repo: issue.Repo,
  445. Issue: issue,
  446. RemovedAssignee: false, // Use RemovedAssignee as !isRequest
  447. AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID
  448. })
  449. if err != nil {
  450. return nil, err
  451. }
  452. return comment, sess.Commit()
  453. }
  454. //RemoveRewiewRequest remove a review request from one reviewer
  455. func RemoveRewiewRequest(issue *Issue, reviewer *User, doer *User) (comment *Comment, err error) {
  456. review, err := GetReviewerByIssueIDAndUserID(issue.ID, reviewer.ID)
  457. if err != nil {
  458. return
  459. }
  460. if review.Type != ReviewTypeRequest {
  461. return nil, nil
  462. }
  463. sess := x.NewSession()
  464. defer sess.Close()
  465. if err := sess.Begin(); err != nil {
  466. return nil, err
  467. }
  468. _, err = sess.Delete(review)
  469. if err != nil {
  470. return nil, err
  471. }
  472. var official bool
  473. official, err = isOfficialReviewer(sess, issue, reviewer)
  474. if err != nil {
  475. return
  476. }
  477. if official {
  478. // recalculate which is the latest official review from that user
  479. var review *Review
  480. review, err = getReviewerByIssueIDAndUserID(sess, issue.ID, reviewer.ID)
  481. if err != nil {
  482. return nil, err
  483. }
  484. if review != nil {
  485. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil {
  486. return nil, err
  487. }
  488. }
  489. }
  490. if err != nil {
  491. return nil, err
  492. }
  493. comment, err = createComment(sess, &CreateCommentOptions{
  494. Type: CommentTypeReviewRequest,
  495. Doer: doer,
  496. Repo: issue.Repo,
  497. Issue: issue,
  498. RemovedAssignee: true, // Use RemovedAssignee as !isRequest
  499. AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID
  500. })
  501. if err != nil {
  502. return nil, err
  503. }
  504. return comment, sess.Commit()
  505. }
  506. // MarkConversation Add or remove Conversation mark for a code comment
  507. func MarkConversation(comment *Comment, doer *User, isResolve bool) (err error) {
  508. if comment.Type != CommentTypeCode {
  509. return nil
  510. }
  511. if isResolve {
  512. if comment.ResolveDoerID != 0 {
  513. return nil
  514. }
  515. if _, err = x.Exec("UPDATE `comment` SET resolve_doer_id=? WHERE id=?", doer.ID, comment.ID); err != nil {
  516. return err
  517. }
  518. } else {
  519. if comment.ResolveDoerID == 0 {
  520. return nil
  521. }
  522. if _, err = x.Exec("UPDATE `comment` SET resolve_doer_id=? WHERE id=?", 0, comment.ID); err != nil {
  523. return err
  524. }
  525. }
  526. return nil
  527. }
  528. // CanMarkConversation Add or remove Conversation mark for a code comment permission check
  529. // the PR writer , offfcial reviewer and poster can do it
  530. func CanMarkConversation(issue *Issue, doer *User) (permResult bool, err error) {
  531. if doer == nil || issue == nil {
  532. return false, fmt.Errorf("issue or doer is nil")
  533. }
  534. if doer.ID != issue.PosterID {
  535. if err = issue.LoadRepo(); err != nil {
  536. return false, err
  537. }
  538. perm, err := GetUserRepoPermission(issue.Repo, doer)
  539. if err != nil {
  540. return false, err
  541. }
  542. permResult = perm.CanAccess(AccessModeWrite, UnitTypePullRequests)
  543. if !permResult {
  544. if permResult, err = IsOfficialReviewer(issue, doer); err != nil {
  545. return false, err
  546. }
  547. }
  548. if !permResult {
  549. return false, nil
  550. }
  551. }
  552. return true, nil
  553. }