[GITEA] POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments

Refs: https://codeberg.org/forgejo/forgejo/issues/2109
This commit is contained in:
Earl Warren 2024-01-09 12:49:18 +01:00
parent 5464ec4ad2
commit 8b4ba3dce7
No known key found for this signature in database
GPG key ID: 0579CB2928A78A00
7 changed files with 298 additions and 21 deletions

View file

@ -89,6 +89,9 @@ type CreatePullReviewComment struct {
NewLineNum int64 `json:"new_position"` NewLineNum int64 `json:"new_position"`
} }
// CreatePullReviewCommentOptions are options to create a pull review comment
type CreatePullReviewCommentOptions CreatePullReviewComment
// SubmitPullReviewOptions are options to submit a pending pull review // SubmitPullReviewOptions are options to submit a pending pull review
type SubmitPullReviewOptions struct { type SubmitPullReviewOptions struct {
Event ReviewStateType `json:"event"` Event ReviewStateType `json:"event"`

View file

@ -1217,7 +1217,8 @@ func Routes() *web.Route {
Delete(reqToken(), repo.DeletePullReview). Delete(reqToken(), repo.DeletePullReview).
Post(reqToken(), bind(api.SubmitPullReviewOptions{}), repo.SubmitPullReview) Post(reqToken(), bind(api.SubmitPullReviewOptions{}), repo.SubmitPullReview)
m.Combo("/comments"). m.Combo("/comments").
Get(repo.GetPullReviewComments) Get(repo.GetPullReviewComments).
Post(reqToken(), bind(api.CreatePullReviewCommentOptions{}), repo.CreatePullReviewComment)
m.Post("/dismissals", reqToken(), bind(api.DismissPullReviewOptions{}), repo.DismissPullReview) m.Post("/dismissals", reqToken(), bind(api.DismissPullReviewOptions{}), repo.DismissPullReview)
m.Post("/undismissals", reqToken(), repo.UnDismissPullReview) m.Post("/undismissals", reqToken(), repo.UnDismissPullReview)
}) })

View file

@ -208,6 +208,97 @@ func GetPullReviewComments(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, apiComments) ctx.JSON(http.StatusOK, apiComments)
} }
// CreatePullReviewComments add a new comment to a pull request review
func CreatePullReviewComment(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments repository repoCreatePullReviewComment
// ---
// summary: Add a new comment to a pull request review
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the pull request
// type: integer
// format: int64
// required: true
// - name: id
// in: path
// description: id of the review
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/CreatePullReviewCommentOptions"
// responses:
// "200":
// "$ref": "#/responses/PullReviewComment"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
opts := web.GetForm(ctx).(*api.CreatePullReviewCommentOptions)
review, pr, statusSet := prepareSingleReview(ctx)
if statusSet {
return
}
if err := pr.Issue.LoadRepo(ctx); err != nil {
ctx.InternalServerError(err)
return
}
line := opts.NewLineNum
if opts.OldLineNum > 0 {
line = opts.OldLineNum * -1
}
comment, err := pull_service.CreateCodeComment(ctx,
ctx.Doer,
ctx.Repo.GitRepo,
pr.Issue,
line,
opts.Body,
opts.Path,
// as of e522e774cae2240279fc48c349fc513c9d3353ee
// isPending is not needed because review.ID is always available
// and does not need to be discovered implicitly
false,
review.ID,
// as of e522e774cae2240279fc48c349fc513c9d3353ee
// latestCommitID is not needed because it is only used to
// create a new review in case it does not already exist
"",
)
if err != nil {
ctx.InternalServerError(err)
return
}
apiComment, err := convert.ToPullReviewComment(ctx, review, comment, ctx.Doer)
if err != nil {
ctx.InternalServerError(err)
return
}
ctx.JSON(http.StatusOK, apiComment)
}
// DeletePullReview delete a specific review from a pull request // DeletePullReview delete a specific review from a pull request
func DeletePullReview(ctx *context.APIContext) { func DeletePullReview(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/reviews/{id} repository repoDeletePullReview // swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/reviews/{id} repository repoDeletePullReview

View file

@ -161,6 +161,9 @@ type swaggerParameterBodies struct {
// in:body // in:body
CreatePullReviewComment api.CreatePullReviewComment CreatePullReviewComment api.CreatePullReviewComment
// in:body
CreatePullReviewCommentOptions api.CreatePullReviewCommentOptions
// in:body // in:body
SubmitPullReviewOptions api.SubmitPullReviewOptions SubmitPullReviewOptions api.SubmitPullReviewOptions

View file

@ -76,6 +76,33 @@ func ToPullReviewList(ctx context.Context, rl []*issues_model.Review, doer *user
return result, nil return result, nil
} }
// ToPullReviewCommentList convert the CodeComments of an review to it's api format
func ToPullReviewComment(ctx context.Context, review *issues_model.Review, comment *issues_model.Comment, doer *user_model.User) (*api.PullReviewComment, error) {
apiComment := &api.PullReviewComment{
ID: comment.ID,
Body: comment.Content,
Poster: ToUser(ctx, comment.Poster, doer),
Resolver: ToUser(ctx, comment.ResolveDoer, doer),
ReviewID: review.ID,
Created: comment.CreatedUnix.AsTime(),
Updated: comment.UpdatedUnix.AsTime(),
Path: comment.TreePath,
CommitID: comment.CommitSHA,
OrigCommitID: comment.OldRef,
DiffHunk: patch2diff(comment.Patch),
HTMLURL: comment.HTMLURL(ctx),
HTMLPullURL: review.Issue.HTMLURL(),
}
if comment.Line < 0 {
apiComment.OldLineNum = comment.UnsignedLine()
} else {
apiComment.LineNum = comment.UnsignedLine()
}
return apiComment, nil
}
// ToPullReviewCommentList convert the CodeComments of an review to it's api format // ToPullReviewCommentList convert the CodeComments of an review to it's api format
func ToPullReviewCommentList(ctx context.Context, review *issues_model.Review, doer *user_model.User) ([]*api.PullReviewComment, error) { func ToPullReviewCommentList(ctx context.Context, review *issues_model.Review, doer *user_model.User) ([]*api.PullReviewComment, error) {
if err := review.LoadAttributes(ctx); err != nil { if err := review.LoadAttributes(ctx); err != nil {
@ -90,26 +117,9 @@ func ToPullReviewCommentList(ctx context.Context, review *issues_model.Review, d
for _, lines := range review.CodeComments { for _, lines := range review.CodeComments {
for _, comments := range lines { for _, comments := range lines {
for _, comment := range comments { for _, comment := range comments {
apiComment := &api.PullReviewComment{ apiComment, err := ToPullReviewComment(ctx, review, comment, doer)
ID: comment.ID, if err != nil {
Body: comment.Content, return nil, err
Poster: ToUser(ctx, comment.Poster, doer),
Resolver: ToUser(ctx, comment.ResolveDoer, doer),
ReviewID: review.ID,
Created: comment.CreatedUnix.AsTime(),
Updated: comment.UpdatedUnix.AsTime(),
Path: comment.TreePath,
CommitID: comment.CommitSHA,
OrigCommitID: comment.OldRef,
DiffHunk: patch2diff(comment.Patch),
HTMLURL: comment.HTMLURL(ctx),
HTMLPullURL: review.Issue.HTMLURL(),
}
if comment.Line < 0 {
apiComment.OldLineNum = comment.UnsignedLine()
} else {
apiComment.LineNum = comment.UnsignedLine()
} }
apiComments = append(apiComments, apiComment) apiComments = append(apiComments, apiComment)
} }

View file

@ -11539,6 +11539,67 @@
"$ref": "#/responses/notFound" "$ref": "#/responses/notFound"
} }
} }
},
"post": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Add a new comment to a pull request review",
"operationId": "repoCreatePullReviewComment",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "integer",
"format": "int64",
"description": "index of the pull request",
"name": "index",
"in": "path",
"required": true
},
{
"type": "integer",
"format": "int64",
"description": "id of the review",
"name": "id",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/CreatePullReviewCommentOptions"
}
}
],
"responses": {
"200": {
"$ref": "#/responses/PullReviewComment"
},
"404": {
"$ref": "#/responses/notFound"
},
"422": {
"$ref": "#/responses/validationError"
}
}
} }
}, },
"/repos/{owner}/{repo}/pulls/{index}/reviews/{id}/dismissals": { "/repos/{owner}/{repo}/pulls/{index}/reviews/{id}/dismissals": {
@ -18517,6 +18578,10 @@
}, },
"x-go-package": "code.gitea.io/gitea/modules/structs" "x-go-package": "code.gitea.io/gitea/modules/structs"
}, },
"CreatePullReviewCommentOptions": {
"description": "CreatePullReviewCommentOptions are options to create a pull review comment",
"$ref": "#/definitions/CreatePullReviewComment"
},
"CreatePullReviewOptions": { "CreatePullReviewOptions": {
"description": "CreatePullReviewOptions are options to create a pull review", "description": "CreatePullReviewOptions are options to create a pull review",
"type": "object", "type": "object",

View file

@ -18,8 +18,112 @@ import (
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestAPIPullReviewCreateComment(t *testing.T) {
defer tests.PrepareTestEnv(t)()
pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
assert.NoError(t, pullIssue.LoadAttributes(db.DefaultContext))
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue.RepoID})
username := "user2"
session := loginUser(t, username)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
// as of e522e774cae2240279fc48c349fc513c9d3353ee
// There should be no reason for CreateComment to behave differently
// depending on the event associated with the review. But the logic of the implementation
// at this point in time is very involved and deserves these seemingly redundant
// test.
for _, event := range []api.ReviewStateType{
api.ReviewStatePending,
api.ReviewStateRequestChanges,
api.ReviewStateApproved,
api.ReviewStateComment,
} {
t.Run("Event_"+string(event), func(t *testing.T) {
path := "README.md"
var review api.PullReview
existingCommentBody := "existing comment body"
var reviewLine int64 = 1
{
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/pulls/%d/reviews", repo.FullName(), pullIssue.Index), &api.CreatePullReviewOptions{
Body: "body1",
Event: event,
Comments: []api.CreatePullReviewComment{
{
Path: path,
Body: existingCommentBody,
OldLineNum: reviewLine,
},
},
}).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &review)
require.EqualValues(t, string(event), review.State)
require.EqualValues(t, 1, review.CodeCommentsCount)
}
{
req := NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/pulls/%d/reviews/%d", repo.FullName(), pullIssue.Index, review.ID).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var getReview api.PullReview
DecodeJSON(t, resp, &getReview)
require.EqualValues(t, getReview, review)
}
{
req := NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/pulls/%d/reviews/%d/comments", repo.FullName(), pullIssue.Index, review.ID).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var reviewComments []*api.PullReviewComment
DecodeJSON(t, resp, &reviewComments)
require.Len(t, reviewComments, 1)
assert.EqualValues(t, username, reviewComments[0].Poster.UserName)
assert.EqualValues(t, existingCommentBody, reviewComments[0].Body)
}
newCommentBody := "first new line"
{
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/pulls/%d/reviews/%d/comments", repo.FullName(), pullIssue.Index, review.ID), &api.CreatePullReviewCommentOptions{
Path: path,
Body: newCommentBody,
OldLineNum: reviewLine,
}).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var reviewComment *api.PullReviewComment
DecodeJSON(t, resp, &reviewComment)
assert.EqualValues(t, review.ID, reviewComment.ReviewID)
}
{
req := NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/pulls/%d/reviews/%d/comments", repo.FullName(), pullIssue.Index, review.ID).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var reviewComments []*api.PullReviewComment
DecodeJSON(t, resp, &reviewComments)
assert.Len(t, reviewComments, 2)
assert.EqualValues(t, existingCommentBody, reviewComments[0].Body)
assert.EqualValues(t, reviewComments[0].OldLineNum, reviewComments[1].OldLineNum)
assert.EqualValues(t, reviewComments[0].LineNum, reviewComments[1].LineNum)
assert.EqualValues(t, newCommentBody, reviewComments[1].Body)
assert.EqualValues(t, path, reviewComments[1].Path)
}
{
req := NewRequestf(t, http.MethodDelete, "/api/v1/repos/%s/pulls/%d/reviews/%d", repo.FullName(), pullIssue.Index, review.ID).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
}
})
}
}
func TestAPIPullReview(t *testing.T) { func TestAPIPullReview(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()
pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}) pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})