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

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