mirror of
https://github.com/go-gitea/gitea.git
synced 2026-02-21 11:28:12 +01:00
feat(api): add in_reply_to field for pull review comment replies
Add `in_reply_to` field to `CreatePullReviewComment` that accepts a comment ID, consistent with GitHub's API. The handler resolves the comment ID to the internal review ID and creates the reply as a non-pending comment so it threads correctly under the original review. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5e9b9b33d1
commit
eb8adf3297
@ -89,6 +89,8 @@ type CreatePullReviewComment struct {
|
||||
OldLineNum int64 `json:"old_position"`
|
||||
// if comment to new file line or 0
|
||||
NewLineNum int64 `json:"new_position"`
|
||||
// if replying to an existing review comment, the comment ID to reply to
|
||||
InReplyToID int64 `json:"in_reply_to_id"`
|
||||
}
|
||||
|
||||
// SubmitPullReviewOptions are options to submit a pending pull request review
|
||||
|
||||
@ -388,6 +388,34 @@ func DeletePullReview(ctx *context.APIContext) {
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// resolveInReplyTo resolves an in_reply_to comment ID to the review ID it
|
||||
// belongs to. Returns 0 when commentID is 0 (not a reply).
|
||||
func resolveInReplyTo(ctx *context.APIContext, commentID int64, pr *issues_model.PullRequest) (int64, error) {
|
||||
if commentID == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
comment, err := issues_model.GetCommentByID(ctx, commentID)
|
||||
if err != nil {
|
||||
if issues_model.IsErrCommentNotExist(err) {
|
||||
ctx.APIErrorNotFound("GetCommentByID", err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
if comment.IssueID != pr.IssueID {
|
||||
err := issues_model.ErrCommentNotExist{ID: commentID}
|
||||
ctx.APIErrorNotFound("GetCommentByID", err)
|
||||
return 0, err
|
||||
}
|
||||
if comment.Type != issues_model.CommentTypeCode || comment.ReviewID == 0 {
|
||||
err := errors.New("comment is not a review comment")
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return 0, err
|
||||
}
|
||||
return comment.ReviewID, nil
|
||||
}
|
||||
|
||||
// CreatePullReview create a review to a pull request
|
||||
func CreatePullReview(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews repository repoCreatePullReview
|
||||
@ -466,12 +494,21 @@ func CreatePullReview(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
// create review comments
|
||||
var lastReplyReviewID int64
|
||||
for _, c := range opts.Comments {
|
||||
line := c.NewLineNum
|
||||
if c.OldLineNum > 0 {
|
||||
line = c.OldLineNum * -1
|
||||
}
|
||||
|
||||
replyReviewID, err := resolveInReplyTo(ctx, c.InReplyToID, pr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if replyReviewID != 0 {
|
||||
lastReplyReviewID = replyReviewID
|
||||
}
|
||||
|
||||
if _, err := pull_service.CreateCodeComment(ctx,
|
||||
ctx.Doer,
|
||||
ctx.Repo.GitRepo,
|
||||
@ -479,8 +516,8 @@ func CreatePullReview(ctx *context.APIContext) {
|
||||
line,
|
||||
c.Body,
|
||||
c.Path,
|
||||
true, // pending review
|
||||
0, // no reply
|
||||
replyReviewID == 0, // pending
|
||||
replyReviewID, // reply
|
||||
opts.CommitID,
|
||||
nil,
|
||||
); err != nil {
|
||||
@ -491,6 +528,10 @@ 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)
|
||||
// reply-only requests have no pending review, fall back to the reply's review
|
||||
if issues_model.IsContentEmptyErr(err) && lastReplyReviewID != 0 {
|
||||
review, err = issues_model.GetReviewByID(ctx, lastReplyReviewID)
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
|
||||
6
templates/swagger/v1_json.tmpl
generated
6
templates/swagger/v1_json.tmpl
generated
@ -23615,6 +23615,12 @@
|
||||
"type": "string",
|
||||
"x-go-name": "Body"
|
||||
},
|
||||
"in_reply_to_id": {
|
||||
"description": "if replying to an existing review comment, the comment ID to reply to",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "InReplyToID"
|
||||
},
|
||||
"new_position": {
|
||||
"description": "if comment to new file line or 0",
|
||||
"type": "integer",
|
||||
|
||||
@ -530,6 +530,48 @@ func TestAPIPullReviewStayDismissed(t *testing.T) {
|
||||
pullIssue.ID, user8.ID, 2, 0, 3, false)
|
||||
}
|
||||
|
||||
func TestAPIPullReviewCommentReply(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
|
||||
require.NoError(t, pullIssue.LoadRepo(t.Context()))
|
||||
require.NoError(t, pullIssue.LoadPullRequest(t.Context()))
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
gitRepo, err := gitrepo.OpenRepository(t.Context(), pullIssue.Repo)
|
||||
require.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
|
||||
commitID, err := gitRepo.GetRefCommitID(pullIssue.PullRequest.GetGitHeadRefName())
|
||||
require.NoError(t, err)
|
||||
|
||||
// create an initial code comment to reply to
|
||||
originalComment, err := pull_service.CreateCodeComment(t.Context(), doer, gitRepo, pullIssue, 1, "original comment", "README.md", false, 0, commitID, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
session := loginUser(t, doer.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", pullIssue.Repo.OwnerName, pullIssue.Repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{
|
||||
Event: "COMMENT",
|
||||
Comments: []api.CreatePullReviewComment{{
|
||||
Path: "README.md", Body: "reply to original", NewLineNum: 1, InReplyToID: originalComment.ID,
|
||||
}},
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var review api.PullReview
|
||||
DecodeJSON(t, resp, &review)
|
||||
assert.EqualValues(t, "COMMENT", review.State)
|
||||
|
||||
// verify the reply is threaded under the original review
|
||||
comments, err := issues_model.FindComments(t.Context(), &issues_model.FindCommentsOptions{
|
||||
ReviewID: originalComment.ReviewID,
|
||||
Type: issues_model.CommentTypeCode,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(comments), 2)
|
||||
}
|
||||
|
||||
func reviewsCountCheck(t *testing.T, name string, issueID, reviewerID int64, expectedDismissed, expectedRequested, expectedTotal int, expectApproval bool) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
unittest.AssertCountByCond(t, "review", builder.Eq{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user