1import type { ChangeRequestTerminology } from "@t3tools/client-runtime/source-control-clients";
2import type { PullRequestDiffResult } from "@t3tools/contracts";
3import { buildReviewParsedDiff, type ReviewParsedDiff } from "../review/reviewModel";
4
5export interface PullRequestDiffPage {
6 readonly cursor: string | null;
7 readonly result: PullRequestDiffResult;
8}
9
10interface CachedPullRequestDiffPages {
11 readonly pages: readonly PullRequestDiffPage[];
12 readonly cache: WeakMap<PullRequestDiffResult, ReviewParsedDiff>;
13 readonly parsed: ReviewParsedDiff;
14}
15
16// Keep only the latest combination per scoped first page; query results own its lifetime.
17const parsedSnapshots = new WeakMap<
18 PullRequestDiffResult,
19 Map<string, CachedPullRequestDiffPages>
20>();
21
22export function getCachedPullRequestDiffPages(
23 pages: readonly PullRequestDiffPage[],
24 scope: string,
25 terminology: ChangeRequestTerminology,
26): ReviewParsedDiff {
27 const first = pages[0];
28 if (!first) return { kind: "empty" };
29 const key = JSON.stringify([scope, terminology.singular, first.cursor]);
30 let snapshots = parsedSnapshots.get(first.result);
31 if (!snapshots) {
32 snapshots = new Map();
33 parsedSnapshots.set(first.result, snapshots);
34 }
35 const previous = snapshots.get(key);
36 if (
37 previous?.pages.length === pages.length &&
38 pages.every((page, index) => {
39 const cached = previous.pages[index];
40 return cached?.cursor === page.cursor && cached.result === page.result;
41 })
42 )
43 return previous.parsed;
44 const cache = previous?.cache ?? new WeakMap<PullRequestDiffResult, ReviewParsedDiff>();
45 const parsed = combinePullRequestDiffPages(pages, cache, scope, terminology);
46 snapshots.set(key, { pages, cache, parsed });
47 return parsed;
48}
49
50/** A changed page invalidates later cursors, which belong to the previous patch. */
51export function retainPullRequestDiffPage(
52 pages: readonly PullRequestDiffPage[],
53 cursor: string | null,
54 result: PullRequestDiffResult,
55): readonly PullRequestDiffPage[] {
56 const index = pages.findIndex((page) => page.cursor === cursor);
57 if (index === -1) return [...pages, { cursor, result }];
58 const previous = pages[index]?.result;
59 if (
60 previous === result ||
61 (previous?.patch === result.patch &&
62 previous.truncated === result.truncated &&
63 previous.nextCursor === result.nextCursor &&
64 JSON.stringify(previous.omittedFileStats ?? []) ===
65 JSON.stringify(result.omittedFileStats ?? []))
66 )
67 return pages;
68 return [...pages.slice(0, index), { cursor, result }];
69}
70
71export function combinePullRequestDiffPages(
72 pages: readonly PullRequestDiffPage[],
73 cache: WeakMap<PullRequestDiffResult, ReviewParsedDiff>,
74 scope: string,
75 terminology: ChangeRequestTerminology,
76): ReviewParsedDiff {
77 const parsed = pages.map((page) => {
78 const cached = cache.get(page.result);
79 if (cached) return cached;
80 let value = buildReviewParsedDiff(page.result.patch, `${scope}:${page.cursor ?? "first"}`);
81 if (value.kind === "files" && page.result.omittedFileStats?.length) {
82 const omittedFileStats = new Map(
83 page.result.omittedFileStats.map((stat) => [stat.path, stat]),
84 );
85 const files = value.files.map((file) => {
86 const stat = omittedFileStats.get(file.path);
87 return stat ? { ...file, additions: stat.additions, deletions: stat.deletions } : file;
88 });
89 value = {
90 ...value,
91 files,
92 additions: files.reduce((total, file) => total + file.additions, 0),
93 deletions: files.reduce((total, file) => total + file.deletions, 0),
94 };
95 }
96 cache.set(page.result, value);
97 return value;
98 });
99 const files = parsed.flatMap((page) => (page.kind === "files" ? page.files : []));
100 if (files.length === 0) return parsed[0] ?? { kind: "empty" };
101 return {
102 kind: "files",
103 files,
104 fileCount: files.length,
105 additions: files.reduce((total, file) => total + file.additions, 0),
106 deletions: files.reduce((total, file) => total + file.deletions, 0),
107 notice:
108 pages.some((page) => page.result.truncated) ||
109 parsed.some((page) => page.kind === "raw" || (page.kind === "files" && page.notice !== null))
110 ? `Some file contents are unavailable. Open the ${terminology.singular} on its provider to see the full changes.`
111 : null,
112 };
113}