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

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