/* RFC-0563: Low-level git operations for the git-mesh subsystem. Thin wrappers around `git` CLI invocations using execFileSync. All functions are async-ready via promisified execFile with a 6-minute timeout for network operations. Do not implement convergence logic — that lives in sync.ts. Do not implement command handlers — those live in sync.ts, status.ts, verify.ts. Do not implement config loading — that lives in config.ts. Git operations are thin execFileSync wrappers — no git logic is reimplemented. RFC-0563: initial implementation — git operations layer for git-mesh. RFC-1097: step 6 — compass.migrate codemod run Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order. RFC-1097: sweep — werkstatt-engine clean Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics. RFC-1149: migrate ambient filesystem and process IO to the WorkspaceIO port across engine and shared */ import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; const GIT_TIMEOUT_MS = 360_000; // 6 minutes export interface CommitInfo { sha: string; signatureStatus: string; // %G? value: G, B, U, X, Y, R, E, N } export interface RemoteInfo { name: string; url: string; } async function git(args: string[], cwd: string, io: WorkspaceIO): Promise { const result = await io.exec("git", args, { cwd, timeoutMs: GIT_TIMEOUT_MS }); if (result.exitCode !== 0) { const err = new Error( `git ${args[0] ?? ""} failed (exit ${result.exitCode}): ${result.stderr.trim()}`, ); (err as { stderr?: string }).stderr = result.stderr; throw err; } return result.stdout.trim(); } export async function gitFetch( remote: string, branch: string, cwd: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { await git(["fetch", remote, branch], cwd, io); } export async function gitMergeFfOnly( commitSha: string, cwd: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { await git(["merge", "--ff-only", commitSha], cwd, io); } export async function gitFsck( cwd: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { try { await git(["fsck", "--no-dangling"], cwd, io); return true; } catch { return false; } } export async function gitRevParseHead( cwd: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { return git(["rev-parse", "HEAD"], cwd, io); } export async function gitRevParseRemote( remote: string, branch: string, cwd: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { return git(["rev-parse", `refs/remotes/${remote}/${branch}`], cwd, io); } export async function gitCommitTimestamp( sha: string, cwd: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { const output = await git(["log", "-1", "--format=%ct", sha], cwd, io); return parseInt(output, 10); } export async function gitIsAncestor( ancestor: string, descendant: string, cwd: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { try { await git(["merge-base", "--is-ancestor", ancestor, descendant], cwd, io); return true; } catch { return false; } } export async function gitHasUncommittedChanges( cwd: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { const output = await git(["status", "--porcelain"], cwd, io); return output.length > 0; } export async function gitRemoteList( cwd: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { const output = await git(["remote", "-v"], cwd, io); const seen = new Set(); const remotes: RemoteInfo[] = []; for (const line of output.split("\n")) { const match = line.match(/^(\S+)\s+(\S+)\s+\((fetch|push)\)$/); if (!match) continue; const [, name, url] = match; const key = `${name}\x1f${url}`; if (seen.has(key)) continue; seen.add(key); remotes.push({ name, url }); } return remotes; } export async function gitRemoteAdd( name: string, url: string, cwd: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { await git(["remote", "add", name, url], cwd, io); } export async function gitRemoteSetUrl( name: string, url: string, cwd: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { await git(["remote", "set-url", name, url], cwd, io); } export async function gitRevListCount( fromSha: string, toSha: string, cwd: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { const output = await git(["rev-list", "--count", `${fromSha}..${toSha}`], cwd, io); return parseInt(output, 10); } export async function gitLogSignatureStatus( range: string, cwd: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { const RECORD_SEP = "\x1e"; const FIELD_SEP = "\x1f"; const output = await git(["log", range, `--format=%H${FIELD_SEP}%G?${RECORD_SEP}`], cwd, io); return output .split(RECORD_SEP) .map((record) => record.trim()) .filter((record) => record.length > 0) .map((record) => { const [sha = "", sigStatus = ""] = record.split(FIELD_SEP); return { sha, signatureStatus: sigStatus }; }); } export async function gitLogAll( range: string, cwd: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { return gitLogSignatureStatus(range, cwd, io); }