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

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