feat: surface inline-comment anchor fields on pr view --reviews (task 0034)

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.
This commit is contained in:
2026-07-18 15:46:30 -04:00
parent db2285b40d
commit b6d247dd4e
5 changed files with 423 additions and 4 deletions

View File

@@ -38,7 +38,7 @@ import {
splitFlag,
} from "../flags.js";
import { fetchChecks } from "../checks.js";
import { fetchPullDiff, truncateDiff } from "../diff.js";
import { fetchPullDiff, trimDiffHunk, truncateDiff } from "../diff.js";
import { checkoutPullHead, currentBranch } from "../git.js";
import { resolveLabelIds, resolveMilestoneId } from "../lookup.js";
import { fetchAllPages, readTotalCount } from "../paginate.js";
@@ -739,6 +739,19 @@ function headSha(pull: PullRequest): string {
return sha;
}
/**
* The id Gitea gave a review comment — the handle a reply targets. The client
* types it optional, but every real comment has one, and an id invented to fill
* the gap would be reported as fact and copied into a `reply_to`, so a comment
* without one is treated as the broken answer it is (mirroring {@link pullNumber}).
*/
function reviewCommentId(comment: PullReviewComment): number {
if (comment.id === undefined) {
throw axiError("Gitea returned a review comment with no id", "UNKNOWN");
}
return comment.id;
}
interface PrDetailOptions {
host: string;
full: boolean;
@@ -813,6 +826,13 @@ interface ReviewRowsOptions {
* `official`/`stale` flags and its inline (diff) comments. One comments fetch per
* review, all in flight at once; review and comment bodies truncate at 800 chars
* unless `--full` is set.
*
* Each inline comment carries its anchor: `id` (the reply handle), `resolved`
* (`yes`/`no` from whether Gitea populated the comment's `resolver`), and
* `diff_hunk` — structurally trimmed to its `@@` header plus tail by default, or
* emitted verbatim under `--full`. The raw `position`/`original_position` diff
* offsets are deliberately not surfaced: they are unmappable to a file line
* without the patch, and the `@@` header already carries the line range.
*/
async function buildReviewRows(
api: GiteaClient,
@@ -837,8 +857,11 @@ async function buildReviewRows(
stale: review.stale ? "yes" : "no",
body: truncate(review.body ?? ""),
comments: commentLists[index]!.map((comment) => ({
id: reviewCommentId(comment),
author: comment.user?.login ?? "",
path: comment.path ?? "",
resolved: comment.resolver ? "yes" : "no",
diff_hunk: options.full ? (comment.diff_hunk ?? "") : trimDiffHunk(comment.diff_hunk ?? ""),
body: truncate(comment.body ?? ""),
})),
}));

View File

@@ -56,3 +56,20 @@ export function truncateDiff(diff: string, full: boolean): DiffResult {
original_length: diff.length,
};
}
/**
* Structurally trim a review comment's `diff_hunk` to its anchor: the `@@`
* header line plus the hunk's last two lines. A hunk of three lines or fewer is
* already covered by that (header + last two), so it is returned unchanged.
*
* This is deliberately not the char-based body truncation: it keeps both the
* file-line anchor (the `@@` header) and the code at the comment (the tail),
* which a keep-head char truncation would get backwards by dropping the tail.
*/
export function trimDiffHunk(hunk: string): string {
const lines = hunk.split("\n");
if (lines.length <= 3) {
return hunk;
}
return [lines[0], ...lines.slice(-2)].join("\n");
}