2019-08-14 18:32:19 +03:00
// Copyright 2019 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 pull
import (
2022-01-20 02:26:57 +03:00
"context"
2019-11-14 05:57:36 +03:00
"fmt"
2021-02-27 21:46:14 +03:00
"io"
2020-10-06 03:18:55 +03:00
"regexp"
2019-11-14 05:57:36 +03:00
"strings"
2019-08-14 18:32:19 +03:00
"code.gitea.io/gitea/models"
2021-09-19 14:49:59 +03:00
"code.gitea.io/gitea/models/db"
2021-12-10 04:27:50 +03:00
repo_model "code.gitea.io/gitea/models/repo"
2021-11-24 12:49:20 +03:00
user_model "code.gitea.io/gitea/models/user"
2019-11-14 05:57:36 +03:00
"code.gitea.io/gitea/modules/git"
2021-02-27 21:46:14 +03:00
"code.gitea.io/gitea/modules/log"
2019-11-05 14:04:08 +03:00
"code.gitea.io/gitea/modules/notification"
2019-11-14 05:57:36 +03:00
"code.gitea.io/gitea/modules/setting"
2019-08-14 18:32:19 +03:00
)
2019-11-14 05:57:36 +03:00
// CreateCodeComment creates a comment on the code line
2022-01-20 02:26:57 +03:00
func CreateCodeComment ( ctx context . Context , doer * user_model . User , gitRepo * git . Repository , issue * models . Issue , line int64 , content , treePath string , isReview bool , replyReviewID int64 , latestCommitID string ) ( * models . Comment , error ) {
2019-11-24 08:46:16 +03:00
var (
existsReview bool
err error
)
// CreateCodeComment() is used for:
// - Single comments
// - Comments that are part of a review
// - Comments that reply to an existing review
2020-08-21 10:53:14 +03:00
if ! isReview && replyReviewID != 0 {
2019-11-24 08:46:16 +03:00
// It's not part of a review; maybe a reply to a review comment or a single comment.
// Check if there are reviews for that line already; if there are, this is a reply
if existsReview , err = models . ReviewExists ( issue , treePath , line ) ; err != nil {
return nil , err
}
}
// Comments that are replies don't require a review header to show up in the issue view
if ! isReview && existsReview {
2022-04-08 12:11:15 +03:00
if err = issue . LoadRepo ( ctx ) ; err != nil {
2019-11-14 05:57:36 +03:00
return nil , err
}
2022-01-20 02:26:57 +03:00
comment , err := createCodeComment ( ctx ,
2019-11-14 05:57:36 +03:00
doer ,
issue . Repo ,
issue ,
content ,
treePath ,
line ,
replyReviewID ,
)
if err != nil {
return nil , err
}
2022-03-29 17:57:33 +03:00
mentions , err := models . FindAndUpdateIssueMentions ( ctx , issue , doer , comment . Content )
2021-01-02 20:04:02 +03:00
if err != nil {
return nil , err
}
notification . NotifyCreateIssueComment ( doer , issue . Repo , issue , comment , mentions )
2019-11-14 05:57:36 +03:00
return comment , nil
}
2022-05-20 17:08:52 +03:00
review , err := models . GetCurrentReview ( ctx , doer , issue )
2019-11-14 05:57:36 +03:00
if err != nil {
if ! models . IsErrReviewNotExist ( err ) {
return nil , err
}
2022-05-20 17:08:52 +03:00
if review , err = models . CreateReview ( ctx , models . CreateReviewOptions {
2019-11-14 05:57:36 +03:00
Type : models . ReviewTypePending ,
Reviewer : doer ,
Issue : issue ,
2019-12-04 04:08:56 +03:00
Official : false ,
2020-01-09 04:47:45 +03:00
CommitID : latestCommitID ,
2020-10-12 22:55:13 +03:00
} ) ; err != nil {
2019-11-14 05:57:36 +03:00
return nil , err
}
}
2022-01-20 02:26:57 +03:00
comment , err := createCodeComment ( ctx ,
2019-11-14 05:57:36 +03:00
doer ,
issue . Repo ,
issue ,
content ,
treePath ,
line ,
review . ID ,
)
2019-08-14 18:32:19 +03:00
if err != nil {
return nil , err
}
2019-11-24 08:46:16 +03:00
if ! isReview && ! existsReview {
// Submit the review we've just created so the comment shows up in the issue view
2022-01-20 02:26:57 +03:00
if _ , _ , err = SubmitReview ( ctx , doer , gitRepo , issue , models . ReviewTypeComment , "" , latestCommitID , nil ) ; err != nil {
2019-11-24 08:46:16 +03:00
return nil , err
}
}
// NOTICE: if it's a pending review the notifications will not be fired until user submit review.
2019-11-14 05:57:36 +03:00
return comment , nil
}
2020-10-06 03:18:55 +03:00
var notEnoughLines = regexp . MustCompile ( ` exit status 128 - fatal: file .* has only \d+ lines? ` )
2019-11-14 05:57:36 +03:00
// createCodeComment creates a plain code comment at the specified line / path
2022-01-20 02:26:57 +03:00
func createCodeComment ( ctx context . Context , doer * user_model . User , repo * repo_model . Repository , issue * models . Issue , content , treePath string , line , reviewID int64 ) ( * models . Comment , error ) {
2019-11-14 05:57:36 +03:00
var commitID , patch string
if err := issue . LoadPullRequest ( ) ; err != nil {
return nil , fmt . Errorf ( "GetPullRequestByIssueID: %v" , err )
}
pr := issue . PullRequest
2022-04-28 14:48:48 +03:00
if err := pr . LoadBaseRepoCtx ( ctx ) ; err != nil {
2020-03-03 01:31:55 +03:00
return nil , fmt . Errorf ( "LoadHeadRepo: %v" , err )
2019-11-14 05:57:36 +03:00
}
2022-01-20 02:26:57 +03:00
gitRepo , closer , err := git . RepositoryFromContextOrOpen ( ctx , pr . BaseRepo . RepoPath ( ) )
2019-11-14 05:57:36 +03:00
if err != nil {
2022-03-29 22:13:41 +03:00
return nil , fmt . Errorf ( "RepositoryFromContextOrOpen: %v" , err )
2019-11-13 21:36:04 +03:00
}
2022-01-20 02:26:57 +03:00
defer closer . Close ( )
2019-11-05 14:04:08 +03:00
2020-11-09 09:15:09 +03:00
invalidated := false
head := pr . GetGitRefName ( )
2019-11-14 05:57:36 +03:00
if line > 0 {
2020-11-09 09:15:09 +03:00
if reviewID != 0 {
2022-05-20 17:08:52 +03:00
first , err := models . FindComments ( ctx , & models . FindCommentsOptions {
2020-11-09 09:15:09 +03:00
ReviewID : reviewID ,
Line : line ,
TreePath : treePath ,
Type : models . CommentTypeCode ,
2021-09-24 14:32:56 +03:00
ListOptions : db . ListOptions {
2020-11-09 09:15:09 +03:00
PageSize : 1 ,
Page : 1 ,
} ,
} )
if err == nil && len ( first ) > 0 {
commitID = first [ 0 ] . CommitSHA
invalidated = first [ 0 ] . Invalidated
patch = first [ 0 ] . Patch
} else if err != nil && ! models . IsErrCommentNotExist ( err ) {
return nil , fmt . Errorf ( "Find first comment for %d line %d path %s. Error: %v" , reviewID , line , treePath , err )
} else {
2022-05-20 17:08:52 +03:00
review , err := models . GetReviewByID ( ctx , reviewID )
2020-11-09 09:15:09 +03:00
if err == nil && len ( review . CommitID ) > 0 {
head = review . CommitID
} else if err != nil && ! models . IsErrReviewNotExist ( err ) {
return nil , fmt . Errorf ( "GetReviewByID %d. Error: %v" , reviewID , err )
}
}
}
if len ( commitID ) == 0 {
// FIXME validate treePath
// Get latest commit referencing the commented line
// No need for get commit for base branch changes
commit , err := gitRepo . LineBlame ( head , gitRepo . Path , treePath , uint ( line ) )
if err == nil {
commitID = commit . ID . String ( )
} else if ! ( strings . Contains ( err . Error ( ) , "exit status 128 - fatal: no such path" ) || notEnoughLines . MatchString ( err . Error ( ) ) ) {
return nil , fmt . Errorf ( "LineBlame[%s, %s, %s, %d]: %v" , pr . GetGitRefName ( ) , gitRepo . Path , treePath , line , err )
}
2019-11-14 05:57:36 +03:00
}
}
// Only fetch diff if comment is review comment
2020-11-09 09:15:09 +03:00
if len ( patch ) == 0 && reviewID != 0 {
2020-12-16 12:54:58 +03:00
headCommitID , err := gitRepo . GetRefCommitID ( pr . GetGitRefName ( ) )
if err != nil {
return nil , fmt . Errorf ( "GetRefCommitID[%s]: %v" , pr . GetGitRefName ( ) , err )
}
2020-11-09 09:15:09 +03:00
if len ( commitID ) == 0 {
2020-12-16 12:54:58 +03:00
commitID = headCommitID
2019-11-14 05:57:36 +03:00
}
2021-02-27 21:46:14 +03:00
reader , writer := io . Pipe ( )
defer func ( ) {
_ = reader . Close ( )
_ = writer . Close ( )
} ( )
go func ( ) {
if err := git . GetRepoRawDiffForFile ( gitRepo , pr . MergeBase , headCommitID , git . RawDiffNormal , treePath , writer ) ; err != nil {
_ = writer . CloseWithError ( fmt . Errorf ( "GetRawDiffForLine[%s, %s, %s, %s]: %v" , gitRepo . Path , pr . MergeBase , headCommitID , treePath , err ) )
return
}
_ = writer . Close ( )
} ( )
patch , err = git . CutDiffAroundLine ( reader , int64 ( ( & models . Comment { Line : line } ) . UnsignedLine ( ) ) , line < 0 , setting . UI . CodeCommentLines )
if err != nil {
log . Error ( "Error whilst generating patch: %v" , err )
return nil , err
2019-11-14 05:57:36 +03:00
}
}
2019-12-16 06:54:24 +03:00
return models . CreateComment ( & models . CreateCommentOptions {
2020-11-09 09:15:09 +03:00
Type : models . CommentTypeCode ,
Doer : doer ,
Repo : repo ,
Issue : issue ,
Content : content ,
LineNum : line ,
TreePath : treePath ,
CommitSHA : commitID ,
ReviewID : reviewID ,
Patch : patch ,
Invalidated : invalidated ,
2019-11-14 05:57:36 +03:00
} )
2019-10-18 11:33:19 +03:00
}
2019-11-14 05:57:36 +03:00
// SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
2022-01-20 02:26:57 +03:00
func SubmitReview ( ctx context . Context , doer * user_model . User , gitRepo * git . Repository , issue * models . Issue , reviewType models . ReviewType , content , commitID string , attachmentUUIDs [ ] string ) ( * models . Review , * models . Comment , error ) {
2020-01-09 04:47:45 +03:00
pr , err := issue . GetPullRequest ( )
2019-10-18 11:33:19 +03:00
if err != nil {
2019-11-14 05:57:36 +03:00
return nil , nil , err
2019-10-18 11:33:19 +03:00
}
2020-01-09 04:47:45 +03:00
var stale bool
if reviewType != models . ReviewTypeApprove && reviewType != models . ReviewTypeReject {
stale = false
} else {
headCommitID , err := gitRepo . GetRefCommitID ( pr . GetGitRefName ( ) )
if err != nil {
return nil , nil , err
}
if headCommitID == commitID {
stale = false
} else {
2022-01-20 02:26:57 +03:00
stale , err = checkIfPRContentChanged ( ctx , pr , commitID , headCommitID )
2020-01-09 04:47:45 +03:00
if err != nil {
return nil , nil , err
}
}
}
2021-06-15 04:12:33 +03:00
review , comm , err := models . SubmitReview ( doer , issue , reviewType , content , commitID , stale , attachmentUUIDs )
2019-11-14 05:57:36 +03:00
if err != nil {
return nil , nil , err
}
2020-01-09 04:47:45 +03:00
2022-03-29 17:57:33 +03:00
mentions , err := models . FindAndUpdateIssueMentions ( ctx , issue , doer , comm . Content )
2021-01-02 20:04:02 +03:00
if err != nil {
return nil , nil , err
}
notification . NotifyPullRequestReview ( pr , review , comm , mentions )
for _ , lines := range review . CodeComments {
for _ , comments := range lines {
for _ , codeComment := range comments {
2022-03-29 17:57:33 +03:00
mentions , err := models . FindAndUpdateIssueMentions ( ctx , issue , doer , codeComment . Content )
2021-01-02 20:04:02 +03:00
if err != nil {
return nil , nil , err
}
notification . NotifyPullRequestCodeComment ( pr , codeComment , mentions )
}
}
}
2019-08-14 18:32:19 +03:00
2019-11-14 05:57:36 +03:00
return review , comm , nil
2019-08-14 18:32:19 +03:00
}
2021-02-11 20:32:25 +03:00
// DismissReview dismissing stale review by repo admin
2022-01-20 02:26:57 +03:00
func DismissReview ( ctx context . Context , reviewID int64 , message string , doer * user_model . User , isDismiss bool ) ( comment * models . Comment , err error ) {
2022-05-20 17:08:52 +03:00
review , err := models . GetReviewByID ( ctx , reviewID )
2021-02-11 20:32:25 +03:00
if err != nil {
return
}
if review . Type != models . ReviewTypeApprove && review . Type != models . ReviewTypeReject {
return nil , fmt . Errorf ( "not need to dismiss this review because it's type is not Approve or change request" )
}
if err = models . DismissReview ( review , isDismiss ) ; err != nil {
return
}
if ! isDismiss {
return nil , nil
}
// load data for notify
2022-01-20 02:26:57 +03:00
if err = review . LoadAttributes ( ctx ) ; err != nil {
2021-02-11 20:32:25 +03:00
return
}
if err = review . Issue . LoadPullRequest ( ) ; err != nil {
return
}
if err = review . Issue . LoadAttributes ( ) ; err != nil {
return
}
comment , err = models . CreateComment ( & models . CreateCommentOptions {
Doer : doer ,
Content : message ,
Type : models . CommentTypeDismissReview ,
ReviewID : review . ID ,
Issue : review . Issue ,
Repo : review . Issue . Repo ,
} )
if err != nil {
return
}
comment . Review = review
comment . Poster = doer
comment . Issue = review . Issue
notification . NotifyPullRevieweDismiss ( doer , review , comment )
return
}