| 1 | import type { PullRequestReviewPosition, PullRequestReviewThread } from "@t3tools/contracts"; |
| 2 | import type { NativeReviewDiffRow } from "../diffs/nativeReviewDiffSurface"; |
| 3 | import type { NativeReviewDiffData } from "../review/nativeReviewDiffAdapter"; |
| 4 | import type { PendingReviewComment } from "./pullRequestReviewDrafts"; |
| 5 | |
| 6 | export 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 | } |
| 21 | export 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 | } |
| 26 | function 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. */ |
| 32 | export 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 | } |