fix(api): allow pending-inline-comment-only reviews (#39433)

Prior to this change, the API rejected reviews without a summary
comment, even if it had pending inline comments. This differs from the
web UI, which accepts such reviews. The affected endpoints are:

1. Submitting via POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}.
2. Creating via POST /repos/{owner}/{repo}/pulls/{index}/reviews, both
when finishing an existing pending review and when creating a new
pending review.

The endpoints ran their own emptiness check, which ignored comments
already in a pending review. The fix drops it for comment and pending
reviews and relies on the model's check, which counts them, as the web
UI does.

A review with neither a body nor inline comments remains invalid.

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: bircni <bircni@icloud.com>
This commit is contained in:
authored and GitHub committed 2026-09-26 16:51:55 +00:00
1 parent 3fac5059de
commit dcef88a233
3 files changed
+35 -30

No files matched your search

+13 -28
View File
@@ -507,7 +507,7 @@ func CreatePullReview(ctx *context.APIContext) {
}
// determine review type
reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body, len(opts.Comments) > 0)
reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body)
if isWrong {
return
}
@@ -562,7 +562,7 @@ func CreatePullReview(ctx *context.APIContext) {
// create review and associate all pending review comments
review, _, err := pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, opts.CommitID, nil)
if err != nil {
if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) {
if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) || issues_model.IsContentEmptyErr(err) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
@@ -634,7 +634,7 @@ func SubmitPullReview(ctx *context.APIContext) {
}
// determine review type
reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body, len(review.Comments) > 0)
reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body)
if isWrong {
return
}
@@ -654,7 +654,7 @@ func SubmitPullReview(ctx *context.APIContext) {
// create review and associate all pending review comments
review, _, err = pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, headCommitID, nil)
if err != nil {
if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) {
if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) || issues_model.IsContentEmptyErr(err) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
@@ -672,16 +672,12 @@ func SubmitPullReview(ctx *context.APIContext) {
}
// preparePullReviewType return ReviewType and false or nil and true if an error happen
func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest, event api.ReviewStateType, body string, hasComments bool) (issues_model.ReviewType, bool) {
func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest, event api.ReviewStateType, body string) (issues_model.ReviewType, bool) {
if err := pr.LoadIssue(ctx); err != nil {
ctx.APIErrorInternal(err)
return -1, true
}
needsBody := true
hasBody := len(strings.TrimSpace(body)) > 0
var reviewType issues_model.ReviewType
switch event {
case api.ReviewStateApproved:
// can not approve your own PR
@@ -689,8 +685,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest
ctx.APIError(http.StatusUnprocessableEntity, "approve your own pull is not allowed")
return -1, true
}
reviewType = issues_model.ReviewTypeApprove
needsBody = false
return issues_model.ReviewTypeApprove, false
case api.ReviewStateRequestChanges:
// can not reject your own PR
@@ -698,27 +693,17 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest
ctx.APIError(http.StatusUnprocessableEntity, "reject your own pull is not allowed")
return -1, true
}
reviewType = issues_model.ReviewTypeReject
case api.ReviewStateComment:
reviewType = issues_model.ReviewTypeComment
needsBody = false
// if there is no body we need to ensure that there are comments
if !hasBody && !hasComments {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("review event %s requires a body or a comment", event))
if strings.TrimSpace(body) == "" {
ctx.APIError(http.StatusUnprocessableEntity, "review event REQUEST_CHANGES requires a body")
return -1, true
}
return issues_model.ReviewTypeReject, false
case api.ReviewStateComment:
return issues_model.ReviewTypeComment, false
default:
reviewType = issues_model.ReviewTypePending
return issues_model.ReviewTypePending, false
}
// reject reviews with empty body if a body is required for this call
if needsBody && !hasBody {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("review event %s requires a body", event))
return -1, true
}
return reviewType, false
}
// prepareSingleReview return review, related pull and false or nil, nil and true if an error happen