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

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