選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

review.go 28KB

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