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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986
  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. // CountReviews returns count of reviews passing FindReviewOptions
  183. func CountReviews(opts FindReviewOptions) (int64, error) {
  184. return x.Where(opts.toCond()).Count(&Review{})
  185. }
  186. // CreateReviewOptions represent the options to create a review. Type, Issue and Reviewer are required.
  187. type CreateReviewOptions struct {
  188. Content string
  189. Type ReviewType
  190. Issue *Issue
  191. Reviewer *User
  192. ReviewerTeam *Team
  193. Official bool
  194. CommitID string
  195. Stale bool
  196. }
  197. // IsOfficialReviewer check if at least one of the provided reviewers can make official reviews in issue (counts towards required approvals)
  198. func IsOfficialReviewer(issue *Issue, reviewers ...*User) (bool, error) {
  199. return isOfficialReviewer(x, issue, reviewers...)
  200. }
  201. func isOfficialReviewer(e Engine, issue *Issue, reviewers ...*User) (bool, error) {
  202. pr, err := getPullRequestByIssueID(e, issue.ID)
  203. if err != nil {
  204. return false, err
  205. }
  206. if err = pr.loadProtectedBranch(e); err != nil {
  207. return false, err
  208. }
  209. if pr.ProtectedBranch == nil {
  210. return false, nil
  211. }
  212. for _, reviewer := range reviewers {
  213. official, err := pr.ProtectedBranch.isUserOfficialReviewer(e, reviewer)
  214. if official || err != nil {
  215. return official, err
  216. }
  217. }
  218. return false, nil
  219. }
  220. // IsOfficialReviewerTeam check if reviewer in this team can make official reviews in issue (counts towards required approvals)
  221. func IsOfficialReviewerTeam(issue *Issue, team *Team) (bool, error) {
  222. return isOfficialReviewerTeam(x, issue, team)
  223. }
  224. func isOfficialReviewerTeam(e Engine, issue *Issue, team *Team) (bool, error) {
  225. pr, err := getPullRequestByIssueID(e, issue.ID)
  226. if err != nil {
  227. return false, err
  228. }
  229. if err = pr.loadProtectedBranch(e); err != nil {
  230. return false, err
  231. }
  232. if pr.ProtectedBranch == nil {
  233. return false, nil
  234. }
  235. if !pr.ProtectedBranch.EnableApprovalsWhitelist {
  236. return team.Authorize >= AccessModeWrite, nil
  237. }
  238. return base.Int64sContains(pr.ProtectedBranch.ApprovalsWhitelistTeamIDs, team.ID), nil
  239. }
  240. func createReview(e Engine, opts CreateReviewOptions) (*Review, error) {
  241. review := &Review{
  242. Type: opts.Type,
  243. Issue: opts.Issue,
  244. IssueID: opts.Issue.ID,
  245. Reviewer: opts.Reviewer,
  246. ReviewerTeam: opts.ReviewerTeam,
  247. Content: opts.Content,
  248. Official: opts.Official,
  249. CommitID: opts.CommitID,
  250. Stale: opts.Stale,
  251. }
  252. if opts.Reviewer != nil {
  253. review.ReviewerID = opts.Reviewer.ID
  254. } else {
  255. if review.Type != ReviewTypeRequest {
  256. review.Type = ReviewTypeRequest
  257. }
  258. review.ReviewerTeamID = opts.ReviewerTeam.ID
  259. }
  260. if _, err := e.Insert(review); err != nil {
  261. return nil, err
  262. }
  263. return review, nil
  264. }
  265. // CreateReview creates a new review based on opts
  266. func CreateReview(opts CreateReviewOptions) (*Review, error) {
  267. return createReview(x, opts)
  268. }
  269. func getCurrentReview(e Engine, reviewer *User, issue *Issue) (*Review, error) {
  270. if reviewer == nil {
  271. return nil, nil
  272. }
  273. reviews, err := findReviews(e, FindReviewOptions{
  274. Type: ReviewTypePending,
  275. IssueID: issue.ID,
  276. ReviewerID: reviewer.ID,
  277. })
  278. if err != nil {
  279. return nil, err
  280. }
  281. if len(reviews) == 0 {
  282. return nil, ErrReviewNotExist{}
  283. }
  284. reviews[0].Reviewer = reviewer
  285. reviews[0].Issue = issue
  286. return reviews[0], nil
  287. }
  288. // ReviewExists returns whether a review exists for a particular line of code in the PR
  289. func ReviewExists(issue *Issue, treePath string, line int64) (bool, error) {
  290. return x.Cols("id").Exist(&Comment{IssueID: issue.ID, TreePath: treePath, Line: line, Type: CommentTypeCode})
  291. }
  292. // GetCurrentReview returns the current pending review of reviewer for given issue
  293. func GetCurrentReview(reviewer *User, issue *Issue) (*Review, error) {
  294. return getCurrentReview(x, reviewer, issue)
  295. }
  296. // ContentEmptyErr represents an content empty error
  297. type ContentEmptyErr struct{}
  298. func (ContentEmptyErr) Error() string {
  299. return "Review content is empty"
  300. }
  301. // IsContentEmptyErr returns true if err is a ContentEmptyErr
  302. func IsContentEmptyErr(err error) bool {
  303. _, ok := err.(ContentEmptyErr)
  304. return ok
  305. }
  306. // SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
  307. func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content, commitID string, stale bool, attachmentUUIDs []string) (*Review, *Comment, error) {
  308. sess := x.NewSession()
  309. defer sess.Close()
  310. if err := sess.Begin(); err != nil {
  311. return nil, nil, err
  312. }
  313. official := false
  314. review, err := getCurrentReview(sess, doer, issue)
  315. if err != nil {
  316. if !IsErrReviewNotExist(err) {
  317. return nil, nil, err
  318. }
  319. if reviewType != ReviewTypeApprove && len(strings.TrimSpace(content)) == 0 {
  320. return nil, nil, ContentEmptyErr{}
  321. }
  322. if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject {
  323. // Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared
  324. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, doer.ID); err != nil {
  325. return nil, nil, err
  326. }
  327. if official, err = isOfficialReviewer(sess, issue, doer); err != nil {
  328. return nil, nil, err
  329. }
  330. }
  331. // No current review. Create a new one!
  332. if review, err = createReview(sess, CreateReviewOptions{
  333. Type: reviewType,
  334. Issue: issue,
  335. Reviewer: doer,
  336. Content: content,
  337. Official: official,
  338. CommitID: commitID,
  339. Stale: stale,
  340. }); err != nil {
  341. return nil, nil, err
  342. }
  343. } else {
  344. if err := review.loadCodeComments(sess); err != nil {
  345. return nil, nil, err
  346. }
  347. if reviewType != ReviewTypeApprove && len(review.CodeComments) == 0 && len(strings.TrimSpace(content)) == 0 {
  348. return nil, nil, ContentEmptyErr{}
  349. }
  350. if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject {
  351. // Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared
  352. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, doer.ID); err != nil {
  353. return nil, nil, err
  354. }
  355. if official, err = isOfficialReviewer(sess, issue, doer); err != nil {
  356. return nil, nil, err
  357. }
  358. }
  359. review.Official = official
  360. review.Issue = issue
  361. review.Content = content
  362. review.Type = reviewType
  363. review.CommitID = commitID
  364. review.Stale = stale
  365. if _, err := sess.ID(review.ID).Cols("content, type, official, commit_id, stale").Update(review); err != nil {
  366. return nil, nil, err
  367. }
  368. }
  369. comm, err := createComment(sess, &CreateCommentOptions{
  370. Type: CommentTypeReview,
  371. Doer: doer,
  372. Content: review.Content,
  373. Issue: issue,
  374. Repo: issue.Repo,
  375. ReviewID: review.ID,
  376. Attachments: attachmentUUIDs,
  377. })
  378. if err != nil || comm == nil {
  379. return nil, nil, err
  380. }
  381. // try to remove team review request if need
  382. if issue.Repo.Owner.IsOrganization() && (reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject) {
  383. teamReviewRequests := make([]*Review, 0, 10)
  384. if err := sess.SQL("SELECT * FROM review WHERE reviewer_team_id > 0 AND type = ?", ReviewTypeRequest).Find(&teamReviewRequests); err != nil {
  385. return nil, nil, err
  386. }
  387. for _, teamReviewRequest := range teamReviewRequests {
  388. ok, err := isTeamMember(sess, issue.Repo.OwnerID, teamReviewRequest.ReviewerTeamID, doer.ID)
  389. if err != nil {
  390. return nil, nil, err
  391. } else if !ok {
  392. continue
  393. }
  394. if _, err := sess.Delete(teamReviewRequest); err != nil {
  395. return nil, nil, err
  396. }
  397. }
  398. }
  399. comm.Review = review
  400. return review, comm, sess.Commit()
  401. }
  402. // GetReviewersByIssueID gets the latest review of each reviewer for a pull request
  403. func GetReviewersByIssueID(issueID int64) ([]*Review, error) {
  404. reviews := make([]*Review, 0, 10)
  405. sess := x.NewSession()
  406. defer sess.Close()
  407. if err := sess.Begin(); err != nil {
  408. return nil, err
  409. }
  410. // Get latest review of each reviewer, sorted in order they were made
  411. 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",
  412. issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, false).
  413. Find(&reviews); err != nil {
  414. return nil, err
  415. }
  416. teamReviewRequests := make([]*Review, 0, 5)
  417. 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",
  418. issueID).
  419. Find(&teamReviewRequests); err != nil {
  420. return nil, err
  421. }
  422. if len(teamReviewRequests) > 0 {
  423. reviews = append(reviews, teamReviewRequests...)
  424. }
  425. return reviews, nil
  426. }
  427. // GetReviewersFromOriginalAuthorsByIssueID gets the latest review of each original authors for a pull request
  428. func GetReviewersFromOriginalAuthorsByIssueID(issueID int64) ([]*Review, error) {
  429. reviews := make([]*Review, 0, 10)
  430. // Get latest review of each reviewer, sorted in order they were made
  431. 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",
  432. issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest).
  433. Find(&reviews); err != nil {
  434. return nil, err
  435. }
  436. return reviews, nil
  437. }
  438. // GetReviewByIssueIDAndUserID get the latest review of reviewer for a pull request
  439. func GetReviewByIssueIDAndUserID(issueID, userID int64) (*Review, error) {
  440. return getReviewByIssueIDAndUserID(x, issueID, userID)
  441. }
  442. func getReviewByIssueIDAndUserID(e Engine, issueID, userID int64) (*Review, error) {
  443. review := new(Review)
  444. 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 (?, ?, ?))",
  445. issueID, userID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest).
  446. Get(review)
  447. if err != nil {
  448. return nil, err
  449. }
  450. if !has {
  451. return nil, ErrReviewNotExist{}
  452. }
  453. return review, nil
  454. }
  455. // GetTeamReviewerByIssueIDAndTeamID get the latest review requst of reviewer team for a pull request
  456. func GetTeamReviewerByIssueIDAndTeamID(issueID, teamID int64) (review *Review, err error) {
  457. return getTeamReviewerByIssueIDAndTeamID(x, issueID, teamID)
  458. }
  459. func getTeamReviewerByIssueIDAndTeamID(e Engine, issueID, teamID int64) (review *Review, err error) {
  460. review = new(Review)
  461. has := false
  462. 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 = ?)",
  463. issueID, teamID).
  464. Get(review); err != nil {
  465. return nil, err
  466. }
  467. if !has {
  468. return nil, ErrReviewNotExist{0}
  469. }
  470. return
  471. }
  472. // MarkReviewsAsStale marks existing reviews as stale
  473. func MarkReviewsAsStale(issueID int64) (err error) {
  474. _, err = x.Exec("UPDATE `review` SET stale=? WHERE issue_id=?", true, issueID)
  475. return
  476. }
  477. // MarkReviewsAsNotStale marks existing reviews as not stale for a giving commit SHA
  478. func MarkReviewsAsNotStale(issueID int64, commitID string) (err error) {
  479. _, err = x.Exec("UPDATE `review` SET stale=? WHERE issue_id=? AND commit_id=?", false, issueID, commitID)
  480. return
  481. }
  482. // DismissReview change the dismiss status of a review
  483. func DismissReview(review *Review, isDismiss bool) (err error) {
  484. if review.Dismissed == isDismiss || (review.Type != ReviewTypeApprove && review.Type != ReviewTypeReject) {
  485. return nil
  486. }
  487. review.Dismissed = isDismiss
  488. if review.ID == 0 {
  489. return ErrReviewNotExist{}
  490. }
  491. _, err = x.ID(review.ID).Cols("dismissed").Update(review)
  492. return
  493. }
  494. // InsertReviews inserts review and review comments
  495. func InsertReviews(reviews []*Review) error {
  496. sess := x.NewSession()
  497. defer sess.Close()
  498. if err := sess.Begin(); err != nil {
  499. return err
  500. }
  501. for _, review := range reviews {
  502. if _, err := sess.NoAutoTime().Insert(review); err != nil {
  503. return err
  504. }
  505. if _, err := sess.NoAutoTime().Insert(&Comment{
  506. Type: CommentTypeReview,
  507. Content: review.Content,
  508. PosterID: review.ReviewerID,
  509. OriginalAuthor: review.OriginalAuthor,
  510. OriginalAuthorID: review.OriginalAuthorID,
  511. IssueID: review.IssueID,
  512. ReviewID: review.ID,
  513. CreatedUnix: review.CreatedUnix,
  514. UpdatedUnix: review.UpdatedUnix,
  515. }); err != nil {
  516. return err
  517. }
  518. for _, c := range review.Comments {
  519. c.ReviewID = review.ID
  520. }
  521. if len(review.Comments) > 0 {
  522. if _, err := sess.NoAutoTime().Insert(review.Comments); err != nil {
  523. return err
  524. }
  525. }
  526. }
  527. return sess.Commit()
  528. }
  529. // AddReviewRequest add a review request from one reviewer
  530. func AddReviewRequest(issue *Issue, reviewer, doer *User) (*Comment, error) {
  531. sess := x.NewSession()
  532. defer sess.Close()
  533. if err := sess.Begin(); err != nil {
  534. return nil, err
  535. }
  536. review, err := getReviewByIssueIDAndUserID(sess, issue.ID, reviewer.ID)
  537. if err != nil && !IsErrReviewNotExist(err) {
  538. return nil, err
  539. }
  540. // skip it when reviewer hase been request to review
  541. if review != nil && review.Type == ReviewTypeRequest {
  542. return nil, nil
  543. }
  544. official, err := isOfficialReviewer(sess, issue, reviewer, doer)
  545. if err != nil {
  546. return nil, err
  547. } else if official {
  548. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, reviewer.ID); err != nil {
  549. return nil, err
  550. }
  551. }
  552. review, err = createReview(sess, CreateReviewOptions{
  553. Type: ReviewTypeRequest,
  554. Issue: issue,
  555. Reviewer: reviewer,
  556. Official: official,
  557. Stale: false,
  558. })
  559. if err != nil {
  560. return nil, err
  561. }
  562. comment, err := createComment(sess, &CreateCommentOptions{
  563. Type: CommentTypeReviewRequest,
  564. Doer: doer,
  565. Repo: issue.Repo,
  566. Issue: issue,
  567. RemovedAssignee: false, // Use RemovedAssignee as !isRequest
  568. AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID
  569. ReviewID: review.ID,
  570. })
  571. if err != nil {
  572. return nil, err
  573. }
  574. return comment, sess.Commit()
  575. }
  576. // RemoveReviewRequest remove a review request from one reviewer
  577. func RemoveReviewRequest(issue *Issue, reviewer, doer *User) (*Comment, error) {
  578. sess := x.NewSession()
  579. defer sess.Close()
  580. if err := sess.Begin(); err != nil {
  581. return nil, err
  582. }
  583. review, err := getReviewByIssueIDAndUserID(sess, issue.ID, reviewer.ID)
  584. if err != nil && !IsErrReviewNotExist(err) {
  585. return nil, err
  586. }
  587. if review == nil || review.Type != ReviewTypeRequest {
  588. return nil, nil
  589. }
  590. if _, err = sess.Delete(review); err != nil {
  591. return nil, err
  592. }
  593. official, err := isOfficialReviewer(sess, issue, reviewer)
  594. if err != nil {
  595. return nil, err
  596. } else if official {
  597. // recalculate the latest official review for reviewer
  598. review, err := getReviewByIssueIDAndUserID(sess, issue.ID, reviewer.ID)
  599. if err != nil && !IsErrReviewNotExist(err) {
  600. return nil, err
  601. }
  602. if review != nil {
  603. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil {
  604. return nil, err
  605. }
  606. }
  607. }
  608. comment, err := createComment(sess, &CreateCommentOptions{
  609. Type: CommentTypeReviewRequest,
  610. Doer: doer,
  611. Repo: issue.Repo,
  612. Issue: issue,
  613. RemovedAssignee: true, // Use RemovedAssignee as !isRequest
  614. AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID
  615. })
  616. if err != nil {
  617. return nil, err
  618. }
  619. return comment, sess.Commit()
  620. }
  621. // AddTeamReviewRequest add a review request from one team
  622. func AddTeamReviewRequest(issue *Issue, reviewer *Team, doer *User) (*Comment, error) {
  623. sess := x.NewSession()
  624. defer sess.Close()
  625. if err := sess.Begin(); err != nil {
  626. return nil, err
  627. }
  628. review, err := getTeamReviewerByIssueIDAndTeamID(sess, issue.ID, reviewer.ID)
  629. if err != nil && !IsErrReviewNotExist(err) {
  630. return nil, err
  631. }
  632. // This team already has been requested to review - therefore skip this.
  633. if review != nil {
  634. return nil, nil
  635. }
  636. official, err := isOfficialReviewerTeam(sess, issue, reviewer)
  637. if err != nil {
  638. return nil, fmt.Errorf("isOfficialReviewerTeam(): %v", err)
  639. } else if !official {
  640. if official, err = isOfficialReviewer(sess, issue, doer); err != nil {
  641. return nil, fmt.Errorf("isOfficialReviewer(): %v", err)
  642. }
  643. }
  644. if review, err = createReview(sess, CreateReviewOptions{
  645. Type: ReviewTypeRequest,
  646. Issue: issue,
  647. ReviewerTeam: reviewer,
  648. Official: official,
  649. Stale: false,
  650. }); err != nil {
  651. return nil, err
  652. }
  653. if official {
  654. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_team_id=?", false, issue.ID, reviewer.ID); err != nil {
  655. return nil, err
  656. }
  657. }
  658. comment, err := createComment(sess, &CreateCommentOptions{
  659. Type: CommentTypeReviewRequest,
  660. Doer: doer,
  661. Repo: issue.Repo,
  662. Issue: issue,
  663. RemovedAssignee: false, // Use RemovedAssignee as !isRequest
  664. AssigneeTeamID: reviewer.ID, // Use AssigneeTeamID as reviewer team ID
  665. ReviewID: review.ID,
  666. })
  667. if err != nil {
  668. return nil, fmt.Errorf("createComment(): %v", err)
  669. }
  670. return comment, sess.Commit()
  671. }
  672. // RemoveTeamReviewRequest remove a review request from one team
  673. func RemoveTeamReviewRequest(issue *Issue, reviewer *Team, doer *User) (*Comment, error) {
  674. sess := x.NewSession()
  675. defer sess.Close()
  676. if err := sess.Begin(); err != nil {
  677. return nil, err
  678. }
  679. review, err := getTeamReviewerByIssueIDAndTeamID(sess, issue.ID, reviewer.ID)
  680. if err != nil && !IsErrReviewNotExist(err) {
  681. return nil, err
  682. }
  683. if review == nil {
  684. return nil, nil
  685. }
  686. if _, err = sess.Delete(review); err != nil {
  687. return nil, err
  688. }
  689. official, err := isOfficialReviewerTeam(sess, issue, reviewer)
  690. if err != nil {
  691. return nil, fmt.Errorf("isOfficialReviewerTeam(): %v", err)
  692. }
  693. if official {
  694. // recalculate which is the latest official review from that team
  695. review, err := getReviewByIssueIDAndUserID(sess, issue.ID, -reviewer.ID)
  696. if err != nil && !IsErrReviewNotExist(err) {
  697. return nil, err
  698. }
  699. if review != nil {
  700. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil {
  701. return nil, err
  702. }
  703. }
  704. }
  705. if doer == nil {
  706. return nil, sess.Commit()
  707. }
  708. comment, err := createComment(sess, &CreateCommentOptions{
  709. Type: CommentTypeReviewRequest,
  710. Doer: doer,
  711. Repo: issue.Repo,
  712. Issue: issue,
  713. RemovedAssignee: true, // Use RemovedAssignee as !isRequest
  714. AssigneeTeamID: reviewer.ID, // Use AssigneeTeamID as reviewer team ID
  715. })
  716. if err != nil {
  717. return nil, fmt.Errorf("createComment(): %v", err)
  718. }
  719. return comment, sess.Commit()
  720. }
  721. // MarkConversation Add or remove Conversation mark for a code comment
  722. func MarkConversation(comment *Comment, doer *User, isResolve bool) (err error) {
  723. if comment.Type != CommentTypeCode {
  724. return nil
  725. }
  726. if isResolve {
  727. if comment.ResolveDoerID != 0 {
  728. return nil
  729. }
  730. if _, err = x.Exec("UPDATE `comment` SET resolve_doer_id=? WHERE id=?", doer.ID, comment.ID); err != nil {
  731. return err
  732. }
  733. } else {
  734. if comment.ResolveDoerID == 0 {
  735. return nil
  736. }
  737. if _, err = x.Exec("UPDATE `comment` SET resolve_doer_id=? WHERE id=?", 0, comment.ID); err != nil {
  738. return err
  739. }
  740. }
  741. return nil
  742. }
  743. // CanMarkConversation Add or remove Conversation mark for a code comment permission check
  744. // the PR writer , offfcial reviewer and poster can do it
  745. func CanMarkConversation(issue *Issue, doer *User) (permResult bool, err error) {
  746. if doer == nil || issue == nil {
  747. return false, fmt.Errorf("issue or doer is nil")
  748. }
  749. if doer.ID != issue.PosterID {
  750. if err = issue.LoadRepo(); err != nil {
  751. return false, err
  752. }
  753. perm, err := GetUserRepoPermission(issue.Repo, doer)
  754. if err != nil {
  755. return false, err
  756. }
  757. permResult = perm.CanAccess(AccessModeWrite, UnitTypePullRequests)
  758. if !permResult {
  759. if permResult, err = IsOfficialReviewer(issue, doer); err != nil {
  760. return false, err
  761. }
  762. }
  763. if !permResult {
  764. return false, nil
  765. }
  766. }
  767. return true, nil
  768. }
  769. // DeleteReview delete a review and it's code comments
  770. func DeleteReview(r *Review) error {
  771. sess := x.NewSession()
  772. defer sess.Close()
  773. if err := sess.Begin(); err != nil {
  774. return err
  775. }
  776. if r.ID == 0 {
  777. return fmt.Errorf("review is not allowed to be 0")
  778. }
  779. if r.Type == ReviewTypeRequest {
  780. return fmt.Errorf("review request can not be deleted using this method")
  781. }
  782. opts := FindCommentsOptions{
  783. Type: CommentTypeCode,
  784. IssueID: r.IssueID,
  785. ReviewID: r.ID,
  786. }
  787. if _, err := sess.Where(opts.toConds()).Delete(new(Comment)); err != nil {
  788. return err
  789. }
  790. opts = FindCommentsOptions{
  791. Type: CommentTypeReview,
  792. IssueID: r.IssueID,
  793. ReviewID: r.ID,
  794. }
  795. if _, err := sess.Where(opts.toConds()).Delete(new(Comment)); err != nil {
  796. return err
  797. }
  798. if _, err := sess.ID(r.ID).Delete(new(Review)); err != nil {
  799. return err
  800. }
  801. return sess.Commit()
  802. }
  803. // GetCodeCommentsCount return count of CodeComments a Review has
  804. func (r *Review) GetCodeCommentsCount() int {
  805. opts := FindCommentsOptions{
  806. Type: CommentTypeCode,
  807. IssueID: r.IssueID,
  808. ReviewID: r.ID,
  809. }
  810. conds := opts.toConds()
  811. if r.ID == 0 {
  812. conds = conds.And(builder.Eq{"invalidated": false})
  813. }
  814. count, err := x.Where(conds).Count(new(Comment))
  815. if err != nil {
  816. return 0
  817. }
  818. return int(count)
  819. }
  820. // HTMLURL formats a URL-string to the related review issue-comment
  821. func (r *Review) HTMLURL() string {
  822. opts := FindCommentsOptions{
  823. Type: CommentTypeReview,
  824. IssueID: r.IssueID,
  825. ReviewID: r.ID,
  826. }
  827. comment := new(Comment)
  828. has, err := x.Where(opts.toConds()).Get(comment)
  829. if err != nil || !has {
  830. return ""
  831. }
  832. return comment.HTMLURL()
  833. }