@ -8,6 +8,7 @@ import (
"fmt"
"fmt"
"strings"
"strings"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/builder"
"xorm.io/builder"
@ -54,6 +55,8 @@ type Review struct {
Type ReviewType
Type ReviewType
Reviewer * User ` xorm:"-" `
Reviewer * User ` xorm:"-" `
ReviewerID int64 ` xorm:"index" `
ReviewerID int64 ` xorm:"index" `
ReviewerTeamID int64 ` xorm:"NOT NULL DEFAULT 0" `
ReviewerTeam * Team ` xorm:"-" `
OriginalAuthor string
OriginalAuthor string
OriginalAuthorID int64
OriginalAuthorID int64
Issue * Issue ` xorm:"-" `
Issue * Issue ` xorm:"-" `
@ -98,18 +101,32 @@ func (r *Review) loadIssue(e Engine) (err error) {
}
}
func ( r * Review ) loadReviewer ( e Engine ) ( err error ) {
func ( r * Review ) loadReviewer ( e Engine ) ( err error ) {
if r . Reviewer != nil || r . ReviewerID == 0 {
if r . ReviewerID == 0 || r . Reviewer != nil {
return nil
return
}
}
r . Reviewer , err = getUserByID ( e , r . ReviewerID )
r . Reviewer , err = getUserByID ( e , r . ReviewerID )
return
return
}
}
func ( r * Review ) loadReviewerTeam ( e Engine ) ( err error ) {
if r . ReviewerTeamID == 0 || r . ReviewerTeam != nil {
return
}
r . ReviewerTeam , err = getTeamByID ( e , r . ReviewerTeamID )
return
}
// LoadReviewer loads reviewer
// LoadReviewer loads reviewer
func ( r * Review ) LoadReviewer ( ) error {
func ( r * Review ) LoadReviewer ( ) error {
return r . loadReviewer ( x )
return r . loadReviewer ( x )
}
}
// LoadReviewerTeam loads reviewer team
func ( r * Review ) LoadReviewerTeam ( ) error {
return r . loadReviewerTeam ( x )
}
func ( r * Review ) loadAttributes ( e Engine ) ( err error ) {
func ( r * Review ) loadAttributes ( e Engine ) ( err error ) {
if err = r . loadIssue ( e ) ; err != nil {
if err = r . loadIssue ( e ) ; err != nil {
return
return
@ -120,6 +137,9 @@ func (r *Review) loadAttributes(e Engine) (err error) {
if err = r . loadReviewer ( e ) ; err != nil {
if err = r . loadReviewer ( e ) ; err != nil {
return
return
}
}
if err = r . loadReviewerTeam ( e ) ; err != nil {
return
}
return
return
}
}
@ -193,17 +213,45 @@ type CreateReviewOptions struct {
Type ReviewType
Type ReviewType
Issue * Issue
Issue * Issue
Reviewer * User
Reviewer * User
ReviewerTeam * Team
Official bool
Official bool
CommitID string
CommitID string
Stale bool
Stale bool
}
}
// IsOfficialReviewer check if reviewer can make official reviews in issue (counts towards required approvals)
// IsOfficialReviewer check if at least one of the provided reviewers can make official reviews in issue (counts towards required approvals)
func IsOfficialReviewer ( issue * Issue , reviewer * User ) ( bool , error ) {
func IsOfficialReviewer ( issue * Issue , reviewers ... * User ) ( bool , error ) {
return isOfficialReviewer ( x , issue , reviewer )
return isOfficialReviewer ( x , issue , reviewers ... )
}
func isOfficialReviewer ( e Engine , issue * Issue , reviewers ... * User ) ( bool , error ) {
pr , err := getPullRequestByIssueID ( e , issue . ID )
if err != nil {
return false , err
}
if err = pr . loadProtectedBranch ( e ) ; err != nil {
return false , err
}
if pr . ProtectedBranch == nil {
return false , nil
}
for _ , reviewer := range reviewers {
official , err := pr . ProtectedBranch . isUserOfficialReviewer ( e , reviewer )
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)
func IsOfficialReviewerTeam ( issue * Issue , team * Team ) ( bool , error ) {
return isOfficialReviewerTeam ( x , issue , team )
}
}
func isOfficialReviewer ( e Engine , issue * Issue , reviewer * User ) ( bool , error ) {
func isOfficialReviewerTeam ( e Engine , issue * Issue , team * Team ) ( bool , error ) {
pr , err := getPullRequestByIssueID ( e , issue . ID )
pr , err := getPullRequestByIssueID ( e , issue . ID )
if err != nil {
if err != nil {
return false , err
return false , err
@ -215,7 +263,11 @@ func isOfficialReviewer(e Engine, issue *Issue, reviewer *User) (bool, error) {
return false , nil
return false , nil
}
}
return pr . ProtectedBranch . isUserOfficialReviewer ( e , reviewer )
if ! pr . ProtectedBranch . EnableApprovalsWhitelist {
return team . Authorize >= AccessModeWrite , nil
}
return base . Int64sContains ( pr . ProtectedBranch . ApprovalsWhitelistTeamIDs , team . ID ) , nil
}
}
func createReview ( e Engine , opts CreateReviewOptions ) ( * Review , error ) {
func createReview ( e Engine , opts CreateReviewOptions ) ( * Review , error ) {
@ -224,12 +276,20 @@ func createReview(e Engine, opts CreateReviewOptions) (*Review, error) {
Issue : opts . Issue ,
Issue : opts . Issue ,
IssueID : opts . Issue . ID ,
IssueID : opts . Issue . ID ,
Reviewer : opts . Reviewer ,
Reviewer : opts . Reviewer ,
ReviewerID : opts . Reviewer . ID ,
ReviewerTeam : opts . ReviewerTeam ,
Content : opts . Content ,
Content : opts . Content ,
Official : opts . Official ,
Official : opts . Official ,
CommitID : opts . CommitID ,
CommitID : opts . CommitID ,
Stale : opts . Stale ,
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
}
if _ , err := e . Insert ( review ) ; err != nil {
if _ , err := e . Insert ( review ) ; err != nil {
return nil , err
return nil , err
}
}
@ -311,14 +371,13 @@ func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content, comm
if _ , err := sess . Exec ( "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?" , false , issue . ID , doer . ID ) ; err != nil {
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
return nil , nil , err
}
}
official , err = isOfficialReviewer ( sess , issue , doer )
if official , err = isOfficialReviewer ( sess , issue , doer ) ; err != nil {
if err != nil {
return nil , nil , err
return nil , nil , err
}
}
}
}
// No current review. Create a new one!
// No current review. Create a new one!
review , err = createReview ( sess , CreateReviewOptions {
if review , err = createReview ( sess , CreateReviewOptions {
Type : reviewType ,
Type : reviewType ,
Issue : issue ,
Issue : issue ,
Reviewer : doer ,
Reviewer : doer ,
@ -326,8 +385,7 @@ func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content, comm
Official : official ,
Official : official ,
CommitID : commitID ,
CommitID : commitID ,
Stale : stale ,
Stale : stale ,
} )
} ) ; err != nil {
if err != nil {
return nil , nil , err
return nil , nil , err
}
}
} else {
} else {
@ -343,8 +401,7 @@ func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content, comm
if _ , err := sess . Exec ( "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?" , false , issue . ID , doer . ID ) ; err != nil {
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
return nil , nil , err
}
}
official , err = isOfficialReviewer ( sess , issue , doer )
if official , err = isOfficialReviewer ( sess , issue , doer ) ; err != nil {
if err != nil {
return nil , nil , err
return nil , nil , err
}
}
}
}
@ -373,13 +430,34 @@ func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content, comm
return nil , nil , err
return nil , nil , err
}
}
// try to remove team review request if need
if issue . Repo . Owner . IsOrganization ( ) && ( reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject ) {
teamReviewRequests := make ( [ ] * Review , 0 , 10 )
if err := sess . SQL ( "SELECT * FROM review WHERE reviewer_team_id > 0 AND type = ?" , ReviewTypeRequest ) . Find ( & teamReviewRequests ) ; err != nil {
return nil , nil , err
}
for _ , teamReviewRequest := range teamReviewRequests {
ok , err := isTeamMember ( sess , issue . Repo . OwnerID , teamReviewRequest . ReviewerTeamID , doer . ID )
if err != nil {
return nil , nil , err
} else if ! ok {
continue
}
if _ , err := sess . Delete ( teamReviewRequest ) ; err != nil {
return nil , nil , err
}
}
}
comm . Review = review
comm . Review = review
return review , comm , sess . Commit ( )
return review , comm , sess . Commit ( )
}
}
// GetReviewersByIssueID gets the latest review of each reviewer for a pull request
// GetReviewersByIssueID gets the latest review of each reviewer for a pull request
func GetReviewersByIssueID ( issueID int64 ) ( reviews [ ] * Review , err error ) {
func GetReviewersByIssueID ( issueID int64 ) ( [ ] * Review , error ) {
reviewsUnfiltered := [ ] * Review { }
reviews := make ( [ ] * Review , 0 , 10 )
sess := x . NewSession ( )
sess := x . NewSession ( )
defer sess . Close ( )
defer sess . Close ( )
@ -388,40 +466,67 @@ func GetReviewersByIssueID(issueID int64) (reviews []*Review, err error) {
}
}
// Get latest review of each reviwer, sorted in order they were made
// Get latest review of each reviwer, sorted in order they were made
if err := sess . SQL ( "SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND type in (?, ?, ?) GROUP BY issue_id, reviewer_id) ORDER BY review.updated_unix ASC" ,
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 original_author_id = 0 GROUP BY issue_id, reviewer_id) ORDER BY review.updated_unix ASC" ,
issueID , ReviewTypeApprove , ReviewTypeReject , ReviewTypeRequest ) .
issueID , ReviewTypeApprove , ReviewTypeReject , ReviewTypeRequest ) .
Find ( & reviewsUnfiltered ) ; err != nil {
Find ( & reviews ) ; err != nil {
return nil , err
return nil , err
}
}
// Load reviewer and skip if user is deleted
teamReviewRequests := make ( [ ] * Review , 0 , 5 )
for _ , review := range reviewsUnfiltered {
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" ,
if err = review . loadReviewer ( sess ) ; err != nil {
issueID ) .
if ! IsErrUserNotExist ( err ) {
Find ( & teamReviewRequests ) ; err != nil {
return nil , err
return nil , err
}
}
} else {
reviews = append ( reviews , review )
if len ( teamReviewRequests ) > 0 {
}
reviews = append ( reviews , teamReviewRequests ... )
}
}
return reviews , nil
return reviews , nil
}
}
// GetReviewer ByIssueIDAndUserID get the latest review of reviewer for a pull request
// GetReviewByIssueIDAndUserID get the latest review of reviewer for a pull request
func GetReviewer ByIssueIDAndUserID ( issueID , userID int64 ) ( review * Review , err error ) {
func GetReviewByIssueIDAndUserID ( issueID , userID int64 ) ( * Review , error ) {
return getReviewer ByIssueIDAndUserID ( x , issueID , userID )
return getReviewByIssueIDAndUserID ( x , issueID , userID )
}
}
func getReviewer ByIssueIDAndUserID ( e Engine , issueID , userID int64 ) ( review * Review , err error ) {
func getReviewByIssueIDAndUserID ( e Engine , issueID , userID int64 ) ( * Review , error ) {
review = new ( Review )
review : = new ( Review )
if _ , err := e . SQL ( "SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_id = ? AND type in (?, ?, ?))" ,
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 (?, ?, ?))" ,
issueID , userID , ReviewTypeApprove , ReviewTypeReject , ReviewTypeRequest ) .
issueID , userID , ReviewTypeApprove , ReviewTypeReject , ReviewTypeRequest ) .
Get ( review )
if err != nil {
return nil , err
}
if ! has {
return nil , ErrReviewNotExist { }
}
return review , nil
}
// GetTeamReviewerByIssueIDAndTeamID get the latest review requst of reviewer team for a pull request
func GetTeamReviewerByIssueIDAndTeamID ( issueID , teamID int64 ) ( review * Review , err error ) {
return getTeamReviewerByIssueIDAndTeamID ( x , issueID , teamID )
}
func getTeamReviewerByIssueIDAndTeamID ( e Engine , issueID , teamID int64 ) ( review * Review , err error ) {
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 ) .
Get ( review ) ; err != nil {
Get ( review ) ; err != nil {
return nil , err
return nil , err
}
}
if ! has {
return nil , ErrReviewNotExist { 0 }
}
return
return
}
}
@ -482,10 +587,16 @@ func InsertReviews(reviews []*Review) error {
}
}
// AddReviewRequest add a review request from one reviewer
// AddReviewRequest add a review request from one reviewer
func AddReviewRequest ( issue * Issue , reviewer * User , doer * User ) ( comment * Comment , err error ) {
func AddReviewRequest ( issue * Issue , reviewer , doer * User ) ( * Comment , error ) {
review , err := GetReviewerByIssueIDAndUserID ( issue . ID , reviewer . ID )
sess := x . NewSession ( )
if err != nil {
defer sess . Close ( )
return
if err := sess . Begin ( ) ; err != nil {
return nil , err
}
review , err := getReviewByIssueIDAndUserID ( sess , issue . ID , reviewer . ID )
if err != nil && ! IsErrReviewNotExist ( err ) {
return nil , err
}
}
// skip it when reviewer hase been request to review
// skip it when reviewer hase been request to review
@ -493,95 +604,181 @@ func AddReviewRequest(issue *Issue, reviewer *User, doer *User) (comment *Commen
return nil , nil
return nil , nil
}
}
official , err := isOfficialReviewer ( sess , issue , reviewer , doer )
if err != nil {
return nil , err
} else if official {
if _ , err := sess . Exec ( "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?" , false , issue . ID , reviewer . ID ) ; err != nil {
return nil , err
}
}
if _ , err = createReview ( sess , CreateReviewOptions {
Type : ReviewTypeRequest ,
Issue : issue ,
Reviewer : reviewer ,
Official : official ,
Stale : false ,
} ) ; err != nil {
return nil , err
}
comment , err := createComment ( sess , & CreateCommentOptions {
Type : CommentTypeReviewRequest ,
Doer : doer ,
Repo : issue . Repo ,
Issue : issue ,
RemovedAssignee : false , // Use RemovedAssignee as !isRequest
AssigneeID : reviewer . ID , // Use AssigneeID as reviewer ID
} )
if err != nil {
return nil , err
}
return comment , sess . Commit ( )
}
//RemoveReviewRequest remove a review request from one reviewer
func RemoveReviewRequest ( issue * Issue , reviewer , doer * User ) ( * Comment , error ) {
sess := x . NewSession ( )
sess := x . NewSession ( )
defer sess . Close ( )
defer sess . Close ( )
if err := sess . Begin ( ) ; err != nil {
if err := sess . Begin ( ) ; err != nil {
return nil , err
return nil , err
}
}
var official bool
review , err := getReviewByIssueIDAndUserID ( sess , issue . ID , reviewer . ID )
official , err = isOfficialReviewer ( sess , issue , reviewer )
if err != nil && ! IsErrReviewNotExist ( err ) {
return nil , err
}
if review == nil || review . Type != ReviewTypeRequest {
return nil , nil
}
if _ , err = sess . Delete ( review ) ; err != nil {
return nil , err
}
official , err := isOfficialReviewer ( sess , issue , reviewer )
if err != nil {
if err != nil {
return nil , err
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 ! official {
if review != nil {
official , err = isOfficialReviewer ( sess , issue , doer )
if _ , err := sess . Exec ( "UPDATE `review` SET official=? WHERE id=?" , true , review . ID ) ; err != nil {
return nil , err
}
}
}
comment , err := createComment ( sess , & CreateCommentOptions {
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 {
if err != nil {
return nil , err
return nil , err
}
}
return comment , sess . Commit ( )
}
// AddTeamReviewRequest add a review request from one team
func AddTeamReviewRequest ( issue * Issue , reviewer * Team , doer * User ) ( * Comment , error ) {
sess := x . NewSession ( )
defer sess . Close ( )
if err := sess . Begin ( ) ; err != nil {
return nil , err
}
}
if official {
review , err := getTeamReviewerByIssueIDAndTeamID ( sess , issue . ID , reviewer . ID )
if _ , err := sess . Exec ( "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?" , false , issue . ID , reviewer . ID ) ; err != nil {
if err != nil && ! IsErrReviewNotExist ( err ) {
return nil , err
return nil , err
}
}
// This team already has been requested to review - therefore skip this.
if review != nil {
return nil , nil
}
official , err := isOfficialReviewerTeam ( sess , issue , reviewer )
if err != nil {
return nil , fmt . Errorf ( "isOfficialReviewerTeam(): %v" , err )
} else if ! official {
if official , err = isOfficialReviewer ( sess , issue , doer ) ; err != nil {
return nil , fmt . Errorf ( "isOfficialReviewer(): %v" , err )
}
}
}
_ , err = createReview ( sess , CreateReviewOptions {
if _ , err = createReview ( sess , CreateReviewOptions {
Type : ReviewTypeRequest ,
Type : ReviewTypeRequest ,
Issue : issue ,
Issue : issue ,
Reviewer : reviewer ,
ReviewerTeam : reviewer ,
Official : official ,
Official : official ,
Stale : false ,
Stale : false ,
} )
} ) ; err != nil {
return nil , err
}
if err != nil {
if official {
return
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
}
}
}
comment , err = createComment ( sess , & CreateCommentOptions {
comment , err : = createComment ( sess , & CreateCommentOptions {
Type : CommentTypeReviewRequest ,
Type : CommentTypeReviewRequest ,
Doer : doer ,
Doer : doer ,
Repo : issue . Repo ,
Repo : issue . Repo ,
Issue : issue ,
Issue : issue ,
RemovedAssignee : false , // Use RemovedAssignee as !isRequest
RemovedAssignee : false , // Use RemovedAssignee as !isRequest
AssigneeID : reviewer . ID , // Use AssigneeID as reviewer ID
AssigneeTeam ID : reviewer . ID , // Use AssigneeTeam ID as reviewer team ID
} )
} )
if err != nil {
if err != nil {
return nil , err
return nil , fmt . Errorf ( "createComment(): %v" , err )
}
}
return comment , sess . Commit ( )
return comment , sess . Commit ( )
}
}
//RemoveReviewRequest remove a review request from one reviewer
//RemoveTeamReviewRequest remove a review request from one team
func RemoveReviewRequest ( issue * Issue , reviewer * User , doer * User ) ( comment * Comment , err error ) {
func RemoveTeamReviewRequest ( issue * Issue , reviewer * Team , doer * User ) ( * Comment , error ) {
review , err := GetReviewerByIssueIDAndUserID ( issue . ID , reviewer . ID )
if err != nil {
return
}
if review . Type != ReviewTypeRequest {
return nil , nil
}
sess := x . NewSession ( )
sess := x . NewSession ( )
defer sess . Close ( )
defer sess . Close ( )
if err := sess . Begin ( ) ; err != nil {
if err := sess . Begin ( ) ; err != nil {
return nil , err
return nil , err
}
}
_ , err = sess . Delete ( review )
review , err := getTeamReviewerByIssueIDAndTeamID ( sess , issue . ID , reviewer . ID )
if err != nil {
if err != nil && ! IsErrReviewNotExist ( err ) {
return nil , err
return nil , err
}
}
var official bool
if review == nil {
official , err = isOfficialReviewer ( sess , issue , reviewer )
return nil , nil
if err != nil {
return
}
}
if officia l {
if _ , err = sess . Delete ( review ) ; err != ni l {
// recalculate which is the latest official review from that user
return nil , err
var review * Review
}
review , err = getReviewerByIssueIDAndUserID ( sess , issue . ID , reviewer . ID )
official , err := isOfficialReviewerTeam ( sess , issue , reviewer )
if err != nil {
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 ) {
return nil , err
return nil , err
}
}
@ -592,21 +789,20 @@ func RemoveReviewRequest(issue *Issue, reviewer *User, doer *User) (comment *Com
}
}
}
}
if err ! = nil {
if doer = = nil {
return nil , err
return nil , sess . Commit ( )
}
}
comment , err = createComment ( sess , & CreateCommentOptions {
comment , err : = createComment ( sess , & CreateCommentOptions {
Type : CommentTypeReviewRequest ,
Type : CommentTypeReviewRequest ,
Doer : doer ,
Doer : doer ,
Repo : issue . Repo ,
Repo : issue . Repo ,
Issue : issue ,
Issue : issue ,
RemovedAssignee : true , // Use RemovedAssignee as !isRequest
RemovedAssignee : true , // Use RemovedAssignee as !isRequest
AssigneeID : reviewer . ID , // Use AssigneeID as reviewer ID
AssigneeTeam ID : reviewer . ID , // Use AssigneeTeam ID as reviewer team ID
} )
} )
if err != nil {
if err != nil {
return nil , err
return nil , fmt . Errorf ( "createComment(): %v" , err )
}
}
return comment , sess . Commit ( )
return comment , sess . Commit ( )