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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  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. "xorm.io/core"
  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. )
  25. // Icon returns the corresponding icon for the review type
  26. func (rt ReviewType) Icon() string {
  27. switch rt {
  28. case ReviewTypeApprove:
  29. return "eye"
  30. case ReviewTypeReject:
  31. return "x"
  32. case ReviewTypeComment, ReviewTypeUnknown:
  33. return "comment"
  34. default:
  35. return "comment"
  36. }
  37. }
  38. // Review represents collection of code comments giving feedback for a PR
  39. type Review struct {
  40. ID int64 `xorm:"pk autoincr"`
  41. Type ReviewType
  42. Reviewer *User `xorm:"-"`
  43. ReviewerID int64 `xorm:"index"`
  44. Issue *Issue `xorm:"-"`
  45. IssueID int64 `xorm:"index"`
  46. Content string
  47. CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
  48. UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
  49. // CodeComments are the initial code comments of the review
  50. CodeComments CodeComments `xorm:"-"`
  51. }
  52. func (r *Review) loadCodeComments(e Engine) (err error) {
  53. if r.CodeComments == nil {
  54. r.CodeComments, err = fetchCodeCommentsByReview(e, r.Issue, nil, r)
  55. }
  56. return
  57. }
  58. // LoadCodeComments loads CodeComments
  59. func (r *Review) LoadCodeComments() error {
  60. return r.loadCodeComments(x)
  61. }
  62. func (r *Review) loadIssue(e Engine) (err error) {
  63. r.Issue, err = getIssueByID(e, r.IssueID)
  64. return
  65. }
  66. func (r *Review) loadReviewer(e Engine) (err error) {
  67. if r.ReviewerID == 0 {
  68. return nil
  69. }
  70. r.Reviewer, err = getUserByID(e, r.ReviewerID)
  71. return
  72. }
  73. // LoadReviewer loads reviewer
  74. func (r *Review) LoadReviewer() error {
  75. return r.loadReviewer(x)
  76. }
  77. func (r *Review) loadAttributes(e Engine) (err error) {
  78. if err = r.loadReviewer(e); err != nil {
  79. return
  80. }
  81. if err = r.loadIssue(e); err != nil {
  82. return
  83. }
  84. return
  85. }
  86. // LoadAttributes loads all attributes except CodeComments
  87. func (r *Review) LoadAttributes() error {
  88. return r.loadAttributes(x)
  89. }
  90. func getReviewByID(e Engine, id int64) (*Review, error) {
  91. review := new(Review)
  92. if has, err := e.ID(id).Get(review); err != nil {
  93. return nil, err
  94. } else if !has {
  95. return nil, ErrReviewNotExist{ID: id}
  96. } else {
  97. return review, nil
  98. }
  99. }
  100. // GetReviewByID returns the review by the given ID
  101. func GetReviewByID(id int64) (*Review, error) {
  102. return getReviewByID(x, id)
  103. }
  104. func getUniqueApprovalsByPullRequestID(e Engine, prID int64) (reviews []*Review, err error) {
  105. reviews = make([]*Review, 0)
  106. if err := e.
  107. Where("issue_id = ? AND type = ?", prID, ReviewTypeApprove).
  108. OrderBy("updated_unix").
  109. GroupBy("reviewer_id").
  110. Find(&reviews); err != nil {
  111. return nil, err
  112. }
  113. return
  114. }
  115. // GetUniqueApprovalsByPullRequestID returns all reviews submitted for a specific pull request
  116. func GetUniqueApprovalsByPullRequestID(prID int64) ([]*Review, error) {
  117. return getUniqueApprovalsByPullRequestID(x, prID)
  118. }
  119. // FindReviewOptions represent possible filters to find reviews
  120. type FindReviewOptions struct {
  121. Type ReviewType
  122. IssueID int64
  123. ReviewerID int64
  124. }
  125. func (opts *FindReviewOptions) toCond() builder.Cond {
  126. var cond = builder.NewCond()
  127. if opts.IssueID > 0 {
  128. cond = cond.And(builder.Eq{"issue_id": opts.IssueID})
  129. }
  130. if opts.ReviewerID > 0 {
  131. cond = cond.And(builder.Eq{"reviewer_id": opts.ReviewerID})
  132. }
  133. if opts.Type != ReviewTypeUnknown {
  134. cond = cond.And(builder.Eq{"type": opts.Type})
  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. }
  157. func createReview(e Engine, opts CreateReviewOptions) (*Review, error) {
  158. review := &Review{
  159. Type: opts.Type,
  160. Issue: opts.Issue,
  161. IssueID: opts.Issue.ID,
  162. Reviewer: opts.Reviewer,
  163. ReviewerID: opts.Reviewer.ID,
  164. Content: opts.Content,
  165. }
  166. if _, err := e.Insert(review); err != nil {
  167. return nil, err
  168. }
  169. return review, nil
  170. }
  171. // CreateReview creates a new review based on opts
  172. func CreateReview(opts CreateReviewOptions) (*Review, error) {
  173. return createReview(x, opts)
  174. }
  175. func getCurrentReview(e Engine, reviewer *User, issue *Issue) (*Review, error) {
  176. if reviewer == nil {
  177. return nil, nil
  178. }
  179. reviews, err := findReviews(e, FindReviewOptions{
  180. Type: ReviewTypePending,
  181. IssueID: issue.ID,
  182. ReviewerID: reviewer.ID,
  183. })
  184. if err != nil {
  185. return nil, err
  186. }
  187. if len(reviews) == 0 {
  188. return nil, ErrReviewNotExist{}
  189. }
  190. reviews[0].Reviewer = reviewer
  191. reviews[0].Issue = issue
  192. return reviews[0], nil
  193. }
  194. // ReviewExists returns whether a review exists for a particular line of code in the PR
  195. func ReviewExists(issue *Issue, treePath string, line int64) (bool, error) {
  196. return x.Cols("id").Exist(&Comment{IssueID: issue.ID, TreePath: treePath, Line: line, Type: CommentTypeCode})
  197. }
  198. // GetCurrentReview returns the current pending review of reviewer for given issue
  199. func GetCurrentReview(reviewer *User, issue *Issue) (*Review, error) {
  200. return getCurrentReview(x, reviewer, issue)
  201. }
  202. // ContentEmptyErr represents an content empty error
  203. type ContentEmptyErr struct {
  204. }
  205. func (ContentEmptyErr) Error() string {
  206. return "Review content is empty"
  207. }
  208. // IsContentEmptyErr returns true if err is a ContentEmptyErr
  209. func IsContentEmptyErr(err error) bool {
  210. _, ok := err.(ContentEmptyErr)
  211. return ok
  212. }
  213. // SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
  214. func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content string) (*Review, *Comment, error) {
  215. sess := x.NewSession()
  216. defer sess.Close()
  217. if err := sess.Begin(); err != nil {
  218. return nil, nil, err
  219. }
  220. review, err := getCurrentReview(sess, doer, issue)
  221. if err != nil {
  222. if !IsErrReviewNotExist(err) {
  223. return nil, nil, err
  224. }
  225. if reviewType != ReviewTypeApprove && len(strings.TrimSpace(content)) == 0 {
  226. return nil, nil, ContentEmptyErr{}
  227. }
  228. // No current review. Create a new one!
  229. review, err = createReview(sess, CreateReviewOptions{
  230. Type: reviewType,
  231. Issue: issue,
  232. Reviewer: doer,
  233. Content: content,
  234. })
  235. if err != nil {
  236. return nil, nil, err
  237. }
  238. } else {
  239. if err := review.loadCodeComments(sess); err != nil {
  240. return nil, nil, err
  241. }
  242. if reviewType != ReviewTypeApprove && len(review.CodeComments) == 0 && len(strings.TrimSpace(content)) == 0 {
  243. return nil, nil, ContentEmptyErr{}
  244. }
  245. review.Issue = issue
  246. review.Content = content
  247. review.Type = reviewType
  248. if _, err := sess.ID(review.ID).Cols("content, type").Update(review); err != nil {
  249. return nil, nil, err
  250. }
  251. }
  252. comm, err := createCommentWithNoAction(sess, &CreateCommentOptions{
  253. Type: CommentTypeReview,
  254. Doer: doer,
  255. Content: review.Content,
  256. Issue: issue,
  257. Repo: issue.Repo,
  258. ReviewID: review.ID,
  259. })
  260. if err != nil || comm == nil {
  261. return nil, nil, err
  262. }
  263. comm.Review = review
  264. return review, comm, sess.Commit()
  265. }
  266. // PullReviewersWithType represents the type used to display a review overview
  267. type PullReviewersWithType struct {
  268. User `xorm:"extends"`
  269. Type ReviewType
  270. ReviewUpdatedUnix timeutil.TimeStamp `xorm:"review_updated_unix"`
  271. }
  272. // GetReviewersByPullID gets all reviewers for a pull request with the statuses
  273. func GetReviewersByPullID(pullID int64) (issueReviewers []*PullReviewersWithType, err error) {
  274. irs := []*PullReviewersWithType{}
  275. if x.Dialect().DBType() == core.MSSQL {
  276. err = x.SQL(`SELECT [user].*, review.type, review.review_updated_unix FROM
  277. (SELECT review.id, review.type, review.reviewer_id, max(review.updated_unix) as review_updated_unix
  278. FROM review WHERE review.issue_id=? AND (review.type = ? OR review.type = ?)
  279. GROUP BY review.id, review.type, review.reviewer_id) as review
  280. INNER JOIN [user] ON review.reviewer_id = [user].id ORDER BY review_updated_unix DESC`,
  281. pullID, ReviewTypeApprove, ReviewTypeReject).
  282. Find(&irs)
  283. } else {
  284. err = x.Select("`user`.*, review.type, max(review.updated_unix) as review_updated_unix").
  285. Table("review").
  286. Join("INNER", "`user`", "review.reviewer_id = `user`.id").
  287. Where("review.issue_id = ? AND (review.type = ? OR review.type = ?)",
  288. pullID, ReviewTypeApprove, ReviewTypeReject).
  289. GroupBy("`user`.id, review.type").
  290. OrderBy("review_updated_unix DESC").
  291. Find(&irs)
  292. }
  293. // We need to group our results by user id _and_ review type, otherwise the query fails when using postgresql.
  294. // But becaus we're doing this, we need to manually filter out multiple reviews of different types by the
  295. // same person because we only want to show the newest review grouped by user. Thats why we're using a map here.
  296. issueReviewers = []*PullReviewersWithType{}
  297. usersInArray := make(map[int64]bool)
  298. for _, ir := range irs {
  299. if !usersInArray[ir.ID] {
  300. issueReviewers = append(issueReviewers, ir)
  301. usersInArray[ir.ID] = true
  302. }
  303. }
  304. return
  305. }