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

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