feat: add git diff utility module with staged/unstaged support
This commit is contained in:
+87
@@ -0,0 +1,87 @@
|
|||||||
|
import { exec } from "child_process"
|
||||||
|
import { promisify } from "util"
|
||||||
|
|
||||||
|
const execAsync = promisify(exec)
|
||||||
|
|
||||||
|
async function isGitInstalled(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await execAsync("git --version")
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isGitRepo(cwd: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await execAsync("git rev-parse --git-dir", { cwd })
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hasCommits(cwd: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await execAsync("git rev-parse HEAD", { cwd })
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getGitDiff(cwd: string, stagedOnly = false): Promise<string> {
|
||||||
|
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<string> {
|
||||||
|
try {
|
||||||
|
return await getGitDiff(cwd, true)
|
||||||
|
} catch {
|
||||||
|
return await getGitDiff(cwd, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user