From e516961a094e59fd96087348714b60a8c3bebef1 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 6 Nov 2026 07:35:28 +0100 Subject: [PATCH] fix(diff): tolerate disappearing highlight sources Source context is optional after Git has produced the patch. Missing worktree link metadata must preserve the existing hunk-based highlighting fallback instead of failing the revision. --- packages/server/src/server/diff/git.test.ts | 35 +++++++++++++++++++++ packages/server/src/server/diff/sources.ts | 11 ++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/packages/server/src/server/diff/git.test.ts b/packages/server/src/server/diff/git.test.ts index ebea9003f7..d06fa2ae03 100644 --- a/packages/server/src/server/diff/git.test.ts +++ b/packages/server/src/server/diff/git.test.ts @@ -558,3 +558,38 @@ test.skipIf(process.platform === "win32")( } }, ); + +test("files removed between listing and highlighting retain their deletion patch", async () => { + const { encodeDiffFrame, decodeDiffFrame } = + await import("@getpaseo/protocol/binary-frames/index"); + const cwd = await mkdtemp(join(tmpdir(), "diff-source-disappeared-")); + const git = (...args: string[]) => execFileSync("git", args, { cwd, encoding: "utf8" }); + try { + git("init", "-b", "main"); + git("config", "user.name", "Test"); + git("config", "user.email", "test@example.com"); + await writeFile(join(cwd, "a.ts"), "export const old = 1;\n"); + git("add", "."); + git("-c", "commit.gpgsign=false", "commit", "-m", "base"); + await writeFile(join(cwd, "a.ts"), "export const old = 2;\n"); + const comparison = await listComparison({ + cwd, + base: { kind: "rev", rev: "HEAD" }, + head: { kind: "worktree" }, + }); + try { + expect(comparison.files[0].entry.change).toBe("modified"); + await rm(join(cwd, "a.ts")); + const record = await readBody(comparison); + expect(record.body.hunks.flatMap((hunk) => hunk.lines)).toContainEqual( + expect.objectContaining({ type: "remove", content: "export const old = 1;" }), + ); + 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 ab47ef2260..776303a9eb 100644 --- a/packages/server/src/server/diff/sources.ts +++ b/packages/server/src/server/diff/sources.ts @@ -40,7 +40,7 @@ export class HighlightSources { 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); + newFileContent = await readWorktreeLink(path); } return { oldFileContent, newFileContent }; } @@ -65,3 +65,12 @@ export class HighlightSources { return content; } } + +async function readWorktreeLink(path: string): Promise { + try { + if ((await lstat(path)).isSymbolicLink()) return await readlink(path); + } catch { + // Source context is optional; files can disappear after Git reads the patch. + } + return undefined; +}