2019-08-14 18:32:19 +03:00
// Copyright 2019 The Gitea Authors.
// All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2019-08-14 18:32:19 +03:00
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"
2021-09-19 14:49:59 +03:00
"code.gitea.io/gitea/models/db"
2022-06-13 12:37:59 +03:00
issues_model "code.gitea.io/gitea/models/issues"
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-14 05:57:36 +03:00
"code.gitea.io/gitea/modules/setting"
2022-07-19 16:20:28 +03:00
"code.gitea.io/gitea/modules/util"
2023-09-05 21:37:47 +03:00
notify_service "code.gitea.io/gitea/services/notify"
2019-08-14 18:32:19 +03:00
)
2023-01-18 00:03:44 +03:00
var notEnoughLines = regexp . MustCompile ( ` fatal: file .* has only \d+ lines? ` )
// checkInvalidation checks if the line of code comment got changed by another commit.
// If the line got changed the comment is going to be invalidated.
func checkInvalidation ( ctx context . Context , c * issues_model . Comment , doer * user_model . User , repo * git . Repository , branch string ) error {
// FIXME differentiate between previous and proposed line
commit , err := repo . LineBlame ( branch , repo . Path , c . TreePath , uint ( c . UnsignedLine ( ) ) )
if err != nil && ( strings . Contains ( err . Error ( ) , "fatal: no such path" ) || notEnoughLines . MatchString ( err . Error ( ) ) ) {
c . Invalidated = true
return issues_model . UpdateCommentInvalidate ( ctx , c )
}
if err != nil {
return err
}
if c . CommitSHA != "" && c . CommitSHA != commit . ID . String ( ) {
c . Invalidated = true
return issues_model . UpdateCommentInvalidate ( ctx , c )
}
return nil
}
// InvalidateCodeComments will lookup the prs for code comments which got invalidated by change
func InvalidateCodeComments ( ctx context . Context , prs issues_model . PullRequestList , doer * user_model . User , repo * git . Repository , branch string ) error {
if len ( prs ) == 0 {
return nil
}
issueIDs := prs . GetIssueIDs ( )
2023-11-24 06:49:41 +03:00
codeComments , err := db . Find [ issues_model . Comment ] ( ctx , issues_model . FindCommentsOptions {
2023-01-18 00:03:44 +03:00
ListOptions : db . ListOptions {
ListAll : true ,
} ,
Type : issues_model . CommentTypeCode ,
Invalidated : util . OptionalBoolFalse ,
IssueIDs : issueIDs ,
2023-11-24 06:49:41 +03:00
} )
if err != nil {
2023-01-18 00:03:44 +03:00
return fmt . Errorf ( "find code comments: %v" , err )
}
for _ , comment := range codeComments {
if err := checkInvalidation ( ctx , comment , doer , repo , branch ) ; err != nil {
return err
}
}
return nil
}
2019-11-14 05:57:36 +03:00
// CreateCodeComment creates a comment on the code line
2023-03-04 10:13:37 +03:00
func CreateCodeComment ( ctx context . Context , doer * user_model . User , gitRepo * git . Repository , issue * issues_model . Issue , line int64 , content , treePath string , pendingReview bool , replyReviewID int64 , latestCommitID string ) ( * issues_model . 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
2023-03-04 10:13:37 +03:00
if ! pendingReview && 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
2023-09-29 15:12:54 +03:00
if existsReview , err = issues_model . ReviewExists ( ctx , issue , treePath , line ) ; err != nil {
2019-11-24 08:46:16 +03:00
return nil , err
}
}
// Comments that are replies don't require a review header to show up in the issue view
2023-03-04 10:13:37 +03:00
if ! pendingReview && 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-06-13 12:37:59 +03:00
mentions , err := issues_model . FindAndUpdateIssueMentions ( ctx , issue , doer , comment . Content )
2021-01-02 20:04:02 +03:00
if err != nil {
return nil , err
}
2023-09-05 21:37:47 +03:00
notify_service . CreateIssueComment ( ctx , doer , issue . Repo , issue , comment , mentions )
2019-11-14 05:57:36 +03:00
return comment , nil
}
2022-06-13 12:37:59 +03:00
review , err := issues_model . GetCurrentReview ( ctx , doer , issue )
2019-11-14 05:57:36 +03:00
if err != nil {
2022-06-13 12:37:59 +03:00
if ! issues_model . IsErrReviewNotExist ( err ) {
2019-11-14 05:57:36 +03:00
return nil , err
}
2022-06-13 12:37:59 +03:00
if review , err = issues_model . CreateReview ( ctx , issues_model . CreateReviewOptions {
Type : issues_model . ReviewTypePending ,
2019-11-14 05:57:36 +03:00
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
}
2023-03-04 10:13:37 +03:00
if ! pendingReview && ! existsReview {
2019-11-24 08:46:16 +03:00
// Submit the review we've just created so the comment shows up in the issue view
2022-06-13 12:37:59 +03:00
if _ , _ , err = SubmitReview ( ctx , doer , gitRepo , issue , issues_model . 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
}
// createCodeComment creates a plain code comment at the specified line / path
2022-06-13 12:37:59 +03:00
func createCodeComment ( ctx context . Context , doer * user_model . User , repo * repo_model . Repository , issue * issues_model . Issue , content , treePath string , line , reviewID int64 ) ( * issues_model . Comment , error ) {
2019-11-14 05:57:36 +03:00
var commitID , patch string
2022-11-19 11:12:33 +03:00
if err := issue . LoadPullRequest ( ctx ) ; err != nil {
return nil , fmt . Errorf ( "LoadPullRequest: %w" , err )
2019-11-14 05:57:36 +03:00
}
pr := issue . PullRequest
2022-11-19 11:12:33 +03:00
if err := pr . LoadBaseRepo ( ctx ) ; err != nil {
return nil , fmt . Errorf ( "LoadBaseRepo: %w" , 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-10-24 22:29:17 +03:00
return nil , fmt . Errorf ( "RepositoryFromContextOrOpen: %w" , 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-06-13 12:37:59 +03:00
first , err := issues_model . FindComments ( ctx , & issues_model . FindCommentsOptions {
2020-11-09 09:15:09 +03:00
ReviewID : reviewID ,
Line : line ,
TreePath : treePath ,
2022-06-13 12:37:59 +03:00
Type : issues_model . 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
2022-06-13 12:37:59 +03:00
} else if err != nil && ! issues_model . IsErrCommentNotExist ( err ) {
2022-10-24 22:29:17 +03:00
return nil , fmt . Errorf ( "Find first comment for %d line %d path %s. Error: %w" , reviewID , line , treePath , err )
2020-11-09 09:15:09 +03:00
} else {
2022-06-13 12:37:59 +03:00
review , err := issues_model . GetReviewByID ( ctx , reviewID )
2020-11-09 09:15:09 +03:00
if err == nil && len ( review . CommitID ) > 0 {
head = review . CommitID
2022-06-13 12:37:59 +03:00
} else if err != nil && ! issues_model . IsErrReviewNotExist ( err ) {
2022-10-24 22:29:17 +03:00
return nil , fmt . Errorf ( "GetReviewByID %d. Error: %w" , reviewID , err )
2020-11-09 09:15:09 +03:00
}
}
}
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 ( ) ) ) {
2022-10-24 22:29:17 +03:00
return nil , fmt . Errorf ( "LineBlame[%s, %s, %s, %d]: %w" , pr . GetGitRefName ( ) , gitRepo . Path , treePath , line , err )
2020-11-09 09:15:09 +03:00
}
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 {
2022-10-24 22:29:17 +03:00
return nil , fmt . Errorf ( "GetRefCommitID[%s]: %w" , pr . GetGitRefName ( ) , err )
2020-12-16 12:54:58 +03:00
}
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 {
2022-10-24 22:29:17 +03:00
_ = writer . CloseWithError ( fmt . Errorf ( "GetRawDiffForLine[%s, %s, %s, %s]: %w" , gitRepo . Path , pr . MergeBase , headCommitID , treePath , err ) )
2021-02-27 21:46:14 +03:00
return
}
_ = writer . Close ( )
} ( )
2022-06-13 12:37:59 +03:00
patch , err = git . CutDiffAroundLine ( reader , int64 ( ( & issues_model . Comment { Line : line } ) . UnsignedLine ( ) ) , line < 0 , setting . UI . CodeCommentLines )
2021-02-27 21:46:14 +03:00
if err != nil {
log . Error ( "Error whilst generating patch: %v" , err )
return nil , err
2019-11-14 05:57:36 +03:00
}
}
2023-08-04 16:34:34 +03:00
return issues_model . CreateComment ( ctx , & issues_model . CreateCommentOptions {
2022-06-13 12:37:59 +03:00
Type : issues_model . CommentTypeCode ,
2020-11-09 09:15:09 +03:00
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-06-13 12:37:59 +03:00
func SubmitReview ( ctx context . Context , doer * user_model . User , gitRepo * git . Repository , issue * issues_model . Issue , reviewType issues_model . ReviewType , content , commitID string , attachmentUUIDs [ ] string ) ( * issues_model . Review , * issues_model . Comment , error ) {
2023-10-03 13:30:41 +03:00
pr , err := issue . GetPullRequest ( ctx )
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
2022-06-13 12:37:59 +03:00
if reviewType != issues_model . ReviewTypeApprove && reviewType != issues_model . ReviewTypeReject {
2020-01-09 04:47:45 +03:00
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
}
}
}
2023-09-29 15:12:54 +03:00
review , comm , err := issues_model . SubmitReview ( ctx , 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-06-13 12:37:59 +03:00
mentions , err := issues_model . FindAndUpdateIssueMentions ( ctx , issue , doer , comm . Content )
2021-01-02 20:04:02 +03:00
if err != nil {
return nil , nil , err
}
2023-09-05 21:37:47 +03:00
notify_service . PullRequestReview ( ctx , pr , review , comm , mentions )
2021-01-02 20:04:02 +03:00
for _ , lines := range review . CodeComments {
for _ , comments := range lines {
for _ , codeComment := range comments {
2022-06-13 12:37:59 +03:00
mentions , err := issues_model . FindAndUpdateIssueMentions ( ctx , issue , doer , codeComment . Content )
2021-01-02 20:04:02 +03:00
if err != nil {
return nil , nil , err
}
2023-09-05 21:37:47 +03:00
notify_service . PullRequestCodeComment ( ctx , pr , codeComment , mentions )
2021-01-02 20:04:02 +03:00
}
}
}
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
2023-07-20 10:18:52 +03:00
// DismissApprovalReviews dismiss all approval reviews because of new commits
func DismissApprovalReviews ( ctx context . Context , doer * user_model . User , pull * issues_model . PullRequest ) error {
reviews , err := issues_model . FindReviews ( ctx , issues_model . FindReviewOptions {
ListOptions : db . ListOptions {
ListAll : true ,
} ,
IssueID : pull . IssueID ,
Type : issues_model . ReviewTypeApprove ,
Dismissed : util . OptionalBoolFalse ,
} )
if err != nil {
return err
}
if err := reviews . LoadIssues ( ctx ) ; err != nil {
return err
}
2023-08-28 07:06:47 +03:00
return db . WithTx ( ctx , func ( ctx context . Context ) error {
2023-07-20 10:18:52 +03:00
for _ , review := range reviews {
2023-08-28 07:06:47 +03:00
if err := issues_model . DismissReview ( ctx , review , true ) ; err != nil {
2023-07-20 10:18:52 +03:00
return err
}
2023-08-04 16:34:34 +03:00
comment , err := issues_model . CreateComment ( ctx , & issues_model . CreateCommentOptions {
2023-07-20 10:18:52 +03:00
Doer : doer ,
Content : "New commits pushed, approval review dismissed automatically according to repository settings" ,
Type : issues_model . CommentTypeDismissReview ,
ReviewID : review . ID ,
Issue : review . Issue ,
Repo : review . Issue . Repo ,
} )
if err != nil {
return err
}
comment . Review = review
comment . Poster = doer
comment . Issue = review . Issue
2023-09-05 21:37:47 +03:00
notify_service . PullReviewDismiss ( ctx , doer , review , comment )
2023-07-20 10:18:52 +03:00
}
return nil
} )
}
2021-02-11 20:32:25 +03:00
// DismissReview dismissing stale review by repo admin
2022-07-19 16:20:28 +03:00
func DismissReview ( ctx context . Context , reviewID , repoID int64 , message string , doer * user_model . User , isDismiss , dismissPriors bool ) ( comment * issues_model . Comment , err error ) {
2022-06-13 12:37:59 +03:00
review , err := issues_model . GetReviewByID ( ctx , reviewID )
2021-02-11 20:32:25 +03:00
if err != nil {
2023-07-07 08:31:56 +03:00
return nil , err
2021-02-11 20:32:25 +03:00
}
2022-06-13 12:37:59 +03:00
if review . Type != issues_model . ReviewTypeApprove && review . Type != issues_model . ReviewTypeReject {
2021-02-11 20:32:25 +03:00
return nil , fmt . Errorf ( "not need to dismiss this review because it's type is not Approve or change request" )
}
2022-06-30 18:55:08 +03:00
// load data for notify
2023-07-07 08:31:56 +03:00
if err := review . LoadAttributes ( ctx ) ; err != nil {
2022-06-30 18:55:08 +03:00
return nil , err
}
// Check if the review's repoID is the one we're currently expecting.
if review . Issue . RepoID != repoID {
return nil , fmt . Errorf ( "reviews's repository is not the same as the one we expect" )
}
2023-07-20 10:18:52 +03:00
if err := issues_model . DismissReview ( ctx , review , isDismiss ) ; err != nil {
2023-07-07 08:31:56 +03:00
return nil , err
2021-02-11 20:32:25 +03:00
}
2022-07-19 16:20:28 +03:00
if dismissPriors {
2023-07-20 10:18:52 +03:00
reviews , err := issues_model . FindReviews ( ctx , issues_model . FindReviewOptions {
2022-07-19 16:20:28 +03:00
IssueID : review . IssueID ,
ReviewerID : review . ReviewerID ,
Dismissed : util . OptionalBoolFalse ,
} )
if err != nil {
return nil , err
}
for _ , oldReview := range reviews {
2023-07-20 10:18:52 +03:00
if err = issues_model . DismissReview ( ctx , oldReview , true ) ; err != nil {
2022-07-19 16:20:28 +03:00
return nil , err
}
}
}
2021-02-11 20:32:25 +03:00
if ! isDismiss {
return nil , nil
}
2023-07-07 08:31:56 +03:00
if err := review . Issue . LoadAttributes ( ctx ) ; err != nil {
return nil , err
2021-02-11 20:32:25 +03:00
}
2023-08-04 16:34:34 +03:00
comment , err = issues_model . CreateComment ( ctx , & issues_model . CreateCommentOptions {
2021-02-11 20:32:25 +03:00
Doer : doer ,
Content : message ,
2022-06-13 12:37:59 +03:00
Type : issues_model . CommentTypeDismissReview ,
2021-02-11 20:32:25 +03:00
ReviewID : review . ID ,
Issue : review . Issue ,
Repo : review . Issue . Repo ,
} )
if err != nil {
2023-07-07 08:31:56 +03:00
return nil , err
2021-02-11 20:32:25 +03:00
}
comment . Review = review
comment . Poster = doer
comment . Issue = review . Issue
2023-09-05 21:37:47 +03:00
notify_service . PullReviewDismiss ( ctx , doer , review , comment )
2021-02-11 20:32:25 +03:00
2023-07-07 08:31:56 +03:00
return comment , nil
2021-02-11 20:32:25 +03:00
}