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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976
  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. _, err = x.Cols("dismissed").Update(review)
  484. return
  485. }
  486. // InsertReviews inserts review and review comments
  487. func InsertReviews(reviews []*Review) error {
  488. sess := x.NewSession()
  489. defer sess.Close()
  490. if err := sess.Begin(); err != nil {
  491. return err
  492. }
  493. for _, review := range reviews {
  494. if _, err := sess.NoAutoTime().Insert(review); err != nil {
  495. return err
  496. }
  497. if _, err := sess.NoAutoTime().Insert(&Comment{
  498. Type: CommentTypeReview,
  499. Content: review.Content,
  500. PosterID: review.ReviewerID,
  501. OriginalAuthor: review.OriginalAuthor,
  502. OriginalAuthorID: review.OriginalAuthorID,
  503. IssueID: review.IssueID,
  504. ReviewID: review.ID,
  505. CreatedUnix: review.CreatedUnix,
  506. UpdatedUnix: review.UpdatedUnix,
  507. }); err != nil {
  508. return err
  509. }
  510. for _, c := range review.Comments {
  511. c.ReviewID = review.ID
  512. }
  513. if len(review.Comments) > 0 {
  514. if _, err := sess.NoAutoTime().Insert(review.Comments); err != nil {
  515. return err
  516. }
  517. }
  518. }
  519. return sess.Commit()
  520. }
  521. // AddReviewRequest add a review request from one reviewer
  522. func AddReviewRequest(issue *Issue, reviewer, doer *User) (*Comment, error) {
  523. sess := x.NewSession()
  524. defer sess.Close()
  525. if err := sess.Begin(); err != nil {
  526. return nil, err
  527. }
  528. review, err := getReviewByIssueIDAndUserID(sess, issue.ID, reviewer.ID)
  529. if err != nil && !IsErrReviewNotExist(err) {
  530. return nil, err
  531. }
  532. // skip it when reviewer hase been request to review
  533. if review != nil && review.Type == ReviewTypeRequest {
  534. return nil, nil
  535. }
  536. official, err := isOfficialReviewer(sess, issue, reviewer, doer)
  537. if err != nil {
  538. return nil, err
  539. } else if official {
  540. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, reviewer.ID); err != nil {
  541. return nil, err
  542. }
  543. }
  544. review, err = createReview(sess, CreateReviewOptions{
  545. Type: ReviewTypeRequest,
  546. Issue: issue,
  547. Reviewer: reviewer,
  548. Official: official,
  549. Stale: false,
  550. })
  551. if err != nil {
  552. return nil, err
  553. }
  554. comment, err := createComment(sess, &CreateCommentOptions{
  555. Type: CommentTypeReviewRequest,
  556. Doer: doer,
  557. Repo: issue.Repo,
  558. Issue: issue,
  559. RemovedAssignee: false, // Use RemovedAssignee as !isRequest
  560. AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID
  561. ReviewID: review.ID,
  562. })
  563. if err != nil {
  564. return nil, err
  565. }
  566. return comment, sess.Commit()
  567. }
  568. // RemoveReviewRequest remove a review request from one reviewer
  569. func RemoveReviewRequest(issue *Issue, reviewer, doer *User) (*Comment, error) {
  570. sess := x.NewSession()
  571. defer sess.Close()
  572. if err := sess.Begin(); err != nil {
  573. return nil, err
  574. }
  575. review, err := getReviewByIssueIDAndUserID(sess, issue.ID, reviewer.ID)
  576. if err != nil && !IsErrReviewNotExist(err) {
  577. return nil, err
  578. }
  579. if review == nil || review.Type != ReviewTypeRequest {
  580. return nil, nil
  581. }
  582. if _, err = sess.Delete(review); err != nil {
  583. return nil, err
  584. }
  585. official, err := isOfficialReviewer(sess, issue, reviewer)
  586. if err != nil {
  587. return nil, err
  588. } else if official {
  589. // recalculate the latest official review for reviewer
  590. review, err := getReviewByIssueIDAndUserID(sess, issue.ID, reviewer.ID)
  591. if err != nil && !IsErrReviewNotExist(err) {
  592. return nil, err
  593. }
  594. if review != nil {
  595. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil {
  596. return nil, err
  597. }
  598. }
  599. }
  600. comment, err := createComment(sess, &CreateCommentOptions{
  601. Type: CommentTypeReviewRequest,
  602. Doer: doer,
  603. Repo: issue.Repo,
  604. Issue: issue,
  605. RemovedAssignee: true, // Use RemovedAssignee as !isRequest
  606. AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID
  607. })
  608. if err != nil {
  609. return nil, err
  610. }
  611. return comment, sess.Commit()
  612. }
  613. // AddTeamReviewRequest add a review request from one team
  614. func AddTeamReviewRequest(issue *Issue, reviewer *Team, doer *User) (*Comment, error) {
  615. sess := x.NewSession()
  616. defer sess.Close()
  617. if err := sess.Begin(); err != nil {
  618. return nil, err
  619. }
  620. review, err := getTeamReviewerByIssueIDAndTeamID(sess, issue.ID, reviewer.ID)
  621. if err != nil && !IsErrReviewNotExist(err) {
  622. return nil, err
  623. }
  624. // This team already has been requested to review - therefore skip this.
  625. if review != nil {
  626. return nil, nil
  627. }
  628. official, err := isOfficialReviewerTeam(sess, issue, reviewer)
  629. if err != nil {
  630. return nil, fmt.Errorf("isOfficialReviewerTeam(): %v", err)
  631. } else if !official {
  632. if official, err = isOfficialReviewer(sess, issue, doer); err != nil {
  633. return nil, fmt.Errorf("isOfficialReviewer(): %v", err)
  634. }
  635. }
  636. if review, err = createReview(sess, CreateReviewOptions{
  637. Type: ReviewTypeRequest,
  638. Issue: issue,
  639. ReviewerTeam: reviewer,
  640. Official: official,
  641. Stale: false,
  642. }); err != nil {
  643. return nil, err
  644. }
  645. if official {
  646. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_team_id=?", false, issue.ID, reviewer.ID); err != nil {
  647. return nil, err
  648. }
  649. }
  650. comment, err := createComment(sess, &CreateCommentOptions{
  651. Type: CommentTypeReviewRequest,
  652. Doer: doer,
  653. Repo: issue.Repo,
  654. Issue: issue,
  655. RemovedAssignee: false, // Use RemovedAssignee as !isRequest
  656. AssigneeTeamID: reviewer.ID, // Use AssigneeTeamID as reviewer team ID
  657. ReviewID: review.ID,
  658. })
  659. if err != nil {
  660. return nil, fmt.Errorf("createComment(): %v", err)
  661. }
  662. return comment, sess.Commit()
  663. }
  664. // RemoveTeamReviewRequest remove a review request from one team
  665. func RemoveTeamReviewRequest(issue *Issue, reviewer *Team, doer *User) (*Comment, error) {
  666. sess := x.NewSession()
  667. defer sess.Close()
  668. if err := sess.Begin(); err != nil {
  669. return nil, err
  670. }
  671. review, err := getTeamReviewerByIssueIDAndTeamID(sess, issue.ID, reviewer.ID)
  672. if err != nil && !IsErrReviewNotExist(err) {
  673. return nil, err
  674. }
  675. if review == nil {
  676. return nil, nil
  677. }
  678. if _, err = sess.Delete(review); err != nil {
  679. return nil, err
  680. }
  681. official, err := isOfficialReviewerTeam(sess, issue, reviewer)
  682. if err != nil {
  683. return nil, fmt.Errorf("isOfficialReviewerTeam(): %v", err)
  684. }
  685. if official {
  686. // recalculate which is the latest official review from that team
  687. review, err := getReviewByIssueIDAndUserID(sess, issue.ID, -reviewer.ID)
  688. if err != nil && !IsErrReviewNotExist(err) {
  689. return nil, err
  690. }
  691. if review != nil {
  692. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil {
  693. return nil, err
  694. }
  695. }
  696. }
  697. if doer == nil {
  698. return nil, sess.Commit()
  699. }
  700. comment, err := createComment(sess, &CreateCommentOptions{
  701. Type: CommentTypeReviewRequest,
  702. Doer: doer,
  703. Repo: issue.Repo,
  704. Issue: issue,
  705. RemovedAssignee: true, // Use RemovedAssignee as !isRequest
  706. AssigneeTeamID: reviewer.ID, // Use AssigneeTeamID as reviewer team ID
  707. })
  708. if err != nil {
  709. return nil, fmt.Errorf("createComment(): %v", err)
  710. }
  711. return comment, sess.Commit()
  712. }
  713. // MarkConversation Add or remove Conversation mark for a code comment
  714. func MarkConversation(comment *Comment, doer *User, isResolve bool) (err error) {
  715. if comment.Type != CommentTypeCode {
  716. return nil
  717. }
  718. if isResolve {
  719. if comment.ResolveDoerID != 0 {
  720. return nil
  721. }
  722. if _, err = x.Exec("UPDATE `comment` SET resolve_doer_id=? WHERE id=?", doer.ID, comment.ID); err != nil {
  723. return err
  724. }
  725. } else {
  726. if comment.ResolveDoerID == 0 {
  727. return nil
  728. }
  729. if _, err = x.Exec("UPDATE `comment` SET resolve_doer_id=? WHERE id=?", 0, comment.ID); err != nil {
  730. return err
  731. }
  732. }
  733. return nil
  734. }
  735. // CanMarkConversation Add or remove Conversation mark for a code comment permission check
  736. // the PR writer , offfcial reviewer and poster can do it
  737. func CanMarkConversation(issue *Issue, doer *User) (permResult bool, err error) {
  738. if doer == nil || issue == nil {
  739. return false, fmt.Errorf("issue or doer is nil")
  740. }
  741. if doer.ID != issue.PosterID {
  742. if err = issue.LoadRepo(); err != nil {
  743. return false, err
  744. }
  745. perm, err := GetUserRepoPermission(issue.Repo, doer)
  746. if err != nil {
  747. return false, err
  748. }
  749. permResult = perm.CanAccess(AccessModeWrite, UnitTypePullRequests)
  750. if !permResult {
  751. if permResult, err = IsOfficialReviewer(issue, doer); err != nil {
  752. return false, err
  753. }
  754. }
  755. if !permResult {
  756. return false, nil
  757. }
  758. }
  759. return true, nil
  760. }
  761. // DeleteReview delete a review and it's code comments
  762. func DeleteReview(r *Review) error {
  763. sess := x.NewSession()
  764. defer sess.Close()
  765. if err := sess.Begin(); err != nil {
  766. return err
  767. }
  768. if r.ID == 0 {
  769. return fmt.Errorf("review is not allowed to be 0")
  770. }
  771. if r.Type == ReviewTypeRequest {
  772. return fmt.Errorf("review request can not be deleted using this method")
  773. }
  774. opts := FindCommentsOptions{
  775. Type: CommentTypeCode,
  776. IssueID: r.IssueID,
  777. ReviewID: r.ID,
  778. }
  779. if _, err := sess.Where(opts.toConds()).Delete(new(Comment)); err != nil {
  780. return err
  781. }
  782. opts = FindCommentsOptions{
  783. Type: CommentTypeReview,
  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. if _, err := sess.ID(r.ID).Delete(new(Review)); err != nil {
  791. return err
  792. }
  793. return sess.Commit()
  794. }
  795. // GetCodeCommentsCount return count of CodeComments a Review has
  796. func (r *Review) GetCodeCommentsCount() int {
  797. opts := FindCommentsOptions{
  798. Type: CommentTypeCode,
  799. IssueID: r.IssueID,
  800. ReviewID: r.ID,
  801. }
  802. conds := opts.toConds()
  803. if r.ID == 0 {
  804. conds = conds.And(builder.Eq{"invalidated": false})
  805. }
  806. count, err := x.Where(conds).Count(new(Comment))
  807. if err != nil {
  808. return 0
  809. }
  810. return int(count)
  811. }
  812. // HTMLURL formats a URL-string to the related review issue-comment
  813. func (r *Review) HTMLURL() string {
  814. opts := FindCommentsOptions{
  815. Type: CommentTypeReview,
  816. IssueID: r.IssueID,
  817. ReviewID: r.ID,
  818. }
  819. comment := new(Comment)
  820. has, err := x.Where(opts.toConds()).Get(comment)
  821. if err != nil || !has {
  822. return ""
  823. }
  824. return comment.HTMLURL()
  825. }