import { execFileSync } from "node:child_process"; import { basename } from "node:path"; let cache = null; let lastFetch = 0; function getRepoName(cwd) { try { const root = execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"], timeout: 1500, }).trim(); return root ? basename(root) : basename(cwd) || null; } catch { return basename(cwd) || null; } } function getLastCommit(cwd) { try { const raw = execFileSync("git", ["log", "-1", "--format=%cr|%s"], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"], timeout: 1500, }).trim(); const sep = raw.indexOf("|"); if (sep > 0) { return { timeAgo: raw.slice(0, sep).replace(/ ago$/, ""), message: raw.slice(sep + 1), }; } } catch { /* ignore */ } return null; } function getDiffStats(cwd) { try { const raw = execFileSync("git", ["diff", "HEAD", "--stat"], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"], timeout: 1500, }); let added = 0; let deleted = 0; let modified = 0; for (const line of raw.split("\n")) { const addMatch = line.match(/(\d+) insertion/); const delMatch = line.match(/(\d+) deletion/); if (addMatch || delMatch) { const a = addMatch ? parseInt(addMatch[1], 10) : 0; const d = delMatch ? parseInt(delMatch[1], 10) : 0; if (a) added += a; if (d) deleted += d; if (a || d) modified++; } } return { added, deleted, modified }; } catch { return { added: 0, deleted: 0, modified: 0 }; } } export function refreshGitStatus(cwd) { const now = Date.now(); if (now - lastFetch < 400 && cache) return cache; lastFetch = now; const repo = getRepoName(cwd); let branch = null; try { branch = execFileSync("git", ["branch", "--show-current"], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"], timeout: 1500, }).trim() || null; } catch { cache = { repo, branch: null, dirty: false, untracked: false, ahead: 0, behind: 0, added: 0, deleted: 0, modified: 0, lastCommit: null, }; return cache; } try { const status = execFileSync("git", ["status", "--porcelain"], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"], timeout: 1500, }); const lines = status.split("\n").filter((l) => l.length > 2); const dirty = lines.some((l) => { const x = l[0] ?? " "; const y = l[1] ?? " "; return (x !== "?" && x !== " " && x !== "!") || (y !== " " && y !== "?"); }); const untracked = lines.some((l) => l.startsWith("??")); let ahead = 0; let behind = 0; try { const ab = execFileSync( "git", ["rev-list", "--left-right", "--count", "HEAD...@{u}"], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"], timeout: 1500, }, ).trim(); const [a, b] = ab.split("\t").map((n) => parseInt(n, 10)); ahead = Number.isNaN(a) ? 0 : a; behind = Number.isNaN(b) ? 0 : b; } catch { /* no upstream */ } const diff = getDiffStats(cwd); const lastCommit = getLastCommit(cwd); cache = { repo, branch, dirty, untracked, ahead, behind, ...diff, lastCommit, }; } catch { cache = { repo, branch, dirty: false, untracked: false, ahead: 0, behind: 0, added: 0, deleted: 0, modified: 0, lastCommit: getLastCommit(cwd), }; } return cache; } export function invalidateGitStatus() { lastFetch = 0; }