From 33db25cf5c80123862e18ed099e72a0d11c2d8fa Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 6 Nov 2026 07:29:33 +0100 Subject: [PATCH] fix(diff): highlight symlinks from their stored target text Working tree source context must match the symlink blob emitted by Git rather than the contents reached by following the link. Otherwise highlight runs do not cover the patch line and encoding fails the revision. --- packages/server/src/server/diff/git.test.ts | 39 +++++++++++++++++++++ packages/server/src/server/diff/sources.ts | 8 ++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/server/src/server/diff/git.test.ts b/packages/server/src/server/diff/git.test.ts index 3bfd447d05..ebea9003f7 100644 --- a/packages/server/src/server/diff/git.test.ts +++ b/packages/server/src/server/diff/git.test.ts @@ -519,3 +519,42 @@ test.skipIf(process.platform === "win32")( } }, ); + +test.skipIf(process.platform === "win32")( + "highlighted symlink replacements use stored link text instead of target contents", + async () => { + const { encodeDiffFrame, decodeDiffFrame } = + await import("@getpaseo/protocol/binary-frames/index"); + const cwd = await mkdtemp(join(tmpdir(), "diff-highlighted-link-")); + const git = (...args: string[]) => execFileSync("git", args, { cwd, encoding: "utf8" }); + const { symlink, unlink } = await import("node:fs/promises"); + try { + git("init", "-b", "main"); + git("config", "user.name", "Test"); + git("config", "user.email", "test@example.com"); + await writeFile(join(cwd, "file.ts"), "export const old = 1;\n"); + await writeFile(join(cwd, "target.ts"), "export const entirelyDifferentLength = 2;\n"); + git("add", "."); + git("-c", "commit.gpgsign=false", "commit", "-m", "base"); + await unlink(join(cwd, "file.ts")); + await symlink("target.ts", join(cwd, "file.ts")); + const comparison = await listComparison({ + cwd, + base: { kind: "rev", rev: "HEAD" }, + head: { kind: "worktree" }, + }); + try { + const record = await readBody(comparison); + expect(record.body.hunks.flatMap((hunk) => hunk.lines)).toContainEqual( + expect.objectContaining({ type: "add", content: "target.ts" }), + ); + const frame = { opcode: 0x41 as const, slot: 0, revision: 1, files: [record.body] }; + expect(decodeDiffFrame(encodeDiffFrame(frame))).toEqual(frame); + } finally { + await comparison.close(); + } + } finally { + await rm(cwd, { recursive: true, force: true }); + } + }, +); diff --git a/packages/server/src/server/diff/sources.ts b/packages/server/src/server/diff/sources.ts index 01aacf4c82..ab47ef2260 100644 --- a/packages/server/src/server/diff/sources.ts +++ b/packages/server/src/server/diff/sources.ts @@ -1,3 +1,5 @@ +import { lstat, readlink } from "node:fs/promises"; +import { resolve } from "node:path"; import { readGitFileContents, readGitFileContentAtRef } from "../../utils/git-file-contents.js"; import type { ListedFile } from "./git.js"; @@ -34,8 +36,12 @@ export class HighlightSources { ? null : await this.content(contents, this.options.baseSha, file.entry.oldPath ?? file.entry.path); let newFileContent: string | null | undefined; - if (this.options.headSha !== null) + if (this.options.headSha !== null) { newFileContent = await this.content(contents, this.options.headSha, file.entry.path); + } else if (file.entry.change !== "deleted") { + const path = resolve(this.options.cwd, file.entry.path); + if ((await lstat(path)).isSymbolicLink()) newFileContent = await readlink(path); + } return { oldFileContent, newFileContent }; } private async load(start: number): Promise | null> {