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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980
  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/base"
  9. "code.gitea.io/gitea/modules/timeutil"
  10. "xorm.io/builder"
  11. )
  12. // ReviewType defines the sort of feedback a review gives
  13. type ReviewType int
  14. // ReviewTypeUnknown unknown review type
  15. const ReviewTypeUnknown ReviewType = -1
  16. const (
  17. // ReviewTypePending is a review which is not published yet
  18. ReviewTypePending ReviewType = iota
  19. // ReviewTypeApprove approves changes
  20. ReviewTypeApprove
  21. // ReviewTypeComment gives general feedback
  22. ReviewTypeComment
  23. // ReviewTypeReject gives feedback blocking merge
  24. ReviewTypeReject
  25. // ReviewTypeRequest request review from others
  26. ReviewTypeRequest
  27. )
  28. // Icon returns the corresponding icon for the review type
  29. func (rt ReviewType) Icon() string {
  30. switch rt {
  31. case ReviewTypeApprove:
  32. return "check"
  33. case ReviewTypeReject:
  34. return "diff"
  35. case ReviewTypeComment:
  36. return "comment"
  37. case ReviewTypeRequest:
  38. return "dot-fill"
  39. default:
  40. return "comment"
  41. }
  42. }
  43. // Review represents collection of code comments giving feedback for a PR
  44. type Review struct {
  45. ID int64 `xorm:"pk autoincr"`
  46. Type ReviewType
  47. Reviewer *User `xorm:"-"`
  48. ReviewerID int64 `xorm:"index"`
  49. ReviewerTeamID int64 `xorm:"NOT NULL DEFAULT 0"`
  50. ReviewerTeam *Team `xorm:"-"`
  51. OriginalAuthor string
  52. OriginalAuthorID int64
  53. Issue *Issue `xorm:"-"`
  54. IssueID int64 `xorm:"index"`
  55. Content string `xorm:"TEXT"`
  56. // Official is a review made by an assigned approver (counts towards approval)
  57. Official bool `xorm:"NOT NULL DEFAULT false"`
  58. CommitID string `xorm:"VARCHAR(40)"`
  59. Stale bool `xorm:"NOT NULL DEFAULT false"`
  60. Dismissed bool `xorm:"NOT NULL DEFAULT false"`
  61. CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
  62. UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
  63. // CodeComments are the initial code comments of the review
  64. CodeComments CodeComments `xorm:"-"`
  65. Comments []*Comment `xorm:"-"`
  66. }
  67. func (r *Review) loadCodeComments(e Engine) (err error) {
  68. if r.CodeComments != nil {
  69. return
  70. }
  71. if err = r.loadIssue(e); err != nil {
  72. return
  73. }
  74. r.CodeComments, err = fetchCodeCommentsByReview(e, r.Issue, nil, r)
  75. return
  76. }
  77. // LoadCodeComments loads CodeComments
  78. func (r *Review) LoadCodeComments() error {
  79. return r.loadCodeComments(x)
  80. }
  81. func (r *Review) loadIssue(e Engine) (err error) {
  82. if r.Issue != nil {
  83. return
  84. }
  85. r.Issue, err = getIssueByID(e, r.IssueID)
  86. return
  87. }
  88. func (r *Review) loadReviewer(e Engine) (err error) {
  89. if r.ReviewerID == 0 || r.Reviewer != nil {
  90. return
  91. }
  92. r.Reviewer, err = getUserByID(e, r.ReviewerID)
  93. return
  94. }
  95. func (r *Review) loadReviewerTeam(e Engine) (err error) {
  96. if r.ReviewerTeamID == 0 || r.ReviewerTeam != nil {
  97. return
  98. }
  99. r.ReviewerTeam, err = getTeamByID(e, r.ReviewerTeamID)
  100. return
  101. }
  102. // LoadReviewer loads reviewer
  103. func (r *Review) LoadReviewer() error {
  104. return r.loadReviewer(x)
  105. }
  106. // LoadReviewerTeam loads reviewer team
  107. func (r *Review) LoadReviewerTeam() error {
  108. return r.loadReviewerTeam(x)
  109. }
  110. func (r *Review) loadAttributes(e Engine) (err error) {
  111. if err = r.loadIssue(e); err != nil {
  112. return
  113. }
  114. if err = r.loadCodeComments(e); err != nil {
  115. return
  116. }
  117. if err = r.loadReviewer(e); err != nil {
  118. return
  119. }
  120. if err = r.loadReviewerTeam(e); err != nil {
  121. return
  122. }
  123. return
  124. }
  125. // LoadAttributes loads all attributes except CodeComments
  126. func (r *Review) LoadAttributes() error {
  127. return r.loadAttributes(x)
  128. }
  129. func getReviewByID(e Engine, id int64) (*Review, error) {
  130. review := new(Review)
  131. if has, err := e.ID(id).Get(review); err != nil {
  132. return nil, err
  133. } else if !has {
  134. return nil, ErrReviewNotExist{ID: id}
  135. } else {
  136. return review, nil
  137. }
  138. }
  139. // GetReviewByID returns the review by the given ID
  140. func GetReviewByID(id int64) (*Review, error) {
  141. return getReviewByID(x, id)
  142. }
  143. // FindReviewOptions represent possible filters to find reviews
  144. type FindReviewOptions struct {
  145. ListOptions
  146. Type ReviewType
  147. IssueID int64
  148. ReviewerID int64
  149. OfficialOnly bool
  150. }
  151. func (opts *FindReviewOptions) toCond() builder.Cond {
  152. cond := builder.NewCond()
  153. if opts.IssueID > 0 {
  154. cond = cond.And(builder.Eq{"issue_id": opts.IssueID})
  155. }
  156. if opts.ReviewerID > 0 {
  157. cond = cond.And(builder.Eq{"reviewer_id": opts.ReviewerID})
  158. }
  159. if opts.Type != ReviewTypeUnknown {
  160. cond = cond.And(builder.Eq{"type": opts.Type})
  161. }
  162. if opts.OfficialOnly {
  163. cond = cond.And(builder.Eq{"official": true})
  164. }
  165. return cond
  166. }
  167. func findReviews(e Engine, opts FindReviewOptions) ([]*Review, error) {
  168. reviews := make([]*Review, 0, 10)
  169. sess := e.Where(opts.toCond())
  170. if opts.Page > 0 {
  171. sess = opts.ListOptions.setSessionPagination(sess)
  172. }
  173. return reviews, sess.
  174. Asc("created_unix").
  175. Asc("id").
  176. Find(&reviews)
  177. }
  178. // FindReviews returns reviews passing FindReviewOptions
  179. func FindReviews(opts FindReviewOptions) ([]*Review, error) {
  180. return findReviews(x, opts)
  181. }
  182. // CreateReviewOptions represent the options to create a review. Type, Issue and Reviewer are required.
  183. type CreateReviewOptions struct {
  184. Content string
  185. Type ReviewType
  186. Issue *Issue
  187. Reviewer *User
  188. ReviewerTeam *Team
  189. Official bool
  190. CommitID string
  191. Stale bool
  192. }
  193. // IsOfficialReviewer check if at least one of the provided reviewers can make official reviews in issue (counts towards required approvals)
  194. func IsOfficialReviewer(issue *Issue, reviewers ...*User) (bool, error) {
  195. return isOfficialReviewer(x, issue, reviewers...)
  196. }
  197. func isOfficialReviewer(e Engine, issue *Issue, reviewers ...*User) (bool, error) {
  198. pr, err := getPullRequestByIssueID(e, issue.ID)
  199. if err != nil {
  200. return false, err
  201. }
  202. if err = pr.loadProtectedBranch(e); err != nil {
  203. return false, err
  204. }
  205. if pr.ProtectedBranch == nil {
  206. return false, nil
  207. }
  208. for _, reviewer := range reviewers {
  209. official, err := pr.ProtectedBranch.isUserOfficialReviewer(e, reviewer)
  210. if official || err != nil {
  211. return official, err
  212. }
  213. }
  214. return false, nil
  215. }
  216. // IsOfficialReviewerTeam check if reviewer in this team can make official reviews in issue (counts towards required approvals)
  217. func IsOfficialReviewerTeam(issue *Issue, team *Team) (bool, error) {
  218. return isOfficialReviewerTeam(x, issue, team)
  219. }
  220. func isOfficialReviewerTeam(e Engine, issue *Issue, team *Team) (bool, error) {
  221. pr, err := getPullRequestByIssueID(e, issue.ID)
  222. if err != nil {
  223. return false, err
  224. }
  225. if err = pr.loadProtectedBranch(e); err != nil {
  226. return false, err
  227. }
  228. if pr.ProtectedBranch == nil {
  229. return false, nil
  230. }
  231. if !pr.ProtectedBranch.EnableApprovalsWhitelist {
  232. return team.Authorize >= AccessModeWrite, nil
  233. }
  234. return base.Int64sContains(pr.ProtectedBranch.ApprovalsWhitelistTeamIDs, team.ID), nil
  235. }
  236. func createReview(e Engine, opts CreateReviewOptions) (*Review, error) {
  237. review := &Review{
  238. Type: opts.Type,
  239. Issue: opts.Issue,
  240. IssueID: opts.Issue.ID,
  241. Reviewer: opts.Reviewer,
  242. ReviewerTeam: opts.ReviewerTeam,
  243. Content: opts.Content,
  244. Official: opts.Official,
  245. CommitID: opts.CommitID,
  246. Stale: opts.Stale,
  247. }
  248. if opts.Reviewer != nil {
  249. review.ReviewerID = opts.Reviewer.ID
  250. } else {
  251. if review.Type != ReviewTypeRequest {
  252. review.Type = ReviewTypeRequest
  253. }
  254. review.ReviewerTeamID = opts.ReviewerTeam.ID
  255. }
  256. if _, err := e.Insert(review); err != nil {
  257. return nil, err
  258. }
  259. return review, nil
  260. }
  261. // CreateReview creates a new review based on opts
  262. func CreateReview(opts CreateReviewOptions) (*Review, error) {
  263. return createReview(x, opts)
  264. }
  265. func getCurrentReview(e Engine, reviewer *User, issue *Issue) (*Review, error) {
  266. if reviewer == nil {
  267. return nil, nil
  268. }
  269. reviews, err := findReviews(e, FindReviewOptions{
  270. Type: ReviewTypePending,
  271. IssueID: issue.ID,
  272. ReviewerID: reviewer.ID,
  273. })
  274. if err != nil {
  275. return nil, err
  276. }
  277. if len(reviews) == 0 {
  278. return nil, ErrReviewNotExist{}
  279. }
  280. reviews[0].Reviewer = reviewer
  281. reviews[0].Issue = issue
  282. return reviews[0], nil
  283. }
  284. // ReviewExists returns whether a review exists for a particular line of code in the PR
  285. func ReviewExists(issue *Issue, treePath string, line int64) (bool, error) {
  286. return x.Cols("id").Exist(&Comment{IssueID: issue.ID, TreePath: treePath, Line: line, Type: CommentTypeCode})
  287. }
  288. // GetCurrentReview returns the current pending review of reviewer for given issue
  289. func GetCurrentReview(reviewer *User, issue *Issue) (*Review, error) {
  290. return getCurrentReview(x, reviewer, issue)
  291. }
  292. // ContentEmptyErr represents an content empty error
  293. type ContentEmptyErr struct{}
  294. func (ContentEmptyErr) Error() string {
  295. return "Review content is empty"
  296. }
  297. // IsContentEmptyErr returns true if err is a ContentEmptyErr
  298. func IsContentEmptyErr(err error) bool {
  299. _, ok := err.(ContentEmptyErr)
  300. return ok
  301. }
  302. // SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
  303. func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content, commitID string, stale bool) (*Review, *Comment, error) {
  304. sess := x.NewSession()
  305. defer sess.Close()
  306. if err := sess.Begin(); err != nil {
  307. return nil, nil, err
  308. }
  309. official := false
  310. review, err := getCurrentReview(sess, doer, issue)
  311. if err != nil {
  312. if !IsErrReviewNotExist(err) {
  313. return nil, nil, err
  314. }
  315. if reviewType != ReviewTypeApprove && len(strings.TrimSpace(content)) == 0 {
  316. return nil, nil, ContentEmptyErr{}
  317. }
  318. if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject {
  319. // Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared
  320. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, doer.ID); err != nil {
  321. return nil, nil, err
  322. }
  323. if official, err = isOfficialReviewer(sess, issue, doer); err != nil {
  324. return nil, nil, err
  325. }
  326. }
  327. // No current review. Create a new one!
  328. if review, err = createReview(sess, CreateReviewOptions{
  329. Type: reviewType,
  330. Issue: issue,
  331. Reviewer: doer,
  332. Content: content,
  333. Official: official,
  334. CommitID: commitID,
  335. Stale: stale,
  336. }); err != nil {
  337. return nil, nil, err
  338. }
  339. } else {
  340. if err := review.loadCodeComments(sess); err != nil {
  341. return nil, nil, err
  342. }
  343. if reviewType != ReviewTypeApprove && len(review.CodeComments) == 0 && len(strings.TrimSpace(content)) == 0 {
  344. return nil, nil, ContentEmptyErr{}
  345. }
  346. if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject {
  347. // Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared
  348. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, doer.ID); err != nil {
  349. return nil, nil, err
  350. }
  351. if official, err = isOfficialReviewer(sess, issue, doer); err != nil {
  352. return nil, nil, err
  353. }
  354. }
  355. review.Official = official
  356. review.Issue = issue
  357. review.Content = content
  358. review.Type = reviewType
  359. review.CommitID = commitID
  360. review.Stale = stale
  361. if _, err := sess.ID(review.ID).Cols("content, type, official, commit_id, stale").Update(review); err != nil {
  362. return nil, nil, err
  363. }
  364. }
  365. comm, err := createComment(sess, &CreateCommentOptions{
  366. Type: CommentTypeReview,
  367. Doer: doer,
  368. Content: review.Content,
  369. Issue: issue,
  370. Repo: issue.Repo,
  371. ReviewID: review.ID,
  372. })
  373. if err != nil || comm == nil {
  374. return nil, nil, err
  375. }
  376. // try to remove team review request if need
  377. if issue.Repo.Owner.IsOrganization() && (reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject) {
  378. teamReviewRequests := make([]*Review, 0, 10)
  379. if err := sess.SQL("SELECT * FROM review WHERE reviewer_team_id > 0 AND type = ?", ReviewTypeRequest).Find(&teamReviewRequests); err != nil {
  380. return nil, nil, err
  381. }
  382. for _, teamReviewRequest := range teamReviewRequests {
  383. ok, err := isTeamMember(sess, issue.Repo.OwnerID, teamReviewRequest.ReviewerTeamID, doer.ID)
  384. if err != nil {
  385. return nil, nil, err
  386. } else if !ok {
  387. continue
  388. }
  389. if _, err := sess.Delete(teamReviewRequest); err != nil {
  390. return nil, nil, err
  391. }
  392. }
  393. }
  394. comm.Review = review
  395. return review, comm, sess.Commit()
  396. }
  397. // GetReviewersByIssueID gets the latest review of each reviewer for a pull request
  398. func GetReviewersByIssueID(issueID int64) ([]*Review, error) {
  399. reviews := make([]*Review, 0, 10)
  400. sess := x.NewSession()
  401. defer sess.Close()
  402. if err := sess.Begin(); err != nil {
  403. return nil, err
  404. }
  405. // Get latest review of each reviwer, sorted in order they were made
  406. if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = 0 AND type in (?, ?, ?) AND dismissed = ? AND original_author_id = 0 GROUP BY issue_id, reviewer_id) ORDER BY review.updated_unix ASC",
  407. issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, false).
  408. Find(&reviews); err != nil {
  409. return nil, err
  410. }
  411. teamReviewRequests := make([]*Review, 0, 5)
  412. if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id <> 0 AND original_author_id = 0 GROUP BY issue_id, reviewer_team_id) ORDER BY review.updated_unix ASC",
  413. issueID).
  414. Find(&teamReviewRequests); err != nil {
  415. return nil, err
  416. }
  417. if len(teamReviewRequests) > 0 {
  418. reviews = append(reviews, teamReviewRequests...)
  419. }
  420. return reviews, nil
  421. }
  422. // GetReviewersFromOriginalAuthorsByIssueID gets the latest review of each original authors for a pull request
  423. func GetReviewersFromOriginalAuthorsByIssueID(issueID int64) ([]*Review, error) {
  424. reviews := make([]*Review, 0, 10)
  425. // Get latest review of each reviwer, sorted in order they were made
  426. if err := x.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = 0 AND type in (?, ?, ?) AND original_author_id <> 0 GROUP BY issue_id, original_author_id) ORDER BY review.updated_unix ASC",
  427. issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest).
  428. Find(&reviews); err != nil {
  429. return nil, err
  430. }
  431. return reviews, nil
  432. }
  433. // GetReviewByIssueIDAndUserID get the latest review of reviewer for a pull request
  434. func GetReviewByIssueIDAndUserID(issueID, userID int64) (*Review, error) {
  435. return getReviewByIssueIDAndUserID(x, issueID, userID)
  436. }
  437. func getReviewByIssueIDAndUserID(e Engine, issueID, userID int64) (*Review, error) {
  438. review := new(Review)
  439. has, err := e.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_id = ? AND original_author_id = 0 AND type in (?, ?, ?))",
  440. issueID, userID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest).
  441. Get(review)
  442. if err != nil {
  443. return nil, err
  444. }
  445. if !has {
  446. return nil, ErrReviewNotExist{}
  447. }
  448. return review, nil
  449. }
  450. // GetTeamReviewerByIssueIDAndTeamID get the latest review requst of reviewer team for a pull request
  451. func GetTeamReviewerByIssueIDAndTeamID(issueID, teamID int64) (review *Review, err error) {
  452. return getTeamReviewerByIssueIDAndTeamID(x, issueID, teamID)
  453. }
  454. func getTeamReviewerByIssueIDAndTeamID(e Engine, issueID, teamID int64) (review *Review, err error) {
  455. review = new(Review)
  456. has := false
  457. if has, err = e.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = ?)",
  458. issueID, teamID).
  459. Get(review); err != nil {
  460. return nil, err
  461. }
  462. if !has {
  463. return nil, ErrReviewNotExist{0}
  464. }
  465. return
  466. }
  467. // MarkReviewsAsStale marks existing reviews as stale
  468. func MarkReviewsAsStale(issueID int64) (err error) {
  469. _, err = x.Exec("UPDATE `review` SET stale=? WHERE issue_id=?", true, issueID)
  470. return
  471. }
  472. // MarkReviewsAsNotStale marks existing reviews as not stale for a giving commit SHA
  473. func MarkReviewsAsNotStale(issueID int64, commitID string) (err error) {
  474. _, err = x.Exec("UPDATE `review` SET stale=? WHERE issue_id=? AND commit_id=?", false, issueID, commitID)
  475. return
  476. }
  477. // DismissReview change the dismiss status of a review
  478. func DismissReview(review *Review, isDismiss bool) (err error) {
  479. if review.Dismissed == isDismiss || (review.Type != ReviewTypeApprove && review.Type != ReviewTypeReject) {
  480. return nil
  481. }
  482. review.Dismissed = isDismiss
  483. if review.ID == 0 {
  484. return ErrReviewNotExist{}
  485. }
  486. _, err = x.ID(review.ID).Cols("dismissed").Update(review)
  487. return
  488. }
  489. // InsertReviews inserts review and review comments
  490. func InsertReviews(reviews []*Review) error {
  491. sess := x.NewSession()
  492. defer sess.Close()
  493. if err := sess.Begin(); err != nil {
  494. return err
  495. }
  496. for _, review := range reviews {
  497. if _, err := sess.NoAutoTime().Insert(review); err != nil {
  498. return err
  499. }
  500. if _, err := sess.NoAutoTime().Insert(&Comment{
  501. Type: CommentTypeReview,
  502. Content: review.Content,
  503. PosterID: review.ReviewerID,
  504. OriginalAuthor: review.OriginalAuthor,
  505. OriginalAuthorID: review.OriginalAuthorID,
  506. IssueID: review.IssueID,
  507. ReviewID: review.ID,
  508. CreatedUnix: review.CreatedUnix,
  509. UpdatedUnix: review.UpdatedUnix,
  510. }); err != nil {
  511. return err
  512. }
  513. for _, c := range review.Comments {
  514. c.ReviewID = review.ID
  515. }
  516. if len(review.Comments) > 0 {
  517. if _, err := sess.NoAutoTime().Insert(review.Comments); err != nil {
  518. return err
  519. }
  520. }
  521. }
  522. return sess.Commit()
  523. }
  524. // AddReviewRequest add a review request from one reviewer
  525. func AddReviewRequest(issue *Issue, reviewer, doer *User) (*Comment, error) {
  526. sess := x.NewSession()
  527. defer sess.Close()
  528. if err := sess.Begin(); err != nil {
  529. return nil, err
  530. }
  531. review, err := getReviewByIssueIDAndUserID(sess, issue.ID, reviewer.ID)
  532. if err != nil && !IsErrReviewNotExist(err) {
  533. return nil, err
  534. }
  535. // skip it when reviewer hase been request to review
  536. if review != nil && review.Type == ReviewTypeRequest {
  537. return nil, nil
  538. }
  539. official, err := isOfficialReviewer(sess, issue, reviewer, doer)
  540. if err != nil {
  541. return nil, err
  542. } else if official {
  543. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, reviewer.ID); err != nil {
  544. return nil, err
  545. }
  546. }
  547. review, err = createReview(sess, CreateReviewOptions{
  548. Type: ReviewTypeRequest,
  549. Issue: issue,
  550. Reviewer: reviewer,
  551. Official: official,
  552. Stale: false,
  553. })
  554. if err != nil {
  555. return nil, err
  556. }
  557. comment, err := createComment(sess, &CreateCommentOptions{
  558. Type: CommentTypeReviewRequest,
  559. Doer: doer,
  560. Repo: issue.Repo,
  561. Issue: issue,
  562. RemovedAssignee: false, // Use RemovedAssignee as !isRequest
  563. AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID
  564. ReviewID: review.ID,
  565. })
  566. if err != nil {
  567. return nil, err
  568. }
  569. return comment, sess.Commit()
  570. }
  571. // RemoveReviewRequest remove a review request from one reviewer
  572. func RemoveReviewRequest(issue *Issue, reviewer, doer *User) (*Comment, error) {
  573. sess := x.NewSession()
  574. defer sess.Close()
  575. if err := sess.Begin(); err != nil {
  576. return nil, err
  577. }
  578. review, err := getReviewByIssueIDAndUserID(sess, issue.ID, reviewer.ID)
  579. if err != nil && !IsErrReviewNotExist(err) {
  580. return nil, err
  581. }
  582. if review == nil || review.Type != ReviewTypeRequest {
  583. return nil, nil
  584. }
  585. if _, err = sess.Delete(review); err != nil {
  586. return nil, err
  587. }
  588. official, err := isOfficialReviewer(sess, issue, reviewer)
  589. if err != nil {
  590. return nil, err
  591. } else if official {
  592. // recalculate the latest official review for reviewer
  593. review, err := getReviewByIssueIDAndUserID(sess, issue.ID, reviewer.ID)
  594. if err != nil && !IsErrReviewNotExist(err) {
  595. return nil, err
  596. }
  597. if review != nil {
  598. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil {
  599. return nil, err
  600. }
  601. }
  602. }
  603. comment, err := createComment(sess, &CreateCommentOptions{
  604. Type: CommentTypeReviewRequest,
  605. Doer: doer,
  606. Repo: issue.Repo,
  607. Issue: issue,
  608. RemovedAssignee: true, // Use RemovedAssignee as !isRequest
  609. AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID
  610. })
  611. if err != nil {
  612. return nil, err
  613. }
  614. return comment, sess.Commit()
  615. }
  616. // AddTeamReviewRequest add a review request from one team
  617. func AddTeamReviewRequest(issue *Issue, reviewer *Team, doer *User) (*Comment, error) {
  618. sess := x.NewSession()
  619. defer sess.Close()
  620. if err := sess.Begin(); err != nil {
  621. return nil, err
  622. }
  623. review, err := getTeamReviewerByIssueIDAndTeamID(sess, issue.ID, reviewer.ID)
  624. if err != nil && !IsErrReviewNotExist(err) {
  625. return nil, err
  626. }
  627. // This team already has been requested to review - therefore skip this.
  628. if review != nil {
  629. return nil, nil
  630. }
  631. official, err := isOfficialReviewerTeam(sess, issue, reviewer)
  632. if err != nil {
  633. return nil, fmt.Errorf("isOfficialReviewerTeam(): %v", err)
  634. } else if !official {
  635. if official, err = isOfficialReviewer(sess, issue, doer); err != nil {
  636. return nil, fmt.Errorf("isOfficialReviewer(): %v", err)
  637. }
  638. }
  639. if review, err = createReview(sess, CreateReviewOptions{
  640. Type: ReviewTypeRequest,
  641. Issue: issue,
  642. ReviewerTeam: reviewer,
  643. Official: official,
  644. Stale: false,
  645. }); err != nil {
  646. return nil, err
  647. }
  648. if official {
  649. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_team_id=?", false, issue.ID, reviewer.ID); err != nil {
  650. return nil, err
  651. }
  652. }
  653. comment, err := createComment(sess, &CreateCommentOptions{
  654. Type: CommentTypeReviewRequest,
  655. Doer: doer,
  656. Repo: issue.Repo,
  657. Issue: issue,
  658. RemovedAssignee: false, // Use RemovedAssignee as !isRequest
  659. AssigneeTeamID: reviewer.ID, // Use AssigneeTeamID as reviewer team ID
  660. ReviewID: review.ID,
  661. })
  662. if err != nil {
  663. return nil, fmt.Errorf("createComment(): %v", err)
  664. }
  665. return comment, sess.Commit()
  666. }
  667. // RemoveTeamReviewRequest remove a review request from one team
  668. func RemoveTeamReviewRequest(issue *Issue, reviewer *Team, doer *User) (*Comment, error) {
  669. sess := x.NewSession()
  670. defer sess.Close()
  671. if err := sess.Begin(); err != nil {
  672. return nil, err
  673. }
  674. review, err := getTeamReviewerByIssueIDAndTeamID(sess, issue.ID, reviewer.ID)
  675. if err != nil && !IsErrReviewNotExist(err) {
  676. return nil, err
  677. }
  678. if review == nil {
  679. return nil, nil
  680. }
  681. if _, err = sess.Delete(review); err != nil {
  682. return nil, err
  683. }
  684. official, err := isOfficialReviewerTeam(sess, issue, reviewer)
  685. if err != nil {
  686. return nil, fmt.Errorf("isOfficialReviewerTeam(): %v", err)
  687. }
  688. if official {
  689. // recalculate which is the latest official review from that team
  690. review, err := getReviewByIssueIDAndUserID(sess, issue.ID, -reviewer.ID)
  691. if err != nil && !IsErrReviewNotExist(err) {
  692. return nil, err
  693. }
  694. if review != nil {
  695. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil {
  696. return nil, err
  697. }
  698. }
  699. }
  700. if doer == nil {
  701. return nil, sess.Commit()
  702. }
  703. comment, err := createComment(sess, &CreateCommentOptions{
  704. Type: CommentTypeReviewRequest,
  705. Doer: doer,
  706. Repo: issue.Repo,
  707. Issue: issue,
  708. RemovedAssignee: true, // Use RemovedAssignee as !isRequest
  709. AssigneeTeamID: reviewer.ID, // Use AssigneeTeamID as reviewer team ID
  710. })
  711. if err != nil {
  712. return nil, fmt.Errorf("createComment(): %v", err)
  713. }
  714. return comment, sess.Commit()
  715. }
  716. // MarkConversation Add or remove Conversation mark for a code comment
  717. func MarkConversation(comment *Comment, doer *User, isResolve bool) (err error) {
  718. if comment.Type != CommentTypeCode {
  719. return nil
  720. }
  721. if isResolve {
  722. if comment.ResolveDoerID != 0 {
  723. return nil
  724. }
  725. if _, err = x.Exec("UPDATE `comment` SET resolve_doer_id=? WHERE id=?", doer.ID, comment.ID); err != nil {
  726. return err
  727. }
  728. } else {
  729. if comment.ResolveDoerID == 0 {
  730. return nil
  731. }
  732. if _, err = x.Exec("UPDATE `comment` SET resolve_doer_id=? WHERE id=?", 0, comment.ID); err != nil {
  733. return err
  734. }
  735. }
  736. return nil
  737. }
  738. // CanMarkConversation Add or remove Conversation mark for a code comment permission check
  739. // the PR writer , offfcial reviewer and poster can do it
  740. func CanMarkConversation(issue *Issue, doer *User) (permResult bool, err error) {
  741. if doer == nil || issue == nil {
  742. return false, fmt.Errorf("issue or doer is nil")
  743. }
  744. if doer.ID != issue.PosterID {
  745. if err = issue.LoadRepo(); err != nil {
  746. return false, err
  747. }
  748. perm, err := GetUserRepoPermission(issue.Repo, doer)
  749. if err != nil {
  750. return false, err
  751. }
  752. permResult = perm.CanAccess(AccessModeWrite, UnitTypePullRequests)
  753. if !permResult {
  754. if permResult, err = IsOfficialReviewer(issue, doer); err != nil {
  755. return false, err
  756. }
  757. }
  758. if !permResult {
  759. return false, nil
  760. }
  761. }
  762. return true, nil
  763. }
  764. // DeleteReview delete a review and it's code comments
  765. func DeleteReview(r *Review) error {
  766. sess := x.NewSession()
  767. defer sess.Close()
  768. if err := sess.Begin(); err != nil {
  769. return err
  770. }
  771. if r.ID == 0 {
  772. return fmt.Errorf("review is not allowed to be 0")
  773. }
  774. if r.Type == ReviewTypeRequest {
  775. return fmt.Errorf("review request can not be deleted using this method")
  776. }
  777. opts := FindCommentsOptions{
  778. Type: CommentTypeCode,
  779. IssueID: r.IssueID,
  780. ReviewID: r.ID,
  781. }
  782. if _, err := sess.Where(opts.toConds()).Delete(new(Comment)); err != nil {
  783. return err
  784. }
  785. opts = FindCommentsOptions{
  786. Type: CommentTypeReview,
  787. IssueID: r.IssueID,
  788. ReviewID: r.ID,
  789. }
  790. if _, err := sess.Where(opts.toConds()).Delete(new(Comment)); err != nil {
  791. return err
  792. }
  793. if _, err := sess.ID(r.ID).Delete(new(Review)); err != nil {
  794. return err
  795. }
  796. return sess.Commit()
  797. }
  798. // GetCodeCommentsCount return count of CodeComments a Review has
  799. func (r *Review) GetCodeCommentsCount() int {
  800. opts := FindCommentsOptions{
  801. Type: CommentTypeCode,
  802. IssueID: r.IssueID,
  803. ReviewID: r.ID,
  804. }
  805. conds := opts.toConds()
  806. if r.ID == 0 {
  807. conds = conds.And(builder.Eq{"invalidated": false})
  808. }
  809. count, err := x.Where(conds).Count(new(Comment))
  810. if err != nil {
  811. return 0
  812. }
  813. return int(count)
  814. }
  815. // HTMLURL formats a URL-string to the related review issue-comment
  816. func (r *Review) HTMLURL() string {
  817. opts := FindCommentsOptions{
  818. Type: CommentTypeReview,
  819. IssueID: r.IssueID,
  820. ReviewID: r.ID,
  821. }
  822. comment := new(Comment)
  823. has, err := x.Where(opts.toConds()).Get(comment)
  824. if err != nil || !has {
  825. return ""
  826. }
  827. return comment.HTMLURL()
  828. }