/** * Cached branch + short commit. Injected into the transcript once at * session_start, re-injected on session_compact (transcript cleared) and * only when the cached value changes (e.g. after a mutating git command). * Two parallel `git rev-parse` calls — one call can't combine * `--abbrev-ref` and `--short` cleanly because the `--abbrev-ref` mode * persists to subsequent revs. git itself resolves worktree gitdir * redirection, so either form is worktree-safe. */ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; type GitContext = { branch: string; commit: string; user: string }; // Signature (branch+commit) of the last message pushed into the transcript. // null = transcript has nothing current and needs re-injection. let lastInjectedSig: string | null = null; // undefined = not loaded yet, null = not a git repo / failed, object = valid let cache: GitContext | null | undefined; export async function getGitContext(pi: ExtensionAPI): Promise { if (cache !== undefined) return cache; cache = await loadGitContext(pi); return cache; } export function clearGitContextCache(): void { cache = undefined; } // Detached HEAD emits literal "HEAD" for --abbrev-ref; remap so frontmatter is meaningful. async function loadGitContext(pi: ExtensionAPI): Promise { try { const [branchRes, commitRes] = await Promise.all([ pi.exec("git", ["rev-parse", "--abbrev-ref", "HEAD"], { timeout: 5000 }), pi.exec("git", ["rev-parse", "--short", "HEAD"], { timeout: 5000 }), ]); const rawBranch = branchRes.stdout.trim(); const commit = commitRes.stdout.trim(); if (!rawBranch && !commit) return null; const branch = rawBranch === "HEAD" ? "detached" : rawBranch; let user = ""; try { const r2 = await pi.exec("git", ["config", "user.name"], { timeout: 5000 }); user = r2.stdout.trim(); } catch { // fall through to env fallback } if (!user) user = process.env.USER || "unknown"; return { branch: branch || "no-branch", commit: commit || "no-commit", user, }; } catch { return null; } } export function resetInjectedMarker(): void { lastInjectedSig = null; } // Returns the message content to inject, or null if the transcript is // already up-to-date or we're not in a git repo. Updates the marker // whenever it returns non-null. export async function takeGitContextIfChanged(pi: ExtensionAPI): Promise { const g = await getGitContext(pi); if (!g) return null; const sig = `${g.branch}\n${g.commit}\n${g.user}`; if (sig === lastInjectedSig) return null; lastInjectedSig = sig; return `## Git Context\n- Branch: ${g.branch}\n- Commit: ${g.commit}\n- User: ${g.user}`; } export function isGitMutatingCommand(cmd: string): boolean { return /\bgit\s+(checkout|switch|commit|merge|rebase|pull|reset|revert|cherry-pick|worktree|am|stash)\b/.test(cmd); }