From 4bc3045f68436a62759930f5de9991815194999b Mon Sep 17 00:00:00 2001 From: Devansh Jain <31609257+devanshrj@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:44:45 -0700 Subject: [PATCH] chore: Refactor git context gathering for session and init [LET-7906] (#1379) --- src/cli/App.tsx | 4 +- src/cli/helpers/gitContext.ts | 117 ++++++++++++++++++ src/cli/helpers/initCommand.ts | 83 ++++--------- src/cli/helpers/sessionContext.ts | 60 +++------ .../cli/init-background-subagent.test.ts | 2 +- 5 files changed, 163 insertions(+), 103 deletions(-) create mode 100644 src/cli/helpers/gitContext.ts diff --git a/src/cli/App.tsx b/src/cli/App.tsx index f5e824d..21b7d34 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -230,7 +230,7 @@ import { parsePatchOperations } from "./helpers/formatArgsDisplay"; import { buildInitMessage, fireAutoInit, - gatherGitContext, + gatherInitGitContext, } from "./helpers/initCommand"; import { getReflectionSettings, @@ -9531,7 +9531,7 @@ export default function App({ true, ); - const gitContext = gatherGitContext(); + const { context: gitContext } = gatherInitGitContext(); const memoryDir = settingsManager.isMemfsEnabled(agentId) ? getMemoryFilesystemRoot(agentId) : undefined; diff --git a/src/cli/helpers/gitContext.ts b/src/cli/helpers/gitContext.ts new file mode 100644 index 0000000..704670c --- /dev/null +++ b/src/cli/helpers/gitContext.ts @@ -0,0 +1,117 @@ +import { execFileSync } from "node:child_process"; + +export interface GitContextSnapshot { + isGitRepo: boolean; + branch: string | null; + status: string | null; + recentCommits: string | null; + gitUser: string | null; +} + +export interface GatherGitContextOptions { + cwd?: string; + recentCommitLimit?: number; + /** + * Git log format string passed to `git log --format=...`. + * If omitted, uses `git log --oneline`. + */ + recentCommitFormat?: string; + statusLineLimit?: number; +} + +function runGit(args: string[], cwd: string): string | null { + try { + return execFileSync("git", args, { + cwd, + encoding: "utf-8", + stdio: ["ignore", "pipe", "ignore"], + }).trim(); + } catch { + return null; + } +} + +function truncateLines(value: string, maxLines: number): string { + const lines = value.split("\n"); + if (lines.length <= maxLines) { + return value; + } + return ( + lines.slice(0, maxLines).join("\n") + + `\n... and ${lines.length - maxLines} more changes` + ); +} + +function formatGitUser( + name: string | null, + email: string | null, +): string | null { + if (!name && !email) { + return null; + } + if (name && email) { + return `${name} <${email}>`; + } + return name || email; +} + +export function gatherGitContextSnapshot( + options: GatherGitContextOptions = {}, +): GitContextSnapshot { + const cwd = options.cwd ?? process.cwd(); + const recentCommitLimit = options.recentCommitLimit ?? 3; + + if (!runGit(["rev-parse", "--git-dir"], cwd)) { + return { + isGitRepo: false, + branch: null, + status: null, + recentCommits: null, + gitUser: null, + }; + } + + const branch = runGit(["branch", "--show-current"], cwd); + + const fullStatus = runGit(["status", "--short"], cwd); + const status = + typeof fullStatus === "string" && options.statusLineLimit + ? truncateLines(fullStatus, options.statusLineLimit) + : fullStatus; + + const recentCommits = options.recentCommitFormat + ? runGit( + [ + "log", + `--format=${options.recentCommitFormat}`, + "-n", + String(recentCommitLimit), + ], + cwd, + ) + : runGit(["log", "--oneline", "-n", String(recentCommitLimit)], cwd); + + const userConfig = runGit( + ["config", "--get-regexp", "^user\\.(name|email)$"], + cwd, + ); + let userName: string | null = null; + let userEmail: string | null = null; + if (userConfig) { + for (const line of userConfig.split("\n")) { + if (line.startsWith("user.name ")) + userName = line.slice("user.name ".length); + else if (line.startsWith("user.email ")) + userEmail = line.slice("user.email ".length); + } + } + const gitUser = formatGitUser(userName, userEmail); + + return { + isGitRepo: true, + branch, + status, + recentCommits, + gitUser, + }; +} diff --git a/src/cli/helpers/initCommand.ts b/src/cli/helpers/initCommand.ts index 7559889..63afc19 100644 --- a/src/cli/helpers/initCommand.ts +++ b/src/cli/helpers/initCommand.ts @@ -14,6 +14,7 @@ import { } from "../../agent/memoryFilesystem"; import { SYSTEM_REMINDER_CLOSE, SYSTEM_REMINDER_OPEN } from "../../constants"; import { settingsManager } from "../../settings-manager"; +import { gatherGitContextSnapshot } from "./gitContext"; import { getSnapshot as getSubagentSnapshot } from "./subagentState"; // ── Guard ────────────────────────────────────────────────── @@ -29,69 +30,38 @@ export function hasActiveInitSubagent(): boolean { // ── Git context ──────────────────────────────────────────── -export function gatherGitContext(): string { +export function gatherInitGitContext(): { context: string; identity: string } { try { - const cwd = process.cwd(); + const git = gatherGitContextSnapshot({ + recentCommitLimit: 10, + }); + if (!git.isGitRepo) { + return { + context: "(not a git repository)", + identity: "", + }; + } - try { - execSync("git rev-parse --git-dir", { cwd, stdio: "pipe" }); - - const branch = execSync("git branch --show-current", { - cwd, - encoding: "utf-8", - }).trim(); - const mainBranch = execSync( - "git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo 'main'", - { cwd, encoding: "utf-8", shell: "/bin/bash" }, - ).trim(); - const status = execSync("git status --short", { - cwd, - encoding: "utf-8", - }).trim(); - const recentCommits = execSync( - "git log --oneline -10 2>/dev/null || echo 'No commits yet'", - { cwd, encoding: "utf-8" }, - ).trim(); - - return ` -- branch: ${branch} -- main: ${mainBranch} -- status: ${status || "(clean)"} + return { + context: ` +- branch: ${git.branch ?? "(unknown)"} +- status: ${git.status || "(clean)"} Recent commits: -${recentCommits} -`; - } catch { - return "(not a git repository)"; - } +${git.recentCommits || "No commits yet"} +`, + identity: git.gitUser ?? "", + }; } catch { - // execSync import failed (shouldn't happen with static import, but be safe) - return ""; + return { + context: "", + identity: "", + }; } } // ── Shallow init (background subagent) ─────────────────── -/** Gather git identity for the local user. */ -function gatherGitIdentity(): string { - const cwd = process.cwd(); - try { - const userName = execSync("git config user.name 2>/dev/null || true", { - cwd, - encoding: "utf-8", - }).trim(); - const userEmail = execSync("git config user.email 2>/dev/null || true", { - cwd, - encoding: "utf-8", - }).trim(); - - if (userName || userEmail) return `${userName} <${userEmail}>`; - return ""; - } catch { - return ""; - } -} - /** Read existing memory files from the local filesystem. */ function gatherExistingMemory(agentId: string): string { const systemDir = getMemorySystemDir(agentId); @@ -258,8 +228,7 @@ export async function fireAutoInit( if (hasActiveInitSubagent()) return false; if (!settingsManager.isMemfsEnabled(agentId)) return false; - const gitContext = gatherGitContext(); - const gitIdentity = gatherGitIdentity(); + const gitDetails = gatherInitGitContext(); const existingMemory = gatherExistingMemory(agentId); const dirListing = gatherDirListing(); @@ -267,8 +236,8 @@ export async function fireAutoInit( agentId, workingDirectory: process.cwd(), memoryDir: getMemoryFilesystemRoot(agentId), - gitContext, - gitIdentity, + gitContext: gitDetails.context, + gitIdentity: gitDetails.identity, existingMemory, dirListing, }); diff --git a/src/cli/helpers/sessionContext.ts b/src/cli/helpers/sessionContext.ts index b113258..a93e9ab 100644 --- a/src/cli/helpers/sessionContext.ts +++ b/src/cli/helpers/sessionContext.ts @@ -2,10 +2,10 @@ // Generates session context system reminder for the first message of each CLI session // Contains device/environment information only. Agent metadata is in agentMetadata.ts. -import { execSync } from "node:child_process"; import { platform } from "node:os"; import { SYSTEM_REMINDER_CLOSE, SYSTEM_REMINDER_OPEN } from "../../constants"; import { getVersion } from "../../version"; +import { gatherGitContextSnapshot } from "./gitContext"; /** * Get the current local time in a human-readable format @@ -40,17 +40,6 @@ export function getDeviceType(): string { } } -/** - * Safely execute a git command, returning null on failure - */ -function safeGitExec(command: string, cwd: string): string | null { - try { - return execSync(command, { cwd, encoding: "utf-8", stdio: "pipe" }).trim(); - } catch { - return null; - } -} - /** * Gather git information if in a git repository * Returns truncated commits (3) and status (20 lines) @@ -61,41 +50,25 @@ function getGitInfo(): { branch?: string; recentCommits?: string; status?: string; + gitUser?: string; } { - const cwd = process.cwd(); + const git = gatherGitContextSnapshot({ + recentCommitLimit: 3, + recentCommitFormat: "%h %s (%an)", + statusLineLimit: 20, + }); - try { - // Check if we're in a git repo - execSync("git rev-parse --git-dir", { cwd, stdio: "pipe" }); - - // Get current branch (with fallback) - const branch = safeGitExec("git branch --show-current", cwd) ?? "(unknown)"; - - // Get recent commits (3 commits with author, with fallback) - const recentCommits = - safeGitExec('git log --format="%h %s (%an)" -3', cwd) ?? - "(failed to get commits)"; - - // Get git status (truncate to 20 lines, with fallback) - const fullStatus = - safeGitExec("git status --short", cwd) ?? "(failed to get status)"; - const statusLines = fullStatus.split("\n"); - let status = fullStatus; - if (statusLines.length > 20) { - status = - statusLines.slice(0, 20).join("\n") + - `\n... and ${statusLines.length - 20} more files`; - } - - return { - isGitRepo: true, - branch, - recentCommits, - status: status || "(clean working tree)", - }; - } catch { + if (!git.isGitRepo) { return { isGitRepo: false }; } + + return { + isGitRepo: true, + branch: git.branch ?? "(unknown)", + recentCommits: git.recentCommits ?? "(failed to get commits)", + status: git.status || "(clean working tree)", + gitUser: git.gitUser ?? "(not configured)", + }; } /** @@ -146,6 +119,7 @@ The user has just initiated a new connection via the [Letta Code CLI client](htt // Add git info if available if (gitInfo.isGitRepo) { context += `- **Git repository**: Yes (branch: ${gitInfo.branch}) +- **Git user**: ${gitInfo.gitUser} ### Recent Commits \`\`\` diff --git a/src/tests/cli/init-background-subagent.test.ts b/src/tests/cli/init-background-subagent.test.ts index 927ee01..f1056ac 100644 --- a/src/tests/cli/init-background-subagent.test.ts +++ b/src/tests/cli/init-background-subagent.test.ts @@ -40,7 +40,7 @@ describe("init wiring", () => { const helperSource = readSource("../../cli/helpers/initCommand.ts"); expect(helperSource).toContain("export function hasActiveInitSubagent("); - expect(helperSource).toContain("export function gatherGitContext()"); + expect(helperSource).toContain("export function gatherInitGitContext()"); expect(helperSource).toContain("export function buildShallowInitPrompt("); expect(helperSource).toContain("export function buildInitMessage("); });