1import type { PullRequestReviewPosition, PullRequestReviewThread } from "@t3tools/contracts";
2import type { NativeReviewDiffRow } from "../diffs/nativeReviewDiffSurface";
3import type { NativeReviewDiffData } from "../review/nativeReviewDiffAdapter";
4import type { PendingReviewComment } from "./pullRequestReviewDrafts";
5
6export function reviewPosition(row: NativeReviewDiffRow): PullRequestReviewPosition | null {
7 if (row.kind !== "line") return null;
8 if (row.change === "add" && row.newLineNumber)
9 return { kind: "added", newLine: row.newLineNumber };
10 if (row.change === "delete" && row.oldLineNumber)
11 return { kind: "deleted", oldLine: row.oldLineNumber };
12 if (row.oldLineNumber && row.newLineNumber)
13 return {
14 kind: "context",
15 oldLine: row.oldLineNumber,
16 newLine: row.newLineNumber,
17 side: "right",
18 };
19 return null;
20}
21export function reviewPositionLabel(position: PullRequestReviewPosition) {
22 const oldSide =
23 position.kind === "deleted" || (position.kind === "context" && position.side === "left");
24 return `${oldSide ? "Old line" : "Line"} ${reviewLine(position)}`;
25}
26function reviewLine(position: PullRequestReviewPosition) {
27 if (position.kind === "deleted" || (position.kind === "context" && position.side === "left"))
28 return position.oldLine;
29 return position.newLine;
30}
31/** Attach remarks by path and side, keeping outdated or missing anchors off the code. */
32export function annotatePullRequestDiff(
33 data: NativeReviewDiffData,
34 threads: readonly PullRequestReviewThread[],
35 drafts: readonly PendingReviewComment[],
36 headSha: string | undefined,
37): NativeReviewDiffData {
38 const anchors = new Map<string, NativeReviewDiffRow[]>();
39 const firstByPath = new Map<string, NativeReviewDiffRow>();
40 const lines = new Map<string, NativeReviewDiffRow>();
41 const paths = new Map(data.files.map((file) => [file.id, file.path]));
42 for (const original of data.rows) {
43 const row = {
44 ...original,
45 filePath: original.filePath ?? (original.fileId ? paths.get(original.fileId) : undefined),
46 };
47 if (row.kind === "file" && row.filePath) firstByPath.set(row.filePath, row);
48 if (row.kind !== "line" || !row.filePath) continue;
49 if (row.oldLineNumber)
50 lines.set(JSON.stringify([row.filePath, "left", row.oldLineNumber]), row);
51 if (row.newLineNumber)
52 lines.set(JSON.stringify([row.filePath, "right", row.newLineNumber]), row);
53 }
54 const attach = (
55 anchor: NativeReviewDiffRow | undefined,
56 id: string,
57 body: string,
58 label: string,
59 ) => {
60 if (!anchor) return;
61 const rows = anchors.get(anchor.id) ?? [];
62 rows.push({
63 kind: "comment",
64 id,
65 fileId: anchor.fileId,
66 filePath: anchor.filePath,
67 commentText: body,
68 commentRangeLabel: label,
69 });
70 anchors.set(anchor.id, rows);
71 };
72 for (const thread of threads) {
73 if (thread.isOutdated) continue;
74 const anchor =
75 thread.line === null
76 ? firstByPath.get(thread.path)
77 : lines.get(JSON.stringify([thread.path, thread.side, thread.line]));
78 const first = thread.comments[0];
79 attach(
80 anchor,
81 `discussion:${thread.id}`,
82 `${first?.author?.login ?? "Discussion"}${thread.isResolved ? " · Resolved" : ""}\n${first?.body ?? ""}\n${thread.commentCount ?? thread.comments.length} comments · Tap to open`,
83 thread.line === null ? "file" : String(thread.line),
84 );
85 }
86 for (const draft of drafts) {
87 if (draft.headSha !== headSha) continue;
88 const side =
89 draft.position.kind === "deleted"
90 ? "left"
91 : draft.position.kind === "context"
92 ? draft.position.side
93 : "right";
94 const line = reviewLine(draft.position);
95 attach(
96 lines.get(JSON.stringify([draft.path, side, line])),
97 `draft:${draft.id}`,
98 `Pending review · Tap to edit\n${draft.body}`,
99 String(line),
100 );
101 }
102 return { ...data, rows: data.rows.flatMap((row) => [row, ...(anchors.get(row.id) ?? [])]) };
103}