diff --git a/packages/server/src/utils/checkout-git.patch-format.test.ts b/packages/server/src/utils/checkout-git.patch-format.test.ts index 834a41b871..d30ae1d7ce 100644 --- a/packages/server/src/utils/checkout-git.patch-format.test.ts +++ b/packages/server/src/utils/checkout-git.patch-format.test.ts @@ -1,13 +1,14 @@ import { execFileSync } from "node:child_process"; -import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { getCheckoutDiff, getCommitFileDiff } from "./checkout-git.js"; const repos: string[] = []; afterEach(() => { + vi.unstubAllEnvs(); for (const repo of repos.splice(0)) rmSync(repo, { recursive: true, force: true }); }); @@ -62,6 +63,95 @@ function createChangedRepo(config: string): string { return cwd; } +function createExternalDiffCommand() { + const directory = realpathSync.native(mkdtempSync(join(tmpdir(), "checkout-external-diff-"))); + repos.push(directory); + const script = join(directory, "external-diff.cjs"); + const marker = join(directory, "invoked"); + writeFileSync( + script, + 'require("node:fs").writeFileSync(process.argv[2], "invoked");\n' + + 'process.stdout.write("CUSTOM EXTERNAL DIFF\\n");\n', + ); + const command = [process.execPath, script, marker] + .map((argument) => `'${argument.replaceAll("'", "'\\''")}'`) + .join(" "); + return { command, marker }; +} + +function createExternalDiffRepo() { + vi.stubEnv("GIT_EXTERNAL_DIFF", undefined); + const cwd = createChangedRepo("color.diff=always"); + const { command, marker } = createExternalDiffCommand(); + git(cwd, "config", "diff.external", command); + expect(git(cwd, "diff", "HEAD", "--", "file.txt")).toBe("CUSTOM EXTERNAL DIFF\n"); + expect(existsSync(marker)).toBe(true); + rmSync(marker); + return { cwd, command, marker }; +} + +function expectStandardPatch(result: Awaited>) { + expect(result.structured).toMatchObject([ + { + path: "file.txt", + additions: 1, + deletions: 1, + status: "ok", + hunks: [ + { + lines: [ + { type: "header", content: "@@ -1 +1 @@" }, + { type: "remove", content: "before" }, + { type: "add", content: "after" }, + ], + }, + ], + }, + ]); +} + +it("returns Git patch hunks when a repository configures an external diff command", async () => { + const { cwd, command, marker } = createExternalDiffRepo(); + + const result = await getCheckoutDiff(cwd, { mode: "uncommitted", includeStructured: true }); + + expectStandardPatch(result); + expect(existsSync(marker)).toBe(false); + expect(git(cwd, "config", "--get", "diff.external").trim()).toBe(command); +}); + +it("returns Git patch hunks against the base branch with an external diff command", async () => { + const { cwd, command, marker } = createExternalDiffRepo(); + git(cwd, "checkout", "-b", "feature"); + git(cwd, "add", "."); + git(cwd, "-c", "commit.gpgsign=false", "commit", "-m", "edit"); + + const result = await getCheckoutDiff(cwd, { + mode: "base", + baseRef: "main", + includeStructured: true, + }); + + expectStandardPatch(result); + expect(existsSync(marker)).toBe(false); + expect(git(cwd, "config", "--get", "diff.external").trim()).toBe(command); +}); + +it("returns Git patch hunks when GIT_EXTERNAL_DIFF configures the external command", async () => { + const cwd = createChangedRepo("color.diff=always"); + const { command, marker } = createExternalDiffCommand(); + vi.stubEnv("GIT_EXTERNAL_DIFF", command); + expect(git(cwd, "diff", "HEAD", "--", "file.txt")).toBe("CUSTOM EXTERNAL DIFF\n"); + expect(existsSync(marker)).toBe(true); + rmSync(marker); + + const result = await getCheckoutDiff(cwd, { mode: "uncommitted", includeStructured: true }); + + expectStandardPatch(result); + expect(existsSync(marker)).toBe(false); + expect(process.env.GIT_EXTERNAL_DIFF).toBe(command); +}); + describe.each(["diff.mnemonicPrefix=true", "color.diff=always"])( "patch format with %s", (config) => { diff --git a/packages/server/src/utils/checkout-git.test.ts b/packages/server/src/utils/checkout-git.test.ts index 47d2aa76ea..21791fa49e 100644 --- a/packages/server/src/utils/checkout-git.test.ts +++ b/packages/server/src/utils/checkout-git.test.ts @@ -1627,9 +1627,11 @@ const x = 1; expect(diff.diff).toContain("# generated.js: diff too large omitted"); expect(diff.diff).toContain(`-export const value = "old";`); expect(diff.diff).toContain(`+export const value = "new";`); - expect(commands).toContain("diff --no-color --src-prefix=a/ --dst-prefix=b/ --numstat HEAD"); expect(commands).toContain( - "diff --no-color --src-prefix=a/ --dst-prefix=b/ HEAD -- :(literal)generated.js :(literal)small.ts", + "diff --no-ext-diff --no-color --src-prefix=a/ --dst-prefix=b/ --numstat HEAD", + ); + expect(commands).toContain( + "diff --no-ext-diff --no-color --src-prefix=a/ --dst-prefix=b/ HEAD -- :(literal)generated.js :(literal)small.ts", ); expect(metrics.maxConcurrent).toBeLessThanOrEqual(8); }); diff --git a/packages/server/src/utils/checkout-git.ts b/packages/server/src/utils/checkout-git.ts index 9b31fd6665..2e317810ba 100644 --- a/packages/server/src/utils/checkout-git.ts +++ b/packages/server/src/utils/checkout-git.ts @@ -637,7 +637,7 @@ function normalizeNumstatPath(pathField: string): string { } // Parsed patches need a stable wire format regardless of terminal presentation settings. -const GIT_PATCH_FORMAT_ARGS = ["--no-color", "--src-prefix=a/", "--dst-prefix=b/"]; +const GIT_PATCH_FORMAT_ARGS = ["--no-ext-diff", "--no-color", "--src-prefix=a/", "--dst-prefix=b/"]; function buildGitDiffArgs(args: { ignoreWhitespace?: boolean; extra: string[] }): string[] { return [