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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026
  1. // Copyright 2018 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package issues
  4. import (
  5. "context"
  6. "fmt"
  7. "slices"
  8. "strings"
  9. "code.gitea.io/gitea/models/db"
  10. git_model "code.gitea.io/gitea/models/git"
  11. "code.gitea.io/gitea/models/organization"
  12. "code.gitea.io/gitea/models/perm"
  13. access_model "code.gitea.io/gitea/models/perm/access"
  14. "code.gitea.io/gitea/models/unit"
  15. user_model "code.gitea.io/gitea/models/user"
  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(64)"`
  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 err
  118. }
  119. if err = r.loadIssue(ctx); err != nil {
  120. return err
  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 err
  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 err
  136. }
  137. r.Reviewer, err = user_model.GetPossibleUserByID(ctx, r.ReviewerID)
  138. if err != nil {
  139. if !user_model.IsErrUserNotExist(err) {
  140. return fmt.Errorf("GetPossibleUserByID [%d]: %w", r.ReviewerID, err)
  141. }
  142. r.ReviewerID = user_model.GhostUserID
  143. r.Reviewer = user_model.NewGhostUser()
  144. return nil
  145. }
  146. return err
  147. }
  148. // LoadReviewerTeam loads reviewer team
  149. func (r *Review) LoadReviewerTeam(ctx context.Context) (err error) {
  150. if r.ReviewerTeamID == 0 || r.ReviewerTeam != nil {
  151. return nil
  152. }
  153. r.ReviewerTeam, err = organization.GetTeamByID(ctx, r.ReviewerTeamID)
  154. return err
  155. }
  156. // LoadAttributes loads all attributes except CodeComments
  157. func (r *Review) LoadAttributes(ctx context.Context) (err error) {
  158. if err = r.loadIssue(ctx); err != nil {
  159. return err
  160. }
  161. if err = r.LoadCodeComments(ctx); err != nil {
  162. return err
  163. }
  164. if err = r.LoadReviewer(ctx); err != nil {
  165. return err
  166. }
  167. if err = r.LoadReviewerTeam(ctx); err != nil {
  168. return err
  169. }
  170. return err
  171. }
  172. func (r *Review) HTMLTypeColorName() string {
  173. switch r.Type {
  174. case ReviewTypeApprove:
  175. if r.Stale {
  176. return "yellow"
  177. }
  178. return "green"
  179. case ReviewTypeComment:
  180. return "grey"
  181. case ReviewTypeReject:
  182. return "red"
  183. case ReviewTypeRequest:
  184. return "yellow"
  185. }
  186. return "grey"
  187. }
  188. // GetReviewByID returns the review by the given ID
  189. func GetReviewByID(ctx context.Context, id int64) (*Review, error) {
  190. review := new(Review)
  191. if has, err := db.GetEngine(ctx).ID(id).Get(review); err != nil {
  192. return nil, err
  193. } else if !has {
  194. return nil, ErrReviewNotExist{ID: id}
  195. }
  196. return review, nil
  197. }
  198. // CreateReviewOptions represent the options to create a review. Type, Issue and Reviewer are required.
  199. type CreateReviewOptions struct {
  200. Content string
  201. Type ReviewType
  202. Issue *Issue
  203. Reviewer *user_model.User
  204. ReviewerTeam *organization.Team
  205. Official bool
  206. CommitID string
  207. Stale bool
  208. }
  209. // IsOfficialReviewer check if at least one of the provided reviewers can make official reviews in issue (counts towards required approvals)
  210. func IsOfficialReviewer(ctx context.Context, issue *Issue, reviewer *user_model.User) (bool, error) {
  211. if err := issue.LoadPullRequest(ctx); err != nil {
  212. return false, err
  213. }
  214. pr := issue.PullRequest
  215. rule, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
  216. if err != nil {
  217. return false, err
  218. }
  219. if rule == nil {
  220. // if no rule is found, then user with write access can make official reviews
  221. err := pr.LoadBaseRepo(ctx)
  222. if err != nil {
  223. return false, err
  224. }
  225. writeAccess, err := access_model.HasAccessUnit(ctx, reviewer, pr.BaseRepo, unit.TypeCode, perm.AccessModeWrite)
  226. if err != nil {
  227. return false, err
  228. }
  229. return writeAccess, nil
  230. }
  231. official, err := git_model.IsUserOfficialReviewer(ctx, rule, reviewer)
  232. if official || err != nil {
  233. return official, err
  234. }
  235. return false, nil
  236. }
  237. // IsOfficialReviewerTeam check if reviewer in this team can make official reviews in issue (counts towards required approvals)
  238. func IsOfficialReviewerTeam(ctx context.Context, issue *Issue, team *organization.Team) (bool, error) {
  239. if err := issue.LoadPullRequest(ctx); err != nil {
  240. return false, err
  241. }
  242. pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, issue.PullRequest.BaseRepoID, issue.PullRequest.BaseBranch)
  243. if err != nil {
  244. return false, err
  245. }
  246. if pb == nil {
  247. return false, nil
  248. }
  249. if !pb.EnableApprovalsWhitelist {
  250. return team.UnitAccessMode(ctx, unit.TypeCode) >= perm.AccessModeWrite, nil
  251. }
  252. return slices.Contains(pb.ApprovalsWhitelistTeamIDs, team.ID), nil
  253. }
  254. // CreateReview creates a new review based on opts
  255. func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error) {
  256. ctx, committer, err := db.TxContext(ctx)
  257. if err != nil {
  258. return nil, err
  259. }
  260. defer committer.Close()
  261. sess := db.GetEngine(ctx)
  262. review := &Review{
  263. Issue: opts.Issue,
  264. IssueID: opts.Issue.ID,
  265. Reviewer: opts.Reviewer,
  266. ReviewerTeam: opts.ReviewerTeam,
  267. Content: opts.Content,
  268. Official: opts.Official,
  269. CommitID: opts.CommitID,
  270. Stale: opts.Stale,
  271. }
  272. if opts.Reviewer != nil {
  273. review.Type = opts.Type
  274. review.ReviewerID = opts.Reviewer.ID
  275. reviewCond := builder.Eq{"reviewer_id": opts.Reviewer.ID, "issue_id": opts.Issue.ID}
  276. // make sure user review requests are cleared
  277. if opts.Type != ReviewTypePending {
  278. if _, err := sess.Where(reviewCond.And(builder.Eq{"type": ReviewTypeRequest})).Delete(new(Review)); err != nil {
  279. return nil, err
  280. }
  281. }
  282. // make sure if the created review gets dismissed no old review surface
  283. // other types can be ignored, as they don't affect branch protection
  284. if opts.Type == ReviewTypeApprove || opts.Type == ReviewTypeReject {
  285. if _, err := sess.Where(reviewCond.And(builder.In("type", ReviewTypeApprove, ReviewTypeReject))).
  286. Cols("dismissed").Update(&Review{Dismissed: true}); err != nil {
  287. return nil, err
  288. }
  289. }
  290. } else if opts.ReviewerTeam != nil {
  291. review.Type = ReviewTypeRequest
  292. review.ReviewerTeamID = opts.ReviewerTeam.ID
  293. } else {
  294. return nil, fmt.Errorf("provide either reviewer or reviewer team")
  295. }
  296. if _, err := sess.Insert(review); err != nil {
  297. return nil, err
  298. }
  299. return review, committer.Commit()
  300. }
  301. // GetCurrentReview returns the current pending review of reviewer for given issue
  302. func GetCurrentReview(ctx context.Context, reviewer *user_model.User, issue *Issue) (*Review, error) {
  303. if reviewer == nil {
  304. return nil, nil
  305. }
  306. reviews, err := FindReviews(ctx, FindReviewOptions{
  307. Type: ReviewTypePending,
  308. IssueID: issue.ID,
  309. ReviewerID: reviewer.ID,
  310. })
  311. if err != nil {
  312. return nil, err
  313. }
  314. if len(reviews) == 0 {
  315. return nil, ErrReviewNotExist{}
  316. }
  317. reviews[0].Reviewer = reviewer
  318. reviews[0].Issue = issue
  319. return reviews[0], nil
  320. }
  321. // ReviewExists returns whether a review exists for a particular line of code in the PR
  322. func ReviewExists(ctx context.Context, issue *Issue, treePath string, line int64) (bool, error) {
  323. return db.GetEngine(ctx).Cols("id").Exist(&Comment{IssueID: issue.ID, TreePath: treePath, Line: line, Type: CommentTypeCode})
  324. }
  325. // ContentEmptyErr represents an content empty error
  326. type ContentEmptyErr struct{}
  327. func (ContentEmptyErr) Error() string {
  328. return "Review content is empty"
  329. }
  330. // IsContentEmptyErr returns true if err is a ContentEmptyErr
  331. func IsContentEmptyErr(err error) bool {
  332. _, ok := err.(ContentEmptyErr)
  333. return ok
  334. }
  335. // SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
  336. func SubmitReview(ctx context.Context, doer *user_model.User, issue *Issue, reviewType ReviewType, content, commitID string, stale bool, attachmentUUIDs []string) (*Review, *Comment, error) {
  337. ctx, committer, err := db.TxContext(ctx)
  338. if err != nil {
  339. return nil, nil, err
  340. }
  341. defer committer.Close()
  342. sess := db.GetEngine(ctx)
  343. official := false
  344. review, err := GetCurrentReview(ctx, doer, issue)
  345. if err != nil {
  346. if !IsErrReviewNotExist(err) {
  347. return nil, nil, err
  348. }
  349. if reviewType != ReviewTypeApprove && len(strings.TrimSpace(content)) == 0 {
  350. return nil, nil, ContentEmptyErr{}
  351. }
  352. if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject {
  353. // Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared
  354. if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, doer.ID); err != nil {
  355. return nil, nil, err
  356. }
  357. if official, err = IsOfficialReviewer(ctx, issue, doer); err != nil {
  358. return nil, nil, err
  359. }
  360. }
  361. // No current review. Create a new one!
  362. if review, err = CreateReview(ctx, CreateReviewOptions{
  363. Type: reviewType,
  364. Issue: issue,
  365. Reviewer: doer,
  366. Content: content,
  367. Official: official,
  368. CommitID: commitID,
  369. Stale: stale,
  370. }); err != nil {
  371. return nil, nil, err
  372. }
  373. } else {
  374. if err := review.LoadCodeComments(ctx); err != nil {
  375. return nil, nil, err
  376. }
  377. if reviewType != ReviewTypeApprove && len(review.CodeComments) == 0 && len(strings.TrimSpace(content)) == 0 {
  378. return nil, nil, ContentEmptyErr{}
  379. }
  380. if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject {
  381. // Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared
  382. if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, doer.ID); err != nil {
  383. return nil, nil, err
  384. }
  385. if official, err = IsOfficialReviewer(ctx, issue, doer); err != nil {
  386. return nil, nil, err
  387. }
  388. }
  389. review.Official = official
  390. review.Issue = issue
  391. review.Content = content
  392. review.Type = reviewType
  393. review.CommitID = commitID
  394. review.Stale = stale
  395. if _, err := sess.ID(review.ID).Cols("content, type, official, commit_id, stale").Update(review); err != nil {
  396. return nil, nil, err
  397. }
  398. }
  399. comm, err := CreateComment(ctx, &CreateCommentOptions{
  400. Type: CommentTypeReview,
  401. Doer: doer,
  402. Content: review.Content,
  403. Issue: issue,
  404. Repo: issue.Repo,
  405. ReviewID: review.ID,
  406. Attachments: attachmentUUIDs,
  407. })
  408. if err != nil || comm == nil {
  409. return nil, nil, err
  410. }
  411. // try to remove team review request if need
  412. if issue.Repo.Owner.IsOrganization() && (reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject) {
  413. teamReviewRequests := make([]*Review, 0, 10)
  414. if err := sess.SQL("SELECT * FROM review WHERE issue_id = ? AND reviewer_team_id > 0 AND type = ?", issue.ID, ReviewTypeRequest).Find(&teamReviewRequests); err != nil {
  415. return nil, nil, err
  416. }
  417. for _, teamReviewRequest := range teamReviewRequests {
  418. ok, err := organization.IsTeamMember(ctx, issue.Repo.OwnerID, teamReviewRequest.ReviewerTeamID, doer.ID)
  419. if err != nil {
  420. return nil, nil, err
  421. } else if !ok {
  422. continue
  423. }
  424. if _, err := db.DeleteByID[Review](ctx, teamReviewRequest.ID); err != nil {
  425. return nil, nil, err
  426. }
  427. }
  428. }
  429. comm.Review = review
  430. return review, comm, committer.Commit()
  431. }
  432. // GetReviewByIssueIDAndUserID get the latest review of reviewer for a pull request
  433. func GetReviewByIssueIDAndUserID(ctx context.Context, issueID, userID int64) (*Review, error) {
  434. review := new(Review)
  435. has, err := db.GetEngine(ctx).Where(
  436. builder.In("type", ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest).
  437. And(builder.Eq{"issue_id": issueID, "reviewer_id": userID, "original_author_id": 0})).
  438. Desc("id").
  439. Get(review)
  440. if err != nil {
  441. return nil, err
  442. }
  443. if !has {
  444. return nil, ErrReviewNotExist{}
  445. }
  446. return review, nil
  447. }
  448. // GetTeamReviewerByIssueIDAndTeamID get the latest review request of reviewer team for a pull request
  449. func GetTeamReviewerByIssueIDAndTeamID(ctx context.Context, issueID, teamID int64) (*Review, error) {
  450. review := new(Review)
  451. has, err := db.GetEngine(ctx).Where(builder.Eq{"issue_id": issueID, "reviewer_team_id": teamID}).
  452. Desc("id").
  453. Get(review)
  454. if err != nil {
  455. return nil, err
  456. }
  457. if !has {
  458. return nil, ErrReviewNotExist{0}
  459. }
  460. return review, err
  461. }
  462. // MarkReviewsAsStale marks existing reviews as stale
  463. func MarkReviewsAsStale(ctx context.Context, issueID int64) (err error) {
  464. _, err = db.GetEngine(ctx).Exec("UPDATE `review` SET stale=? WHERE issue_id=?", true, issueID)
  465. return err
  466. }
  467. // MarkReviewsAsNotStale marks existing reviews as not stale for a giving commit SHA
  468. func MarkReviewsAsNotStale(ctx context.Context, issueID int64, commitID string) (err error) {
  469. _, err = db.GetEngine(ctx).Exec("UPDATE `review` SET stale=? WHERE issue_id=? AND commit_id=?", false, issueID, commitID)
  470. return err
  471. }
  472. // DismissReview change the dismiss status of a review
  473. func DismissReview(ctx context.Context, review *Review, isDismiss bool) (err error) {
  474. if review.Dismissed == isDismiss || (review.Type != ReviewTypeApprove && review.Type != ReviewTypeReject) {
  475. return nil
  476. }
  477. review.Dismissed = isDismiss
  478. if review.ID == 0 {
  479. return ErrReviewNotExist{}
  480. }
  481. _, err = db.GetEngine(ctx).ID(review.ID).Cols("dismissed").Update(review)
  482. return err
  483. }
  484. // InsertReviews inserts review and review comments
  485. func InsertReviews(ctx context.Context, reviews []*Review) error {
  486. ctx, committer, err := db.TxContext(ctx)
  487. if err != nil {
  488. return err
  489. }
  490. defer committer.Close()
  491. sess := db.GetEngine(ctx)
  492. for _, review := range reviews {
  493. if _, err := sess.NoAutoTime().Insert(review); err != nil {
  494. return err
  495. }
  496. if _, err := sess.NoAutoTime().Insert(&Comment{
  497. Type: CommentTypeReview,
  498. Content: review.Content,
  499. PosterID: review.ReviewerID,
  500. OriginalAuthor: review.OriginalAuthor,
  501. OriginalAuthorID: review.OriginalAuthorID,
  502. IssueID: review.IssueID,
  503. ReviewID: review.ID,
  504. CreatedUnix: review.CreatedUnix,
  505. UpdatedUnix: review.UpdatedUnix,
  506. }); err != nil {
  507. return err
  508. }
  509. for _, c := range review.Comments {
  510. c.ReviewID = review.ID
  511. }
  512. if len(review.Comments) > 0 {
  513. if _, err := sess.NoAutoTime().Insert(review.Comments); err != nil {
  514. return err
  515. }
  516. }
  517. }
  518. return committer.Commit()
  519. }
  520. // AddReviewRequest add a review request from one reviewer
  521. func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_model.User) (*Comment, error) {
  522. ctx, committer, err := db.TxContext(ctx)
  523. if err != nil {
  524. return nil, err
  525. }
  526. defer committer.Close()
  527. sess := db.GetEngine(ctx)
  528. review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID)
  529. if err != nil && !IsErrReviewNotExist(err) {
  530. return nil, err
  531. }
  532. // skip it when reviewer hase been request to review
  533. if review != nil && review.Type == ReviewTypeRequest {
  534. return nil, nil
  535. }
  536. // if the reviewer is an official reviewer,
  537. // remove the official flag in the all previous reviews
  538. official, err := IsOfficialReviewer(ctx, issue, reviewer)
  539. if err != nil {
  540. return nil, err
  541. } else if official {
  542. if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, reviewer.ID); err != nil {
  543. return nil, err
  544. }
  545. }
  546. review, err = CreateReview(ctx, CreateReviewOptions{
  547. Type: ReviewTypeRequest,
  548. Issue: issue,
  549. Reviewer: reviewer,
  550. Official: official,
  551. Stale: false,
  552. })
  553. if err != nil {
  554. return nil, err
  555. }
  556. comment, err := CreateComment(ctx, &CreateCommentOptions{
  557. Type: CommentTypeReviewRequest,
  558. Doer: doer,
  559. Repo: issue.Repo,
  560. Issue: issue,
  561. RemovedAssignee: false, // Use RemovedAssignee as !isRequest
  562. AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID
  563. ReviewID: review.ID,
  564. })
  565. if err != nil {
  566. return nil, err
  567. }
  568. // func caller use the created comment to retrieve created review too.
  569. comment.Review = review
  570. return comment, committer.Commit()
  571. }
  572. // RemoveReviewRequest remove a review request from one reviewer
  573. func RemoveReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_model.User) (*Comment, error) {
  574. ctx, committer, err := db.TxContext(ctx)
  575. if err != nil {
  576. return nil, err
  577. }
  578. defer committer.Close()
  579. review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID)
  580. if err != nil && !IsErrReviewNotExist(err) {
  581. return nil, err
  582. }
  583. if review == nil || review.Type != ReviewTypeRequest {
  584. return nil, nil
  585. }
  586. if _, err = db.DeleteByBean(ctx, review); err != nil {
  587. return nil, err
  588. }
  589. official, err := IsOfficialReviewer(ctx, issue, reviewer)
  590. if err != nil {
  591. return nil, err
  592. } else if official {
  593. if err := restoreLatestOfficialReview(ctx, issue.ID, reviewer.ID); err != nil {
  594. return nil, err
  595. }
  596. }
  597. comment, err := CreateComment(ctx, &CreateCommentOptions{
  598. Type: CommentTypeReviewRequest,
  599. Doer: doer,
  600. Repo: issue.Repo,
  601. Issue: issue,
  602. RemovedAssignee: true, // Use RemovedAssignee as !isRequest
  603. AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID
  604. })
  605. if err != nil {
  606. return nil, err
  607. }
  608. return comment, committer.Commit()
  609. }
  610. // Recalculate the latest official review for reviewer
  611. func restoreLatestOfficialReview(ctx context.Context, issueID, reviewerID int64) error {
  612. review, err := GetReviewByIssueIDAndUserID(ctx, issueID, reviewerID)
  613. if err != nil && !IsErrReviewNotExist(err) {
  614. return err
  615. }
  616. if review != nil {
  617. if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil {
  618. return err
  619. }
  620. }
  621. return nil
  622. }
  623. // AddTeamReviewRequest add a review request from one team
  624. func AddTeamReviewRequest(ctx context.Context, issue *Issue, reviewer *organization.Team, doer *user_model.User) (*Comment, error) {
  625. ctx, committer, err := db.TxContext(ctx)
  626. if err != nil {
  627. return nil, err
  628. }
  629. defer committer.Close()
  630. review, err := GetTeamReviewerByIssueIDAndTeamID(ctx, issue.ID, reviewer.ID)
  631. if err != nil && !IsErrReviewNotExist(err) {
  632. return nil, err
  633. }
  634. // This team already has been requested to review - therefore skip this.
  635. if review != nil {
  636. return nil, nil
  637. }
  638. official, err := IsOfficialReviewerTeam(ctx, issue, reviewer)
  639. if err != nil {
  640. return nil, fmt.Errorf("isOfficialReviewerTeam(): %w", err)
  641. } else if !official {
  642. if official, err = IsOfficialReviewer(ctx, issue, doer); err != nil {
  643. return nil, fmt.Errorf("isOfficialReviewer(): %w", err)
  644. }
  645. }
  646. if review, err = CreateReview(ctx, CreateReviewOptions{
  647. Type: ReviewTypeRequest,
  648. Issue: issue,
  649. ReviewerTeam: reviewer,
  650. Official: official,
  651. Stale: false,
  652. }); err != nil {
  653. return nil, err
  654. }
  655. if official {
  656. if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_team_id=?", false, issue.ID, reviewer.ID); err != nil {
  657. return nil, err
  658. }
  659. }
  660. comment, err := CreateComment(ctx, &CreateCommentOptions{
  661. Type: CommentTypeReviewRequest,
  662. Doer: doer,
  663. Repo: issue.Repo,
  664. Issue: issue,
  665. RemovedAssignee: false, // Use RemovedAssignee as !isRequest
  666. AssigneeTeamID: reviewer.ID, // Use AssigneeTeamID as reviewer team ID
  667. ReviewID: review.ID,
  668. })
  669. if err != nil {
  670. return nil, fmt.Errorf("CreateComment(): %w", err)
  671. }
  672. return comment, committer.Commit()
  673. }
  674. // RemoveTeamReviewRequest remove a review request from one team
  675. func RemoveTeamReviewRequest(ctx context.Context, issue *Issue, reviewer *organization.Team, doer *user_model.User) (*Comment, error) {
  676. ctx, committer, err := db.TxContext(ctx)
  677. if err != nil {
  678. return nil, err
  679. }
  680. defer committer.Close()
  681. review, err := GetTeamReviewerByIssueIDAndTeamID(ctx, issue.ID, reviewer.ID)
  682. if err != nil && !IsErrReviewNotExist(err) {
  683. return nil, err
  684. }
  685. if review == nil {
  686. return nil, nil
  687. }
  688. if _, err = db.DeleteByBean(ctx, review); err != nil {
  689. return nil, err
  690. }
  691. official, err := IsOfficialReviewerTeam(ctx, issue, reviewer)
  692. if err != nil {
  693. return nil, fmt.Errorf("isOfficialReviewerTeam(): %w", err)
  694. }
  695. if official {
  696. // recalculate which is the latest official review from that team
  697. review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, -reviewer.ID)
  698. if err != nil && !IsErrReviewNotExist(err) {
  699. return nil, err
  700. }
  701. if review != nil {
  702. if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil {
  703. return nil, err
  704. }
  705. }
  706. }
  707. if doer == nil {
  708. return nil, committer.Commit()
  709. }
  710. comment, err := CreateComment(ctx, &CreateCommentOptions{
  711. Type: CommentTypeReviewRequest,
  712. Doer: doer,
  713. Repo: issue.Repo,
  714. Issue: issue,
  715. RemovedAssignee: true, // Use RemovedAssignee as !isRequest
  716. AssigneeTeamID: reviewer.ID, // Use AssigneeTeamID as reviewer team ID
  717. })
  718. if err != nil {
  719. return nil, fmt.Errorf("CreateComment(): %w", err)
  720. }
  721. return comment, committer.Commit()
  722. }
  723. // MarkConversation Add or remove Conversation mark for a code comment
  724. func MarkConversation(ctx context.Context, comment *Comment, doer *user_model.User, isResolve bool) (err error) {
  725. if comment.Type != CommentTypeCode {
  726. return nil
  727. }
  728. if isResolve {
  729. if comment.ResolveDoerID != 0 {
  730. return nil
  731. }
  732. if _, err = db.GetEngine(ctx).Exec("UPDATE `comment` SET resolve_doer_id=? WHERE id=?", doer.ID, comment.ID); err != nil {
  733. return err
  734. }
  735. } else {
  736. if comment.ResolveDoerID == 0 {
  737. return nil
  738. }
  739. if _, err = db.GetEngine(ctx).Exec("UPDATE `comment` SET resolve_doer_id=? WHERE id=?", 0, comment.ID); err != nil {
  740. return err
  741. }
  742. }
  743. return nil
  744. }
  745. // CanMarkConversation Add or remove Conversation mark for a code comment permission check
  746. // the PR writer , offfcial reviewer and poster can do it
  747. func CanMarkConversation(ctx context.Context, issue *Issue, doer *user_model.User) (permResult bool, err error) {
  748. if doer == nil || issue == nil {
  749. return false, fmt.Errorf("issue or doer is nil")
  750. }
  751. if doer.ID != issue.PosterID {
  752. if err = issue.LoadRepo(ctx); err != nil {
  753. return false, err
  754. }
  755. p, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
  756. if err != nil {
  757. return false, err
  758. }
  759. permResult = p.CanAccess(perm.AccessModeWrite, unit.TypePullRequests)
  760. if !permResult {
  761. if permResult, err = IsOfficialReviewer(ctx, issue, doer); err != nil {
  762. return false, err
  763. }
  764. }
  765. if !permResult {
  766. return false, nil
  767. }
  768. }
  769. return true, nil
  770. }
  771. // DeleteReview delete a review and it's code comments
  772. func DeleteReview(ctx context.Context, r *Review) error {
  773. ctx, committer, err := db.TxContext(ctx)
  774. if err != nil {
  775. return err
  776. }
  777. defer committer.Close()
  778. if r.ID == 0 {
  779. return fmt.Errorf("review is not allowed to be 0")
  780. }
  781. if r.Type == ReviewTypeRequest {
  782. return fmt.Errorf("review request can not be deleted using this method")
  783. }
  784. opts := FindCommentsOptions{
  785. Type: CommentTypeCode,
  786. IssueID: r.IssueID,
  787. ReviewID: r.ID,
  788. }
  789. if _, err := db.Delete[Comment](ctx, opts); err != nil {
  790. return err
  791. }
  792. opts = FindCommentsOptions{
  793. Type: CommentTypeReview,
  794. IssueID: r.IssueID,
  795. ReviewID: r.ID,
  796. }
  797. if _, err := db.Delete[Comment](ctx, opts); err != nil {
  798. return err
  799. }
  800. opts = FindCommentsOptions{
  801. Type: CommentTypeDismissReview,
  802. IssueID: r.IssueID,
  803. ReviewID: r.ID,
  804. }
  805. if _, err := db.Delete[Comment](ctx, opts); err != nil {
  806. return err
  807. }
  808. if _, err := db.DeleteByID[Review](ctx, r.ID); err != nil {
  809. return err
  810. }
  811. if r.Official {
  812. if err := restoreLatestOfficialReview(ctx, r.IssueID, r.ReviewerID); err != nil {
  813. return err
  814. }
  815. }
  816. return committer.Commit()
  817. }
  818. // GetCodeCommentsCount return count of CodeComments a Review has
  819. func (r *Review) GetCodeCommentsCount(ctx context.Context) int {
  820. opts := FindCommentsOptions{
  821. Type: CommentTypeCode,
  822. IssueID: r.IssueID,
  823. ReviewID: r.ID,
  824. }
  825. conds := opts.ToConds()
  826. if r.ID == 0 {
  827. conds = conds.And(builder.Eq{"invalidated": false})
  828. }
  829. count, err := db.GetEngine(ctx).Where(conds).Count(new(Comment))
  830. if err != nil {
  831. return 0
  832. }
  833. return int(count)
  834. }
  835. // HTMLURL formats a URL-string to the related review issue-comment
  836. func (r *Review) HTMLURL(ctx context.Context) string {
  837. opts := FindCommentsOptions{
  838. Type: CommentTypeReview,
  839. IssueID: r.IssueID,
  840. ReviewID: r.ID,
  841. }
  842. comment := new(Comment)
  843. has, err := db.GetEngine(ctx).Where(opts.ToConds()).Get(comment)
  844. if err != nil || !has {
  845. return ""
  846. }
  847. return comment.HTMLURL(ctx)
  848. }
  849. // RemapExternalUser ExternalUserRemappable interface
  850. func (r *Review) RemapExternalUser(externalName string, externalID, userID int64) error {
  851. r.OriginalAuthor = externalName
  852. r.OriginalAuthorID = externalID
  853. r.ReviewerID = userID
  854. return nil
  855. }
  856. // GetUserID ExternalUserRemappable interface
  857. func (r *Review) GetUserID() int64 { return r.ReviewerID }
  858. // GetExternalName ExternalUserRemappable interface
  859. func (r *Review) GetExternalName() string { return r.OriginalAuthor }
  860. // GetExternalID ExternalUserRemappable interface
  861. func (r *Review) GetExternalID() int64 { return r.OriginalAuthorID }
  862. // UpdateReviewsMigrationsByType updates reviews' migrations information via given git service type and original id and poster id
  863. func UpdateReviewsMigrationsByType(ctx context.Context, tp structs.GitServiceType, originalAuthorID string, posterID int64) error {
  864. _, err := db.GetEngine(ctx).Table("review").
  865. Where("original_author_id = ?", originalAuthorID).
  866. And(migratedIssueCond(tp)).
  867. Update(map[string]any{
  868. "reviewer_id": posterID,
  869. "original_author": "",
  870. "original_author_id": 0,
  871. })
  872. return err
  873. }