fix(pull): don't add a timeline entry for pending API reviews (#40260)

Pending reviews created through the API went through `SubmitReview`,
which added a timeline entry and sent notifications before the review
was submitted, so submitting it later showed the review twice.

- Pending reviews skip the timeline entry and notifications until
submitted, like on GitHub
- `CreatePullReview` rejects comments without a `path` or a positive
line with 422, like GitHub
- Already duplicated reviews stay as they are

Fixes https://github.com/go-gitea/gitea/issues/40259

Assisted-by: Claude Code:claude-opus-5-5

---------

Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
Lazizbek Ergashev
2026-11-07 14:19:50 +00:00
committed by GitHub
parent 462ab3801a
commit 884884308d
7 changed files with 36 additions and 0 deletions

View File

@@ -545,6 +545,10 @@ func SubmitReview(ctx context.Context, doer *user_model.User, issue *Issue, revi
}
}
if reviewType == ReviewTypePending {
return review, nil, committer.Commit()
}
comm, err := CreateComment(ctx, &CreateCommentOptions{
Type: CommentTypeReview,
Doer: doer,

View File

@@ -86,6 +86,7 @@ type CreatePullReviewOptions struct {
// CreatePullReviewComment represent a review comment for creation api
type CreatePullReviewComment struct {
// the tree path
// required: true
Path string `json:"path"`
Body string `json:"body"`
// if comment to old file line or 0

View File

@@ -487,6 +487,13 @@ func CreatePullReview(ctx *context.APIContext) {
// "$ref": "#/responses/error"
opts := web.GetForm[*api.CreatePullReviewOptions](ctx)
for _, comment := range opts.Comments {
if comment.Path == "" || (comment.NewLineNum <= 0 && comment.OldLineNum <= 0) {
ctx.APIError(http.StatusUnprocessableEntity, "review comments require a path and a positive new_position or old_position")
return
}
}
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorAuto(err)

View File

@@ -332,6 +332,9 @@ func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repos
if err != nil {
return nil, nil, err
}
if reviewType == issues_model.ReviewTypePending {
return review, nil, nil
}
mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, doer, comm.Content)
if err != nil {

View File

@@ -4590,6 +4590,9 @@
"x-go-name": "Path"
}
},
"required": [
"path"
],
"type": "object",
"x-go-package": "gitea.dev/modules/structs"
},

View File

@@ -27027,6 +27027,9 @@
"CreatePullReviewComment": {
"description": "CreatePullReviewComment represent a review comment for creation api",
"type": "object",
"required": [
"path"
],
"properties": {
"body": {
"type": "string",

View File

@@ -113,6 +113,20 @@ func testAPIPullReviewGeneral(t *testing.T) {
assert.EqualValues(t, comment.UpdatedUnix, reviewComments[0].Updated.Unix())
assert.Equal(t, comment.HTMLURL(t.Context()), reviewComments[0].HTMLURL)
for _, invalidComment := range []api.CreatePullReviewComment{
{Body: "missing path", NewLineNum: 1},
{Path: "README.md", Body: "missing line"},
} {
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{
Comments: []api.CreatePullReviewComment{
{Path: "README.md", Body: "comment before invalid anchor", NewLineNum: 1},
invalidComment,
},
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusUnprocessableEntity)
}
unittest.AssertNotExistsBean(t, &issues_model.Comment{IssueID: pullIssue.ID, Content: "comment before invalid anchor"})
// test CreatePullReview
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{
Body: "body1",
@@ -153,6 +167,7 @@ func testAPIPullReviewGeneral(t *testing.T) {
assert.EqualValues(t, "APPROVED", review.State)
assert.Equal(t, 3, review.CodeCommentsCount)
unittest.AssertCount(t, &issues_model.Comment{ReviewID: review.ID, Type: issues_model.CommentTypeReview}, 1)
reviewComment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ReviewID: review.ID, Type: issues_model.CommentTypeReview})
req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d", repo.OwnerName, repo.Name, reviewComment.ID), &api.EditIssueCommentOption{Body: "just one nit"}).AddTokenAuth(getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue))
MakeRequest(t, req, http.StatusOK)