2018-08-05 23:43:22 -05:00
// Copyright 2018 The Gitea Authors. All rights reserved.
2022-11-27 13:20:29 -05:00
// SPDX-License-Identifier: MIT
2018-08-05 23:43:22 -05:00
package repo
import (
2022-06-30 10:55:08 -05:00
"errors"
2018-08-05 23:43:22 -05:00
"fmt"
2021-04-05 10:30:52 -05:00
"net/http"
2018-08-05 23:43:22 -05:00
2022-06-13 04:37:59 -05:00
issues_model "code.gitea.io/gitea/models/issues"
2022-05-07 13:28:10 -05:00
pull_model "code.gitea.io/gitea/models/pull"
2021-01-08 16:49:55 -05:00
"code.gitea.io/gitea/modules/base"
2018-08-05 23:43:22 -05:00
"code.gitea.io/gitea/modules/context"
2022-05-07 13:28:10 -05:00
"code.gitea.io/gitea/modules/json"
2018-08-05 23:43:22 -05:00
"code.gitea.io/gitea/modules/log"
2021-06-14 20:12:33 -05:00
"code.gitea.io/gitea/modules/setting"
2021-01-26 10:36:53 -05:00
"code.gitea.io/gitea/modules/web"
2021-04-06 14:44:05 -05:00
"code.gitea.io/gitea/services/forms"
2019-09-26 19:22:36 -05:00
pull_service "code.gitea.io/gitea/services/pull"
2018-08-05 23:43:22 -05:00
)
2021-01-08 16:49:55 -05:00
const (
2024-01-23 22:26:28 -05:00
tplDiffConversation base . TplName = "repo/diff/conversation"
tplTimelineConversation base . TplName = "repo/issue/view_content/conversation"
tplNewComment base . TplName = "repo/diff/new_comment"
2021-01-08 16:49:55 -05:00
)
// RenderNewCodeCommentForm will render the form for creating a new review comment
func RenderNewCodeCommentForm ( ctx * context . Context ) {
issue := GetActionIssue ( ctx )
2023-07-05 13:52:12 -05:00
if ctx . Written ( ) {
return
}
2021-01-08 16:49:55 -05:00
if ! issue . IsPull {
return
}
2022-06-13 04:37:59 -05:00
currentReview , err := issues_model . GetCurrentReview ( ctx , ctx . Doer , issue )
if err != nil && ! issues_model . IsErrReviewNotExist ( err ) {
2021-01-08 16:49:55 -05:00
ctx . ServerError ( "GetCurrentReview" , err )
return
}
ctx . Data [ "PageIsPullFiles" ] = true
ctx . Data [ "Issue" ] = issue
ctx . Data [ "CurrentReview" ] = currentReview
pullHeadCommitID , err := ctx . Repo . GitRepo . GetRefCommitID ( issue . PullRequest . GetGitRefName ( ) )
if err != nil {
ctx . ServerError ( "GetRefCommitID" , err )
return
}
ctx . Data [ "AfterCommitID" ] = pullHeadCommitID
2021-04-05 10:30:52 -05:00
ctx . HTML ( http . StatusOK , tplNewComment )
2021-01-08 16:49:55 -05:00
}
2018-08-05 23:43:22 -05:00
// CreateCodeComment will create a code comment including an pending review if required
2021-01-26 10:36:53 -05:00
func CreateCodeComment ( ctx * context . Context ) {
2021-04-06 14:44:05 -05:00
form := web . GetForm ( ctx ) . ( * forms . CodeCommentForm )
2018-08-05 23:43:22 -05:00
issue := GetActionIssue ( ctx )
2023-07-05 13:52:12 -05:00
if ctx . Written ( ) {
2018-08-05 23:43:22 -05:00
return
}
2023-07-05 13:52:12 -05:00
if ! issue . IsPull {
2018-08-05 23:43:22 -05:00
return
}
if ctx . HasError ( ) {
ctx . Flash . Error ( ctx . Data [ "ErrorMsg" ] . ( string ) )
ctx . Redirect ( fmt . Sprintf ( "%s/pulls/%d/files" , ctx . Repo . RepoLink , issue . Index ) )
return
}
2019-11-13 21:57:36 -05:00
2018-08-05 23:43:22 -05:00
signedLine := form . Line
if form . Side == "previous" {
signedLine *= - 1
}
2022-01-19 18:26:57 -05:00
comment , err := pull_service . CreateCodeComment ( ctx ,
2022-03-22 02:03:22 -05:00
ctx . Doer ,
2020-01-08 20:47:45 -05:00
ctx . Repo . GitRepo ,
2018-08-05 23:43:22 -05:00
issue ,
2019-11-13 21:57:36 -05:00
signedLine ,
2018-08-05 23:43:22 -05:00
form . Content ,
form . TreePath ,
2023-03-04 02:13:37 -05:00
! form . SingleReview ,
2019-11-13 21:57:36 -05:00
form . Reply ,
2020-01-08 20:47:45 -05:00
form . LatestCommitID ,
2018-08-05 23:43:22 -05:00
)
if err != nil {
ctx . ServerError ( "CreateCodeComment" , err )
return
}
2020-03-30 13:52:45 -05:00
if comment == nil {
log . Trace ( "Comment not created: %-v #%d[%d]" , ctx . Repo . Repository , issue . Index , issue . ID )
2019-11-13 21:57:36 -05:00
ctx . Redirect ( fmt . Sprintf ( "%s/pulls/%d/files" , ctx . Repo . RepoLink , issue . Index ) )
2020-03-30 13:52:45 -05:00
return
2019-11-13 21:57:36 -05:00
}
2020-03-30 13:52:45 -05:00
log . Trace ( "Comment created: %-v #%d[%d] Comment[%d]" , ctx . Repo . Repository , issue . Index , issue . ID , comment . ID )
2021-01-08 16:49:55 -05:00
2024-01-23 22:26:28 -05:00
renderConversation ( ctx , comment , form . Origin )
2018-08-05 23:43:22 -05:00
}
2020-04-18 08:50:25 -05:00
// UpdateResolveConversation add or remove an Conversation resolved mark
func UpdateResolveConversation ( ctx * context . Context ) {
2021-08-10 19:31:13 -05:00
origin := ctx . FormString ( "origin" )
action := ctx . FormString ( "action" )
2021-07-28 20:42:15 -05:00
commentID := ctx . FormInt64 ( "comment_id" )
2020-04-18 08:50:25 -05:00
2022-06-13 04:37:59 -05:00
comment , err := issues_model . GetCommentByID ( ctx , commentID )
2020-04-18 08:50:25 -05:00
if err != nil {
ctx . ServerError ( "GetIssueByID" , err )
return
}
2022-11-19 03:12:33 -05:00
if err = comment . LoadIssue ( ctx ) ; err != nil {
2020-04-18 08:50:25 -05:00
ctx . ServerError ( "comment.LoadIssue" , err )
return
}
2022-06-30 10:55:08 -05:00
if comment . Issue . RepoID != ctx . Repo . Repository . ID {
ctx . NotFound ( "comment's repoID is incorrect" , errors . New ( "comment's repoID is incorrect" ) )
return
}
2020-04-18 08:50:25 -05:00
var permResult bool
2023-09-29 07:12:54 -05:00
if permResult , err = issues_model . CanMarkConversation ( ctx , comment . Issue , ctx . Doer ) ; err != nil {
2020-04-18 08:50:25 -05:00
ctx . ServerError ( "CanMarkConversation" , err )
return
}
if ! permResult {
2021-04-05 10:30:52 -05:00
ctx . Error ( http . StatusForbidden )
2020-04-18 08:50:25 -05:00
return
}
if ! comment . Issue . IsPull {
2021-04-05 10:30:52 -05:00
ctx . Error ( http . StatusBadRequest )
2020-04-18 08:50:25 -05:00
return
}
if action == "Resolve" || action == "UnResolve" {
2023-09-29 07:12:54 -05:00
err = issues_model . MarkConversation ( ctx , comment , ctx . Doer , action == "Resolve" )
2020-04-18 08:50:25 -05:00
if err != nil {
ctx . ServerError ( "MarkConversation" , err )
return
}
} else {
2021-04-05 10:30:52 -05:00
ctx . Error ( http . StatusBadRequest )
2020-04-18 08:50:25 -05:00
return
}
2024-01-23 22:26:28 -05:00
renderConversation ( ctx , comment , origin )
2020-04-18 08:50:25 -05:00
}
2024-01-23 22:26:28 -05:00
func renderConversation ( ctx * context . Context , comment * issues_model . Comment , origin string ) {
2024-02-04 08:05:01 -05:00
ctx . Data [ "PageIsPullFiles" ] = origin == "diff"
2023-06-21 11:08:12 -05:00
comments , err := issues_model . FetchCodeCommentsByLine ( ctx , comment . Issue , ctx . Doer , comment . TreePath , comment . Line , ctx . Data [ "ShowOutdatedComments" ] . ( bool ) )
2021-01-08 16:49:55 -05:00
if err != nil {
ctx . ServerError ( "FetchCodeCommentsByLine" , err )
return
}
2024-02-04 08:05:01 -05:00
if len ( comments ) == 0 {
// if the comments are empty (deleted, outdated, etc), it doesn't need to render anything, just return an empty body to replace "conversation-holder" on the page
ctx . Resp . WriteHeader ( http . StatusOK )
return
}
2021-01-08 16:49:55 -05:00
ctx . Data [ "comments" ] = comments
2024-01-23 22:26:28 -05:00
if ctx . Data [ "CanMarkConversation" ] , err = issues_model . CanMarkConversation ( ctx , comment . Issue , ctx . Doer ) ; err != nil {
ctx . ServerError ( "CanMarkConversation" , err )
return
}
2021-01-08 16:49:55 -05:00
ctx . Data [ "Issue" ] = comment . Issue
2022-11-19 03:12:33 -05:00
if err = comment . Issue . LoadPullRequest ( ctx ) ; err != nil {
2021-01-08 16:49:55 -05:00
ctx . ServerError ( "comment.Issue.LoadPullRequest" , err )
return
}
pullHeadCommitID , err := ctx . Repo . GitRepo . GetRefCommitID ( comment . Issue . PullRequest . GetGitRefName ( ) )
if err != nil {
ctx . ServerError ( "GetRefCommitID" , err )
return
}
ctx . Data [ "AfterCommitID" ] = pullHeadCommitID
2024-01-23 22:26:28 -05:00
if origin == "diff" {
ctx . HTML ( http . StatusOK , tplDiffConversation )
} else if origin == "timeline" {
ctx . HTML ( http . StatusOK , tplTimelineConversation )
2024-02-04 08:05:01 -05:00
} else {
ctx . Error ( http . StatusBadRequest , "Unknown origin: " + origin )
2024-01-23 22:26:28 -05:00
}
2021-01-08 16:49:55 -05:00
}
2018-08-05 23:43:22 -05:00
// SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
2021-01-26 10:36:53 -05:00
func SubmitReview ( ctx * context . Context ) {
2021-04-06 14:44:05 -05:00
form := web . GetForm ( ctx ) . ( * forms . SubmitReviewForm )
2018-08-05 23:43:22 -05:00
issue := GetActionIssue ( ctx )
2023-07-05 13:52:12 -05:00
if ctx . Written ( ) {
2018-08-05 23:43:22 -05:00
return
}
2023-07-05 13:52:12 -05:00
if ! issue . IsPull {
2018-08-05 23:43:22 -05:00
return
}
if ctx . HasError ( ) {
ctx . Flash . Error ( ctx . Data [ "ErrorMsg" ] . ( string ) )
2023-06-14 03:01:37 -05:00
ctx . JSONRedirect ( fmt . Sprintf ( "%s/pulls/%d/files" , ctx . Repo . RepoLink , issue . Index ) )
2018-08-05 23:43:22 -05:00
return
}
reviewType := form . ReviewType ( )
2018-08-20 00:04:01 -05:00
switch reviewType {
2022-06-13 04:37:59 -05:00
case issues_model . ReviewTypeUnknown :
2019-11-13 21:57:36 -05:00
ctx . ServerError ( "ReviewType" , fmt . Errorf ( "unknown ReviewType: %s" , form . Type ) )
2018-08-05 23:43:22 -05:00
return
2018-08-20 00:04:01 -05:00
// can not approve/reject your own PR
2022-06-13 04:37:59 -05:00
case issues_model . ReviewTypeApprove , issues_model . ReviewTypeReject :
2022-03-22 02:03:22 -05:00
if issue . IsPoster ( ctx . Doer . ID ) {
2018-08-20 00:04:01 -05:00
var translated string
2022-06-13 04:37:59 -05:00
if reviewType == issues_model . ReviewTypeApprove {
2018-08-20 00:04:01 -05:00
translated = ctx . Tr ( "repo.issues.review.self.approval" )
} else {
translated = ctx . Tr ( "repo.issues.review.self.rejection" )
}
ctx . Flash . Error ( translated )
2023-06-14 03:01:37 -05:00
ctx . JSONRedirect ( fmt . Sprintf ( "%s/pulls/%d/files" , ctx . Repo . RepoLink , issue . Index ) )
2018-08-20 00:04:01 -05:00
return
}
2018-08-05 23:43:22 -05:00
}
2018-08-07 12:15:41 -05:00
2021-06-14 20:12:33 -05:00
var attachments [ ] string
if setting . Attachment . Enabled {
attachments = form . Files
}
2022-03-22 02:03:22 -05:00
_ , comm , err := pull_service . SubmitReview ( ctx , ctx . Doer , ctx . Repo . GitRepo , issue , reviewType , form . Content , form . CommitID , attachments )
2018-08-05 23:43:22 -05:00
if err != nil {
2022-06-13 04:37:59 -05:00
if issues_model . IsContentEmptyErr ( err ) {
2019-11-13 21:57:36 -05:00
ctx . Flash . Error ( ctx . Tr ( "repo.issues.review.content.empty" ) )
2023-06-14 03:01:37 -05:00
ctx . JSONRedirect ( fmt . Sprintf ( "%s/pulls/%d/files" , ctx . Repo . RepoLink , issue . Index ) )
2019-11-13 21:57:36 -05:00
} else {
ctx . ServerError ( "SubmitReview" , err )
2018-08-05 23:43:22 -05:00
}
2018-10-18 06:23:05 -05:00
return
}
2023-06-14 03:01:37 -05:00
ctx . JSONRedirect ( fmt . Sprintf ( "%s/pulls/%d#%s" , ctx . Repo . RepoLink , issue . Index , comm . HashTag ( ) ) )
2018-08-05 23:43:22 -05:00
}
2021-02-11 12:32:25 -05:00
// DismissReview dismissing stale review by repo admin
func DismissReview ( ctx * context . Context ) {
2021-04-06 14:44:05 -05:00
form := web . GetForm ( ctx ) . ( * forms . DismissReviewForm )
2022-07-19 08:20:28 -05:00
comm , err := pull_service . DismissReview ( ctx , form . ReviewID , ctx . Repo . Repository . ID , form . Message , ctx . Doer , true , true )
2021-02-11 12:32:25 -05:00
if err != nil {
ctx . ServerError ( "pull_service.DismissReview" , err )
return
}
ctx . Redirect ( fmt . Sprintf ( "%s/pulls/%d#%s" , ctx . Repo . RepoLink , comm . Issue . Index , comm . HashTag ( ) ) )
}
2022-05-07 13:28:10 -05:00
// viewedFilesUpdate Struct to parse the body of a request to update the reviewed files of a PR
// If you want to implement an API to update the review, simply move this struct into modules.
type viewedFilesUpdate struct {
Files map [ string ] bool ` json:"files" `
HeadCommitSHA string ` json:"headCommitSHA" `
}
func UpdateViewedFiles ( ctx * context . Context ) {
// Find corresponding PR
2023-08-06 22:43:18 -05:00
issue , ok := getPullInfo ( ctx )
if ! ok {
2022-05-07 13:28:10 -05:00
return
}
pull := issue . PullRequest
var data * viewedFilesUpdate
err := json . NewDecoder ( ctx . Req . Body ) . Decode ( & data )
if err != nil {
log . Warn ( "Attempted to update a review but could not parse request body: %v" , err )
ctx . Resp . WriteHeader ( http . StatusBadRequest )
return
}
// Expect the review to have been now if no head commit was supplied
if data . HeadCommitSHA == "" {
data . HeadCommitSHA = pull . HeadCommitID
}
updatedFiles := make ( map [ string ] pull_model . ViewedState , len ( data . Files ) )
for file , viewed := range data . Files {
// Only unviewed and viewed are possible, has-changed can not be set from the outside
state := pull_model . Unviewed
if viewed {
state = pull_model . Viewed
}
updatedFiles [ file ] = state
}
if err := pull_model . UpdateReviewState ( ctx , ctx . Doer . ID , pull . ID , data . HeadCommitSHA , updatedFiles ) ; err != nil {
ctx . ServerError ( "UpdateReview" , err )
}
}