Files
pi-config/extensions/rpiv-pi/extensions/rpiv-core/git-context.ts

80 lines
2.8 KiB
TypeScript

/**
* 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<GitContext | null> {
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<GitContext | null> {
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<string | null> {
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);
}