diff --git a/src/git/diff.ts b/src/git/diff.ts index 64a2cc0..fc38e0f 100644 --- a/src/git/diff.ts +++ b/src/git/diff.ts @@ -55,14 +55,43 @@ export function getStagedDiff(): DiffResult { }; } +function getUntrackedDiff(): string { + const files = spawnSync('git', ['ls-files', '--others', '--exclude-standard', '-z'], { + encoding: 'utf-8', + maxBuffer: GIT_DIFF_MAX_BUFFER, + }); + if (files.error) throw files.error; + if (files.status !== 0) { + throw new Error(files.stderr.trim() || `git ls-files exited with code ${files.status}`); + } + + return files.stdout + .split('\0') + .filter(Boolean) + .map((file) => { + const result = spawnSync('git', ['diff', '--no-index', '--', '/dev/null', file], { + encoding: 'utf-8', + maxBuffer: GIT_DIFF_MAX_BUFFER, + }); + if (result.error) throw result.error; + if (result.status !== 0 && result.status !== 1) { + throw new Error(result.stderr.trim() || `git diff --no-index exited with code ${result.status}`); + } + return result.stdout; + }) + .filter(Boolean) + .join('\n'); +} + export function getUnstagedDiff(): DiffResult { - const diff = execSync('git diff', { + const trackedDiff = execSync('git diff', { encoding: 'utf-8', maxBuffer: GIT_DIFF_MAX_BUFFER, }); + const diff = [trackedDiff, getUntrackedDiff()].filter(Boolean).join('\n').trim(); return { - diff: diff.trim(), - hasChanges: diff.trim().length > 0, + diff, + hasChanges: diff.length > 0, staged: false, }; } diff --git a/tests/git-diff.test.mjs b/tests/git-diff.test.mjs index fba896f..579916d 100644 --- a/tests/git-diff.test.mjs +++ b/tests/git-diff.test.mjs @@ -190,6 +190,26 @@ test("getUnstagedDiff returns diff for unstaged changes", () => { } }); +test("getUnstagedDiff includes untracked files", () => { + const repoDir = initRepo(); + + try { + git(["commit", "--allow-empty", "-m", "initial commit"], repoDir); + writeFileSync(join(repoDir, "new-file.txt"), "untracked content\n", "utf-8"); + + withCwd(repoDir, () => { + const result = getUnstagedDiff(); + + assert.equal(result.hasChanges, true); + assert.equal(result.staged, false); + assert.ok(result.diff.includes("new-file.txt")); + assert.ok(result.diff.includes("+untracked content")); + }); + } finally { + rmSync(repoDir, { recursive: true, force: true }); + } +}); + test("getUnstagedDiff handles diffs larger than the default execSync buffer", () => { const repoDir = initRepo();