mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-22 13:25:36 +03:00
[GITEA] POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments
Refs: https://codeberg.org/forgejo/forgejo/issues/2109
This commit is contained in:
parent
5464ec4ad2
commit
8b4ba3dce7
7 changed files with 298 additions and 21 deletions
|
@ -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"`
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
65
templates/swagger/v1_json.tmpl
generated
65
templates/swagger/v1_json.tmpl
generated
|
@ -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",
|
||||||
|
|
|
@ -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})
|
||||||
|
|
Loading…
Reference in a new issue