2018-08-06 10:13:22 +05:30
// Copyright 2018 The Gitea Authors. All rights reserved.
2022-11-27 23:50:29 +05:30
// SPDX-License-Identifier: MIT
2018-08-06 10:13:22 +05:30
package repo
import (
2022-06-30 21:25:08 +05:30
"errors"
2018-08-06 10:13:22 +05:30
"fmt"
2021-04-05 21:00:52 +05:30
"net/http"
2018-08-06 10:13:22 +05:30
2022-06-13 15:07:59 +05:30
issues_model "code.gitea.io/gitea/models/issues"
2022-05-07 23:58:10 +05:30
pull_model "code.gitea.io/gitea/models/pull"
2021-01-09 03:19:55 +05:30
"code.gitea.io/gitea/modules/base"
2018-08-06 10:13:22 +05:30
"code.gitea.io/gitea/modules/context"
2022-05-07 23:58:10 +05:30
"code.gitea.io/gitea/modules/json"
2018-08-06 10:13:22 +05:30
"code.gitea.io/gitea/modules/log"
2021-06-15 06:42:33 +05:30
"code.gitea.io/gitea/modules/setting"
2021-01-26 21:06:53 +05:30
"code.gitea.io/gitea/modules/web"
2021-04-07 01:14:05 +05:30
"code.gitea.io/gitea/services/forms"
2019-09-27 05:52:36 +05:30
pull_service "code.gitea.io/gitea/services/pull"
2018-08-06 10:13:22 +05:30
)
2021-01-09 03:19:55 +05:30
const (
2024-01-24 08:56:28 +05:30
tplDiffConversation base . TplName = "repo/diff/conversation"
tplTimelineConversation base . TplName = "repo/issue/view_content/conversation"
tplNewComment base . TplName = "repo/diff/new_comment"
2021-01-09 03:19:55 +05:30
)
// RenderNewCodeCommentForm will render the form for creating a new review comment
func RenderNewCodeCommentForm ( ctx * context . Context ) {
issue := GetActionIssue ( ctx )
2023-07-06 00:22:12 +05:30
if ctx . Written ( ) {
return
}
2021-01-09 03:19:55 +05:30
if ! issue . IsPull {
return
}
2022-06-13 15:07:59 +05:30
currentReview , err := issues_model . GetCurrentReview ( ctx , ctx . Doer , issue )
if err != nil && ! issues_model . IsErrReviewNotExist ( err ) {
2021-01-09 03:19:55 +05:30
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 21:00:52 +05:30
ctx . HTML ( http . StatusOK , tplNewComment )
2021-01-09 03:19:55 +05:30
}
2018-08-06 10:13:22 +05:30
// CreateCodeComment will create a code comment including an pending review if required
2021-01-26 21:06:53 +05:30
func CreateCodeComment ( ctx * context . Context ) {
2021-04-07 01:14:05 +05:30
form := web . GetForm ( ctx ) . ( * forms . CodeCommentForm )
2018-08-06 10:13:22 +05:30
issue := GetActionIssue ( ctx )
2023-07-06 00:22:12 +05:30
if ctx . Written ( ) {
2018-08-06 10:13:22 +05:30
return
}
2023-07-06 00:22:12 +05:30
if ! issue . IsPull {
2018-08-06 10:13:22 +05:30
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-14 08:27:36 +05:30
2018-08-06 10:13:22 +05:30
signedLine := form . Line
if form . Side == "previous" {
signedLine *= - 1
}
2022-01-20 04:56:57 +05:30
comment , err := pull_service . CreateCodeComment ( ctx ,
2022-03-22 12:33:22 +05:30
ctx . Doer ,
2020-01-09 07:17:45 +05:30
ctx . Repo . GitRepo ,
2018-08-06 10:13:22 +05:30
issue ,
2019-11-14 08:27:36 +05:30
signedLine ,
2018-08-06 10:13:22 +05:30
form . Content ,
form . TreePath ,
2023-03-04 12:43:37 +05:30
! form . SingleReview ,
2019-11-14 08:27:36 +05:30
form . Reply ,
2020-01-09 07:17:45 +05:30
form . LatestCommitID ,
2018-08-06 10:13:22 +05:30
)
if err != nil {
ctx . ServerError ( "CreateCodeComment" , err )
return
}
2020-03-31 00:22:45 +05:30
if comment == nil {
log . Trace ( "Comment not created: %-v #%d[%d]" , ctx . Repo . Repository , issue . Index , issue . ID )
2019-11-14 08:27:36 +05:30
ctx . Redirect ( fmt . Sprintf ( "%s/pulls/%d/files" , ctx . Repo . RepoLink , issue . Index ) )
2020-03-31 00:22:45 +05:30
return
2019-11-14 08:27:36 +05:30
}
2020-03-31 00:22:45 +05:30
log . Trace ( "Comment created: %-v #%d[%d] Comment[%d]" , ctx . Repo . Repository , issue . Index , issue . ID , comment . ID )
2021-01-09 03:19:55 +05:30
2024-01-24 08:56:28 +05:30
renderConversation ( ctx , comment , form . Origin )
2018-08-06 10:13:22 +05:30
}
2020-04-18 19:20:25 +05:30
// UpdateResolveConversation add or remove an Conversation resolved mark
func UpdateResolveConversation ( ctx * context . Context ) {
2021-08-11 06:01:13 +05:30
origin := ctx . FormString ( "origin" )
action := ctx . FormString ( "action" )
2021-07-29 07:12:15 +05:30
commentID := ctx . FormInt64 ( "comment_id" )
2020-04-18 19:20:25 +05:30
2022-06-13 15:07:59 +05:30
comment , err := issues_model . GetCommentByID ( ctx , commentID )
2020-04-18 19:20:25 +05:30
if err != nil {
ctx . ServerError ( "GetIssueByID" , err )
return
}
2022-11-19 13:42:33 +05:30
if err = comment . LoadIssue ( ctx ) ; err != nil {
2020-04-18 19:20:25 +05:30
ctx . ServerError ( "comment.LoadIssue" , err )
return
}
2022-06-30 21:25:08 +05:30
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 19:20:25 +05:30
var permResult bool
2023-09-29 17:42:54 +05:30
if permResult , err = issues_model . CanMarkConversation ( ctx , comment . Issue , ctx . Doer ) ; err != nil {
2020-04-18 19:20:25 +05:30
ctx . ServerError ( "CanMarkConversation" , err )
return
}
if ! permResult {
2021-04-05 21:00:52 +05:30
ctx . Error ( http . StatusForbidden )
2020-04-18 19:20:25 +05:30
return
}
if ! comment . Issue . IsPull {
2021-04-05 21:00:52 +05:30
ctx . Error ( http . StatusBadRequest )
2020-04-18 19:20:25 +05:30
return
}
if action == "Resolve" || action == "UnResolve" {
2023-09-29 17:42:54 +05:30
err = issues_model . MarkConversation ( ctx , comment , ctx . Doer , action == "Resolve" )
2020-04-18 19:20:25 +05:30
if err != nil {
ctx . ServerError ( "MarkConversation" , err )
return
}
} else {
2021-04-05 21:00:52 +05:30
ctx . Error ( http . StatusBadRequest )
2020-04-18 19:20:25 +05:30
return
}
2024-01-24 08:56:28 +05:30
renderConversation ( ctx , comment , origin )
2020-04-18 19:20:25 +05:30
}
2024-01-24 08:56:28 +05:30
func renderConversation ( ctx * context . Context , comment * issues_model . Comment , origin string ) {
2024-02-04 18:35:01 +05:30
ctx . Data [ "PageIsPullFiles" ] = origin == "diff"
2023-06-21 21:38:12 +05:30
comments , err := issues_model . FetchCodeCommentsByLine ( ctx , comment . Issue , ctx . Doer , comment . TreePath , comment . Line , ctx . Data [ "ShowOutdatedComments" ] . ( bool ) )
2021-01-09 03:19:55 +05:30
if err != nil {
ctx . ServerError ( "FetchCodeCommentsByLine" , err )
return
}
2024-02-04 18:35:01 +05:30
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-09 03:19:55 +05:30
ctx . Data [ "comments" ] = comments
2024-01-24 08:56:28 +05:30
if ctx . Data [ "CanMarkConversation" ] , err = issues_model . CanMarkConversation ( ctx , comment . Issue , ctx . Doer ) ; err != nil {
ctx . ServerError ( "CanMarkConversation" , err )
return
}
2021-01-09 03:19:55 +05:30
ctx . Data [ "Issue" ] = comment . Issue
2022-11-19 13:42:33 +05:30
if err = comment . Issue . LoadPullRequest ( ctx ) ; err != nil {
2021-01-09 03:19:55 +05:30
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-24 08:56:28 +05:30
if origin == "diff" {
ctx . HTML ( http . StatusOK , tplDiffConversation )
} else if origin == "timeline" {
ctx . HTML ( http . StatusOK , tplTimelineConversation )
2024-02-04 18:35:01 +05:30
} else {
ctx . Error ( http . StatusBadRequest , "Unknown origin: " + origin )
2024-01-24 08:56:28 +05:30
}
2021-01-09 03:19:55 +05:30
}
2018-08-06 10:13:22 +05:30
// SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
2021-01-26 21:06:53 +05:30
func SubmitReview ( ctx * context . Context ) {
2021-04-07 01:14:05 +05:30
form := web . GetForm ( ctx ) . ( * forms . SubmitReviewForm )
2018-08-06 10:13:22 +05:30
issue := GetActionIssue ( ctx )
2023-07-06 00:22:12 +05:30
if ctx . Written ( ) {
2018-08-06 10:13:22 +05:30
return
}
2023-07-06 00:22:12 +05:30
if ! issue . IsPull {
2018-08-06 10:13:22 +05:30
return
}
if ctx . HasError ( ) {
ctx . Flash . Error ( ctx . Data [ "ErrorMsg" ] . ( string ) )
2023-06-14 13:31:37 +05:30
ctx . JSONRedirect ( fmt . Sprintf ( "%s/pulls/%d/files" , ctx . Repo . RepoLink , issue . Index ) )
2018-08-06 10:13:22 +05:30
return
}
reviewType := form . ReviewType ( )
2018-08-20 10:34:01 +05:30
switch reviewType {
2022-06-13 15:07:59 +05:30
case issues_model . ReviewTypeUnknown :
2019-11-14 08:27:36 +05:30
ctx . ServerError ( "ReviewType" , fmt . Errorf ( "unknown ReviewType: %s" , form . Type ) )
2018-08-06 10:13:22 +05:30
return
2018-08-20 10:34:01 +05:30
// can not approve/reject your own PR
2022-06-13 15:07:59 +05:30
case issues_model . ReviewTypeApprove , issues_model . ReviewTypeReject :
2022-03-22 12:33:22 +05:30
if issue . IsPoster ( ctx . Doer . ID ) {
2018-08-20 10:34:01 +05:30
var translated string
2022-06-13 15:07:59 +05:30
if reviewType == issues_model . ReviewTypeApprove {
2018-08-20 10:34:01 +05:30
translated = ctx . Tr ( "repo.issues.review.self.approval" )
} else {
translated = ctx . Tr ( "repo.issues.review.self.rejection" )
}
ctx . Flash . Error ( translated )
2023-06-14 13:31:37 +05:30
ctx . JSONRedirect ( fmt . Sprintf ( "%s/pulls/%d/files" , ctx . Repo . RepoLink , issue . Index ) )
2018-08-20 10:34:01 +05:30
return
}
2018-08-06 10:13:22 +05:30
}
2018-08-07 22:45:41 +05:30
2021-06-15 06:42:33 +05:30
var attachments [ ] string
if setting . Attachment . Enabled {
attachments = form . Files
}
2022-03-22 12:33:22 +05:30
_ , comm , err := pull_service . SubmitReview ( ctx , ctx . Doer , ctx . Repo . GitRepo , issue , reviewType , form . Content , form . CommitID , attachments )
2018-08-06 10:13:22 +05:30
if err != nil {
2022-06-13 15:07:59 +05:30
if issues_model . IsContentEmptyErr ( err ) {
2019-11-14 08:27:36 +05:30
ctx . Flash . Error ( ctx . Tr ( "repo.issues.review.content.empty" ) )
2023-06-14 13:31:37 +05:30
ctx . JSONRedirect ( fmt . Sprintf ( "%s/pulls/%d/files" , ctx . Repo . RepoLink , issue . Index ) )
2019-11-14 08:27:36 +05:30
} else {
ctx . ServerError ( "SubmitReview" , err )
2018-08-06 10:13:22 +05:30
}
2018-10-18 16:53:05 +05:30
return
}
2023-06-14 13:31:37 +05:30
ctx . JSONRedirect ( fmt . Sprintf ( "%s/pulls/%d#%s" , ctx . Repo . RepoLink , issue . Index , comm . HashTag ( ) ) )
2018-08-06 10:13:22 +05:30
}
2021-02-11 23:02:25 +05:30
// DismissReview dismissing stale review by repo admin
func DismissReview ( ctx * context . Context ) {
2021-04-07 01:14:05 +05:30
form := web . GetForm ( ctx ) . ( * forms . DismissReviewForm )
2022-07-19 18:50:28 +05:30
comm , err := pull_service . DismissReview ( ctx , form . ReviewID , ctx . Repo . Repository . ID , form . Message , ctx . Doer , true , true )
2021-02-11 23:02:25 +05:30
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 23:58:10 +05:30
// 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-07 09:13:18 +05:30
issue , ok := getPullInfo ( ctx )
if ! ok {
2022-05-07 23:58:10 +05:30
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 )
}
}