{"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../../src/core/workspace/git.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,MAAM,WAAW,WAAW;IAC3B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAChC,WAAW,EAAE,mBAAmB,CAAC;CACjC;AAgBD,mDAAmD;AACnD,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE9C;AAED,wDAAwD;AACxD,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAKpF;AAED,4EAA4E;AAC5E,wBAAgB,0BAA0B,CACzC,GAAG,EAAE,MAAM,EACX,UAAU,CAAC,EAAE,MAAM,GACjB;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAMhG;AAED,6FAA6F;AAC7F,wBAAgB,kBAAkB,CACjC,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,MAAM,EAAE,EACvB,IAAI,GAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAA;CAAO,GAClC,WAAW,CA2Eb;AAWD,8FAA8F;AAC9F,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,OAAO,CAAA;CAAE,CAEjF;AAED,kEAAkE;AAClE,wBAAgB,kBAAkB,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAElE;AAED,8DAA8D;AAC9D,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAItE","sourcesContent":["/**\n * Git metadata collection and worktree invalidation.\n *\n * Git is used only as a bounded ranking/invalidation signal, never as a\n * rollback mechanism. Current workspace content remains authoritative. Never\n * runs destructive Git commands.\n */\n\nimport { execSync } from \"node:child_process\";\nimport path from \"node:path\";\n\nexport interface GitFileMeta {\n\tworkspaceRelativePath: string;\n\tlastCommit?: string;\n\tlastCommitTime?: string;\n\tchangeCount: number;\n\tgitBlobId?: string;\n\tisTracked: boolean;\n}\n\nexport interface WorktreeFingerprint {\n\tgitHead?: string;\n\tbranch?: string;\n\tworktreeId?: string;\n\tstatusHash: string;\n\tdirty: boolean;\n}\n\nexport interface GitSnapshot {\n\tfiles: Map<string, GitFileMeta>; // keyed by workspace-relative path\n\tfingerprint: WorktreeFingerprint;\n}\n\nfunction git(cwd: string, args: string[]): { out: string; ok: boolean } {\n\ttry {\n\t\tconst out = execSync(`git ${args.join(\" \")}`, {\n\t\t\tcwd,\n\t\t\tencoding: \"utf-8\",\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\ttimeout: 10000,\n\t\t});\n\t\treturn { out: out.trim(), ok: true };\n\t} catch {\n\t\treturn { out: \"\", ok: false };\n\t}\n}\n\n/** Determine if cwd is inside a git repository. */\nexport function isGitRepo(cwd: string): boolean {\n\treturn git(cwd, [\"rev-parse\", \"--is-inside-work-tree\"]).out === \"true\";\n}\n\n/** Compute a dirty-state fingerprint for a worktree. */\nexport function worktreeStatusSnapshot(cwd: string): { dirty: boolean; hash: string } {\n\tconst res = git(cwd, [\"status\", \"--porcelain\"]);\n\tif (!res.ok) return { dirty: false, hash: \"no-git\" };\n\tconst lines = res.out.split(\"\\n\").filter(Boolean).sort();\n\treturn { dirty: lines.length > 0, hash: lines.join(\"|\") || \"clean\" };\n}\n\n/** Compute the worktree fingerprint used in generation source snapshots. */\nexport function computeWorktreeFingerprint(\n\tcwd: string,\n\tworktreeId?: string,\n): { gitHead?: string; branch?: string; dirty: boolean; statusHash: string; fingerprint: string } {\n\tconst head = git(cwd, [\"rev-parse\", \"HEAD\"]).out || undefined;\n\tconst branch = git(cwd, [\"rev-parse\", \"--abbrev-ref\", \"HEAD\"]).out || undefined;\n\tconst status = worktreeStatusSnapshot(cwd);\n\tconst fingerprint = `${worktreeId ?? \"\"}|${head ?? \"\"}|${branch ?? \"\"}|${status.hash}`;\n\treturn { gitHead: head, branch, dirty: status.dirty, statusHash: status.hash, fingerprint };\n}\n\n/** Collect bounded Git metadata for a set of files (tracked/untracked + shallow history). */\nexport function collectGitMetadata(\n\tcwd: string,\n\trelativePaths: string[],\n\topts: { historyDepth?: number } = {},\n): GitSnapshot {\n\tconst map = new Map<string, GitFileMeta>();\n\tconst fingerprint = computeWorktreeFingerprint(cwd);\n\tconst historyDepth = Math.max(0, opts.historyDepth ?? 5);\n\n\tfor (const rel of relativePaths) {\n\t\tmap.set(rel, {\n\t\t\tworkspaceRelativePath: rel,\n\t\t\tchangeCount: 0,\n\t\t\tisTracked: false,\n\t\t});\n\t}\n\n\t// Which files are tracked? Use git ls-files.\n\tconst tracked = new Set<string>();\n\tconst ls = git(cwd, [\"ls-files\"]);\n\tif (ls.ok) for (const f of ls.out.split(\"\\n\")) if (f) tracked.add(f.replace(/\\\\/g, \"/\"));\n\t// Real paths (files may be symlinked); compare basenames to handle casing.\n\tfor (const rel of relativePaths) {\n\t\tconst meta = map.get(rel);\n\t\tif (!meta) continue;\n\t\tmeta.isTracked = tracked.has(rel);\n\t\tif (!meta.isTracked) {\n\t\t\t// Case-insensitive fallback (Windows).\n\t\t\tfor (const t of tracked) {\n\t\t\t\tif (t.toLowerCase() === rel.toLowerCase()) {\n\t\t\t\t\tmeta.isTracked = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Bounded per-file history (git log for diamonds is bounded by historyDepth).\n\tif (historyDepth > 0) {\n\t\tconst chunks: string[][] = [];\n\t\tfor (let i = 0; i < relativePaths.length; i += 40) chunks.push(relativePaths.slice(i, i + 40));\n\t\tfor (const batch of chunks) {\n\t\t\tif (batch.length === 0) continue;\n\t\t\tconst pathspec = batch.map((p) => `-- '${p.replace(/'/g, \"''\")}'`).join(\" \");\n\t\t\tconst res = git(cwd, [\n\t\t\t\t\"log\",\n\t\t\t\t\"--format=%H|%ct\",\n\t\t\t\t`-n`,\n\t\t\t\t`${historyDepth}`,\n\t\t\t\t`--name-only`,\n\t\t\t\t`--date=unix`,\n\t\t\t\tpathspec,\n\t\t\t]);\n\t\t\tif (!res.ok) continue;\n\t\t\tconst lines = res.out.split(\"\\n\");\n\t\t\tlet currentHash = \"\";\n\t\t\tlet currentTime = \"\";\n\t\t\tfor (const line of lines) {\n\t\t\t\tconst hmm = line.match(/^([0-9a-f]{40})\\|(\\d+)$/);\n\t\t\t\tif (hmm) {\n\t\t\t\t\tcurrentHash = hmm[1];\n\t\t\t\t\tcurrentTime = hmm[2];\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (line.startsWith(\" \") || line === \"\") continue;\n\t\t\t\tconst rel = line.replace(/\\\\/g, \"/\");\n\t\t\t\tconst meta = findMeta(map, rel) ?? map.get(rel);\n\t\t\t\tif (!meta) continue;\n\t\t\t\tif (!meta.lastCommit) {\n\t\t\t\t\tmeta.lastCommit = currentHash || undefined;\n\t\t\t\t\tmeta.lastCommitTime = currentTime ? new Date(Number(currentTime) * 1000).toISOString() : undefined;\n\t\t\t\t}\n\t\t\t\tmeta.changeCount++;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Resolve blob ids for tracked files cheaply (bounded) — optional and lazy.\n\treturn { files: map, fingerprint };\n}\n\nfunction findMeta(map: Map<string, GitFileMeta>, rel: string): GitFileMeta | undefined {\n\tconst direct = map.get(rel);\n\tif (direct) return direct;\n\tfor (const key of map.keys()) {\n\t\tif (key.toLowerCase() === rel.toLowerCase()) return map.get(key);\n\t}\n\treturn undefined;\n}\n\n/** Returns the absolute path of the git blob not needed; helper kept for API completeness. */\nexport function gitExec(cwd: string, args: string[]): { out: string; ok: boolean } {\n\treturn git(cwd, args);\n}\n\n/** Detect whether two worktree fingerprints differ materially. */\nexport function fingerprintChanged(a?: string, b?: string): boolean {\n\treturn Boolean(a && b && a !== b);\n}\n\n/** Verify a workspace root is a git repo without throwing. */\nexport function safeResolveGitTopLevel(cwd: string): string | undefined {\n\tconst res = git(cwd, [\"rev-parse\", \"--show-toplevel\"]);\n\tif (!res.ok) return undefined;\n\treturn path.resolve(cwd, res.out);\n}\n"]}