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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  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. "strings"
  7. "code.gitea.io/gitea/modules/timeutil"
  8. "xorm.io/builder"
  9. )
  10. // ReviewType defines the sort of feedback a review gives
  11. type ReviewType int
  12. // ReviewTypeUnknown unknown review type
  13. const ReviewTypeUnknown ReviewType = -1
  14. const (
  15. // ReviewTypePending is a review which is not published yet
  16. ReviewTypePending ReviewType = iota
  17. // ReviewTypeApprove approves changes
  18. ReviewTypeApprove
  19. // ReviewTypeComment gives general feedback
  20. ReviewTypeComment
  21. // ReviewTypeReject gives feedback blocking merge
  22. ReviewTypeReject
  23. )
  24. // Icon returns the corresponding icon for the review type
  25. func (rt ReviewType) Icon() string {
  26. switch rt {
  27. case ReviewTypeApprove:
  28. return "check"
  29. case ReviewTypeReject:
  30. return "request-changes"
  31. case ReviewTypeComment:
  32. return "comment"
  33. default:
  34. return "comment"
  35. }
  36. }
  37. // Review represents collection of code comments giving feedback for a PR
  38. type Review struct {
  39. ID int64 `xorm:"pk autoincr"`
  40. Type ReviewType
  41. Reviewer *User `xorm:"-"`
  42. ReviewerID int64 `xorm:"index"`
  43. OriginalAuthor string
  44. OriginalAuthorID int64
  45. Issue *Issue `xorm:"-"`
  46. IssueID int64 `xorm:"index"`
  47. Content string `xorm:"TEXT"`
  48. // Official is a review made by an assigned approver (counts towards approval)
  49. Official bool `xorm:"NOT NULL DEFAULT false"`
  50. CommitID string `xorm:"VARCHAR(40)"`
  51. Stale bool `xorm:"NOT NULL DEFAULT false"`
  52. CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
  53. UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
  54. // CodeComments are the initial code comments of the review
  55. CodeComments CodeComments `xorm:"-"`
  56. Comments []*Comment `xorm:"-"`
  57. }
  58. func (r *Review) loadCodeComments(e Engine) (err error) {
  59. if r.CodeComments == nil {
  60. r.CodeComments, err = fetchCodeCommentsByReview(e, r.Issue, nil, r)
  61. }
  62. return
  63. }
  64. // LoadCodeComments loads CodeComments
  65. func (r *Review) LoadCodeComments() error {
  66. return r.loadCodeComments(x)
  67. }
  68. func (r *Review) loadIssue(e Engine) (err error) {
  69. r.Issue, err = getIssueByID(e, r.IssueID)
  70. return
  71. }
  72. func (r *Review) loadReviewer(e Engine) (err error) {
  73. if r.ReviewerID == 0 {
  74. return nil
  75. }
  76. r.Reviewer, err = getUserByID(e, r.ReviewerID)
  77. return
  78. }
  79. // LoadReviewer loads reviewer
  80. func (r *Review) LoadReviewer() error {
  81. return r.loadReviewer(x)
  82. }
  83. func (r *Review) loadAttributes(e Engine) (err error) {
  84. if err = r.loadReviewer(e); err != nil {
  85. return
  86. }
  87. if err = r.loadIssue(e); err != nil {
  88. return
  89. }
  90. return
  91. }
  92. // LoadAttributes loads all attributes except CodeComments
  93. func (r *Review) LoadAttributes() error {
  94. return r.loadAttributes(x)
  95. }
  96. func getReviewByID(e Engine, id int64) (*Review, error) {
  97. review := new(Review)
  98. if has, err := e.ID(id).Get(review); err != nil {
  99. return nil, err
  100. } else if !has {
  101. return nil, ErrReviewNotExist{ID: id}
  102. } else {
  103. return review, nil
  104. }
  105. }
  106. // GetReviewByID returns the review by the given ID
  107. func GetReviewByID(id int64) (*Review, error) {
  108. return getReviewByID(x, id)
  109. }
  110. // FindReviewOptions represent possible filters to find reviews
  111. type FindReviewOptions struct {
  112. Type ReviewType
  113. IssueID int64
  114. ReviewerID int64
  115. OfficialOnly bool
  116. }
  117. func (opts *FindReviewOptions) toCond() builder.Cond {
  118. var cond = builder.NewCond()
  119. if opts.IssueID > 0 {
  120. cond = cond.And(builder.Eq{"issue_id": opts.IssueID})
  121. }
  122. if opts.ReviewerID > 0 {
  123. cond = cond.And(builder.Eq{"reviewer_id": opts.ReviewerID})
  124. }
  125. if opts.Type != ReviewTypeUnknown {
  126. cond = cond.And(builder.Eq{"type": opts.Type})
  127. }
  128. if opts.OfficialOnly {
  129. cond = cond.And(builder.Eq{"official": true})
  130. }
  131. return cond
  132. }
  133. func findReviews(e Engine, opts FindReviewOptions) ([]*Review, error) {
  134. reviews := make([]*Review, 0, 10)
  135. sess := e.Where(opts.toCond())
  136. return reviews, sess.
  137. Asc("created_unix").
  138. Asc("id").
  139. Find(&reviews)
  140. }
  141. // FindReviews returns reviews passing FindReviewOptions
  142. func FindReviews(opts FindReviewOptions) ([]*Review, error) {
  143. return findReviews(x, opts)
  144. }
  145. // CreateReviewOptions represent the options to create a review. Type, Issue and Reviewer are required.
  146. type CreateReviewOptions struct {
  147. Content string
  148. Type ReviewType
  149. Issue *Issue
  150. Reviewer *User
  151. Official bool
  152. CommitID string
  153. Stale bool
  154. }
  155. // IsOfficialReviewer check if reviewer can make official reviews in issue (counts towards required approvals)
  156. func IsOfficialReviewer(issue *Issue, reviewer *User) (bool, error) {
  157. return isOfficialReviewer(x, issue, reviewer)
  158. }
  159. func isOfficialReviewer(e Engine, issue *Issue, reviewer *User) (bool, error) {
  160. pr, err := getPullRequestByIssueID(e, issue.ID)
  161. if err != nil {
  162. return false, err
  163. }
  164. if err = pr.loadProtectedBranch(e); err != nil {
  165. return false, err
  166. }
  167. if pr.ProtectedBranch == nil {
  168. return false, nil
  169. }
  170. return pr.ProtectedBranch.isUserOfficialReviewer(e, reviewer)
  171. }
  172. func createReview(e Engine, opts CreateReviewOptions) (*Review, error) {
  173. review := &Review{
  174. Type: opts.Type,
  175. Issue: opts.Issue,
  176. IssueID: opts.Issue.ID,
  177. Reviewer: opts.Reviewer,
  178. ReviewerID: opts.Reviewer.ID,
  179. Content: opts.Content,
  180. Official: opts.Official,
  181. CommitID: opts.CommitID,
  182. Stale: opts.Stale,
  183. }
  184. if _, err := e.Insert(review); err != nil {
  185. return nil, err
  186. }
  187. return review, nil
  188. }
  189. // CreateReview creates a new review based on opts
  190. func CreateReview(opts CreateReviewOptions) (*Review, error) {
  191. return createReview(x, opts)
  192. }
  193. func getCurrentReview(e Engine, reviewer *User, issue *Issue) (*Review, error) {
  194. if reviewer == nil {
  195. return nil, nil
  196. }
  197. reviews, err := findReviews(e, FindReviewOptions{
  198. Type: ReviewTypePending,
  199. IssueID: issue.ID,
  200. ReviewerID: reviewer.ID,
  201. })
  202. if err != nil {
  203. return nil, err
  204. }
  205. if len(reviews) == 0 {
  206. return nil, ErrReviewNotExist{}
  207. }
  208. reviews[0].Reviewer = reviewer
  209. reviews[0].Issue = issue
  210. return reviews[0], nil
  211. }
  212. // ReviewExists returns whether a review exists for a particular line of code in the PR
  213. func ReviewExists(issue *Issue, treePath string, line int64) (bool, error) {
  214. return x.Cols("id").Exist(&Comment{IssueID: issue.ID, TreePath: treePath, Line: line, Type: CommentTypeCode})
  215. }
  216. // GetCurrentReview returns the current pending review of reviewer for given issue
  217. func GetCurrentReview(reviewer *User, issue *Issue) (*Review, error) {
  218. return getCurrentReview(x, reviewer, issue)
  219. }
  220. // ContentEmptyErr represents an content empty error
  221. type ContentEmptyErr struct {
  222. }
  223. func (ContentEmptyErr) Error() string {
  224. return "Review content is empty"
  225. }
  226. // IsContentEmptyErr returns true if err is a ContentEmptyErr
  227. func IsContentEmptyErr(err error) bool {
  228. _, ok := err.(ContentEmptyErr)
  229. return ok
  230. }
  231. // SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
  232. func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content, commitID string, stale bool) (*Review, *Comment, error) {
  233. sess := x.NewSession()
  234. defer sess.Close()
  235. if err := sess.Begin(); err != nil {
  236. return nil, nil, err
  237. }
  238. var official = false
  239. review, err := getCurrentReview(sess, doer, issue)
  240. if err != nil {
  241. if !IsErrReviewNotExist(err) {
  242. return nil, nil, err
  243. }
  244. if reviewType != ReviewTypeApprove && len(strings.TrimSpace(content)) == 0 {
  245. return nil, nil, ContentEmptyErr{}
  246. }
  247. if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject {
  248. // Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared
  249. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, doer.ID); err != nil {
  250. return nil, nil, err
  251. }
  252. official, err = isOfficialReviewer(sess, issue, doer)
  253. if err != nil {
  254. return nil, nil, err
  255. }
  256. }
  257. // No current review. Create a new one!
  258. review, err = createReview(sess, CreateReviewOptions{
  259. Type: reviewType,
  260. Issue: issue,
  261. Reviewer: doer,
  262. Content: content,
  263. Official: official,
  264. CommitID: commitID,
  265. Stale: stale,
  266. })
  267. if err != nil {
  268. return nil, nil, err
  269. }
  270. } else {
  271. if err := review.loadCodeComments(sess); err != nil {
  272. return nil, nil, err
  273. }
  274. if reviewType != ReviewTypeApprove && len(review.CodeComments) == 0 && len(strings.TrimSpace(content)) == 0 {
  275. return nil, nil, ContentEmptyErr{}
  276. }
  277. if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject {
  278. // Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared
  279. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, doer.ID); err != nil {
  280. return nil, nil, err
  281. }
  282. official, err = isOfficialReviewer(sess, issue, doer)
  283. if err != nil {
  284. return nil, nil, err
  285. }
  286. }
  287. review.Official = official
  288. review.Issue = issue
  289. review.Content = content
  290. review.Type = reviewType
  291. review.CommitID = commitID
  292. review.Stale = stale
  293. if _, err := sess.ID(review.ID).Cols("content, type, official, commit_id, stale").Update(review); err != nil {
  294. return nil, nil, err
  295. }
  296. }
  297. comm, err := createComment(sess, &CreateCommentOptions{
  298. Type: CommentTypeReview,
  299. Doer: doer,
  300. Content: review.Content,
  301. Issue: issue,
  302. Repo: issue.Repo,
  303. ReviewID: review.ID,
  304. })
  305. if err != nil || comm == nil {
  306. return nil, nil, err
  307. }
  308. comm.Review = review
  309. return review, comm, sess.Commit()
  310. }
  311. // GetReviewersByIssueID gets the latest review of each reviewer for a pull request
  312. func GetReviewersByIssueID(issueID int64) (reviews []*Review, err error) {
  313. reviewsUnfiltered := []*Review{}
  314. sess := x.NewSession()
  315. defer sess.Close()
  316. if err := sess.Begin(); err != nil {
  317. return nil, err
  318. }
  319. // Get latest review of each reviwer, sorted in order they were made
  320. 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",
  321. issueID, ReviewTypeApprove, ReviewTypeReject).
  322. Find(&reviewsUnfiltered); err != nil {
  323. return nil, err
  324. }
  325. // Load reviewer and skip if user is deleted
  326. for _, review := range reviewsUnfiltered {
  327. if err := review.loadReviewer(sess); err != nil {
  328. if !IsErrUserNotExist(err) {
  329. return nil, err
  330. }
  331. } else {
  332. reviews = append(reviews, review)
  333. }
  334. }
  335. return reviews, nil
  336. }
  337. // MarkReviewsAsStale marks existing reviews as stale
  338. func MarkReviewsAsStale(issueID int64) (err error) {
  339. _, err = x.Exec("UPDATE `review` SET stale=? WHERE issue_id=?", true, issueID)
  340. return
  341. }
  342. // MarkReviewsAsNotStale marks existing reviews as not stale for a giving commit SHA
  343. func MarkReviewsAsNotStale(issueID int64, commitID string) (err error) {
  344. _, err = x.Exec("UPDATE `review` SET stale=? WHERE issue_id=? AND commit_id=?", false, issueID, commitID)
  345. return
  346. }
  347. // InsertReviews inserts review and review comments
  348. func InsertReviews(reviews []*Review) error {
  349. sess := x.NewSession()
  350. defer sess.Close()
  351. if err := sess.Begin(); err != nil {
  352. return err
  353. }
  354. for _, review := range reviews {
  355. if _, err := sess.NoAutoTime().Insert(review); err != nil {
  356. return err
  357. }
  358. if _, err := sess.NoAutoTime().Insert(&Comment{
  359. Type: CommentTypeReview,
  360. Content: review.Content,
  361. PosterID: review.ReviewerID,
  362. OriginalAuthor: review.OriginalAuthor,
  363. OriginalAuthorID: review.OriginalAuthorID,
  364. IssueID: review.IssueID,
  365. ReviewID: review.ID,
  366. CreatedUnix: review.CreatedUnix,
  367. UpdatedUnix: review.UpdatedUnix,
  368. }); err != nil {
  369. return err
  370. }
  371. for _, c := range review.Comments {
  372. c.ReviewID = review.ID
  373. }
  374. if _, err := sess.NoAutoTime().Insert(review.Comments); err != nil {
  375. return err
  376. }
  377. }
  378. return sess.Commit()
  379. }