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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963
  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. CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
  61. UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
  62. // CodeComments are the initial code comments of the review
  63. CodeComments CodeComments `xorm:"-"`
  64. Comments []*Comment `xorm:"-"`
  65. }
  66. func (r *Review) loadCodeComments(e Engine) (err error) {
  67. if r.CodeComments != nil {
  68. return
  69. }
  70. if err = r.loadIssue(e); err != nil {
  71. return
  72. }
  73. r.CodeComments, err = fetchCodeCommentsByReview(e, r.Issue, nil, r)
  74. return
  75. }
  76. // LoadCodeComments loads CodeComments
  77. func (r *Review) LoadCodeComments() error {
  78. return r.loadCodeComments(x)
  79. }
  80. func (r *Review) loadIssue(e Engine) (err error) {
  81. if r.Issue != nil {
  82. return
  83. }
  84. r.Issue, err = getIssueByID(e, r.IssueID)
  85. return
  86. }
  87. func (r *Review) loadReviewer(e Engine) (err error) {
  88. if r.ReviewerID == 0 || r.Reviewer != nil {
  89. return
  90. }
  91. r.Reviewer, err = getUserByID(e, r.ReviewerID)
  92. return
  93. }
  94. func (r *Review) loadReviewerTeam(e Engine) (err error) {
  95. if r.ReviewerTeamID == 0 || r.ReviewerTeam != nil {
  96. return
  97. }
  98. r.ReviewerTeam, err = getTeamByID(e, r.ReviewerTeamID)
  99. return
  100. }
  101. // LoadReviewer loads reviewer
  102. func (r *Review) LoadReviewer() error {
  103. return r.loadReviewer(x)
  104. }
  105. // LoadReviewerTeam loads reviewer team
  106. func (r *Review) LoadReviewerTeam() error {
  107. return r.loadReviewerTeam(x)
  108. }
  109. func (r *Review) loadAttributes(e Engine) (err error) {
  110. if err = r.loadIssue(e); err != nil {
  111. return
  112. }
  113. if err = r.loadCodeComments(e); err != nil {
  114. return
  115. }
  116. if err = r.loadReviewer(e); err != nil {
  117. return
  118. }
  119. if err = r.loadReviewerTeam(e); err != nil {
  120. return
  121. }
  122. return
  123. }
  124. // LoadAttributes loads all attributes except CodeComments
  125. func (r *Review) LoadAttributes() error {
  126. return r.loadAttributes(x)
  127. }
  128. func getReviewByID(e Engine, id int64) (*Review, error) {
  129. review := new(Review)
  130. if has, err := e.ID(id).Get(review); err != nil {
  131. return nil, err
  132. } else if !has {
  133. return nil, ErrReviewNotExist{ID: id}
  134. } else {
  135. return review, nil
  136. }
  137. }
  138. // GetReviewByID returns the review by the given ID
  139. func GetReviewByID(id int64) (*Review, error) {
  140. return getReviewByID(x, id)
  141. }
  142. // FindReviewOptions represent possible filters to find reviews
  143. type FindReviewOptions struct {
  144. ListOptions
  145. Type ReviewType
  146. IssueID int64
  147. ReviewerID int64
  148. OfficialOnly bool
  149. }
  150. func (opts *FindReviewOptions) toCond() builder.Cond {
  151. var cond = builder.NewCond()
  152. if opts.IssueID > 0 {
  153. cond = cond.And(builder.Eq{"issue_id": opts.IssueID})
  154. }
  155. if opts.ReviewerID > 0 {
  156. cond = cond.And(builder.Eq{"reviewer_id": opts.ReviewerID})
  157. }
  158. if opts.Type != ReviewTypeUnknown {
  159. cond = cond.And(builder.Eq{"type": opts.Type})
  160. }
  161. if opts.OfficialOnly {
  162. cond = cond.And(builder.Eq{"official": true})
  163. }
  164. return cond
  165. }
  166. func findReviews(e Engine, opts FindReviewOptions) ([]*Review, error) {
  167. reviews := make([]*Review, 0, 10)
  168. sess := e.Where(opts.toCond())
  169. if opts.Page > 0 {
  170. sess = opts.ListOptions.setSessionPagination(sess)
  171. }
  172. return reviews, sess.
  173. Asc("created_unix").
  174. Asc("id").
  175. Find(&reviews)
  176. }
  177. // FindReviews returns reviews passing FindReviewOptions
  178. func FindReviews(opts FindReviewOptions) ([]*Review, error) {
  179. return findReviews(x, opts)
  180. }
  181. // CreateReviewOptions represent the options to create a review. Type, Issue and Reviewer are required.
  182. type CreateReviewOptions struct {
  183. Content string
  184. Type ReviewType
  185. Issue *Issue
  186. Reviewer *User
  187. ReviewerTeam *Team
  188. Official bool
  189. CommitID string
  190. Stale bool
  191. }
  192. // IsOfficialReviewer check if at least one of the provided reviewers can make official reviews in issue (counts towards required approvals)
  193. func IsOfficialReviewer(issue *Issue, reviewers ...*User) (bool, error) {
  194. return isOfficialReviewer(x, issue, reviewers...)
  195. }
  196. func isOfficialReviewer(e Engine, issue *Issue, reviewers ...*User) (bool, error) {
  197. pr, err := getPullRequestByIssueID(e, issue.ID)
  198. if err != nil {
  199. return false, err
  200. }
  201. if err = pr.loadProtectedBranch(e); err != nil {
  202. return false, err
  203. }
  204. if pr.ProtectedBranch == nil {
  205. return false, nil
  206. }
  207. for _, reviewer := range reviewers {
  208. official, err := pr.ProtectedBranch.isUserOfficialReviewer(e, reviewer)
  209. if official || err != nil {
  210. return official, err
  211. }
  212. }
  213. return false, nil
  214. }
  215. // IsOfficialReviewerTeam check if reviewer in this team can make official reviews in issue (counts towards required approvals)
  216. func IsOfficialReviewerTeam(issue *Issue, team *Team) (bool, error) {
  217. return isOfficialReviewerTeam(x, issue, team)
  218. }
  219. func isOfficialReviewerTeam(e Engine, issue *Issue, team *Team) (bool, error) {
  220. pr, err := getPullRequestByIssueID(e, issue.ID)
  221. if err != nil {
  222. return false, err
  223. }
  224. if err = pr.loadProtectedBranch(e); err != nil {
  225. return false, err
  226. }
  227. if pr.ProtectedBranch == nil {
  228. return false, nil
  229. }
  230. if !pr.ProtectedBranch.EnableApprovalsWhitelist {
  231. return team.Authorize >= AccessModeWrite, nil
  232. }
  233. return base.Int64sContains(pr.ProtectedBranch.ApprovalsWhitelistTeamIDs, team.ID), nil
  234. }
  235. func createReview(e Engine, opts CreateReviewOptions) (*Review, error) {
  236. review := &Review{
  237. Type: opts.Type,
  238. Issue: opts.Issue,
  239. IssueID: opts.Issue.ID,
  240. Reviewer: opts.Reviewer,
  241. ReviewerTeam: opts.ReviewerTeam,
  242. Content: opts.Content,
  243. Official: opts.Official,
  244. CommitID: opts.CommitID,
  245. Stale: opts.Stale,
  246. }
  247. if opts.Reviewer != nil {
  248. review.ReviewerID = opts.Reviewer.ID
  249. } else {
  250. if review.Type != ReviewTypeRequest {
  251. review.Type = ReviewTypeRequest
  252. }
  253. review.ReviewerTeamID = opts.ReviewerTeam.ID
  254. }
  255. if _, err := e.Insert(review); err != nil {
  256. return nil, err
  257. }
  258. return review, nil
  259. }
  260. // CreateReview creates a new review based on opts
  261. func CreateReview(opts CreateReviewOptions) (*Review, error) {
  262. return createReview(x, opts)
  263. }
  264. func getCurrentReview(e Engine, reviewer *User, issue *Issue) (*Review, error) {
  265. if reviewer == nil {
  266. return nil, nil
  267. }
  268. reviews, err := findReviews(e, FindReviewOptions{
  269. Type: ReviewTypePending,
  270. IssueID: issue.ID,
  271. ReviewerID: reviewer.ID,
  272. })
  273. if err != nil {
  274. return nil, err
  275. }
  276. if len(reviews) == 0 {
  277. return nil, ErrReviewNotExist{}
  278. }
  279. reviews[0].Reviewer = reviewer
  280. reviews[0].Issue = issue
  281. return reviews[0], nil
  282. }
  283. // ReviewExists returns whether a review exists for a particular line of code in the PR
  284. func ReviewExists(issue *Issue, treePath string, line int64) (bool, error) {
  285. return x.Cols("id").Exist(&Comment{IssueID: issue.ID, TreePath: treePath, Line: line, Type: CommentTypeCode})
  286. }
  287. // GetCurrentReview returns the current pending review of reviewer for given issue
  288. func GetCurrentReview(reviewer *User, issue *Issue) (*Review, error) {
  289. return getCurrentReview(x, reviewer, issue)
  290. }
  291. // ContentEmptyErr represents an content empty error
  292. type ContentEmptyErr struct {
  293. }
  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. var 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 original_author_id = 0 GROUP BY issue_id, reviewer_id) ORDER BY review.updated_unix ASC",
  407. issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest).
  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. // InsertReviews inserts review and review comments
  478. func InsertReviews(reviews []*Review) error {
  479. sess := x.NewSession()
  480. defer sess.Close()
  481. if err := sess.Begin(); err != nil {
  482. return err
  483. }
  484. for _, review := range reviews {
  485. if _, err := sess.NoAutoTime().Insert(review); err != nil {
  486. return err
  487. }
  488. if _, err := sess.NoAutoTime().Insert(&Comment{
  489. Type: CommentTypeReview,
  490. Content: review.Content,
  491. PosterID: review.ReviewerID,
  492. OriginalAuthor: review.OriginalAuthor,
  493. OriginalAuthorID: review.OriginalAuthorID,
  494. IssueID: review.IssueID,
  495. ReviewID: review.ID,
  496. CreatedUnix: review.CreatedUnix,
  497. UpdatedUnix: review.UpdatedUnix,
  498. }); err != nil {
  499. return err
  500. }
  501. for _, c := range review.Comments {
  502. c.ReviewID = review.ID
  503. }
  504. if len(review.Comments) > 0 {
  505. if _, err := sess.NoAutoTime().Insert(review.Comments); err != nil {
  506. return err
  507. }
  508. }
  509. }
  510. return sess.Commit()
  511. }
  512. // AddReviewRequest add a review request from one reviewer
  513. func AddReviewRequest(issue *Issue, reviewer, doer *User) (*Comment, error) {
  514. sess := x.NewSession()
  515. defer sess.Close()
  516. if err := sess.Begin(); err != nil {
  517. return nil, err
  518. }
  519. review, err := getReviewByIssueIDAndUserID(sess, issue.ID, reviewer.ID)
  520. if err != nil && !IsErrReviewNotExist(err) {
  521. return nil, err
  522. }
  523. // skip it when reviewer hase been request to review
  524. if review != nil && review.Type == ReviewTypeRequest {
  525. return nil, nil
  526. }
  527. official, err := isOfficialReviewer(sess, issue, reviewer, doer)
  528. if err != nil {
  529. return nil, err
  530. } else if official {
  531. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, reviewer.ID); err != nil {
  532. return nil, err
  533. }
  534. }
  535. review, err = createReview(sess, CreateReviewOptions{
  536. Type: ReviewTypeRequest,
  537. Issue: issue,
  538. Reviewer: reviewer,
  539. Official: official,
  540. Stale: false,
  541. })
  542. if err != nil {
  543. return nil, err
  544. }
  545. comment, err := createComment(sess, &CreateCommentOptions{
  546. Type: CommentTypeReviewRequest,
  547. Doer: doer,
  548. Repo: issue.Repo,
  549. Issue: issue,
  550. RemovedAssignee: false, // Use RemovedAssignee as !isRequest
  551. AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID
  552. ReviewID: review.ID,
  553. })
  554. if err != nil {
  555. return nil, err
  556. }
  557. return comment, sess.Commit()
  558. }
  559. //RemoveReviewRequest remove a review request from one reviewer
  560. func RemoveReviewRequest(issue *Issue, reviewer, doer *User) (*Comment, error) {
  561. sess := x.NewSession()
  562. defer sess.Close()
  563. if err := sess.Begin(); err != nil {
  564. return nil, err
  565. }
  566. review, err := getReviewByIssueIDAndUserID(sess, issue.ID, reviewer.ID)
  567. if err != nil && !IsErrReviewNotExist(err) {
  568. return nil, err
  569. }
  570. if review == nil || review.Type != ReviewTypeRequest {
  571. return nil, nil
  572. }
  573. if _, err = sess.Delete(review); err != nil {
  574. return nil, err
  575. }
  576. official, err := isOfficialReviewer(sess, issue, reviewer)
  577. if err != nil {
  578. return nil, err
  579. } else if official {
  580. // recalculate the latest official review for reviewer
  581. review, err := getReviewByIssueIDAndUserID(sess, issue.ID, reviewer.ID)
  582. if err != nil && !IsErrReviewNotExist(err) {
  583. return nil, err
  584. }
  585. if review != nil {
  586. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil {
  587. return nil, err
  588. }
  589. }
  590. }
  591. comment, err := createComment(sess, &CreateCommentOptions{
  592. Type: CommentTypeReviewRequest,
  593. Doer: doer,
  594. Repo: issue.Repo,
  595. Issue: issue,
  596. RemovedAssignee: true, // Use RemovedAssignee as !isRequest
  597. AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID
  598. })
  599. if err != nil {
  600. return nil, err
  601. }
  602. return comment, sess.Commit()
  603. }
  604. // AddTeamReviewRequest add a review request from one team
  605. func AddTeamReviewRequest(issue *Issue, reviewer *Team, doer *User) (*Comment, error) {
  606. sess := x.NewSession()
  607. defer sess.Close()
  608. if err := sess.Begin(); err != nil {
  609. return nil, err
  610. }
  611. review, err := getTeamReviewerByIssueIDAndTeamID(sess, issue.ID, reviewer.ID)
  612. if err != nil && !IsErrReviewNotExist(err) {
  613. return nil, err
  614. }
  615. // This team already has been requested to review - therefore skip this.
  616. if review != nil {
  617. return nil, nil
  618. }
  619. official, err := isOfficialReviewerTeam(sess, issue, reviewer)
  620. if err != nil {
  621. return nil, fmt.Errorf("isOfficialReviewerTeam(): %v", err)
  622. } else if !official {
  623. if official, err = isOfficialReviewer(sess, issue, doer); err != nil {
  624. return nil, fmt.Errorf("isOfficialReviewer(): %v", err)
  625. }
  626. }
  627. if review, err = createReview(sess, CreateReviewOptions{
  628. Type: ReviewTypeRequest,
  629. Issue: issue,
  630. ReviewerTeam: reviewer,
  631. Official: official,
  632. Stale: false,
  633. }); err != nil {
  634. return nil, err
  635. }
  636. if official {
  637. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_team_id=?", false, issue.ID, reviewer.ID); err != nil {
  638. return nil, err
  639. }
  640. }
  641. comment, err := createComment(sess, &CreateCommentOptions{
  642. Type: CommentTypeReviewRequest,
  643. Doer: doer,
  644. Repo: issue.Repo,
  645. Issue: issue,
  646. RemovedAssignee: false, // Use RemovedAssignee as !isRequest
  647. AssigneeTeamID: reviewer.ID, // Use AssigneeTeamID as reviewer team ID
  648. ReviewID: review.ID,
  649. })
  650. if err != nil {
  651. return nil, fmt.Errorf("createComment(): %v", err)
  652. }
  653. return comment, sess.Commit()
  654. }
  655. //RemoveTeamReviewRequest remove a review request from one team
  656. func RemoveTeamReviewRequest(issue *Issue, reviewer *Team, doer *User) (*Comment, error) {
  657. sess := x.NewSession()
  658. defer sess.Close()
  659. if err := sess.Begin(); err != nil {
  660. return nil, err
  661. }
  662. review, err := getTeamReviewerByIssueIDAndTeamID(sess, issue.ID, reviewer.ID)
  663. if err != nil && !IsErrReviewNotExist(err) {
  664. return nil, err
  665. }
  666. if review == nil {
  667. return nil, nil
  668. }
  669. if _, err = sess.Delete(review); err != nil {
  670. return nil, err
  671. }
  672. official, err := isOfficialReviewerTeam(sess, issue, reviewer)
  673. if err != nil {
  674. return nil, fmt.Errorf("isOfficialReviewerTeam(): %v", err)
  675. }
  676. if official {
  677. // recalculate which is the latest official review from that team
  678. review, err := getReviewByIssueIDAndUserID(sess, issue.ID, -reviewer.ID)
  679. if err != nil && !IsErrReviewNotExist(err) {
  680. return nil, err
  681. }
  682. if review != nil {
  683. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil {
  684. return nil, err
  685. }
  686. }
  687. }
  688. if doer == nil {
  689. return nil, sess.Commit()
  690. }
  691. comment, err := createComment(sess, &CreateCommentOptions{
  692. Type: CommentTypeReviewRequest,
  693. Doer: doer,
  694. Repo: issue.Repo,
  695. Issue: issue,
  696. RemovedAssignee: true, // Use RemovedAssignee as !isRequest
  697. AssigneeTeamID: reviewer.ID, // Use AssigneeTeamID as reviewer team ID
  698. })
  699. if err != nil {
  700. return nil, fmt.Errorf("createComment(): %v", err)
  701. }
  702. return comment, sess.Commit()
  703. }
  704. // MarkConversation Add or remove Conversation mark for a code comment
  705. func MarkConversation(comment *Comment, doer *User, isResolve bool) (err error) {
  706. if comment.Type != CommentTypeCode {
  707. return nil
  708. }
  709. if isResolve {
  710. if comment.ResolveDoerID != 0 {
  711. return nil
  712. }
  713. if _, err = x.Exec("UPDATE `comment` SET resolve_doer_id=? WHERE id=?", doer.ID, comment.ID); err != nil {
  714. return err
  715. }
  716. } else {
  717. if comment.ResolveDoerID == 0 {
  718. return nil
  719. }
  720. if _, err = x.Exec("UPDATE `comment` SET resolve_doer_id=? WHERE id=?", 0, comment.ID); err != nil {
  721. return err
  722. }
  723. }
  724. return nil
  725. }
  726. // CanMarkConversation Add or remove Conversation mark for a code comment permission check
  727. // the PR writer , offfcial reviewer and poster can do it
  728. func CanMarkConversation(issue *Issue, doer *User) (permResult bool, err error) {
  729. if doer == nil || issue == nil {
  730. return false, fmt.Errorf("issue or doer is nil")
  731. }
  732. if doer.ID != issue.PosterID {
  733. if err = issue.LoadRepo(); err != nil {
  734. return false, err
  735. }
  736. perm, err := GetUserRepoPermission(issue.Repo, doer)
  737. if err != nil {
  738. return false, err
  739. }
  740. permResult = perm.CanAccess(AccessModeWrite, UnitTypePullRequests)
  741. if !permResult {
  742. if permResult, err = IsOfficialReviewer(issue, doer); err != nil {
  743. return false, err
  744. }
  745. }
  746. if !permResult {
  747. return false, nil
  748. }
  749. }
  750. return true, nil
  751. }
  752. // DeleteReview delete a review and it's code comments
  753. func DeleteReview(r *Review) error {
  754. sess := x.NewSession()
  755. defer sess.Close()
  756. if err := sess.Begin(); err != nil {
  757. return err
  758. }
  759. if r.ID == 0 {
  760. return fmt.Errorf("review is not allowed to be 0")
  761. }
  762. if r.Type == ReviewTypeRequest {
  763. return fmt.Errorf("review request can not be deleted using this method")
  764. }
  765. opts := FindCommentsOptions{
  766. Type: CommentTypeCode,
  767. IssueID: r.IssueID,
  768. ReviewID: r.ID,
  769. }
  770. if _, err := sess.Where(opts.toConds()).Delete(new(Comment)); err != nil {
  771. return err
  772. }
  773. opts = FindCommentsOptions{
  774. Type: CommentTypeReview,
  775. IssueID: r.IssueID,
  776. ReviewID: r.ID,
  777. }
  778. if _, err := sess.Where(opts.toConds()).Delete(new(Comment)); err != nil {
  779. return err
  780. }
  781. if _, err := sess.ID(r.ID).Delete(new(Review)); err != nil {
  782. return err
  783. }
  784. return sess.Commit()
  785. }
  786. // GetCodeCommentsCount return count of CodeComments a Review has
  787. func (r *Review) GetCodeCommentsCount() int {
  788. opts := FindCommentsOptions{
  789. Type: CommentTypeCode,
  790. IssueID: r.IssueID,
  791. ReviewID: r.ID,
  792. }
  793. conds := opts.toConds()
  794. if r.ID == 0 {
  795. conds = conds.And(builder.Eq{"invalidated": false})
  796. }
  797. count, err := x.Where(conds).Count(new(Comment))
  798. if err != nil {
  799. return 0
  800. }
  801. return int(count)
  802. }
  803. // HTMLURL formats a URL-string to the related review issue-comment
  804. func (r *Review) HTMLURL() string {
  805. opts := FindCommentsOptions{
  806. Type: CommentTypeReview,
  807. IssueID: r.IssueID,
  808. ReviewID: r.ID,
  809. }
  810. comment := new(Comment)
  811. has, err := x.Where(opts.toConds()).Get(comment)
  812. if err != nil || !has {
  813. return ""
  814. }
  815. return comment.HTMLURL()
  816. }