// Git 状态收集(异步 + 缓存) // 分支名由 pi 的 footerData.getGitBranch() 提供(带文件监听),此处仅收集 // status / ahead / behind / sha,避免阻塞 footer 渲染 import { spawn } from "node:child_process"; export type GitStatus = "clean" | "dirty" | "conflicts"; /** 额外 git 状态(分支名除外) */ export interface GitExtra { status: GitStatus; ahead: number; behind: number; sha?: string; } type CacheEntry = GitExtra | null; // null = 非 git 仓库 /** 运行 git 命令,返回 stdout 字符串(失败返回空串) */ function runGit(args: string[], cwd: string): Promise { return new Promise((resolve) => { let out = ""; try { const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "ignore"], windowsHide: true, }); child.stdout.on("data", (d: Buffer) => { out += d.toString(); }); child.on("error", () => resolve("")); child.on("close", () => resolve(out.trim())); } catch { resolve(""); } }); } /** 判断是否为 git 仓库 */ async function isRepo(cwd: string): Promise { const r = await runGit(["--no-optional-locks", "rev-parse", "--git-dir"], cwd); return r.length > 0 && !r.toLowerCase().startsWith("fatal"); } /** 收集额外 git 状态(异步) */ async function collectGitExtra(cwd: string, showSha: boolean): Promise { if (!(await isRepo(cwd))) return null; const statusOut = await runGit(["--no-optional-locks", "status", "--porcelain"], cwd); let status: GitStatus = "clean"; if (statusOut.trim()) { // 冲突标记:UU / AA / DD(任一列) status = /(^|\n)(UU|AA|DD)/.test(statusOut) ? "conflicts" : "dirty"; } const aheadStr = await runGit(["--no-optional-locks", "rev-list", "--count", "@{u}..HEAD"], cwd); const behindStr = await runGit(["--no-optional-locks", "rev-list", "--count", "HEAD..@{u}"], cwd); const ahead = parseInt(aheadStr, 10) || 0; const behind = parseInt(behindStr, 10) || 0; let sha: string | undefined; if (showSha) { const s = await runGit(["--no-optional-locks", "rev-parse", "--short=7", "HEAD"], cwd); if (s) sha = s; } return { status, ahead, behind, sha }; } /** Git 状态缓存:render 时同步读取,后台异步刷新 */ export class GitCache { private cache = new Map(); private inflight = new Map>(); private lastRefresh = new Map(); private showSha = false; /** 刷新节流间隔(毫秒),避免渲染时频繁调用 git */ private throttleMs = 15_000; setShowSha(v: boolean): void { this.showSha = v; } /** 同步读取缓存:undefined=未加载,null=非仓库,对象=已加载 */ get(cwd: string): CacheEntry | undefined { return this.cache.get(cwd); } /** 异步刷新;force=true 绕过节流(用于 turn 结束等事件) */ async refresh(cwd: string, onUpdate?: () => void, force = false): Promise { const now = Date.now(); if ( !force && this.lastRefresh.has(cwd) && now - (this.lastRefresh.get(cwd) ?? 0) < this.throttleMs ) { return; // 节流跳过 } if (this.inflight.has(cwd)) return this.inflight.get(cwd); const p = (async () => { try { const r = await collectGitExtra(cwd, this.showSha); this.cache.set(cwd, r); this.lastRefresh.set(cwd, Date.now()); } catch { this.cache.set(cwd, null); } this.inflight.delete(cwd); onUpdate?.(); })(); this.inflight.set(cwd, p); return p; } dispose(): void { this.cache.clear(); this.inflight.clear(); this.lastRefresh.clear(); } }