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

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