import { execFileSync } from "node:child_process"; import { mkdirSync, mkdtempSync, rmSync, writeFileSync, realpathSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { beforeEach, afterEach, describe, expect, it, vi } from "vitest"; const spawnCounters = vi.hoisted(() => ({ trackedTextDiffCalls: 0, })); vi.mock("child_process", async () => { const actual = await vi.importActual("child_process"); return { ...actual, spawn: (...args: Parameters) => { const [command, commandArgs] = args; if (command === "git" && Array.isArray(commandArgs)) { const normalizedArgs = commandArgs.map((arg) => String(arg)); // `runGitCommand` always prepends `-c core.quotepath=false`; skip it to // find the actual git subcommand. const subcommandIndex = normalizedArgs[0] === "-c" && normalizedArgs[1] === "core.quotepath=false" ? 2 : 0; const isTrackedTextDiff = normalizedArgs[subcommandIndex] === "diff" && normalizedArgs.includes("HEAD") && !normalizedArgs.includes("--numstat") && !normalizedArgs.includes("--no-index") && !normalizedArgs.includes("--shortstat") && !normalizedArgs.includes("--name-status"); if (isTrackedTextDiff) { spawnCounters.trackedTextDiffCalls += 1; } } return actual.spawn(...args); }, }; }); import { getCheckoutDiff } from "./checkout-git.js"; function initRepoWithTrackedChanges(fileCount: number): { tempDir: string; repoDir: string } { const tempDir = realpathSync(mkdtempSync(join(tmpdir(), "checkout-git-batch-test-"))); const repoDir = join(tempDir, "repo"); mkdirSync(repoDir, { recursive: true }); execFileSync("git", ["init", "-b", "main"], { cwd: repoDir }); execFileSync("git", ["config", "user.email", "test@test.com"], { cwd: repoDir }); execFileSync("git", ["config", "user.name", "Test"], { cwd: repoDir }); for (let i = 0; i < fileCount; i += 1) { writeFileSync(join(repoDir, `file-${i}.txt`), `before-${i}\n`); } execFileSync("git", ["add", "."], { cwd: repoDir }); execFileSync("git", ["-c", "commit.gpgsign=false", "commit", "-m", "initial"], { cwd: repoDir, }); for (let i = 0; i < fileCount; i += 1) { writeFileSync(join(repoDir, `file-${i}.txt`), `after-${i}\n`); } return { tempDir, repoDir }; } describe("checkout git diff batching", () => { let tempDir: string; let repoDir: string; beforeEach(() => { const setup = initRepoWithTrackedChanges(20); tempDir = setup.tempDir; repoDir = setup.repoDir; spawnCounters.trackedTextDiffCalls = 0; }); afterEach(() => { rmSync(tempDir, { recursive: true, force: true }); }); it("uses a single tracked git diff command for tracked file diffs", async () => { const result = await getCheckoutDiff(repoDir, { mode: "uncommitted", includeStructured: false, }); expect(result.diff).toContain("file-0.txt"); expect(result.diff).toContain("file-19.txt"); expect(spawnCounters.trackedTextDiffCalls).toBe(1); }); });