From 7e03021b251a4ea422f60276547b42e84484955e Mon Sep 17 00:00:00 2001 From: ace-pm Date: Wed, 15 Apr 2026 16:16:47 +0200 Subject: [PATCH] Add git status utility for TUI. Provides GitStatus interface and refreshGitStatus function for displaying repository branch, dirty state, untracked files, and commit counts. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/resources/extensions/sf-tui/git.ts | 70 ++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/resources/extensions/sf-tui/git.ts diff --git a/src/resources/extensions/sf-tui/git.ts b/src/resources/extensions/sf-tui/git.ts new file mode 100644 index 000000000..5e2d3ab37 --- /dev/null +++ b/src/resources/extensions/sf-tui/git.ts @@ -0,0 +1,70 @@ +import { execFileSync } from "node:child_process"; + +export interface GitStatus { + branch: string | null; + dirty: boolean; + untracked: boolean; + ahead: number; + behind: number; +} + +let cache: GitStatus | null = null; +let lastFetch = 0; + +export function refreshGitStatus(cwd: string): GitStatus { + const now = Date.now(); + if (now - lastFetch < 400 && cache) return cache; + lastFetch = now; + + let branch: string | null = null; + try { + branch = execFileSync("git", ["branch", "--show-current"], { + cwd, + encoding: "utf-8", + stdio: ["pipe", "pipe", "ignore"], + timeout: 1500, + }).trim() || null; + } catch { + cache = { branch: null, dirty: false, untracked: false, ahead: 0, behind: 0 }; + 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 */ } + + cache = { branch, dirty, untracked, ahead, behind }; + } catch { + cache = { branch, dirty: false, untracked: false, ahead: 0, behind: 0 }; + } + return cache; +} + +export function invalidateGitStatus(): void { + lastFetch = 0; +}