/* Facilitates Git operations for tracking file integrity and repository metadata. Do not handle raw Git content parsing outside defined methods. Do not manage Git configuration or transport orchestration. 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 */ /** * Git operations for integrity tracking. * Provides file history, change detection, and repository metadata queries. */ import type { ChangedPaths, GitFileHistory } from "./types.ts"; import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; const DEFAULT_GIT_TIMEOUT_MS = 15_000; interface RunGitOptions { timeoutMs?: number; } async function runGit( cwd: string, args: string[], io: WorkspaceIO, options: RunGitOptions = {}, ): Promise { const { timeoutMs = DEFAULT_GIT_TIMEOUT_MS } = options; const result = await io.exec("git", args, { cwd, timeoutMs }); if (result.exitCode !== 0) { throw new Error( `git ${args[0] ?? ""} failed (exit ${result.exitCode}): ${result.stderr.trim()}`, ); } return result.stdout.trimEnd(); } function uniqueSorted(values: Iterable): string[] { return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b)); } export async function getTrackedFiles( cwd: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { const output = await runGit(cwd, ["ls-files"], io); return output ? output.split("\n").filter(Boolean) : []; } export async function getFileHistory( cwd: string, repoPath: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { try { const latestOutput = await runGit( cwd, ["log", "--follow", "-1", "--format=%aI%x09%H", "--", repoPath], io, { timeoutMs: 10_000 }, ).catch(() => ""); const [updatedAtRaw, lastCommitShaRaw] = latestOutput.split("\t"); const createdOutput = await runGit( cwd, ["log", "--follow", "--diff-filter=A", "--format=%aI", "--", repoPath], io, { timeoutMs: 5_000 }, ).catch(() => ""); const createdLines = createdOutput ? createdOutput.split("\n").filter(Boolean) : []; return { createdAt: createdLines.length ? createdLines[createdLines.length - 1] : null, updatedAt: updatedAtRaw || null, lastCommitSha: lastCommitShaRaw || null, }; } catch { return { createdAt: null, updatedAt: null, lastCommitSha: null }; } } export async function getFileRevisionFromHistory( cwd: string, repoPath: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { try { const output = await runGit( cwd, ["log", "--follow", "--diff-filter=AMT", "--format=%H", "--", repoPath], io, { timeoutMs: 15_000 }, ).catch(() => ""); if (!output) { return 1; } const revisions = output.split("\n").filter(Boolean).length; return Math.max(1, revisions); } catch { return 1; } } function parseNameStatus(output: string): ChangedPaths { const added = new Set(); const modified = new Set(); const deleted = new Set(); const renamed: Array<{ from: string; to: string }> = []; for (const line of output.split("\n").filter(Boolean)) { const parts = line.split("\t"); const status = parts[0] ?? ""; if (status.startsWith("R") && parts[1] && parts[2]) { renamed.push({ from: parts[1], to: parts[2] }); continue; } if (status === "A" && parts[1]) added.add(parts[1]); if ((status === "M" || status === "T") && parts[1]) modified.add(parts[1]); if (status === "D" && parts[1]) deleted.add(parts[1]); } return { added: uniqueSorted(added), modified: uniqueSorted(modified), deleted: uniqueSorted(deleted), renamed, }; } function parsePorcelainStatus(output: string): ChangedPaths { const added = new Set(); const modified = new Set(); const deleted = new Set(); const renamed: Array<{ from: string; to: string }> = []; for (const line of output.split("\n").filter(Boolean)) { if (line.startsWith("?? ")) { added.add(line.slice(3).trim()); continue; } const status = line.slice(0, 2); const body = line.slice(3).trim(); if (body.includes(" -> ") && (status.includes("R") || status.includes("C"))) { const [from, to] = body.split(" -> "); if (from && to) { renamed.push({ from, to }); } continue; } if (status.includes("D")) { deleted.add(body); continue; } if (status.includes("A")) { added.add(body); continue; } if (status.includes("M") || status.includes("T")) { modified.add(body); } } return { added: uniqueSorted(added), modified: uniqueSorted(modified), deleted: uniqueSorted(deleted), renamed, }; } function hasAnyChanges(changes: ChangedPaths): boolean { return Boolean( changes.added.length || changes.modified.length || changes.deleted.length || changes.renamed.length, ); } export async function getChangedPaths( cwd: string, baseRef?: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { try { if (baseRef) { const output = await runGit( cwd, ["diff", "--name-status", "--find-renames", `${baseRef}...HEAD`], io, { timeoutMs: 10_000 }, ); return parseNameStatus(output); } const output = await runGit(cwd, ["status", "--porcelain=1", "--untracked-files=normal"], io, { timeoutMs: 10_000, }); const workingTreeChanges = parsePorcelainStatus(output); if (hasAnyChanges(workingTreeChanges)) { return workingTreeChanges; } const headDiff = await runGit( cwd, ["diff-tree", "--no-commit-id", "--name-status", "--find-renames", "-r", "HEAD"], io, { timeoutMs: 10_000 }, ).catch(() => ""); if (!headDiff) { return workingTreeChanges; } return parseNameStatus(headDiff); } catch { return { added: [], modified: [], deleted: [], renamed: [] }; } } export async function getRepoUrl( cwd: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { try { const output = await runGit(cwd, ["config", "--get", "remote.origin.url"], io, { timeoutMs: 5_000, }); return output || null; } catch { return null; } } export async function getHeadSha( cwd: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { try { const output = await runGit(cwd, ["rev-parse", "HEAD"], io, { timeoutMs: 5_000 }); return output || null; } catch { return null; } }