You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

review.go 26KB

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