2018-08-06 06:43:22 +02:00
// 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 (
2020-04-18 15:50:25 +02:00
"fmt"
2019-11-14 03:57:36 +01:00
"strings"
2018-08-06 06:43:22 +02:00
2019-08-15 16:46:21 +02:00
"code.gitea.io/gitea/modules/timeutil"
2018-08-06 06:43:22 +02:00
2019-06-23 17:22:43 +02:00
"xorm.io/builder"
2018-08-06 06:43:22 +02:00
)
// 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 18:33:34 +02:00
// ReviewTypeRequest request review from others
ReviewTypeRequest
2018-08-06 06:43:22 +02:00
)
// Icon returns the corresponding icon for the review type
func ( rt ReviewType ) Icon ( ) string {
switch rt {
case ReviewTypeApprove :
2020-04-03 07:12:42 +02:00
return "check"
2018-08-06 06:43:22 +02:00
case ReviewTypeReject :
2020-04-03 07:12:42 +02:00
return "request-changes"
case ReviewTypeComment :
2018-10-19 15:36:41 +02:00
return "comment"
2020-04-06 18:33:34 +02:00
case ReviewTypeRequest :
return "primitive-dot"
2018-08-06 06:43:22 +02:00
default :
return "comment"
}
}
// Review represents collection of code comments giving feedback for a PR
type Review struct {
2020-01-23 18:28:15 +01:00
ID int64 ` xorm:"pk autoincr" `
Type ReviewType
Reviewer * User ` xorm:"-" `
ReviewerID int64 ` xorm:"index" `
OriginalAuthor string
OriginalAuthorID int64
Issue * Issue ` xorm:"-" `
IssueID int64 ` xorm:"index" `
Content string ` xorm:"TEXT" `
2019-12-04 02:08:56 +01:00
// Official is a review made by an assigned approver (counts towards approval)
2020-01-09 02:47:45 +01:00
Official bool ` xorm:"NOT NULL DEFAULT false" `
CommitID string ` xorm:"VARCHAR(40)" `
Stale bool ` xorm:"NOT NULL DEFAULT false" `
2018-08-06 06:43:22 +02:00
2019-08-15 16:46:21 +02:00
CreatedUnix timeutil . TimeStamp ` xorm:"INDEX created" `
UpdatedUnix timeutil . TimeStamp ` xorm:"INDEX updated" `
2018-08-06 06:43:22 +02:00
// CodeComments are the initial code comments of the review
CodeComments CodeComments ` xorm:"-" `
2020-01-23 18:28:15 +01:00
Comments [ ] * Comment ` xorm:"-" `
2018-08-06 06:43:22 +02:00
}
func ( r * Review ) loadCodeComments ( e Engine ) ( err error ) {
2019-11-15 13:59:21 +01:00
if r . CodeComments == nil {
r . CodeComments , err = fetchCodeCommentsByReview ( e , r . Issue , nil , r )
}
2018-08-06 06:43:22 +02:00
return
}
// LoadCodeComments loads CodeComments
func ( r * Review ) LoadCodeComments ( ) error {
return r . loadCodeComments ( x )
}
func ( r * Review ) loadIssue ( e Engine ) ( err error ) {
r . Issue , err = getIssueByID ( e , r . IssueID )
return
}
func ( r * Review ) loadReviewer ( e Engine ) ( err error ) {
if r . ReviewerID == 0 {
return nil
}
r . Reviewer , err = getUserByID ( e , r . ReviewerID )
return
}
2019-11-14 03:57:36 +01:00
// LoadReviewer loads reviewer
func ( r * Review ) LoadReviewer ( ) error {
return r . loadReviewer ( x )
}
2018-08-06 06:43:22 +02:00
func ( r * Review ) loadAttributes ( e Engine ) ( err error ) {
if err = r . loadReviewer ( e ) ; err != nil {
return
}
if err = r . loadIssue ( e ) ; err != nil {
return
}
return
}
// LoadAttributes loads all attributes except CodeComments
func ( r * Review ) LoadAttributes ( ) error {
return r . loadAttributes ( x )
}
func getReviewByID ( e Engine , id int64 ) ( * Review , error ) {
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 ) {
return getReviewByID ( x , id )
}
// FindReviewOptions represent possible filters to find reviews
type FindReviewOptions struct {
2019-12-31 00:34:11 +01:00
Type ReviewType
IssueID int64
ReviewerID int64
OfficialOnly bool
2018-08-06 06:43:22 +02:00
}
func ( opts * FindReviewOptions ) toCond ( ) builder . Cond {
var cond = builder . NewCond ( )
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 00:34:11 +01:00
if opts . OfficialOnly {
cond = cond . And ( builder . Eq { "official" : true } )
}
2018-08-06 06:43:22 +02:00
return cond
}
func findReviews ( e Engine , opts FindReviewOptions ) ( [ ] * Review , error ) {
reviews := make ( [ ] * Review , 0 , 10 )
sess := e . Where ( opts . toCond ( ) )
return reviews , sess .
Asc ( "created_unix" ) .
Asc ( "id" ) .
Find ( & reviews )
}
// FindReviews returns reviews passing FindReviewOptions
func FindReviews ( opts FindReviewOptions ) ( [ ] * Review , error ) {
return findReviews ( x , opts )
}
// CreateReviewOptions represent the options to create a review. Type, Issue and Reviewer are required.
type CreateReviewOptions struct {
Content string
Type ReviewType
Issue * Issue
Reviewer * User
2019-12-04 02:08:56 +01:00
Official bool
2020-01-09 02:47:45 +01:00
CommitID string
Stale bool
2019-12-04 02:08:56 +01:00
}
// IsOfficialReviewer check if reviewer can make official reviews in issue (counts towards required approvals)
func IsOfficialReviewer ( issue * Issue , reviewer * User ) ( bool , error ) {
return isOfficialReviewer ( x , issue , reviewer )
}
func isOfficialReviewer ( e Engine , issue * Issue , reviewer * 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
}
return pr . ProtectedBranch . isUserOfficialReviewer ( e , reviewer )
2018-08-06 06:43:22 +02:00
}
func createReview ( e Engine , opts CreateReviewOptions ) ( * Review , error ) {
review := & Review {
Type : opts . Type ,
Issue : opts . Issue ,
IssueID : opts . Issue . ID ,
Reviewer : opts . Reviewer ,
ReviewerID : opts . Reviewer . ID ,
Content : opts . Content ,
2019-12-04 02:08:56 +01:00
Official : opts . Official ,
2020-01-09 02:47:45 +01:00
CommitID : opts . CommitID ,
Stale : opts . Stale ,
2018-08-06 06:43:22 +02:00
}
if _ , err := e . Insert ( review ) ; err != nil {
return nil , err
}
2018-12-27 19:04:30 +01:00
2018-08-06 06:43:22 +02:00
return review , nil
}
// CreateReview creates a new review based on opts
func CreateReview ( opts CreateReviewOptions ) ( * Review , error ) {
return createReview ( x , opts )
}
func getCurrentReview ( e Engine , reviewer * User , issue * Issue ) ( * Review , error ) {
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 13:23:05 +02:00
reviews [ 0 ] . Reviewer = reviewer
reviews [ 0 ] . Issue = issue
2018-08-06 06:43:22 +02:00
return reviews [ 0 ] , nil
}
2019-11-24 06:46:16 +01:00
// 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 ) {
return x . Cols ( "id" ) . Exist ( & Comment { IssueID : issue . ID , TreePath : treePath , Line : line , Type : CommentTypeCode } )
}
2018-08-06 06:43:22 +02:00
// GetCurrentReview returns the current pending review of reviewer for given issue
func GetCurrentReview ( reviewer * User , issue * Issue ) ( * Review , error ) {
return getCurrentReview ( x , reviewer , issue )
}
2019-11-14 03:57:36 +01:00
// ContentEmptyErr represents an content empty error
type ContentEmptyErr struct {
}
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
2020-01-09 02:47:45 +01:00
func SubmitReview ( doer * User , issue * Issue , reviewType ReviewType , content , commitID string , stale bool ) ( * Review , * Comment , error ) {
2019-11-14 03:57:36 +01:00
sess := x . NewSession ( )
defer sess . Close ( )
if err := sess . Begin ( ) ; err != nil {
return nil , nil , err
}
2019-12-04 02:08:56 +01:00
var official = false
2019-11-14 03:57:36 +01:00
review , err := getCurrentReview ( sess , doer , issue )
if err != nil {
if ! IsErrReviewNotExist ( err ) {
return nil , nil , err
}
2019-11-14 21:58:01 +01:00
if reviewType != ReviewTypeApprove && len ( strings . TrimSpace ( content ) ) == 0 {
2019-11-14 03:57:36 +01:00
return nil , nil , ContentEmptyErr { }
}
2019-12-04 02:08:56 +01:00
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
}
official , err = isOfficialReviewer ( sess , issue , doer )
if err != nil {
return nil , nil , err
}
}
2019-11-14 03:57:36 +01:00
// No current review. Create a new one!
review , err = createReview ( sess , CreateReviewOptions {
Type : reviewType ,
Issue : issue ,
Reviewer : doer ,
Content : content ,
2019-12-04 02:08:56 +01:00
Official : official ,
2020-01-09 02:47:45 +01:00
CommitID : commitID ,
Stale : stale ,
2019-11-14 03:57:36 +01:00
} )
if err != nil {
return nil , nil , err
}
} else {
if err := review . loadCodeComments ( sess ) ; err != nil {
return nil , nil , err
}
2019-11-14 21:58:01 +01:00
if reviewType != ReviewTypeApprove && len ( review . CodeComments ) == 0 && len ( strings . TrimSpace ( content ) ) == 0 {
2019-11-14 03:57:36 +01:00
return nil , nil , ContentEmptyErr { }
}
2019-12-04 02:08:56 +01:00
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
}
official , err = isOfficialReviewer ( sess , issue , doer )
if err != nil {
return nil , nil , err
}
}
review . Official = official
2019-11-14 03:57:36 +01:00
review . Issue = issue
review . Content = content
review . Type = reviewType
2020-01-09 02:47:45 +01:00
review . CommitID = commitID
review . Stale = stale
2019-12-04 02:08:56 +01:00
2020-01-09 02:47:45 +01:00
if _ , err := sess . ID ( review . ID ) . Cols ( "content, type, official, commit_id, stale" ) . Update ( review ) ; err != nil {
2019-11-14 03:57:36 +01:00
return nil , nil , err
}
2018-08-06 06:43:22 +02:00
}
2019-11-14 03:57:36 +01:00
2019-12-16 04:54:24 +01:00
comm , err := createComment ( sess , & CreateCommentOptions {
2019-11-14 03:57:36 +01:00
Type : CommentTypeReview ,
Doer : doer ,
Content : review . Content ,
Issue : issue ,
Repo : issue . Repo ,
ReviewID : review . ID ,
} )
if err != nil || comm == nil {
return nil , nil , err
}
comm . Review = review
return review , comm , sess . Commit ( )
2018-08-06 06:43:22 +02:00
}
2018-11-22 14:17:36 +01:00
2019-12-04 02:08:56 +01:00
// GetReviewersByIssueID gets the latest review of each reviewer for a pull request
func GetReviewersByIssueID ( issueID int64 ) ( reviews [ ] * Review , err error ) {
reviewsUnfiltered := [ ] * Review { }
2018-11-22 14:17:36 +01:00
2019-12-04 02:08:56 +01:00
sess := x . NewSession ( )
defer sess . Close ( )
if err := sess . Begin ( ) ; err != nil {
return nil , err
2018-12-11 02:09:46 +01:00
}
2018-11-22 14:17:36 +01:00
2019-12-04 02:08:56 +01:00
// Get latest review of each reviwer, sorted in order they were made
2020-04-06 18:33:34 +02:00
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" ,
issueID , ReviewTypeApprove , ReviewTypeReject , ReviewTypeRequest ) .
2019-12-04 02:08:56 +01:00
Find ( & reviewsUnfiltered ) ; err != nil {
return nil , err
}
// Load reviewer and skip if user is deleted
for _ , review := range reviewsUnfiltered {
2020-04-06 18:33:34 +02:00
if err = review . loadReviewer ( sess ) ; err != nil {
2019-12-04 02:08:56 +01:00
if ! IsErrUserNotExist ( err ) {
return nil , err
}
} else {
reviews = append ( reviews , review )
2018-11-22 14:17:36 +01:00
}
}
2019-12-04 02:08:56 +01:00
return reviews , nil
2018-11-22 14:17:36 +01:00
}
2020-01-09 02:47:45 +01:00
2020-04-06 18:33:34 +02:00
// GetReviewerByIssueIDAndUserID get the latest review of reviewer for a pull request
func GetReviewerByIssueIDAndUserID ( issueID , userID int64 ) ( review * Review , err error ) {
2020-04-11 06:44:50 +02:00
return getReviewerByIssueIDAndUserID ( x , issueID , userID )
}
func getReviewerByIssueIDAndUserID ( e Engine , issueID , userID int64 ) ( review * Review , err error ) {
2020-04-06 18:33:34 +02:00
review = new ( Review )
2020-04-11 06:44:50 +02:00
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 (?, ?, ?))" ,
2020-04-06 18:33:34 +02:00
issueID , userID , ReviewTypeApprove , ReviewTypeReject , ReviewTypeRequest ) .
Get ( review ) ; err != nil {
return nil , err
}
return
}
2020-01-09 02:47:45 +01:00
// MarkReviewsAsStale marks existing reviews as stale
func MarkReviewsAsStale ( issueID int64 ) ( err error ) {
_ , err = x . Exec ( "UPDATE `review` SET stale=? WHERE issue_id=?" , true , issueID )
return
}
// MarkReviewsAsNotStale marks existing reviews as not stale for a giving commit SHA
func MarkReviewsAsNotStale ( issueID int64 , commitID string ) ( err error ) {
_ , err = x . Exec ( "UPDATE `review` SET stale=? WHERE issue_id=? AND commit_id=?" , false , issueID , commitID )
return
}
2020-01-23 18:28:15 +01:00
// InsertReviews inserts review and review comments
func InsertReviews ( reviews [ ] * Review ) error {
sess := x . NewSession ( )
defer sess . Close ( )
if err := sess . Begin ( ) ; err != nil {
return err
}
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 05:04:08 +02:00
if len ( review . Comments ) > 0 {
if _ , err := sess . NoAutoTime ( ) . Insert ( review . Comments ) ; err != nil {
return err
}
2020-01-23 18:28:15 +01:00
}
}
return sess . Commit ( )
}
2020-04-06 18:33:34 +02:00
2020-04-30 22:24:08 +02:00
// AddReviewRequest add a review request from one reviewer
func AddReviewRequest ( issue * Issue , reviewer * User , doer * User ) ( comment * Comment , err error ) {
2020-04-06 18:33:34 +02:00
review , err := GetReviewerByIssueIDAndUserID ( issue . ID , reviewer . ID )
if err != nil {
return
}
// skip it when reviewer hase been request to review
if review != nil && review . Type == ReviewTypeRequest {
return nil , nil
}
sess := x . NewSession ( )
defer sess . Close ( )
if err := sess . Begin ( ) ; err != nil {
return nil , err
}
var official bool
official , err = isOfficialReviewer ( sess , issue , reviewer )
if err != nil {
return nil , err
}
if ! official {
official , err = isOfficialReviewer ( sess , issue , doer )
if err != nil {
return nil , err
}
}
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
}
}
_ , err = createReview ( sess , CreateReviewOptions {
Type : ReviewTypeRequest ,
Issue : issue ,
Reviewer : reviewer ,
Official : official ,
Stale : false ,
} )
if err != nil {
return
}
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 ( )
}
2020-04-30 22:24:08 +02:00
//RemoveReviewRequest remove a review request from one reviewer
func RemoveReviewRequest ( issue * Issue , reviewer * User , doer * User ) ( comment * Comment , err error ) {
2020-04-06 18:33:34 +02:00
review , err := GetReviewerByIssueIDAndUserID ( issue . ID , reviewer . ID )
if err != nil {
return
}
if review . Type != ReviewTypeRequest {
return nil , nil
}
sess := x . NewSession ( )
defer sess . Close ( )
if err := sess . Begin ( ) ; err != nil {
return nil , err
}
_ , err = sess . Delete ( review )
if err != nil {
return nil , err
}
var official bool
official , err = isOfficialReviewer ( sess , issue , reviewer )
if err != nil {
return
}
if official {
// recalculate which is the latest official review from that user
var review * Review
2020-04-11 06:44:50 +02:00
review , err = getReviewerByIssueIDAndUserID ( sess , issue . ID , reviewer . ID )
2020-04-06 18:33:34 +02:00
if err != nil {
return nil , err
}
if review != nil {
if _ , err := sess . Exec ( "UPDATE `review` SET official=? WHERE id=?" , true , review . ID ) ; err != nil {
return nil , err
}
}
}
if err != nil {
return nil , err
}
2020-04-11 06:44:50 +02:00
comment , err = createComment ( sess , & CreateCommentOptions {
2020-04-06 18:33:34 +02:00
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
}
return comment , sess . Commit ( )
}
2020-04-18 15:50:25 +02:00
// MarkConversation Add or remove Conversation mark for a code comment
func MarkConversation ( comment * Comment , doer * User , isResolve bool ) ( err error ) {
if comment . Type != CommentTypeCode {
return nil
}
if isResolve {
if comment . ResolveDoerID != 0 {
return nil
}
if _ , err = x . Exec ( "UPDATE `comment` SET resolve_doer_id=? WHERE id=?" , doer . ID , comment . ID ) ; err != nil {
return err
}
} else {
if comment . ResolveDoerID == 0 {
return nil
}
if _ , err = x . Exec ( "UPDATE `comment` SET resolve_doer_id=? WHERE id=?" , 0 , comment . ID ) ; err != nil {
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
func CanMarkConversation ( issue * Issue , doer * User ) ( permResult bool , err error ) {
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
}
perm , err := GetUserRepoPermission ( issue . Repo , doer )
if err != nil {
return false , err
}
permResult = perm . CanAccess ( AccessModeWrite , UnitTypePullRequests )
if ! permResult {
if permResult , err = IsOfficialReviewer ( issue , doer ) ; err != nil {
return false , err
}
}
if ! permResult {
return false , nil
}
}
return true , nil
}