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

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