diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index d764b9c6a9e2..73a4e36c1f43 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -35,7 +35,7 @@ import { useLocalStorage } from "../hooks/useLocalStorage"; import { useTheme } from "../hooks/useTheme"; import { buildFileDiffContentVersion, - buildFileDiffIdentityKey, + buildFileDiffIdentityKeys, getDiffCollapseIconClassName, getDiffLineStat, getRenderablePatch, @@ -416,15 +416,14 @@ export default function DiffPanel({ }), ); }, [renderablePatch]); - const renderableFileEntries = useMemo( - () => - renderableFiles.map((fileDiff) => ({ - fileDiff, - fileKey: buildFileDiffIdentityKey(fileDiff), - fileVersion: buildFileDiffContentVersion(fileDiff), - })), - [renderableFiles], - ); + const renderableFileEntries = useMemo(() => { + const fileKeys = buildFileDiffIdentityKeys(renderableFiles); + return renderableFiles.map((fileDiff, index) => ({ + fileDiff, + fileKey: fileKeys[index]!, + fileVersion: buildFileDiffContentVersion(fileDiff), + })); + }, [renderableFiles]); const codeViewFiles = useMemo( () => renderableFileEntries.map(({ fileDiff, fileKey, fileVersion }) => { @@ -956,14 +955,10 @@ export default function DiffPanel({ (node): node is HTMLElement => node instanceof HTMLElement && node.hasAttribute("data-diffs-header"), ); - const headerFilePath = header - ?.querySelector("[data-title]") - ?.textContent?.trim(); - if (!headerFilePath) return; - const file = codeViewFiles.find( - (candidate) => candidate.filePath === headerFilePath, - ); - if (file) toggleDiffFileCollapsed(file.fileKey); + const fileKey = + header?.querySelector("[data-diff-file-key]")?.dataset + .diffFileKey; + if (fileKey) toggleDiffFileCollapsed(fileKey); }} > ( - + renderHeaderFilenameSuffix={(fileDiff, fileKey) => ( + <> + + + )} renderHeaderPrefix={(fileDiff, fileKey, collapsed) => { const filePath = resolveFileDiffPath(fileDiff); diff --git a/apps/web/src/components/diffs/AnnotatableCodeView.tsx b/apps/web/src/components/diffs/AnnotatableCodeView.tsx index b8ace2340557..3ead8a454bf6 100644 --- a/apps/web/src/components/diffs/AnnotatableCodeView.tsx +++ b/apps/web/src/components/diffs/AnnotatableCodeView.tsx @@ -86,7 +86,7 @@ interface AnnotatableCodeViewProps { options: StyledDiffCodeViewOptions; viewerRef?: Ref; className?: string; - renderHeaderFilenameSuffix: (fileDiff: FileDiffMetadata) => ReactNode; + renderHeaderFilenameSuffix: (fileDiff: FileDiffMetadata, fileKey: string) => ReactNode; renderHeaderPrefix: ( fileDiff: FileDiffMetadata, fileKey: string, @@ -255,7 +255,7 @@ export function AnnotatableCodeView({ onGutterUtilityClick: beginComment, }} renderHeaderFilenameSuffix={(item) => - item.type === "diff" ? renderHeaderFilenameSuffix(item.fileDiff) : null + item.type === "diff" ? renderHeaderFilenameSuffix(item.fileDiff, item.id) : null } renderHeaderPrefix={(item) => item.type === "diff" diff --git a/apps/web/src/lib/diffRendering.test.ts b/apps/web/src/lib/diffRendering.test.ts index 6ba83cc7a9a4..98a64e38ec35 100644 --- a/apps/web/src/lib/diffRendering.test.ts +++ b/apps/web/src/lib/diffRendering.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vite-plus/test"; import { buildFileDiffContentVersion, buildFileDiffIdentityKey, + buildFileDiffIdentityKeys, buildFileDiffRenderKey, buildPatchCacheKey, getDiffLineStat, @@ -148,6 +149,25 @@ describe("diff file reconciliation", () => { buildFileDiffContentVersion(beforeChanged), ); }); + + it("disambiguates repeated file identities without dropping either diff", () => { + const filePatch = [ + "diff --git a/scripts/push-release.sh b/scripts/push-release.sh", + "--- a/scripts/push-release.sh", + "+++ b/scripts/push-release.sh", + "@@ -1 +1 @@", + "-before", + "+after", + ].join("\n"); + const parsed = getRenderablePatch(`${filePatch}\n${filePatch}`); + expect(parsed?.kind).toBe("files"); + if (parsed?.kind !== "files") return; + + const keys = buildFileDiffIdentityKeys(parsed.files); + expect(parsed.files).toHaveLength(2); + expect(new Set(keys).size).toBe(2); + expect(keys[0]).toBe(buildFileDiffIdentityKey(parsed.files[0]!)); + }); }); describe("getDiffLineStat", () => { diff --git a/apps/web/src/lib/diffRendering.ts b/apps/web/src/lib/diffRendering.ts index 90c456aa9da1..0c21f627896d 100644 --- a/apps/web/src/lib/diffRendering.ts +++ b/apps/web/src/lib/diffRendering.ts @@ -167,6 +167,16 @@ export function buildFileDiffIdentityKey(fileDiff: FileDiffMetadata): string { return `${resolveFileDiffPreviousPath(fileDiff)}\u0000${resolveFileDiffPath(fileDiff)}`; } +export function buildFileDiffIdentityKeys(files: ReadonlyArray): string[] { + const occurrences = new Map(); + return files.map((file) => { + const identity = buildFileDiffIdentityKey(file); + const occurrence = occurrences.get(identity) ?? 0; + occurrences.set(identity, occurrence + 1); + return occurrence === 0 ? identity : `${identity}\u0000${occurrence}`; + }); +} + export function buildFileDiffRenderKey(fileDiff: FileDiffMetadata): string { const cacheKey = fileDiff.cacheKey; if (!cacheKey) return `${fileDiff.prevName ?? "none"}:${fileDiff.name}`;