Skip to content

[kensai] gitignore-aware stat-free file indexing - #7

Merged
xobotyi merged 7 commits into
mainfrom
dev-gitignore
Jun 11, 2026
Merged

[kensai] gitignore-aware stat-free file indexing#7
xobotyi merged 7 commits into
mainfrom
dev-gitignore

Conversation

@xobotyi

@xobotyi xobotyi commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Problem

Index build (session_start) on large repos took tens of seconds: the walker statted every file and descended into ignored trees (node_modules, build output) in full.

Changes

  • gitignore module (src/repofs/git/gitignore/) -- pattern parser + hierarchical matcher chain ported from the Go reference. Full spec coverage: negation, dir-only, anchoring, **, char classes ([!abc] normalized for picomatch), dotfile matching, literal braces/extglobs.
  • Walker integration -- per-directory .gitignore chained onto the parent matcher, .git/info/exclude seeds the chain. Ignored files omitted, ignored directories pruned without descent. Indexing stays a pure filesystem concern -- git is not involved.
  • Stat-free walk -- files are never statted at build. IndexEntryFile.size is null until lazily filled by RepoFs.readFile (buffer length) or a manifest stat. Dead lineCount/maxLineLen fields dropped.
  • Manifest reconciliation -- buildManifest stats changed files (filling lazy sizes) and inserts changed files missing from the gitignore-filtered index via PathIndex.add(). Covers tracked-but-ignored files (123 in dagor), which otherwise silently dropped out of the manifest while present in the diff.
  • Containment hardening -- readFile fallback for unindexed paths uses lstat and rejects non-regular files; a gitignored symlink pointing outside the repo could bypass path containment through the stat-following fallback.
  • Docs actualized -- repofs/lineiter module docs realigned with the post-streaming-rewrite API.
  • Version bump to 0.1.8.

Numbers (warm cache)

Tree Before After
1.1M files 17.9s 1.2s
172k files ~2.4s 1.0s

xobotyi added 7 commits June 11, 2026 20:18
Port of the Go matcher-chain implementation. parsePattern compiles
one .gitignore line via picomatch; Matcher chains per-directory
pattern lists with last-match-wins semantics, child patterns
checked before parent.

Divergences from stock picomatch for gitignore fidelity:
- [!abc] character classes normalized to [^abc]
- dot:true -- globs match dotfiles
- nobrace/noextglob -- braces and extglobs stay literal

Consumed by the pathindex walker; integration follows in a
separate change.
Index build on large repos took tens of seconds: every file was
statted and ignored trees (node_modules, build output) were walked
in full. Both costs removed:

- Walker loads .gitignore per directory, chains it onto the parent
  matcher, and prunes ignored entries without descent;
  .git/info/exclude seeds the chain. Re-inclusion via ! inside a
  pruned directory is impossible, matching git.
- Files are not statted during the walk. IndexEntryFile.size is
  null until lazily filled by RepoFs.readFile (buffer length) or a
  manifest stat. Symlink resolution stays.
- PathIndex.add() inserts walk-pruned paths on demand
  (lstat-classified); used by the review layer to reconcile changed
  tracked-but-ignored files.
- list_dir prints "(N bytes)" only when size is known.
- readFile fallback for unindexed paths uses lstat and rejects
  non-regular files: a gitignored symlink pointing outside the repo
  could bypass path containment through the stat-following
  fallback.

Walk of a 1.1M-file tree drops from 17.9s to 1.2s; the 172k-file
dagor repo indexes in 1.0s.
The index is stat-free and gitignore-filtered, so buildManifest
stats each changed file itself, filling the entry's lazy size, and
inserts changed files missing from the index via index.add().

Without the reconciliation, tracked-but-ignored files (committed
before the ignore pattern existed; 123 such files in dagor)
silently dropped out of the manifest and stayed invisible to
find_files/list_dir while still appearing in the diff under
review. The diff is the source of truth, and git stays out of
index construction.
IndexEntryFile.lineCount and maxLineLen were always 0: line metrics
are computed on demand by countFileLines and never written back to
the entry. Dead since line counting moved out of the index build
(e8e980b).
Docs drifted from the May 27 read_file rewrite: repofs documented a
streaming readFile(path, opts?) with line windowing options and a
listDir method, neither of which exists. readFile returns raw bytes
(windowing lives in the read_file tool); directory listing is served
by the list_dir tool from index.dir(). Dependencies updated to match
actual imports; lineiter's consumer is countFileLines, not the read
tool.
On Windows picomatch auto-enables windows mode and converts
backslashes in patterns to path separators. Gitignore \-escapes
(escaped trailing space, literal backslash) compiled into patterns
that never match anything. windows:false keeps backslashes as
escape characters; patterns and matched paths are POSIX on all
platforms.
@xobotyi
xobotyi merged commit 7b6fe8e into main Jun 11, 2026
5 checks passed
@xobotyi
xobotyi deleted the dev-gitignore branch June 11, 2026 20:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant