Skip to content
Source
Sign inSign up

diffFileContents.ts

189 lines5.9 KB

1import type { FileDiffContentsLoader, FileDiffMetadata } from "@pierre/diffs";
2import {
3 squashAtomCommandFailure,
4 type AtomCommandResult,
5} from "@t3tools/client-runtime/state/runtime";
6import type {
7 EnvironmentId,
8 PullRequestDiffFileContentsInput,
9 PullRequestDiffFileContentsResult,
10 PullRequestRef,
11 ReviewDiffFileContentsInput,
12 ReviewDiffFileContentsResult,
13 ReviewDiffPreviewSourceKind,
14} from "@t3tools/contracts";
15​
16import { resolveFileDiffPath } from "./diffRendering";
17​
18interface GitDiffFileContentsSource {
19 readonly environmentId: EnvironmentId;
20 readonly cwd: string;
21 readonly sourceKind: ReviewDiffPreviewSourceKind;
22 readonly baseRef: string | null;
23 readonly headRef: string | null;
24 readonly cacheKey: string;
26​
27interface PullRequestDiffFileContentsSource {
28 readonly environmentId: EnvironmentId;
29 readonly reference: PullRequestRef;
30 readonly commit: string | null;
31 readonly cacheKey: string;
33​
34type GetDiffFileContents<E> = (request: {
35 readonly environmentId: EnvironmentId;
36 readonly input: ReviewDiffFileContentsInput;
37}) => Promise<AtomCommandResult<ReviewDiffFileContentsResult, E>>;
38​
39type GetPullRequestDiffFileContents<E> = (request: {
40 readonly environmentId: EnvironmentId;
41 readonly input: PullRequestDiffFileContentsInput;
42}) => Promise<AtomCommandResult<PullRequestDiffFileContentsResult, E>>;
43​
44// Mirrors Pierre's splitFileContents so line counts match its hydration.
45function splitLines(contents: string): ReadonlyArray<string> {
46 return contents === "" ? [] : contents.split(/(?<=\n)/);
48​
49// Whitespace-insensitive: `git diff -w` prints context lines from only one side.
50function sameLine(patchLine: string | undefined, fileLine: string | undefined): boolean {
51 return (
52 patchLine !== undefined &&
53 fileLine !== undefined &&
54 patchLine.replace(/\s/g, "") === fileLine.replace(/\s/g, "")
55 );
57​
58function hunkSideEnd(start: number, count: number): number {
59 return start - (count === 0 ? 0 : 1) + count;
61​
62function hunkSideMatches(
63 patchLines: ReadonlyArray<string>,
64 patchIndex: number,
65 fileLines: ReadonlyArray<string>,
66 start: number,
67 count: number,
68): boolean {
69 const fileIndex = hunkSideEnd(start, count) - count;
70 for (let offset = 0; offset < count; offset++) {
71 if (!sameLine(patchLines[patchIndex + offset], fileLines[fileIndex + offset])) return false;
72 }
73 return true;
75​
76// Pierre hydrates a partial patch by trusting its hunk positions, so contents that
77// changed since the patch was computed make it render corrupt hunks or throw.
78function reproducesPatch(fileDiff: FileDiffMetadata, oldContents: string, newContents: string) {
79 const lastHunk = fileDiff.hunks.at(-1);
80 if (lastHunk === undefined) return true;
81 const oldLines = splitLines(oldContents);
82 const newLines = splitLines(newContents);
83 const hunksMatch = fileDiff.hunks.every(
84 (hunk) =>
85 hunkSideMatches(
86 fileDiff.deletionLines,
87 hunk.deletionLineIndex,
88 oldLines,
89 hunk.deletionStart,
90 hunk.deletionCount,
91 ) &&
92 hunkSideMatches(
93 fileDiff.additionLines,
94 hunk.additionLineIndex,
95 newLines,
96 hunk.additionStart,
97 hunk.additionCount,
98 ),
99 );
100 return (
101 hunksMatch &&
102 oldLines.length - hunkSideEnd(lastHunk.deletionStart, lastHunk.deletionCount) ===
103 newLines.length - hunkSideEnd(lastHunk.additionStart, lastHunk.additionCount)
104 );
106​
107function createDiffFileContentsLoader(
108 load: (input: {
109 readonly changeType: PullRequestDiffFileContentsInput["changeType"];
110 readonly oldPath: string;
111 readonly newPath: string;
112 }) => Promise<{ readonly oldContents: string; readonly newContents: string }>,
113 cacheKey: string,
114): FileDiffContentsLoader {
115 return async (fileDiff) => {
116 const newPath = resolveFileDiffPath(fileDiff);
117 const oldPath = fileDiff.prevName
118 ? resolveFileDiffPath({ ...fileDiff, name: fileDiff.prevName })
119 : newPath;
120 const contents = await load({ changeType: fileDiff.type, oldPath, newPath });
121 const newFile = {
122 name: newPath,
123 contents: contents.newContents,
124 cacheKey: `${cacheKey}:new:${newPath}`,
125 };
126 if (fileDiff.type === "rename-pure") {
127 return { oldFile: null, newFile };
128 }
129 if (!reproducesPatch(fileDiff, contents.oldContents, contents.newContents)) {
130 throw new Error(
131 "This file changed since the diff was loaded. Refresh the diff to expand it.",
132 );
133 }
134 return {
135 oldFile: {
136 name: oldPath,
137 contents: contents.oldContents,
138 cacheKey: `${cacheKey}:old:${oldPath}`,
139 },
140 newFile,
141 };
142 };
144​
145export function createGitDiffFileContentsLoader<E>(
146 getDiffFileContents: GetDiffFileContents<E>,
147 source: GitDiffFileContentsSource,
148): FileDiffContentsLoader {
149 return createDiffFileContentsLoader(async ({ changeType, oldPath, newPath }) => {
150 const result = await getDiffFileContents({
151 environmentId: source.environmentId,
152 input: {
153 cwd: source.cwd,
154 sourceKind: source.sourceKind,
155 changeType,
156 baseRef: source.baseRef,
157 headRef: source.headRef,
158 oldPath,
159 newPath,
160 },
161 });
162 if (result._tag !== "Success") {
163 throw squashAtomCommandFailure(result);
164 }
165 return result.value;
166 }, source.cacheKey);
168​
169export function createPullRequestDiffFileContentsLoader<E>(
170 getDiffFileContents: GetPullRequestDiffFileContents<E>,
171 source: PullRequestDiffFileContentsSource,
172): FileDiffContentsLoader {
173 return createDiffFileContentsLoader(async ({ changeType, oldPath, newPath }) => {
174 const result = await getDiffFileContents({
175 environmentId: source.environmentId,
176 input: {
177 ...source.reference,
178 ...(source.commit === null ? {} : { commit: source.commit }),
179 changeType,
180 oldPath,
181 newPath,
182 },
183 });
184 if (result._tag !== "Success") {
185 throw squashAtomCommandFailure(result);
186 }
187 return result.value;
188 }, source.cacheKey);