import { execFile } from "node:child_process"; import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { promisify } from "node:util"; const execFileAsync = promisify(execFile); // Keep ccc's index exclusion in sync with the project's effective .gitignore. // git itself is the source of truth: its ignored-set output is platform-uniform // (always forward-slash, faithful to nesting/negation/anchoring), which sidesteps // ccc's own .gitignore matcher being unreliable on Windows. Best-effort: any // failure (no git, not a repo, unexpected settings format) leaves settings as-is. export async function syncGitignoreExcludes(cwd: string): Promise { try { const cocoDir = join(cwd, ".cocoindex_code"); const settingsPath = join(cocoDir, "settings.yml"); if (!existsSync(settingsPath)) return; // project not initialized for ccc const derived = await computeDerivedExcludes(cwd); const sidecarPath = join(cocoDir, ".vera-gitignore-sync.json"); const previouslyInjected = readSidecar(sidecarPath); const settings = readFileSync(settingsPath, "utf8"); const updated = rewriteExcludePatterns(settings, previouslyInjected, derived); if (updated !== settings) writeFileSync(settingsPath, updated, "utf8"); writeSidecar(sidecarPath, derived); } catch { // Sync is advisory; never block indexing on it. } } // Returns ccc-compatible exclude globs for everything git ignores, or [] when // the directory is not a git work tree / git is unavailable / nothing is ignored. async function computeDerivedExcludes(cwd: string): Promise { try { await execFileAsync("git", ["-C", cwd, "rev-parse", "--is-inside-work-tree"]); } catch { return []; } let raw = ""; try { // -z: NUL-delimited, no quotepath escaping (Unicode-safe on every platform). // --directory collapses fully-ignored dirs to a single trailing-slash entry. const res = await execFileAsync( "git", ["-C", cwd, "ls-files", "-o", "-i", "--exclude-standard", "--directory", "-z"], { maxBuffer: 8 * 1024 * 1024 }, ); raw = String(res.stdout ?? ""); } catch { return []; } const patterns = new Set(); for (const entry of raw.split("\0")) { if (!entry) continue; const isDir = entry.endsWith("/"); const rel = entry.replace(/^\/+/, "").replace(/\/+$/, ""); // git emits '/' on all OSes if (!rel) continue; patterns.add(isDir ? `**/${rel}/**` : `**/${rel}`); } return [...patterns].sort(); } function readSidecar(path: string): string[] { try { const data = JSON.parse(readFileSync(path, "utf8")); return Array.isArray(data?.patterns) ? data.patterns.map(String) : []; } catch { return []; } } function writeSidecar(path: string, patterns: string[]): void { try { writeFileSync(path, `${JSON.stringify({ patterns }, null, 2)}\n`, "utf8"); } catch { // ignore } } function unquote(value: string): string { const v = value.trim(); if ((v.startsWith("'") && v.endsWith("'")) || (v.startsWith('"') && v.endsWith('"'))) { return v.slice(1, -1).replace(/''/g, "'"); } return v; } // Replaces the exclude_patterns block with: (existing minus our last injection) // then the current git-derived set. Preserves ccc defaults and any user-added // patterns; only the patterns we previously injected are reconciled away. function rewriteExcludePatterns(text: string, previouslyInjected: string[], derived: string[]): string { const lines = text.split(/\r?\n/); const headerIdx = lines.findIndex((l) => /^exclude_patterns:[ \t]*$/.test(l)); if (headerIdx === -1) return text; // inline/empty or absent: do not touch const itemRe = /^([ \t]*)-[ \t]+(.*)$/; let cursor = headerIdx + 1; let indent: string | null = null; const existing: string[] = []; while (cursor < lines.length) { const m = lines[cursor].match(itemRe); if (!m) break; if (indent === null) indent = m[1]; else if (m[1] !== indent) break; // nested structure under the key: stop existing.push(unquote(m[2])); cursor += 1; } const injectedSet = new Set(previouslyInjected); const merged = existing.filter((p) => !injectedSet.has(p)); for (const d of derived) if (!merged.includes(d)) merged.push(d); const ind = indent ?? ""; const block = merged.map((p) => `${ind}- '${p.replace(/'/g, "''")}'`); return [...lines.slice(0, headerIdx + 1), ...block, ...lines.slice(cursor)].join("\n"); }