`pr review <n>` gains `--comments-file <path>`, a JSON array of inline
comments submitted with the review. Each entry is one of two exclusive
shapes: a new comment `{ path, line, body }` (mapped to `new_position`,
always the new side) or a reply `{ reply_to, body }`. A reply carries no
line or side — gitea-axi finds the target via the reviews-plus-comments
fan-out (there is no get-comment-by-id endpoint), reconstructs its anchor
from the target's own `diff_hunk`, and infers old/new side from it, so a
same-line post threads with the existing conversation. All entries map
onto the review-submission payload's `comments[]`; no new HTTP layer is
added. An unknown `reply_to` is a VALIDATION_ERROR raised before the POST,
and the submitted inline-comment count rides the action block.
The shared path-resolve-and-read behind --body-file and --comments-file is
extracted into src/flag-file.ts.
4.9 KiB
spec, blocked-by
| spec | blocked-by |
|---|---|
| pr-review-comments | 0034-pr-review-anchor-fields |
What to build
Give pr review <n> a --comments-file <path> flag carrying a JSON array of inline comments submitted as part of the review.
The existing action flag (exactly one of --approve / --request-changes / --comment) is still required, and top-level --body / --body-file stays optional, so an agent can approve/request-changes/comment while attaching inline replies.
Each array entry is one of two shapes, with no side field anywhere:
- New comment:
{ "path": "...", "line": <new-file line>, "body": "..." }—linemaps tonew_position; it is always the new side, because a line addressable by new-file number is by definition on the new side. - Reply:
{ "reply_to": <comment-id>, "body": "..." }— carries no line or side. gitea-axi locates the target comment (no get-comment-by-id endpoint exists, so it reuses the same reviews-plus-comments fan-out the read side performs), reconstructs that comment's anchor from its owndiff_hunk, and posts a matching inline comment; because Gitea threads comments by line, a same-line post joins the existing conversation, so side is inferred from the target rather than supplied.
All entries map onto the comments[] array of the review-submission payload (each element { path, new_position | old_position, body }), which the SDK already accepts but gitea-axi previously left unpopulated — no new HTTP layer or endpoint is added.
A reply_to id not found among the PR's review comments is a VALIDATION_ERROR raised before submission, mirroring how pr review validates its action flags up front.
Mutation output follows the established action-block/entity-block convention; the inline-comment count is reflected in the reported result.
Acceptance criteria
pr review N --comment --comments-file <f>submits a review whose payloadcomments[]reflects the file's entries- A new-comment entry maps
linetonew_positionon the givenpath, always the new side - A reply entry (
reply_to) posts an inline comment whose anchor is reconstructed from the target comment'sdiff_hunk, with side inferred from the target (nosidefield consumed) - The action flag remains required and top-level
--body/--body-filestill composes with the inline batch - A
reply_toid absent from the PR's review comments yieldsVALIDATION_ERROR(exit 2) before any submission request is made - The mutation output reflects the inline-comment count in its result
- Fixture-server tests assert the captured submission body's
comments[]for both the new-comment (path+new_position) and reply (reconstructed anchor) cases, theVALIDATION_ERRORno-request failure path, and the action-block output
Implementation Notes
--comments-fileparsing/validation and payload mapping live in a new modulesrc/review-comments.ts.loadInlineCommentsreads and shape-validates the JSON batch up front (before any client is created);resolveInlineCommentsmaps entries ontoCreatePullReviewComment[].- The reply anchor is reconstructed by
anchorFromDiffHunkinsrc/diff.ts(besidetrimDiffHunk): it walks the unified-diff hunk — whose last line is the commented line, by Gitea's convention — tracking old/new line numbers, and reads the last line off. An added/context last line anchors onnew_position; a deleted last line onold_position. Both sides are covered by tests. - The reply-target lookup reuses a new
fetchAllReviewCommentsinsrc/review.ts— the reviews-plus-comments fan-out the read side already performs — since Gitea has no get-comment-by-id endpoint. It only runs when a reply is present; a new-comment-only batch makes no extra GETs. - The submitted count rides the action block as
review: { number, action, comments: N }, added only when a batch was submitted so a plain review's output is unchanged. - The action-flag-required criterion needed no new test:
resolveReviewActionstill runs first, so the existing zero/multiple-action tests cover it unchanged even with--comments-filepresent.
Review follow-ups (/review-uncommitted), all addressed in this branch:
- Standards flagged
readCommentsFileas duplicatingbody-source.ts'sreadBodyFile. Extracted the shared path-resolve-and-read intosrc/flag-file.ts(readFlagFile); both--body-fileand--comments-filenow go through it, keeping their flag-specific error messages. - Spec flagged that an entry mixing both shapes (
reply_to+path/line) was silently resolved to a reply.validateEntrynow rejects the contradictory mix as aVALIDATION_ERRORup front (with a regression test), making the two-shape contract exact. - Spec flagged the
target.path ?? ""fallback as a silent mis-anchor. A reply target returned withoutpath/diff_hunknow raisesUNKNOWNinstead of posting an empty path, mirroring the repo's other "never fabricate an anchor/identifier" guards.