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