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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746
  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. return
  66. }
  67. if err = r.loadIssue(e); err != nil {
  68. return
  69. }
  70. r.CodeComments, err = fetchCodeCommentsByReview(e, r.Issue, nil, r)
  71. return
  72. }
  73. // LoadCodeComments loads CodeComments
  74. func (r *Review) LoadCodeComments() error {
  75. return r.loadCodeComments(x)
  76. }
  77. func (r *Review) loadIssue(e Engine) (err error) {
  78. if r.Issue != nil {
  79. return
  80. }
  81. r.Issue, err = getIssueByID(e, r.IssueID)
  82. return
  83. }
  84. func (r *Review) loadReviewer(e Engine) (err error) {
  85. if r.Reviewer != nil || r.ReviewerID == 0 {
  86. return nil
  87. }
  88. r.Reviewer, err = getUserByID(e, r.ReviewerID)
  89. return
  90. }
  91. // LoadReviewer loads reviewer
  92. func (r *Review) LoadReviewer() error {
  93. return r.loadReviewer(x)
  94. }
  95. func (r *Review) loadAttributes(e Engine) (err error) {
  96. if err = r.loadIssue(e); err != nil {
  97. return
  98. }
  99. if err = r.loadCodeComments(e); err != nil {
  100. return
  101. }
  102. if err = r.loadReviewer(e); err != nil {
  103. return
  104. }
  105. return
  106. }
  107. // LoadAttributes loads all attributes except CodeComments
  108. func (r *Review) LoadAttributes() error {
  109. return r.loadAttributes(x)
  110. }
  111. func getReviewByID(e Engine, id int64) (*Review, error) {
  112. review := new(Review)
  113. if has, err := e.ID(id).Get(review); err != nil {
  114. return nil, err
  115. } else if !has {
  116. return nil, ErrReviewNotExist{ID: id}
  117. } else {
  118. return review, nil
  119. }
  120. }
  121. // GetReviewByID returns the review by the given ID
  122. func GetReviewByID(id int64) (*Review, error) {
  123. return getReviewByID(x, id)
  124. }
  125. // FindReviewOptions represent possible filters to find reviews
  126. type FindReviewOptions struct {
  127. ListOptions
  128. Type ReviewType
  129. IssueID int64
  130. ReviewerID int64
  131. OfficialOnly bool
  132. }
  133. func (opts *FindReviewOptions) toCond() builder.Cond {
  134. var cond = builder.NewCond()
  135. if opts.IssueID > 0 {
  136. cond = cond.And(builder.Eq{"issue_id": opts.IssueID})
  137. }
  138. if opts.ReviewerID > 0 {
  139. cond = cond.And(builder.Eq{"reviewer_id": opts.ReviewerID})
  140. }
  141. if opts.Type != ReviewTypeUnknown {
  142. cond = cond.And(builder.Eq{"type": opts.Type})
  143. }
  144. if opts.OfficialOnly {
  145. cond = cond.And(builder.Eq{"official": true})
  146. }
  147. return cond
  148. }
  149. func findReviews(e Engine, opts FindReviewOptions) ([]*Review, error) {
  150. reviews := make([]*Review, 0, 10)
  151. sess := e.Where(opts.toCond())
  152. if opts.Page > 0 {
  153. sess = opts.ListOptions.setSessionPagination(sess)
  154. }
  155. return reviews, sess.
  156. Asc("created_unix").
  157. Asc("id").
  158. Find(&reviews)
  159. }
  160. // FindReviews returns reviews passing FindReviewOptions
  161. func FindReviews(opts FindReviewOptions) ([]*Review, error) {
  162. return findReviews(x, opts)
  163. }
  164. // CreateReviewOptions represent the options to create a review. Type, Issue and Reviewer are required.
  165. type CreateReviewOptions struct {
  166. Content string
  167. Type ReviewType
  168. Issue *Issue
  169. Reviewer *User
  170. Official bool
  171. CommitID string
  172. Stale bool
  173. }
  174. // IsOfficialReviewer check if reviewer can make official reviews in issue (counts towards required approvals)
  175. func IsOfficialReviewer(issue *Issue, reviewer *User) (bool, error) {
  176. return isOfficialReviewer(x, issue, reviewer)
  177. }
  178. func isOfficialReviewer(e Engine, issue *Issue, reviewer *User) (bool, error) {
  179. pr, err := getPullRequestByIssueID(e, issue.ID)
  180. if err != nil {
  181. return false, err
  182. }
  183. if err = pr.loadProtectedBranch(e); err != nil {
  184. return false, err
  185. }
  186. if pr.ProtectedBranch == nil {
  187. return false, nil
  188. }
  189. return pr.ProtectedBranch.isUserOfficialReviewer(e, reviewer)
  190. }
  191. func createReview(e Engine, opts CreateReviewOptions) (*Review, error) {
  192. review := &Review{
  193. Type: opts.Type,
  194. Issue: opts.Issue,
  195. IssueID: opts.Issue.ID,
  196. Reviewer: opts.Reviewer,
  197. ReviewerID: opts.Reviewer.ID,
  198. Content: opts.Content,
  199. Official: opts.Official,
  200. CommitID: opts.CommitID,
  201. Stale: opts.Stale,
  202. }
  203. if _, err := e.Insert(review); err != nil {
  204. return nil, err
  205. }
  206. return review, nil
  207. }
  208. // CreateReview creates a new review based on opts
  209. func CreateReview(opts CreateReviewOptions) (*Review, error) {
  210. return createReview(x, opts)
  211. }
  212. func getCurrentReview(e Engine, reviewer *User, issue *Issue) (*Review, error) {
  213. if reviewer == nil {
  214. return nil, nil
  215. }
  216. reviews, err := findReviews(e, FindReviewOptions{
  217. Type: ReviewTypePending,
  218. IssueID: issue.ID,
  219. ReviewerID: reviewer.ID,
  220. })
  221. if err != nil {
  222. return nil, err
  223. }
  224. if len(reviews) == 0 {
  225. return nil, ErrReviewNotExist{}
  226. }
  227. reviews[0].Reviewer = reviewer
  228. reviews[0].Issue = issue
  229. return reviews[0], nil
  230. }
  231. // ReviewExists returns whether a review exists for a particular line of code in the PR
  232. func ReviewExists(issue *Issue, treePath string, line int64) (bool, error) {
  233. return x.Cols("id").Exist(&Comment{IssueID: issue.ID, TreePath: treePath, Line: line, Type: CommentTypeCode})
  234. }
  235. // GetCurrentReview returns the current pending review of reviewer for given issue
  236. func GetCurrentReview(reviewer *User, issue *Issue) (*Review, error) {
  237. return getCurrentReview(x, reviewer, issue)
  238. }
  239. // ContentEmptyErr represents an content empty error
  240. type ContentEmptyErr struct {
  241. }
  242. func (ContentEmptyErr) Error() string {
  243. return "Review content is empty"
  244. }
  245. // IsContentEmptyErr returns true if err is a ContentEmptyErr
  246. func IsContentEmptyErr(err error) bool {
  247. _, ok := err.(ContentEmptyErr)
  248. return ok
  249. }
  250. // SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
  251. func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content, commitID string, stale bool) (*Review, *Comment, error) {
  252. sess := x.NewSession()
  253. defer sess.Close()
  254. if err := sess.Begin(); err != nil {
  255. return nil, nil, err
  256. }
  257. var official = false
  258. review, err := getCurrentReview(sess, doer, issue)
  259. if err != nil {
  260. if !IsErrReviewNotExist(err) {
  261. return nil, nil, err
  262. }
  263. if reviewType != ReviewTypeApprove && len(strings.TrimSpace(content)) == 0 {
  264. return nil, nil, ContentEmptyErr{}
  265. }
  266. if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject {
  267. // Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared
  268. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, doer.ID); err != nil {
  269. return nil, nil, err
  270. }
  271. official, err = isOfficialReviewer(sess, issue, doer)
  272. if err != nil {
  273. return nil, nil, err
  274. }
  275. }
  276. // No current review. Create a new one!
  277. review, err = createReview(sess, CreateReviewOptions{
  278. Type: reviewType,
  279. Issue: issue,
  280. Reviewer: doer,
  281. Content: content,
  282. Official: official,
  283. CommitID: commitID,
  284. Stale: stale,
  285. })
  286. if err != nil {
  287. return nil, nil, err
  288. }
  289. } else {
  290. if err := review.loadCodeComments(sess); err != nil {
  291. return nil, nil, err
  292. }
  293. if reviewType != ReviewTypeApprove && len(review.CodeComments) == 0 && len(strings.TrimSpace(content)) == 0 {
  294. return nil, nil, ContentEmptyErr{}
  295. }
  296. if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject {
  297. // Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared
  298. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, doer.ID); err != nil {
  299. return nil, nil, err
  300. }
  301. official, err = isOfficialReviewer(sess, issue, doer)
  302. if err != nil {
  303. return nil, nil, err
  304. }
  305. }
  306. review.Official = official
  307. review.Issue = issue
  308. review.Content = content
  309. review.Type = reviewType
  310. review.CommitID = commitID
  311. review.Stale = stale
  312. if _, err := sess.ID(review.ID).Cols("content, type, official, commit_id, stale").Update(review); err != nil {
  313. return nil, nil, err
  314. }
  315. }
  316. comm, err := createComment(sess, &CreateCommentOptions{
  317. Type: CommentTypeReview,
  318. Doer: doer,
  319. Content: review.Content,
  320. Issue: issue,
  321. Repo: issue.Repo,
  322. ReviewID: review.ID,
  323. })
  324. if err != nil || comm == nil {
  325. return nil, nil, err
  326. }
  327. comm.Review = review
  328. return review, comm, sess.Commit()
  329. }
  330. // GetReviewersByIssueID gets the latest review of each reviewer for a pull request
  331. func GetReviewersByIssueID(issueID int64) (reviews []*Review, err error) {
  332. reviewsUnfiltered := []*Review{}
  333. sess := x.NewSession()
  334. defer sess.Close()
  335. if err := sess.Begin(); err != nil {
  336. return nil, err
  337. }
  338. // Get latest review of each reviwer, sorted in order they were made
  339. 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",
  340. issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest).
  341. Find(&reviewsUnfiltered); err != nil {
  342. return nil, err
  343. }
  344. // Load reviewer and skip if user is deleted
  345. for _, review := range reviewsUnfiltered {
  346. if err = review.loadReviewer(sess); err != nil {
  347. if !IsErrUserNotExist(err) {
  348. return nil, err
  349. }
  350. } else {
  351. reviews = append(reviews, review)
  352. }
  353. }
  354. return reviews, nil
  355. }
  356. // GetReviewerByIssueIDAndUserID get the latest review of reviewer for a pull request
  357. func GetReviewerByIssueIDAndUserID(issueID, userID int64) (review *Review, err error) {
  358. return getReviewerByIssueIDAndUserID(x, issueID, userID)
  359. }
  360. func getReviewerByIssueIDAndUserID(e Engine, issueID, userID int64) (review *Review, err error) {
  361. review = new(Review)
  362. 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 (?, ?, ?))",
  363. issueID, userID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest).
  364. Get(review); err != nil {
  365. return nil, err
  366. }
  367. return
  368. }
  369. // MarkReviewsAsStale marks existing reviews as stale
  370. func MarkReviewsAsStale(issueID int64) (err error) {
  371. _, err = x.Exec("UPDATE `review` SET stale=? WHERE issue_id=?", true, issueID)
  372. return
  373. }
  374. // MarkReviewsAsNotStale marks existing reviews as not stale for a giving commit SHA
  375. func MarkReviewsAsNotStale(issueID int64, commitID string) (err error) {
  376. _, err = x.Exec("UPDATE `review` SET stale=? WHERE issue_id=? AND commit_id=?", false, issueID, commitID)
  377. return
  378. }
  379. // InsertReviews inserts review and review comments
  380. func InsertReviews(reviews []*Review) error {
  381. sess := x.NewSession()
  382. defer sess.Close()
  383. if err := sess.Begin(); err != nil {
  384. return err
  385. }
  386. for _, review := range reviews {
  387. if _, err := sess.NoAutoTime().Insert(review); err != nil {
  388. return err
  389. }
  390. if _, err := sess.NoAutoTime().Insert(&Comment{
  391. Type: CommentTypeReview,
  392. Content: review.Content,
  393. PosterID: review.ReviewerID,
  394. OriginalAuthor: review.OriginalAuthor,
  395. OriginalAuthorID: review.OriginalAuthorID,
  396. IssueID: review.IssueID,
  397. ReviewID: review.ID,
  398. CreatedUnix: review.CreatedUnix,
  399. UpdatedUnix: review.UpdatedUnix,
  400. }); err != nil {
  401. return err
  402. }
  403. for _, c := range review.Comments {
  404. c.ReviewID = review.ID
  405. }
  406. if len(review.Comments) > 0 {
  407. if _, err := sess.NoAutoTime().Insert(review.Comments); err != nil {
  408. return err
  409. }
  410. }
  411. }
  412. return sess.Commit()
  413. }
  414. // AddReviewRequest add a review request from one reviewer
  415. func AddReviewRequest(issue *Issue, reviewer *User, doer *User) (comment *Comment, err error) {
  416. review, err := GetReviewerByIssueIDAndUserID(issue.ID, reviewer.ID)
  417. if err != nil {
  418. return
  419. }
  420. // skip it when reviewer hase been request to review
  421. if review != nil && review.Type == ReviewTypeRequest {
  422. return nil, nil
  423. }
  424. sess := x.NewSession()
  425. defer sess.Close()
  426. if err := sess.Begin(); err != nil {
  427. return nil, err
  428. }
  429. var official bool
  430. official, err = isOfficialReviewer(sess, issue, reviewer)
  431. if err != nil {
  432. return nil, err
  433. }
  434. if !official {
  435. official, err = isOfficialReviewer(sess, issue, doer)
  436. if err != nil {
  437. return nil, err
  438. }
  439. }
  440. if official {
  441. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, reviewer.ID); err != nil {
  442. return nil, err
  443. }
  444. }
  445. _, err = createReview(sess, CreateReviewOptions{
  446. Type: ReviewTypeRequest,
  447. Issue: issue,
  448. Reviewer: reviewer,
  449. Official: official,
  450. Stale: false,
  451. })
  452. if err != nil {
  453. return
  454. }
  455. comment, err = createComment(sess, &CreateCommentOptions{
  456. Type: CommentTypeReviewRequest,
  457. Doer: doer,
  458. Repo: issue.Repo,
  459. Issue: issue,
  460. RemovedAssignee: false, // Use RemovedAssignee as !isRequest
  461. AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID
  462. })
  463. if err != nil {
  464. return nil, err
  465. }
  466. return comment, sess.Commit()
  467. }
  468. //RemoveReviewRequest remove a review request from one reviewer
  469. func RemoveReviewRequest(issue *Issue, reviewer *User, doer *User) (comment *Comment, err error) {
  470. review, err := GetReviewerByIssueIDAndUserID(issue.ID, reviewer.ID)
  471. if err != nil {
  472. return
  473. }
  474. if review.Type != ReviewTypeRequest {
  475. return nil, nil
  476. }
  477. sess := x.NewSession()
  478. defer sess.Close()
  479. if err := sess.Begin(); err != nil {
  480. return nil, err
  481. }
  482. _, err = sess.Delete(review)
  483. if err != nil {
  484. return nil, err
  485. }
  486. var official bool
  487. official, err = isOfficialReviewer(sess, issue, reviewer)
  488. if err != nil {
  489. return
  490. }
  491. if official {
  492. // recalculate which is the latest official review from that user
  493. var review *Review
  494. review, err = getReviewerByIssueIDAndUserID(sess, issue.ID, reviewer.ID)
  495. if err != nil {
  496. return nil, err
  497. }
  498. if review != nil {
  499. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil {
  500. return nil, err
  501. }
  502. }
  503. }
  504. if err != nil {
  505. return nil, err
  506. }
  507. comment, err = createComment(sess, &CreateCommentOptions{
  508. Type: CommentTypeReviewRequest,
  509. Doer: doer,
  510. Repo: issue.Repo,
  511. Issue: issue,
  512. RemovedAssignee: true, // Use RemovedAssignee as !isRequest
  513. AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID
  514. })
  515. if err != nil {
  516. return nil, err
  517. }
  518. return comment, sess.Commit()
  519. }
  520. // MarkConversation Add or remove Conversation mark for a code comment
  521. func MarkConversation(comment *Comment, doer *User, isResolve bool) (err error) {
  522. if comment.Type != CommentTypeCode {
  523. return nil
  524. }
  525. if isResolve {
  526. if comment.ResolveDoerID != 0 {
  527. return nil
  528. }
  529. if _, err = x.Exec("UPDATE `comment` SET resolve_doer_id=? WHERE id=?", doer.ID, comment.ID); err != nil {
  530. return err
  531. }
  532. } else {
  533. if comment.ResolveDoerID == 0 {
  534. return nil
  535. }
  536. if _, err = x.Exec("UPDATE `comment` SET resolve_doer_id=? WHERE id=?", 0, comment.ID); err != nil {
  537. return err
  538. }
  539. }
  540. return nil
  541. }
  542. // CanMarkConversation Add or remove Conversation mark for a code comment permission check
  543. // the PR writer , offfcial reviewer and poster can do it
  544. func CanMarkConversation(issue *Issue, doer *User) (permResult bool, err error) {
  545. if doer == nil || issue == nil {
  546. return false, fmt.Errorf("issue or doer is nil")
  547. }
  548. if doer.ID != issue.PosterID {
  549. if err = issue.LoadRepo(); err != nil {
  550. return false, err
  551. }
  552. perm, err := GetUserRepoPermission(issue.Repo, doer)
  553. if err != nil {
  554. return false, err
  555. }
  556. permResult = perm.CanAccess(AccessModeWrite, UnitTypePullRequests)
  557. if !permResult {
  558. if permResult, err = IsOfficialReviewer(issue, doer); err != nil {
  559. return false, err
  560. }
  561. }
  562. if !permResult {
  563. return false, nil
  564. }
  565. }
  566. return true, nil
  567. }
  568. // DeleteReview delete a review and it's code comments
  569. func DeleteReview(r *Review) error {
  570. sess := x.NewSession()
  571. defer sess.Close()
  572. if err := sess.Begin(); err != nil {
  573. return err
  574. }
  575. if r.ID == 0 {
  576. return fmt.Errorf("review is not allowed to be 0")
  577. }
  578. opts := FindCommentsOptions{
  579. Type: CommentTypeCode,
  580. IssueID: r.IssueID,
  581. ReviewID: r.ID,
  582. }
  583. if _, err := sess.Where(opts.toConds()).Delete(new(Comment)); err != nil {
  584. return err
  585. }
  586. opts = FindCommentsOptions{
  587. Type: CommentTypeReview,
  588. IssueID: r.IssueID,
  589. ReviewID: r.ID,
  590. }
  591. if _, err := sess.Where(opts.toConds()).Delete(new(Comment)); err != nil {
  592. return err
  593. }
  594. if _, err := sess.ID(r.ID).Delete(new(Review)); err != nil {
  595. return err
  596. }
  597. return sess.Commit()
  598. }
  599. // GetCodeCommentsCount return count of CodeComments a Review has
  600. func (r *Review) GetCodeCommentsCount() int {
  601. opts := FindCommentsOptions{
  602. Type: CommentTypeCode,
  603. IssueID: r.IssueID,
  604. ReviewID: r.ID,
  605. }
  606. conds := opts.toConds()
  607. if r.ID == 0 {
  608. conds = conds.And(builder.Eq{"invalidated": false})
  609. }
  610. count, err := x.Where(conds).Count(new(Comment))
  611. if err != nil {
  612. return 0
  613. }
  614. return int(count)
  615. }
  616. // HTMLURL formats a URL-string to the related review issue-comment
  617. func (r *Review) HTMLURL() string {
  618. opts := FindCommentsOptions{
  619. Type: CommentTypeReview,
  620. IssueID: r.IssueID,
  621. ReviewID: r.ID,
  622. }
  623. comment := new(Comment)
  624. has, err := x.Where(opts.toConds()).Get(comment)
  625. if err != nil || !has {
  626. return ""
  627. }
  628. return comment.HTMLURL()
  629. }