/** * Git tracking cache for efficient git-ignore checking. * * Problem: Calling git check-ignore on every file is expensive (spawns process each time). * Solution: Cache results and pre-populate with git ls-files (tracked + untracked non-ignored). */ /** * Options for {@link GitTracker.initialize}. */ export interface GitTrackerInitOptions { /** * When true (default), pre-populate the "active set" from * `git ls-files --cached --others --exclude-standard`, which returns all * tracked + untracked-but-not-gitignored files. This enables O(1) bulk * `isIgnoredByActiveSet` lookups without spawning `git check-ignore` per * path. * * When false, only tracked files are pre-populated (legacy v0.1.31 behavior). * Use this only when you know the caller will still rely on * {@link GitTracker.isIgnored}'s cache-miss fallback and untracked * non-ignored files are rare. */ includeUntracked?: boolean; } /** * Git tracking cache service. * * Provides efficient git-ignore checking with caching and pre-population from git ls-files. * * Bulk callers (directory walkers that process hundreds+ of paths) should * prefer {@link isIgnoredByActiveSet} and {@link hasActiveDescendant} — both * answer in O(1) against the pre-populated active set and never spawn a git * subprocess for paths inside the project root. * * One-off callers (e.g. link validators that only check a handful of paths) * can use {@link isIgnored}, which falls back to `git check-ignore` on cache * miss. * * @example * ```typescript * const tracker = new GitTracker('/project'); * await tracker.initialize(); // defaults to includeUntracked: true * * // Bulk path: O(1) lookup against pre-populated active set * if (!tracker.isIgnoredByActiveSet('/project/docs/file.md')) { ... } * * // One-off path: may spawn `git check-ignore` on miss * if (!tracker.isIgnored('/project/docs/file.md')) { ... } * ``` */ export declare class GitTracker { private readonly projectRoot; private readonly normalizedProjectRoot; private readonly cache; /** Absolute paths of all files known to be NOT ignored (tracked + untracked non-ignored). */ private readonly activeSet; /** Absolute paths of every directory that contains at least one active-set file. */ private readonly activeAncestors; private initialized; private activeSetPopulated; /** Whether `git ls-files` actually answered during {@link initialize}. */ private gitAnswered; constructor(projectRoot: string); /** * Initialize the tracker by pre-populating cache from git ls-files. * * With `includeUntracked: true` (default), the tracker builds an "active set" * of all files that are NOT gitignored (tracked + untracked-not-ignored). * This lets {@link isIgnoredByActiveSet} answer in O(1) without spawning * `git check-ignore` per file. * * With `includeUntracked: false`, only tracked files are pre-populated. * Untracked non-ignored files will miss the cache and fall through to * `git check-ignore` via {@link isIgnored}. */ initialize(options?: GitTrackerInitOptions): Promise; /** * Did git actually answer, or is this tracker an empty shell? * * `gitLsFiles` returns `null` for every way asking can fail — no `git` on * `PATH`, a corrupt `.git`, an unreadable `.git`, a non-repository cwd — and * that `null` is otherwise indistinguishable here from "git answered, and the * repository is empty": both leave the active set with zero entries, after * which {@link isIgnoredByActiveSet} reports every path as NOT ignored. * * For a walker that is fine — unfiltered is the safe default. For a caller that * INFERS something from "not ignored" (provenance, publication, leakage) it is * not: the inference silently becomes a fixed answer. Such callers must ask * this first and treat `false` as "no answer available", never as a verdict. */ isUsable(): boolean; /** * Walk up from each active-set file's directory and record every ancestor up to projectRoot. * * `activeSet` keys are forward-slash (via `safePath.resolve`) but `node:path.dirname` * returns backslashes on Windows. Wrap every `dirname()` result with `toForwardSlash()` * so the `activeAncestors` set uses the same key shape as `activeSet` — otherwise every * `hasActiveDescendant` / `isIgnoredByActiveSet` ancestor lookup misses on Windows. */ private populateAncestorSet; /** * Returns true if the given absolute path IS an active-set file OR is an * ancestor directory of at least one active-set file. * * Used by walkers to decide whether descending into a directory is worth * the cost: an ignored directory with no active descendants can be skipped * outright. Requires {@link initialize} with `includeUntracked: true` * (the default); returns `true` for any path otherwise, to preserve the * legacy behavior where walkers descended unconditionally. * * @param absolutePath - Absolute path to check (file or directory) */ hasActiveDescendant(absolutePath: string): boolean; /** * Fast O(1) ignore check against the pre-populated active set. * * For paths INSIDE the project root **that exist on disk**, membership in the * active set is authoritative: such a path is ignored iff it is not in the * active set AND not an ancestor of any active-set path. No `git * check-ignore` spawn. * * The existence qualifier is load-bearing, not a caveat. The active set is * built from `git ls-files`, so it can only ever contain paths that EXIST — a * path that does not exist is trivially absent from it, and a bare set lookup * would call every typo'd or never-built path "ignored". Callers acted on * that: a broken markdown link was reported as a gitignored-data leak rather * than a broken link. Such paths therefore fall back to {@link isIgnored}, * i.e. `git check-ignore`, which answers from the ignore PATTERNS and is * correct for a path that is merely named (`dist/out.js` under an ignored * `dist/` is ignored; `docs/typo.md` is not). The fallback result is cached, * so a repeated miss on the same path stays O(1). * * For paths OUTSIDE the project root, falls back to {@link isIgnored} so * legacy behavior is preserved. * * Requires {@link initialize} with `includeUntracked: true` (the default). * When initialized without untracked files, this method delegates to * {@link isIgnored} so callers still get correct results at the cost of a * possible per-path spawn. * * @param absolutePath - Absolute path to check */ isIgnoredByActiveSet(absolutePath: string): boolean; private isWithinProjectRoot; /** * Check if a file is ignored by git. * * Uses cache if available, otherwise calls git check-ignore and caches result. * * @param filePath - Absolute path to file * @returns true if file is gitignored, false otherwise */ isIgnored(filePath: string): boolean; /** * Get cache statistics. */ getStats(): { cacheSize: number; activeSetSize: number; activeAncestorsSize: number; }; /** * Clear the cache. */ clear(): void; } //# sourceMappingURL=git-tracker.d.ts.map