diff --git a/src/git.ts b/src/git.ts new file mode 100644 index 0000000..8f5f69b --- /dev/null +++ b/src/git.ts @@ -0,0 +1,87 @@ +import { exec } from "child_process" +import { promisify } from "util" + +const execAsync = promisify(exec) + +async function isGitInstalled(): Promise { + try { + await execAsync("git --version") + return true + } catch { + return false + } +} + +async function isGitRepo(cwd: string): Promise { + try { + await execAsync("git rev-parse --git-dir", { cwd }) + return true + } catch { + return false + } +} + +async function hasCommits(cwd: string): Promise { + try { + await execAsync("git rev-parse HEAD", { cwd }) + return true + } catch { + return false + } +} + +export async function getGitDiff(cwd: string, stagedOnly = false): Promise { + if (!(await isGitInstalled())) { + throw new Error("Git is not installed") + } + if (!(await isGitRepo(cwd))) { + throw new Error("Not a git repository") + } + + let diff = "" + + if (await hasCommits(cwd)) { + const { stdout: staged } = await execAsync("git --no-pager diff --staged --diff-filter=d", { cwd }) + diff = staged.trim() + } + + if (!stagedOnly && !diff) { + const { stdout: unstaged } = await execAsync("git --no-pager diff HEAD --diff-filter=d", { cwd }) + diff = unstaged.trim() + } + + // New repo with no commits yet — show everything that's staged/tracked + if (!diff) { + const { stdout: newRepo } = await execAsync("git --no-pager diff --cached --diff-filter=d", { cwd }) + diff = newRepo.trim() + } + + // Include untracked (new) files when not limiting to staged only + if (!stagedOnly) { + const { stdout: untrackedList } = await execAsync("git ls-files --others --exclude-standard", { cwd }) + const untrackedFiles = untrackedList.trim().split("\n").filter(Boolean) + for (const file of untrackedFiles) { + // git diff --no-index exits with code 1 when files differ — that's normal + const result = await execAsync( + `git --no-pager diff --no-index -- /dev/null ${JSON.stringify(file)}`, + { cwd }, + ).catch((e: any) => ({ stdout: e.stdout as string | undefined })) + if (result.stdout) diff += (diff ? "\n" : "") + result.stdout + } + diff = diff.trim() + } + + if (!diff) { + throw new Error("No changes found to generate a commit message from") + } + + return diff +} + +export async function getGitDiffStagedFirst(cwd: string): Promise { + try { + return await getGitDiff(cwd, true) + } catch { + return await getGitDiff(cwd, false) + } +}