2018-08-06 10:13:22 +05:30
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package models
import (
2021-12-10 06:57:50 +05:30
"context"
2020-04-18 19:20:25 +05:30
"fmt"
2019-11-14 08:27:36 +05:30
"strings"
2018-08-06 10:13:22 +05:30
2021-09-19 17:19:59 +05:30
"code.gitea.io/gitea/models/db"
2022-03-29 11:59:02 +05:30
"code.gitea.io/gitea/models/organization"
2021-11-28 17:28:28 +05:30
"code.gitea.io/gitea/models/perm"
2021-11-10 01:27:58 +05:30
"code.gitea.io/gitea/models/unit"
2021-11-24 15:19:20 +05:30
user_model "code.gitea.io/gitea/models/user"
2020-10-13 01:25:13 +05:30
"code.gitea.io/gitea/modules/base"
2019-08-15 20:16:21 +05:30
"code.gitea.io/gitea/modules/timeutil"
2018-08-06 10:13:22 +05:30
2019-06-23 20:52:43 +05:30
"xorm.io/builder"
2018-08-06 10:13:22 +05:30
)
// ReviewType defines the sort of feedback a review gives
type ReviewType int
// ReviewTypeUnknown unknown review type
const ReviewTypeUnknown ReviewType = - 1
const (
// ReviewTypePending is a review which is not published yet
ReviewTypePending ReviewType = iota
// ReviewTypeApprove approves changes
ReviewTypeApprove
// ReviewTypeComment gives general feedback
ReviewTypeComment
// ReviewTypeReject gives feedback blocking merge
ReviewTypeReject
2020-04-06 22:03:34 +05:30
// ReviewTypeRequest request review from others
ReviewTypeRequest
2018-08-06 10:13:22 +05:30
)
// Icon returns the corresponding icon for the review type
func ( rt ReviewType ) Icon ( ) string {
switch rt {
case ReviewTypeApprove :
2020-04-03 10:42:42 +05:30
return "check"
2018-08-06 10:13:22 +05:30
case ReviewTypeReject :
2020-07-17 20:45:12 +05:30
return "diff"
2020-04-03 10:42:42 +05:30
case ReviewTypeComment :
2018-10-19 19:06:41 +05:30
return "comment"
2020-04-06 22:03:34 +05:30
case ReviewTypeRequest :
2020-07-17 20:45:12 +05:30
return "dot-fill"
2018-08-06 10:13:22 +05:30
default :
return "comment"
}
}
// Review represents collection of code comments giving feedback for a PR
type Review struct {
2020-01-23 22:58:15 +05:30
ID int64 ` xorm:"pk autoincr" `
Type ReviewType
2022-03-29 11:59:02 +05:30
Reviewer * user_model . User ` xorm:"-" `
ReviewerID int64 ` xorm:"index" `
ReviewerTeamID int64 ` xorm:"NOT NULL DEFAULT 0" `
ReviewerTeam * organization . Team ` xorm:"-" `
2020-01-23 22:58:15 +05:30
OriginalAuthor string
OriginalAuthorID int64
Issue * Issue ` xorm:"-" `
IssueID int64 ` xorm:"index" `
Content string ` xorm:"TEXT" `
2019-12-04 06:38:56 +05:30
// Official is a review made by an assigned approver (counts towards approval)
2021-02-11 23:02:25 +05:30
Official bool ` xorm:"NOT NULL DEFAULT false" `
CommitID string ` xorm:"VARCHAR(40)" `
Stale bool ` xorm:"NOT NULL DEFAULT false" `
Dismissed bool ` xorm:"NOT NULL DEFAULT false" `
2018-08-06 10:13:22 +05:30
2019-08-15 20:16:21 +05:30
CreatedUnix timeutil . TimeStamp ` xorm:"INDEX created" `
UpdatedUnix timeutil . TimeStamp ` xorm:"INDEX updated" `
2018-08-06 10:13:22 +05:30
// CodeComments are the initial code comments of the review
CodeComments CodeComments ` xorm:"-" `
2020-01-23 22:58:15 +05:30
Comments [ ] * Comment ` xorm:"-" `
2018-08-06 10:13:22 +05:30
}
2021-09-19 17:19:59 +05:30
func init ( ) {
db . RegisterModel ( new ( Review ) )
}
2022-01-20 04:56:57 +05:30
// LoadCodeComments loads CodeComments
func ( r * Review ) LoadCodeComments ( ctx context . Context ) ( err error ) {
2020-05-02 05:50:51 +05:30
if r . CodeComments != nil {
return
}
2021-12-10 06:57:50 +05:30
if err = r . loadIssue ( db . GetEngine ( ctx ) ) ; err != nil {
2020-05-02 05:50:51 +05:30
return
2019-11-15 18:29:21 +05:30
}
2021-12-10 06:57:50 +05:30
r . CodeComments , err = fetchCodeCommentsByReview ( ctx , r . Issue , nil , r )
2018-08-06 10:13:22 +05:30
return
}
2021-09-19 17:19:59 +05:30
func ( r * Review ) loadIssue ( e db . Engine ) ( err error ) {
2020-05-02 05:50:51 +05:30
if r . Issue != nil {
return
}
2018-08-06 10:13:22 +05:30
r . Issue , err = getIssueByID ( e , r . IssueID )
return
}
2021-09-19 17:19:59 +05:30
func ( r * Review ) loadReviewer ( e db . Engine ) ( err error ) {
2020-10-13 01:25:13 +05:30
if r . ReviewerID == 0 || r . Reviewer != nil {
return
2018-08-06 10:13:22 +05:30
}
2021-11-24 15:19:20 +05:30
r . Reviewer , err = user_model . GetUserByIDEngine ( e , r . ReviewerID )
2018-08-06 10:13:22 +05:30
return
}
2022-03-29 11:59:02 +05:30
func ( r * Review ) loadReviewerTeam ( ctx context . Context ) ( err error ) {
2020-10-13 01:25:13 +05:30
if r . ReviewerTeamID == 0 || r . ReviewerTeam != nil {
return
}
2022-03-29 11:59:02 +05:30
r . ReviewerTeam , err = organization . GetTeamByIDCtx ( ctx , r . ReviewerTeamID )
2020-10-13 01:25:13 +05:30
return
}
2019-11-14 08:27:36 +05:30
// LoadReviewer loads reviewer
func ( r * Review ) LoadReviewer ( ) error {
2021-09-23 21:15:36 +05:30
return r . loadReviewer ( db . GetEngine ( db . DefaultContext ) )
2019-11-14 08:27:36 +05:30
}
2020-10-13 01:25:13 +05:30
// LoadReviewerTeam loads reviewer team
func ( r * Review ) LoadReviewerTeam ( ) error {
2022-03-29 11:59:02 +05:30
return r . loadReviewerTeam ( db . DefaultContext )
2020-10-13 01:25:13 +05:30
}
2022-01-20 04:56:57 +05:30
// LoadAttributes loads all attributes except CodeComments
func ( r * Review ) LoadAttributes ( ctx context . Context ) ( err error ) {
2021-12-10 06:57:50 +05:30
e := db . GetEngine ( ctx )
2020-05-02 05:50:51 +05:30
if err = r . loadIssue ( e ) ; err != nil {
2018-08-06 10:13:22 +05:30
return
}
2022-01-20 04:56:57 +05:30
if err = r . LoadCodeComments ( ctx ) ; err != nil {
2020-05-02 05:50:51 +05:30
return
}
if err = r . loadReviewer ( e ) ; err != nil {
2018-08-06 10:13:22 +05:30
return
}
2022-03-29 11:59:02 +05:30
if err = r . loadReviewerTeam ( ctx ) ; err != nil {
2020-10-13 01:25:13 +05:30
return
}
2018-08-06 10:13:22 +05:30
return
}
2021-09-19 17:19:59 +05:30
func getReviewByID ( e db . Engine , id int64 ) ( * Review , error ) {
2018-08-06 10:13:22 +05:30
review := new ( Review )
if has , err := e . ID ( id ) . Get ( review ) ; err != nil {
return nil , err
} else if ! has {
return nil , ErrReviewNotExist { ID : id }
} else {
return review , nil
}
}
// GetReviewByID returns the review by the given ID
func GetReviewByID ( id int64 ) ( * Review , error ) {
2021-09-23 21:15:36 +05:30
return getReviewByID ( db . GetEngine ( db . DefaultContext ) , id )
2018-08-06 10:13:22 +05:30
}
// FindReviewOptions represent possible filters to find reviews
type FindReviewOptions struct {
2021-09-24 17:02:56 +05:30
db . ListOptions
2019-12-31 05:04:11 +05:30
Type ReviewType
IssueID int64
ReviewerID int64
OfficialOnly bool
2018-08-06 10:13:22 +05:30
}
func ( opts * FindReviewOptions ) toCond ( ) builder . Cond {
2021-03-15 00:22:12 +05:30
cond := builder . NewCond ( )
2018-08-06 10:13:22 +05:30
if opts . IssueID > 0 {
cond = cond . And ( builder . Eq { "issue_id" : opts . IssueID } )
}
if opts . ReviewerID > 0 {
cond = cond . And ( builder . Eq { "reviewer_id" : opts . ReviewerID } )
}
if opts . Type != ReviewTypeUnknown {
cond = cond . And ( builder . Eq { "type" : opts . Type } )
}
2019-12-31 05:04:11 +05:30
if opts . OfficialOnly {
cond = cond . And ( builder . Eq { "official" : true } )
}
2018-08-06 10:13:22 +05:30
return cond
}
2021-09-19 17:19:59 +05:30
func findReviews ( e db . Engine , opts FindReviewOptions ) ( [ ] * Review , error ) {
2018-08-06 10:13:22 +05:30
reviews := make ( [ ] * Review , 0 , 10 )
sess := e . Where ( opts . toCond ( ) )
2020-05-02 05:50:51 +05:30
if opts . Page > 0 {
2021-09-24 17:02:56 +05:30
sess = db . SetSessionPagination ( sess , & opts )
2020-05-02 05:50:51 +05:30
}
2018-08-06 10:13:22 +05:30
return reviews , sess .
Asc ( "created_unix" ) .
Asc ( "id" ) .
Find ( & reviews )
}
// FindReviews returns reviews passing FindReviewOptions
func FindReviews ( opts FindReviewOptions ) ( [ ] * Review , error ) {
2021-09-23 21:15:36 +05:30
return findReviews ( db . GetEngine ( db . DefaultContext ) , opts )
2018-08-06 10:13:22 +05:30
}
2021-08-12 18:13:08 +05:30
// CountReviews returns count of reviews passing FindReviewOptions
func CountReviews ( opts FindReviewOptions ) ( int64 , error ) {
2021-09-23 21:15:36 +05:30
return db . GetEngine ( db . DefaultContext ) . Where ( opts . toCond ( ) ) . Count ( & Review { } )
2021-08-12 18:13:08 +05:30
}
2018-08-06 10:13:22 +05:30
// CreateReviewOptions represent the options to create a review. Type, Issue and Reviewer are required.
type CreateReviewOptions struct {
2020-10-13 01:25:13 +05:30
Content string
Type ReviewType
Issue * Issue
2021-11-24 15:19:20 +05:30
Reviewer * user_model . User
2022-03-29 11:59:02 +05:30
ReviewerTeam * organization . Team
2020-10-13 01:25:13 +05:30
Official bool
CommitID string
Stale bool
2019-12-04 06:38:56 +05:30
}
2020-10-13 01:25:13 +05:30
// IsOfficialReviewer check if at least one of the provided reviewers can make official reviews in issue (counts towards required approvals)
2021-11-24 15:19:20 +05:30
func IsOfficialReviewer ( issue * Issue , reviewers ... * user_model . User ) ( bool , error ) {
2021-12-10 06:57:50 +05:30
return isOfficialReviewer ( db . DefaultContext , issue , reviewers ... )
2019-12-04 06:38:56 +05:30
}
2021-12-10 06:57:50 +05:30
func isOfficialReviewer ( ctx context . Context , issue * Issue , reviewers ... * user_model . User ) ( bool , error ) {
pr , err := getPullRequestByIssueID ( db . GetEngine ( ctx ) , issue . ID )
2019-12-04 06:38:56 +05:30
if err != nil {
return false , err
}
2021-12-10 06:57:50 +05:30
if err = pr . loadProtectedBranch ( ctx ) ; err != nil {
2019-12-04 06:38:56 +05:30
return false , err
}
if pr . ProtectedBranch == nil {
return false , nil
}
2020-10-13 01:25:13 +05:30
for _ , reviewer := range reviewers {
2021-12-10 06:57:50 +05:30
official , err := isUserOfficialReviewer ( ctx , pr . ProtectedBranch , reviewer )
2020-10-13 01:25:13 +05:30
if official || err != nil {
return official , err
}
}
return false , nil
}
// IsOfficialReviewerTeam check if reviewer in this team can make official reviews in issue (counts towards required approvals)
2022-03-29 11:59:02 +05:30
func IsOfficialReviewerTeam ( issue * Issue , team * organization . Team ) ( bool , error ) {
2021-12-10 06:57:50 +05:30
return isOfficialReviewerTeam ( db . DefaultContext , issue , team )
2020-10-13 01:25:13 +05:30
}
2022-03-29 11:59:02 +05:30
func isOfficialReviewerTeam ( ctx context . Context , issue * Issue , team * organization . Team ) ( bool , error ) {
2021-12-10 06:57:50 +05:30
pr , err := getPullRequestByIssueID ( db . GetEngine ( ctx ) , issue . ID )
2020-10-13 01:25:13 +05:30
if err != nil {
return false , err
}
2021-12-10 06:57:50 +05:30
if err = pr . loadProtectedBranch ( ctx ) ; err != nil {
2020-10-13 01:25:13 +05:30
return false , err
}
if pr . ProtectedBranch == nil {
return false , nil
}
if ! pr . ProtectedBranch . EnableApprovalsWhitelist {
2022-04-01 11:35:31 +05:30
return team . UnitAccessModeCtx ( ctx , unit . TypeCode ) >= perm . AccessModeWrite , nil
2020-10-13 01:25:13 +05:30
}
return base . Int64sContains ( pr . ProtectedBranch . ApprovalsWhitelistTeamIDs , team . ID ) , nil
2018-08-06 10:13:22 +05:30
}
2021-09-19 17:19:59 +05:30
func createReview ( e db . Engine , opts CreateReviewOptions ) ( * Review , error ) {
2018-08-06 10:13:22 +05:30
review := & Review {
2020-10-13 01:25:13 +05:30
Type : opts . Type ,
Issue : opts . Issue ,
IssueID : opts . Issue . ID ,
Reviewer : opts . Reviewer ,
ReviewerTeam : opts . ReviewerTeam ,
Content : opts . Content ,
Official : opts . Official ,
CommitID : opts . CommitID ,
Stale : opts . Stale ,
}
if opts . Reviewer != nil {
review . ReviewerID = opts . Reviewer . ID
} else {
if review . Type != ReviewTypeRequest {
review . Type = ReviewTypeRequest
}
review . ReviewerTeamID = opts . ReviewerTeam . ID
2018-08-06 10:13:22 +05:30
}
if _ , err := e . Insert ( review ) ; err != nil {
return nil , err
}
2018-12-27 23:34:30 +05:30
2018-08-06 10:13:22 +05:30
return review , nil
}
// CreateReview creates a new review based on opts
func CreateReview ( opts CreateReviewOptions ) ( * Review , error ) {
2021-09-23 21:15:36 +05:30
return createReview ( db . GetEngine ( db . DefaultContext ) , opts )
2018-08-06 10:13:22 +05:30
}
2021-11-24 15:19:20 +05:30
func getCurrentReview ( e db . Engine , reviewer * user_model . User , issue * Issue ) ( * Review , error ) {
2018-08-06 10:13:22 +05:30
if reviewer == nil {
return nil , nil
}
reviews , err := findReviews ( e , FindReviewOptions {
Type : ReviewTypePending ,
IssueID : issue . ID ,
ReviewerID : reviewer . ID ,
} )
if err != nil {
return nil , err
}
if len ( reviews ) == 0 {
return nil , ErrReviewNotExist { }
}
2018-10-18 16:53:05 +05:30
reviews [ 0 ] . Reviewer = reviewer
reviews [ 0 ] . Issue = issue
2018-08-06 10:13:22 +05:30
return reviews [ 0 ] , nil
}
2019-11-24 11:16:16 +05:30
// ReviewExists returns whether a review exists for a particular line of code in the PR
func ReviewExists ( issue * Issue , treePath string , line int64 ) ( bool , error ) {
2021-09-23 21:15:36 +05:30
return db . GetEngine ( db . DefaultContext ) . Cols ( "id" ) . Exist ( & Comment { IssueID : issue . ID , TreePath : treePath , Line : line , Type : CommentTypeCode } )
2019-11-24 11:16:16 +05:30
}
2018-08-06 10:13:22 +05:30
// GetCurrentReview returns the current pending review of reviewer for given issue
2021-11-24 15:19:20 +05:30
func GetCurrentReview ( reviewer * user_model . User , issue * Issue ) ( * Review , error ) {
2021-09-23 21:15:36 +05:30
return getCurrentReview ( db . GetEngine ( db . DefaultContext ) , reviewer , issue )
2018-08-06 10:13:22 +05:30
}
2019-11-14 08:27:36 +05:30
// ContentEmptyErr represents an content empty error
2021-03-15 00:22:12 +05:30
type ContentEmptyErr struct { }
2019-11-14 08:27:36 +05:30
func ( ContentEmptyErr ) Error ( ) string {
return "Review content is empty"
}
// IsContentEmptyErr returns true if err is a ContentEmptyErr
func IsContentEmptyErr ( err error ) bool {
_ , ok := err . ( ContentEmptyErr )
return ok
}
// SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
2021-11-24 15:19:20 +05:30
func SubmitReview ( doer * user_model . User , issue * Issue , reviewType ReviewType , content , commitID string , stale bool , attachmentUUIDs [ ] string ) ( * Review , * Comment , error ) {
2021-11-19 19:09:57 +05:30
ctx , committer , err := db . TxContext ( )
if err != nil {
2019-11-14 08:27:36 +05:30
return nil , nil , err
}
2021-11-19 19:09:57 +05:30
defer committer . Close ( )
sess := db . GetEngine ( ctx )
2019-11-14 08:27:36 +05:30
2021-03-15 00:22:12 +05:30
official := false
2019-12-04 06:38:56 +05:30
2019-11-14 08:27:36 +05:30
review , err := getCurrentReview ( sess , doer , issue )
if err != nil {
if ! IsErrReviewNotExist ( err ) {
return nil , nil , err
}
2019-11-15 02:28:01 +05:30
if reviewType != ReviewTypeApprove && len ( strings . TrimSpace ( content ) ) == 0 {
2019-11-14 08:27:36 +05:30
return nil , nil , ContentEmptyErr { }
}
2019-12-04 06:38:56 +05:30
if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject {
// Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared
if _ , err := sess . Exec ( "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?" , false , issue . ID , doer . ID ) ; err != nil {
return nil , nil , err
}
2021-12-10 06:57:50 +05:30
if official , err = isOfficialReviewer ( ctx , issue , doer ) ; err != nil {
2019-12-04 06:38:56 +05:30
return nil , nil , err
}
}
2019-11-14 08:27:36 +05:30
// No current review. Create a new one!
2020-10-13 01:25:13 +05:30
if review , err = createReview ( sess , CreateReviewOptions {
2019-11-14 08:27:36 +05:30
Type : reviewType ,
Issue : issue ,
Reviewer : doer ,
Content : content ,
2019-12-04 06:38:56 +05:30
Official : official ,
2020-01-09 07:17:45 +05:30
CommitID : commitID ,
Stale : stale ,
2020-10-13 01:25:13 +05:30
} ) ; err != nil {
2019-11-14 08:27:36 +05:30
return nil , nil , err
}
} else {
2022-01-20 04:56:57 +05:30
if err := review . LoadCodeComments ( ctx ) ; err != nil {
2019-11-14 08:27:36 +05:30
return nil , nil , err
}
2019-11-15 02:28:01 +05:30
if reviewType != ReviewTypeApprove && len ( review . CodeComments ) == 0 && len ( strings . TrimSpace ( content ) ) == 0 {
2019-11-14 08:27:36 +05:30
return nil , nil , ContentEmptyErr { }
}
2019-12-04 06:38:56 +05:30
if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject {
// Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared
if _ , err := sess . Exec ( "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?" , false , issue . ID , doer . ID ) ; err != nil {
return nil , nil , err
}
2021-12-10 06:57:50 +05:30
if official , err = isOfficialReviewer ( ctx , issue , doer ) ; err != nil {
2019-12-04 06:38:56 +05:30
return nil , nil , err
}
}
review . Official = official
2019-11-14 08:27:36 +05:30
review . Issue = issue
review . Content = content
review . Type = reviewType
2020-01-09 07:17:45 +05:30
review . CommitID = commitID
review . Stale = stale
2019-12-04 06:38:56 +05:30
2020-01-09 07:17:45 +05:30
if _ , err := sess . ID ( review . ID ) . Cols ( "content, type, official, commit_id, stale" ) . Update ( review ) ; err != nil {
2019-11-14 08:27:36 +05:30
return nil , nil , err
}
2018-08-06 10:13:22 +05:30
}
2019-11-14 08:27:36 +05:30
2021-11-19 19:09:57 +05:30
comm , err := createComment ( ctx , & CreateCommentOptions {
2021-06-15 06:42:33 +05:30
Type : CommentTypeReview ,
Doer : doer ,
Content : review . Content ,
Issue : issue ,
Repo : issue . Repo ,
ReviewID : review . ID ,
Attachments : attachmentUUIDs ,
2019-11-14 08:27:36 +05:30
} )
if err != nil || comm == nil {
return nil , nil , err
}
2020-10-13 01:25:13 +05:30
// try to remove team review request if need
if issue . Repo . Owner . IsOrganization ( ) && ( reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject ) {
teamReviewRequests := make ( [ ] * Review , 0 , 10 )
2021-10-08 02:09:59 +05:30
if err := sess . SQL ( "SELECT * FROM review WHERE issue_id = ? AND reviewer_team_id > 0 AND type = ?" , issue . ID , ReviewTypeRequest ) . Find ( & teamReviewRequests ) ; err != nil {
2020-10-13 01:25:13 +05:30
return nil , nil , err
}
for _ , teamReviewRequest := range teamReviewRequests {
2022-03-29 11:59:02 +05:30
ok , err := organization . IsTeamMember ( ctx , issue . Repo . OwnerID , teamReviewRequest . ReviewerTeamID , doer . ID )
2020-10-13 01:25:13 +05:30
if err != nil {
return nil , nil , err
} else if ! ok {
continue
}
if _ , err := sess . Delete ( teamReviewRequest ) ; err != nil {
return nil , nil , err
}
}
}
2019-11-14 08:27:36 +05:30
comm . Review = review
2021-11-19 19:09:57 +05:30
return review , comm , committer . Commit ( )
2018-08-06 10:13:22 +05:30
}
2018-11-22 18:47:36 +05:30
2019-12-04 06:38:56 +05:30
// GetReviewersByIssueID gets the latest review of each reviewer for a pull request
2020-10-13 01:25:13 +05:30
func GetReviewersByIssueID ( issueID int64 ) ( [ ] * Review , error ) {
reviews := make ( [ ] * Review , 0 , 10 )
2018-11-22 18:47:36 +05:30
2021-11-21 21:11:00 +05:30
sess := db . GetEngine ( db . DefaultContext )
2018-11-22 18:47:36 +05:30
2021-07-08 17:08:13 +05:30
// Get latest review of each reviewer, sorted in order they were made
2021-02-11 23:02:25 +05:30
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" ,
issueID , ReviewTypeApprove , ReviewTypeReject , ReviewTypeRequest , false ) .
2020-10-13 01:25:13 +05:30
Find ( & reviews ) ; err != nil {
2019-12-04 06:38:56 +05:30
return nil , err
}
2020-10-13 01:25:13 +05:30
teamReviewRequests := make ( [ ] * Review , 0 , 5 )
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" ,
issueID ) .
Find ( & teamReviewRequests ) ; err != nil {
return nil , err
}
if len ( teamReviewRequests ) > 0 {
reviews = append ( reviews , teamReviewRequests ... )
2018-11-22 18:47:36 +05:30
}
2019-12-04 06:38:56 +05:30
return reviews , nil
2018-11-22 18:47:36 +05:30
}
2020-01-09 07:17:45 +05:30
2020-10-14 17:41:11 +05:30
// GetReviewersFromOriginalAuthorsByIssueID gets the latest review of each original authors for a pull request
func GetReviewersFromOriginalAuthorsByIssueID ( issueID int64 ) ( [ ] * Review , error ) {
reviews := make ( [ ] * Review , 0 , 10 )
2021-07-08 17:08:13 +05:30
// Get latest review of each reviewer, sorted in order they were made
2021-09-23 21:15:36 +05:30
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" ,
2020-10-14 17:41:11 +05:30
issueID , ReviewTypeApprove , ReviewTypeReject , ReviewTypeRequest ) .
Find ( & reviews ) ; err != nil {
return nil , err
}
return reviews , nil
}
2020-10-13 01:25:13 +05:30
// GetReviewByIssueIDAndUserID get the latest review of reviewer for a pull request
func GetReviewByIssueIDAndUserID ( issueID , userID int64 ) ( * Review , error ) {
2021-09-23 21:15:36 +05:30
return getReviewByIssueIDAndUserID ( db . GetEngine ( db . DefaultContext ) , issueID , userID )
2020-04-11 10:14:50 +05:30
}
2021-09-19 17:19:59 +05:30
func getReviewByIssueIDAndUserID ( e db . Engine , issueID , userID int64 ) ( * Review , error ) {
2020-10-13 01:25:13 +05:30
review := new ( Review )
2020-04-06 22:03:34 +05:30
2020-10-13 01:25:13 +05:30
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 (?, ?, ?))" ,
2020-04-06 22:03:34 +05:30
issueID , userID , ReviewTypeApprove , ReviewTypeReject , ReviewTypeRequest ) .
2020-10-13 01:25:13 +05:30
Get ( review )
if err != nil {
return nil , err
}
if ! has {
return nil , ErrReviewNotExist { }
}
return review , nil
}
2022-01-10 15:02:37 +05:30
// GetTeamReviewerByIssueIDAndTeamID get the latest review request of reviewer team for a pull request
2020-10-13 01:25:13 +05:30
func GetTeamReviewerByIssueIDAndTeamID ( issueID , teamID int64 ) ( review * Review , err error ) {
2021-09-23 21:15:36 +05:30
return getTeamReviewerByIssueIDAndTeamID ( db . GetEngine ( db . DefaultContext ) , issueID , teamID )
2020-10-13 01:25:13 +05:30
}
2021-09-19 17:19:59 +05:30
func getTeamReviewerByIssueIDAndTeamID ( e db . Engine , issueID , teamID int64 ) ( review * Review , err error ) {
2020-10-13 01:25:13 +05:30
review = new ( Review )
has := false
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 = ?)" ,
issueID , teamID ) .
2020-04-06 22:03:34 +05:30
Get ( review ) ; err != nil {
return nil , err
}
2020-10-13 01:25:13 +05:30
if ! has {
return nil , ErrReviewNotExist { 0 }
}
2020-04-06 22:03:34 +05:30
return
}
2020-01-09 07:17:45 +05:30
// MarkReviewsAsStale marks existing reviews as stale
func MarkReviewsAsStale ( issueID int64 ) ( err error ) {
2021-09-23 21:15:36 +05:30
_ , err = db . GetEngine ( db . DefaultContext ) . Exec ( "UPDATE `review` SET stale=? WHERE issue_id=?" , true , issueID )
2020-01-09 07:17:45 +05:30
return
}
// MarkReviewsAsNotStale marks existing reviews as not stale for a giving commit SHA
func MarkReviewsAsNotStale ( issueID int64 , commitID string ) ( err error ) {
2021-09-23 21:15:36 +05:30
_ , err = db . GetEngine ( db . DefaultContext ) . Exec ( "UPDATE `review` SET stale=? WHERE issue_id=? AND commit_id=?" , false , issueID , commitID )
2020-01-09 07:17:45 +05:30
return
}
2020-01-23 22:58:15 +05:30
2021-02-11 23:02:25 +05:30
// DismissReview change the dismiss status of a review
func DismissReview ( review * Review , isDismiss bool ) ( err error ) {
if review . Dismissed == isDismiss || ( review . Type != ReviewTypeApprove && review . Type != ReviewTypeReject ) {
return nil
}
review . Dismissed = isDismiss
2021-04-15 15:33:11 +05:30
if review . ID == 0 {
return ErrReviewNotExist { }
}
2021-09-23 21:15:36 +05:30
_ , err = db . GetEngine ( db . DefaultContext ) . ID ( review . ID ) . Cols ( "dismissed" ) . Update ( review )
2021-02-11 23:02:25 +05:30
return
}
2020-01-23 22:58:15 +05:30
// InsertReviews inserts review and review comments
func InsertReviews ( reviews [ ] * Review ) error {
2021-11-21 21:11:00 +05:30
ctx , committer , err := db . TxContext ( )
if err != nil {
2020-01-23 22:58:15 +05:30
return err
}
2021-11-21 21:11:00 +05:30
defer committer . Close ( )
sess := db . GetEngine ( ctx )
2020-01-23 22:58:15 +05:30
for _ , review := range reviews {
if _ , err := sess . NoAutoTime ( ) . Insert ( review ) ; err != nil {
return err
}
if _ , err := sess . NoAutoTime ( ) . Insert ( & Comment {
Type : CommentTypeReview ,
Content : review . Content ,
PosterID : review . ReviewerID ,
OriginalAuthor : review . OriginalAuthor ,
OriginalAuthorID : review . OriginalAuthorID ,
IssueID : review . IssueID ,
ReviewID : review . ID ,
CreatedUnix : review . CreatedUnix ,
UpdatedUnix : review . UpdatedUnix ,
} ) ; err != nil {
return err
}
for _ , c := range review . Comments {
c . ReviewID = review . ID
}
2020-04-20 08:34:08 +05:30
if len ( review . Comments ) > 0 {
if _ , err := sess . NoAutoTime ( ) . Insert ( review . Comments ) ; err != nil {
return err
}
2020-01-23 22:58:15 +05:30
}
}
2021-11-21 21:11:00 +05:30
return committer . Commit ( )
2020-01-23 22:58:15 +05:30
}
2020-04-06 22:03:34 +05:30
2020-05-01 01:54:08 +05:30
// AddReviewRequest add a review request from one reviewer
2021-11-24 15:19:20 +05:30
func AddReviewRequest ( issue * Issue , reviewer , doer * user_model . User ) ( * Comment , error ) {
2021-11-19 19:09:57 +05:30
ctx , committer , err := db . TxContext ( )
if err != nil {
2020-04-06 22:03:34 +05:30
return nil , err
}
2021-11-19 19:09:57 +05:30
defer committer . Close ( )
sess := db . GetEngine ( ctx )
2020-04-06 22:03:34 +05:30
2020-10-13 01:25:13 +05:30
review , err := getReviewByIssueIDAndUserID ( sess , issue . ID , reviewer . ID )
if err != nil && ! IsErrReviewNotExist ( err ) {
2020-04-06 22:03:34 +05:30
return nil , err
}
2020-10-13 01:25:13 +05:30
// skip it when reviewer hase been request to review
if review != nil && review . Type == ReviewTypeRequest {
return nil , nil
2020-04-06 22:03:34 +05:30
}
2021-12-10 06:57:50 +05:30
official , err := isOfficialReviewer ( ctx , issue , reviewer , doer )
2020-10-13 01:25:13 +05:30
if err != nil {
return nil , err
} else if official {
2020-04-06 22:03:34 +05:30
if _ , err := sess . Exec ( "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?" , false , issue . ID , reviewer . ID ) ; err != nil {
return nil , err
}
}
2020-10-20 23:48:25 +05:30
review , err = createReview ( sess , CreateReviewOptions {
2020-04-06 22:03:34 +05:30
Type : ReviewTypeRequest ,
Issue : issue ,
Reviewer : reviewer ,
Official : official ,
Stale : false ,
2020-10-20 23:48:25 +05:30
} )
if err != nil {
2020-10-13 01:25:13 +05:30
return nil , err
2020-04-06 22:03:34 +05:30
}
2021-11-19 19:09:57 +05:30
comment , err := createComment ( ctx , & CreateCommentOptions {
2020-04-06 22:03:34 +05:30
Type : CommentTypeReviewRequest ,
Doer : doer ,
Repo : issue . Repo ,
Issue : issue ,
RemovedAssignee : false , // Use RemovedAssignee as !isRequest
AssigneeID : reviewer . ID , // Use AssigneeID as reviewer ID
2020-10-20 23:48:25 +05:30
ReviewID : review . ID ,
2020-04-06 22:03:34 +05:30
} )
if err != nil {
return nil , err
}
2021-11-19 19:09:57 +05:30
return comment , committer . Commit ( )
2020-04-06 22:03:34 +05:30
}
2021-03-15 00:22:12 +05:30
// RemoveReviewRequest remove a review request from one reviewer
2021-11-24 15:19:20 +05:30
func RemoveReviewRequest ( issue * Issue , reviewer , doer * user_model . User ) ( * Comment , error ) {
2021-11-19 19:09:57 +05:30
ctx , committer , err := db . TxContext ( )
if err != nil {
2020-10-13 01:25:13 +05:30
return nil , err
}
2021-11-19 19:09:57 +05:30
defer committer . Close ( )
sess := db . GetEngine ( ctx )
2020-10-13 01:25:13 +05:30
review , err := getReviewByIssueIDAndUserID ( sess , issue . ID , reviewer . ID )
if err != nil && ! IsErrReviewNotExist ( err ) {
return nil , err
2020-04-06 22:03:34 +05:30
}
2020-10-13 01:25:13 +05:30
if review == nil || review . Type != ReviewTypeRequest {
2020-04-06 22:03:34 +05:30
return nil , nil
}
2020-10-13 01:25:13 +05:30
if _ , err = sess . Delete ( review ) ; err != nil {
return nil , err
}
2021-12-10 06:57:50 +05:30
official , err := isOfficialReviewer ( ctx , issue , reviewer )
2020-10-13 01:25:13 +05:30
if err != nil {
return nil , err
} else if official {
// recalculate the latest official review for reviewer
review , err := getReviewByIssueIDAndUserID ( sess , issue . ID , reviewer . ID )
if err != nil && ! IsErrReviewNotExist ( err ) {
return nil , err
}
if review != nil {
if _ , err := sess . Exec ( "UPDATE `review` SET official=? WHERE id=?" , true , review . ID ) ; err != nil {
return nil , err
}
}
}
2021-11-19 19:09:57 +05:30
comment , err := createComment ( ctx , & CreateCommentOptions {
2020-10-13 01:25:13 +05:30
Type : CommentTypeReviewRequest ,
Doer : doer ,
Repo : issue . Repo ,
Issue : issue ,
RemovedAssignee : true , // Use RemovedAssignee as !isRequest
AssigneeID : reviewer . ID , // Use AssigneeID as reviewer ID
} )
if err != nil {
return nil , err
}
2021-11-19 19:09:57 +05:30
return comment , committer . Commit ( )
2020-10-13 01:25:13 +05:30
}
// AddTeamReviewRequest add a review request from one team
2022-03-29 11:59:02 +05:30
func AddTeamReviewRequest ( issue * Issue , reviewer * organization . Team , doer * user_model . User ) ( * Comment , error ) {
2021-11-19 19:09:57 +05:30
ctx , committer , err := db . TxContext ( )
if err != nil {
2020-04-06 22:03:34 +05:30
return nil , err
}
2021-11-19 19:09:57 +05:30
defer committer . Close ( )
sess := db . GetEngine ( ctx )
2020-04-06 22:03:34 +05:30
2020-10-13 01:25:13 +05:30
review , err := getTeamReviewerByIssueIDAndTeamID ( sess , issue . ID , reviewer . ID )
if err != nil && ! IsErrReviewNotExist ( err ) {
2020-04-06 22:03:34 +05:30
return nil , err
}
2020-10-13 01:25:13 +05:30
// This team already has been requested to review - therefore skip this.
if review != nil {
return nil , nil
}
2021-12-10 06:57:50 +05:30
official , err := isOfficialReviewerTeam ( ctx , issue , reviewer )
2020-04-06 22:03:34 +05:30
if err != nil {
2020-10-13 01:25:13 +05:30
return nil , fmt . Errorf ( "isOfficialReviewerTeam(): %v" , err )
} else if ! official {
2021-12-10 06:57:50 +05:30
if official , err = isOfficialReviewer ( ctx , issue , doer ) ; err != nil {
2020-10-13 01:25:13 +05:30
return nil , fmt . Errorf ( "isOfficialReviewer(): %v" , err )
}
}
2020-10-20 23:48:25 +05:30
if review , err = createReview ( sess , CreateReviewOptions {
2020-10-13 01:25:13 +05:30
Type : ReviewTypeRequest ,
Issue : issue ,
ReviewerTeam : reviewer ,
Official : official ,
Stale : false ,
} ) ; err != nil {
return nil , err
2020-04-06 22:03:34 +05:30
}
if official {
2020-10-13 01:25:13 +05:30
if _ , err := sess . Exec ( "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_team_id=?" , false , issue . ID , reviewer . ID ) ; err != nil {
return nil , err
}
}
2021-11-19 19:09:57 +05:30
comment , err := createComment ( ctx , & CreateCommentOptions {
2020-10-13 01:25:13 +05:30
Type : CommentTypeReviewRequest ,
Doer : doer ,
Repo : issue . Repo ,
Issue : issue ,
RemovedAssignee : false , // Use RemovedAssignee as !isRequest
AssigneeTeamID : reviewer . ID , // Use AssigneeTeamID as reviewer team ID
2020-10-20 23:48:25 +05:30
ReviewID : review . ID ,
2020-10-13 01:25:13 +05:30
} )
if err != nil {
return nil , fmt . Errorf ( "createComment(): %v" , err )
}
2021-11-19 19:09:57 +05:30
return comment , committer . Commit ( )
2020-10-13 01:25:13 +05:30
}
2021-03-15 00:22:12 +05:30
// RemoveTeamReviewRequest remove a review request from one team
2022-03-29 11:59:02 +05:30
func RemoveTeamReviewRequest ( issue * Issue , reviewer * organization . Team , doer * user_model . User ) ( * Comment , error ) {
2021-11-19 19:09:57 +05:30
ctx , committer , err := db . TxContext ( )
if err != nil {
2020-10-13 01:25:13 +05:30
return nil , err
}
2021-11-19 19:09:57 +05:30
defer committer . Close ( )
sess := db . GetEngine ( ctx )
2020-10-13 01:25:13 +05:30
review , err := getTeamReviewerByIssueIDAndTeamID ( sess , issue . ID , reviewer . ID )
if err != nil && ! IsErrReviewNotExist ( err ) {
return nil , err
}
if review == nil {
return nil , nil
}
2020-04-06 22:03:34 +05:30
2020-10-13 01:25:13 +05:30
if _ , err = sess . Delete ( review ) ; err != nil {
return nil , err
}
2021-12-10 06:57:50 +05:30
official , err := isOfficialReviewerTeam ( ctx , issue , reviewer )
2020-10-13 01:25:13 +05:30
if err != nil {
return nil , fmt . Errorf ( "isOfficialReviewerTeam(): %v" , err )
}
if official {
// recalculate which is the latest official review from that team
review , err := getReviewByIssueIDAndUserID ( sess , issue . ID , - reviewer . ID )
if err != nil && ! IsErrReviewNotExist ( err ) {
2020-04-06 22:03:34 +05:30
return nil , err
}
if review != nil {
if _ , err := sess . Exec ( "UPDATE `review` SET official=? WHERE id=?" , true , review . ID ) ; err != nil {
return nil , err
}
}
}
2020-10-13 01:25:13 +05:30
if doer == nil {
2021-11-19 19:09:57 +05:30
return nil , committer . Commit ( )
2020-04-06 22:03:34 +05:30
}
2021-11-19 19:09:57 +05:30
comment , err := createComment ( ctx , & CreateCommentOptions {
2020-04-06 22:03:34 +05:30
Type : CommentTypeReviewRequest ,
Doer : doer ,
Repo : issue . Repo ,
Issue : issue ,
RemovedAssignee : true , // Use RemovedAssignee as !isRequest
2020-10-13 01:25:13 +05:30
AssigneeTeamID : reviewer . ID , // Use AssigneeTeamID as reviewer team ID
2020-04-06 22:03:34 +05:30
} )
if err != nil {
2020-10-13 01:25:13 +05:30
return nil , fmt . Errorf ( "createComment(): %v" , err )
2020-04-06 22:03:34 +05:30
}
2021-11-19 19:09:57 +05:30
return comment , committer . Commit ( )
2020-04-06 22:03:34 +05:30
}
2020-04-18 19:20:25 +05:30
// MarkConversation Add or remove Conversation mark for a code comment
2021-11-24 15:19:20 +05:30
func MarkConversation ( comment * Comment , doer * user_model . User , isResolve bool ) ( err error ) {
2020-04-18 19:20:25 +05:30
if comment . Type != CommentTypeCode {
return nil
}
if isResolve {
if comment . ResolveDoerID != 0 {
return nil
}
2021-09-23 21:15:36 +05:30
if _ , err = db . GetEngine ( db . DefaultContext ) . Exec ( "UPDATE `comment` SET resolve_doer_id=? WHERE id=?" , doer . ID , comment . ID ) ; err != nil {
2020-04-18 19:20:25 +05:30
return err
}
} else {
if comment . ResolveDoerID == 0 {
return nil
}
2021-09-23 21:15:36 +05:30
if _ , err = db . GetEngine ( db . DefaultContext ) . Exec ( "UPDATE `comment` SET resolve_doer_id=? WHERE id=?" , 0 , comment . ID ) ; err != nil {
2020-04-18 19:20:25 +05:30
return err
}
}
return nil
}
// CanMarkConversation Add or remove Conversation mark for a code comment permission check
// the PR writer , offfcial reviewer and poster can do it
2021-11-24 15:19:20 +05:30
func CanMarkConversation ( issue * Issue , doer * user_model . User ) ( permResult bool , err error ) {
2020-04-18 19:20:25 +05:30
if doer == nil || issue == nil {
return false , fmt . Errorf ( "issue or doer is nil" )
}
if doer . ID != issue . PosterID {
if err = issue . LoadRepo ( ) ; err != nil {
return false , err
}
2021-11-28 17:28:28 +05:30
p , err := GetUserRepoPermission ( issue . Repo , doer )
2020-04-18 19:20:25 +05:30
if err != nil {
return false , err
}
2021-11-28 17:28:28 +05:30
permResult = p . CanAccess ( perm . AccessModeWrite , unit . TypePullRequests )
2020-04-18 19:20:25 +05:30
if ! permResult {
if permResult , err = IsOfficialReviewer ( issue , doer ) ; err != nil {
return false , err
}
}
if ! permResult {
return false , nil
}
}
return true , nil
}
2020-05-02 05:50:51 +05:30
// DeleteReview delete a review and it's code comments
func DeleteReview ( r * Review ) error {
2021-11-21 21:11:00 +05:30
ctx , committer , err := db . TxContext ( )
if err != nil {
2020-05-02 05:50:51 +05:30
return err
}
2021-11-21 21:11:00 +05:30
defer committer . Close ( )
sess := db . GetEngine ( ctx )
2020-05-02 05:50:51 +05:30
if r . ID == 0 {
return fmt . Errorf ( "review is not allowed to be 0" )
}
2020-10-20 23:48:25 +05:30
if r . Type == ReviewTypeRequest {
return fmt . Errorf ( "review request can not be deleted using this method" )
}
2020-05-02 05:50:51 +05:30
opts := FindCommentsOptions {
Type : CommentTypeCode ,
IssueID : r . IssueID ,
ReviewID : r . ID ,
}
if _ , err := sess . Where ( opts . toConds ( ) ) . Delete ( new ( Comment ) ) ; err != nil {
return err
}
opts = FindCommentsOptions {
Type : CommentTypeReview ,
IssueID : r . IssueID ,
ReviewID : r . ID ,
}
if _ , err := sess . Where ( opts . toConds ( ) ) . Delete ( new ( Comment ) ) ; err != nil {
return err
}
if _ , err := sess . ID ( r . ID ) . Delete ( new ( Review ) ) ; err != nil {
return err
}
2021-11-21 21:11:00 +05:30
return committer . Commit ( )
2020-05-02 05:50:51 +05:30
}
// GetCodeCommentsCount return count of CodeComments a Review has
func ( r * Review ) GetCodeCommentsCount ( ) int {
opts := FindCommentsOptions {
Type : CommentTypeCode ,
IssueID : r . IssueID ,
ReviewID : r . ID ,
}
conds := opts . toConds ( )
if r . ID == 0 {
conds = conds . And ( builder . Eq { "invalidated" : false } )
}
2021-09-23 21:15:36 +05:30
count , err := db . GetEngine ( db . DefaultContext ) . Where ( conds ) . Count ( new ( Comment ) )
2020-05-02 05:50:51 +05:30
if err != nil {
return 0
}
return int ( count )
}
// HTMLURL formats a URL-string to the related review issue-comment
func ( r * Review ) HTMLURL ( ) string {
opts := FindCommentsOptions {
Type : CommentTypeReview ,
IssueID : r . IssueID ,
ReviewID : r . ID ,
}
comment := new ( Comment )
2021-09-23 21:15:36 +05:30
has , err := db . GetEngine ( db . DefaultContext ) . Where ( opts . toConds ( ) ) . Get ( comment )
2020-05-02 05:50:51 +05:30
if err != nil || ! has {
return ""
}
return comment . HTMLURL ( )
}
2022-02-01 23:50:28 +05:30
// RemapExternalUser ExternalUserRemappable interface
func ( r * Review ) RemapExternalUser ( externalName string , externalID , userID int64 ) error {
r . OriginalAuthor = externalName
r . OriginalAuthorID = externalID
r . ReviewerID = userID
return nil
}
// GetUserID ExternalUserRemappable interface
func ( r * Review ) GetUserID ( ) int64 { return r . ReviewerID }
// GetExternalName ExternalUserRemappable interface
func ( r * Review ) GetExternalName ( ) string { return r . OriginalAuthor }
// GetExternalID ExternalUserRemappable interface
func ( r * Review ) GetExternalID ( ) int64 { return r . OriginalAuthorID }