diff --git a/packages/git/src/diff.ts b/packages/git/src/diff.ts index b13a9f2..dd1c9c1 100644 --- a/packages/git/src/diff.ts +++ b/packages/git/src/diff.ts @@ -1,7 +1,22 @@ import { exec, execLarge, execLines, execWithStdin } from './exec.js'; +/** + * Flags that neutralize user git config which would otherwise alter the diff + * format and break parsing: + * - `--no-color` guards against `color.ui=always` / `color.diff=always` + * - `--no-ext-diff` guards against a configured `diff.external` driver + * - `--src-prefix`/`--dst-prefix` force the standard `a/`/`b/` prefixes, + * overriding `diff.mnemonicPrefix`, `diff.noprefix` and custom prefixes + */ +const DIFF_FORMAT_ARGS = [ + '--no-color', + '--no-ext-diff', + '--src-prefix=a/', + '--dst-prefix=b/', +]; + export function getDiff(args: string[] = []): string { - const cmd = ['git', 'diff', ...args].join(' '); + const cmd = ['git', 'diff', ...DIFF_FORMAT_ARGS, ...args].join(' '); return execLarge(cmd); } @@ -14,7 +29,7 @@ export function getUntrackedDiff(files: string[]): string { for (const file of files) { try { - execLarge(`git diff --no-index -- /dev/null "${file}"`); + execLarge(`git diff ${DIFF_FORMAT_ARGS.join(' ')} --no-index -- /dev/null "${file}"`); } catch (err: unknown) { const error = err as { stdout?: string; status?: number }; if (error.status === 1 && error.stdout) { diff --git a/packages/git/tests/diff-format.test.ts b/packages/git/tests/diff-format.test.ts new file mode 100644 index 0000000..12916f9 --- /dev/null +++ b/packages/git/tests/diff-format.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { execSync } from 'node:child_process'; +import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; + +let repoDir: string; +let origCwd: string; + +function git(cmd: string) { + execSync(`git ${cmd}`, { cwd: repoDir, stdio: 'pipe' }); +} + +function writeFile(name: string, content: string) { + writeFileSync(join(repoDir, name), content); +} + +beforeAll(() => { + origCwd = process.cwd(); + repoDir = mkdtempSync(join(tmpdir(), 'diffity-format-test-')); + + git('init -b main'); + git('config user.email "test@test.com"'); + git('config user.name "Test"'); + + // git config that alters the default diff format and previously broke parsing + git('config diff.mnemonicprefix true'); + git('config color.ui always'); + + writeFile('base.txt', 'a\nb\n'); + git('add .'); + git('commit -m "initial commit"'); + + process.chdir(repoDir); +}); + +afterAll(() => { + process.chdir(origCwd); + rmSync(repoDir, { recursive: true, force: true }); +}); + +describe('diff format neutralization', () => { + it('emits standard a/ b/ prefixes despite diff.mnemonicprefix', async () => { + const { resolveRef } = await import('../src/diff.js'); + writeFile('base.txt', 'a\nc\n'); + + const raw = resolveRef('unstaged'); + + expect(raw).toContain('diff --git a/base.txt b/base.txt'); + expect(raw).toContain('--- a/base.txt'); + expect(raw).toContain('+++ b/base.txt'); + // no ANSI color escapes even with color.ui=always + expect(raw).not.toMatch(/\x1b\[/); + + git('checkout -- base.txt'); + }); + + it('emits standard prefixes for untracked files', async () => { + const { resolveRef } = await import('../src/diff.js'); + writeFile('untracked.txt', 'x\ny\n'); + + const raw = resolveRef('work'); + + expect(raw).toContain('diff --git a/untracked.txt b/untracked.txt'); + expect(raw).toContain('+++ b/untracked.txt'); + expect(raw).not.toMatch(/\x1b\[/); + + execSync(`rm "${join(repoDir, 'untracked.txt')}"`, { stdio: 'pipe' }); + }); +});