All checks were successful
CI / test (pull_request) Successful in 53s
Each inline review comment under `pr view <n> --reviews` now renders its `id` (the handle a reply targets), `resolved` (`yes`/`no`, from whether Gitea populated the comment's `resolver`), and `diff_hunk`. The hunk is structurally trimmed to its `@@` header line plus its last two lines by default (hunks of three lines or fewer are left whole) and emitted verbatim under `--full`, so the trim never touches the char-based body truncation path. The raw `position`/`original_position` diff offsets stay unsurfaced. The fields ride the existing reviews-plus-per-review-comments fetch — no extra API calls.
89 lines
9.5 KiB
Markdown
89 lines
9.5 KiB
Markdown
## Problem Statement
|
|
|
|
An agent doing a PR review round-trip with gitea-axi cannot complete it inside the tool.
|
|
|
|
When reading a reviewer's inline comments via `pr view --reviews`, the output gives the author, file path, and body, but not which line each comment is anchored to, nor the comment's id.
|
|
Comments like "what does this do?" or "wasn't this set earlier?" are unanswerable from the output alone, forcing a fallback to a raw Gitea API call (and scraping the token out of tea's config) just to recover the anchor.
|
|
|
|
When writing, `pr review` accepts only a single `--body` for the whole review, so "reply to each of ten inline comments" collapses into one consolidated body that restates each thread by hand, rather than a reply landing under each comment where the reviewer left it.
|
|
|
|
## Solution
|
|
|
|
Complete the read and write halves of the inline-review round-trip, both at the existing `pr` commands.
|
|
|
|
On the read side, `pr view --reviews` surfaces each inline comment's `id`, its `diff_hunk` (so the anchoring code travels with the comment), and whether the thread is already `resolved`.
|
|
|
|
On the write side, `pr review` gains a `--comments-file` flag that carries a batch of inline comments — each either a reply into an existing thread (by comment id) or a fresh comment on a new-file line — mapped onto the review-submission payload Gitea already accepts.
|
|
|
|
## User Stories
|
|
|
|
1. As a reviewing agent, I want each inline review comment's anchoring `diff_hunk` in the `--reviews` output, so that I can answer a bare "what does this do?" without a second API call.
|
|
2. As a reviewing agent, I want each inline review comment's `id` in the `--reviews` output, so that I can target that exact comment when replying.
|
|
3. As a reviewing agent, I want to see whether each inline comment's thread is already `resolved`, so that I skip settled conversations instead of re-answering them.
|
|
4. As a reviewing agent, I want the `diff_hunk` trimmed to its header line plus its last couple of lines by default, so that I get the file-line anchor and the code at the comment without paying for the whole hunk.
|
|
5. As a reviewing agent, I want `--full` to expand each `diff_hunk` to its complete text, so that I can read the entire hunk when the trimmed tail is not enough.
|
|
6. As a PR author, I want to reply to an existing inline comment by its id, so that my reply lands in that reviewer's thread without me computing any line number or side.
|
|
7. As a PR author, I want to post a fresh inline comment on a new-file line, so that I can raise a point on code no one has commented on yet.
|
|
8. As a PR author, I want to submit a batch of inline comments in one file alongside my review, so that "reply to each of ten comments" is one command, not ten.
|
|
9. As a PR author, I want gitea-axi to figure out the new-vs-old side of a reply for me, so that I never have to reason about diff sides.
|
|
10. As a PR author, I want the inline-comment batch to compose with the existing review action and optional top-level body, so that I can approve/request-changes/comment while attaching inline replies.
|
|
11. As a PR author, I want a clear validation error when a reply targets a comment id that isn't on the PR, so that a typo fails fast instead of silently posting nowhere.
|
|
|
|
## Implementation Decisions
|
|
|
|
### Read side — anchor fields on `pr view --reviews`
|
|
|
|
- The `--reviews` review rows add three fields per inline comment: `id`, `diff_hunk`, and `resolved`.
|
|
- `resolved` is `yes`/`no`, derived client-side from whether the comment's `resolver` is set (Gitea returns `resolver` as a populated user once a thread is resolved).
|
|
- The raw `position` / `original_position` diff offsets are deliberately **not** surfaced — they are diff offsets an agent cannot map to a file line without the patch, and the `diff_hunk` header already carries the human-meaningful line range.
|
|
- `diff_hunk` is rendered with a bespoke **structural trim**, not the char-based content-truncation used for bodies: by default, the hunk's `@@` header line plus its last two lines (collapsed when the hunk is three lines or fewer).
|
|
This keeps both the file-line anchor (the `@@` header) and the code at the comment (the tail), which the keep-head char truncation would get backwards by dropping the tail.
|
|
- Under `--full`, the entire `diff_hunk` is emitted verbatim, consistent with `--full` meaning "no trimming anywhere"; the hunk is never run through the body char-truncation path.
|
|
- These fields ride the existing `--reviews` fetch (reviews list plus one inline-comments fetch per review); no extra API calls are introduced.
|
|
|
|
### Write side — `--comments-file` on `pr review`
|
|
|
|
- `pr review` gains `--comments-file <path>`, a JSON array of inline-comment entries submitted as part of the review; the existing action flag (one of `--approve` / `--request-changes` / `--comment`) is still required, and top-level `--body` stays optional.
|
|
- Each array entry is one of two shapes, and there is **no `side` field anywhere**:
|
|
- Reply: `{ "reply_to": <comment-id>, "body": "..." }`.
|
|
- New comment: `{ "path": "...", "line": <new-file line>, "body": "..." }`.
|
|
- A new comment maps `line` to `new_position`; it is always the new side, because a line addressable by new-file number is by definition on the new side.
|
|
A prototype against the live host confirmed the create payload treats `new_position` as a **file line number** (not a diff offset) and that a single inline comment posts successfully this way.
|
|
- A reply carries no line or side.
|
|
gitea-axi locates the target comment (there is no get-comment-by-id endpoint, so it reuses the same reviews-plus-comments fan-out the read side already performs), reconstructs that comment's anchor from its own `diff_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 a `{ 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 that is not found among the PR's review comments is a `VALIDATION_ERROR` raised before submission, mirroring how `pr review` already validates its action flags up front.
|
|
- Mutation output follows the established action-block/entity-block convention for `pr review`; the inline-comment count is reflected in the reported result.
|
|
|
|
### Sequencing
|
|
|
|
- The read side ships first: it is pure read, and its surfaced `id` is the handle the write side's replies target.
|
|
- The write side ships second, on top of the id exposed by the read side.
|
|
|
|
## Testing Decisions
|
|
|
|
- Both halves are tested at the single existing **fixture-server CLI seam**: a fixture server maps request path/method to recorded Gitea JSON, the built CLI is driven via the run-CLI test helper, and assertions are on rendered `stdout` and on the recorded outbound requests.
|
|
No new seam is introduced.
|
|
- Good tests here assert external behavior only — the exact rendered TOON lines and the captured request payload — never internal rendering helpers.
|
|
- Read side: drive `pr view N --reviews` (and again with `--full`) against fixture reviews and inline comments whose JSON carries `id`, `diff_hunk`, and a set/unset `resolver`; assert the rendered `id`, the trimmed-vs-full `diff_hunk`, and `resolved: yes/no`.
|
|
Prior art: the existing `pr view --reviews` test that already stubs the reviews and per-review comments endpoints and asserts the `reviews[...]` block.
|
|
- Write side: write a `--comments-file` via the existing temp-file test helper, drive `pr review N --comment --comments-file <f>` against a stubbed review-submission endpoint, and assert the **captured request body's** `comments[]` (path + `new_position`, and the reply case's reconstructed anchor) plus the action-block output.
|
|
Prior art: the existing `pr review` test that inspects the recorded POST body via the fixture server's request log, and the reply case additionally stubs the reviews-plus-comments GETs used for the lookup.
|
|
- The reply-lookup failure path is covered by asserting a `VALIDATION_ERROR` and that no submission request was made, matching the existing "rejects ... before any API call" tests.
|
|
|
|
## Out of Scope
|
|
|
|
- Resolving / unresolving review conversations (issue #39).
|
|
Gitea exposes no REST endpoint for this — verified exhaustively against the live host — and the only mechanism is a CSRF-guarded internal web route returning HTML, which a prototype confirmed rejects token auth.
|
|
Parked as blocked-upstream; the read side's `resolved` field covers only *seeing* resolution state, not changing it.
|
|
- A raw `api` passthrough / generic escape hatch (issue #40); dropped from this work.
|
|
- Surfacing raw `position` / `original_position` diff offsets as rendered fields.
|
|
- Inline repeatable comment flags (e.g. paired `--on path:line` / `--body`); the JSON `--comments-file` is the sole input shape.
|
|
- Old-side *new* comments authored by hand; old-side anchoring is reachable only through the reply path, where it is inferred from the target comment.
|
|
|
|
## Further Notes
|
|
|
|
- The prototype that validated the write-side `new_position` semantics left one non-removable residue on the live repo: a closed throwaway PR (#41) carrying one review comment, since Gitea cannot hard-delete PRs.
|
|
- The "no REST resolve endpoint; web-route-only and CSRF-guarded" finding for the parked #39 is recorded so it is not re-derived.
|
|
- Keeping gitea-js as the sole HTTP layer is preserved: dropping the passthrough and confining resolve to out-of-scope means no raw-request path is introduced by this work.
|