Skip to content

feat(inputs): defer file enumeration to git ls-files (Turbo/Nx parity, v14) - #69

Merged
Exelord merged 1 commit into
mainfrom
claude/git-ls-files
May 13, 2026
Merged

feat(inputs): defer file enumeration to git ls-files (Turbo/Nx parity, v14)#69
Exelord merged 1 commit into
mainfrom
claude/git-ls-files

Conversation

@Exelord

@Exelord Exelord commented May 13, 2026

Copy link
Copy Markdown
Member

Summary

Adopt the Turbo / Nx file-enumeration model: ask git which files belong to the project, rather than walking the filesystem and parsing .gitignore ourselves.

When the project is inside a git repo, src/cache/inputs.ts:resolveFiles now uses git ls-files --cached --others --exclude-standard -z .. The user's cache.inputs.files globs are applied as a filter over that file set. When git isn't available (no .git, missing binary), we fall back to the pre-v14 Bun.Glob + ignore-library walker.

Why

Both Turbo and Nx defer to git at the bottom of their hash pipelines. We were trying to do gitignore-parsing ourselves with the ignore npm package and getting it subtly wrong:

  • Nested .gitignore patterns were anchored to the workspace root instead of the gitignore's own directory, so pkg/.gitignore: src/skip.ts never matched.
  • .git/info/exclude and global excludes were invisible to us.
  • Untracked files needed careful logic to participate (we did it; tests now pin it).

Letting git handle the cascade is correct by construction.

User-visible effects

  1. Nested .gitignore anchored correctly (fixes the v13 footgun).
  2. .git/info/exclude + global excludes participate.
  3. Untracked-but-not-ignored files enter inputs immediately (no git add needed).
  4. Non-git workspaces continue to work via the FS-walker fallback.

CACHE_VERSION v13 → v14

The file set for the same inputs.files: ['**/*'] can differ when a project had mis-handled nested gitignores. The bump forces one-time invalidation so users don't pick up stale entries under the old enumeration. Pre-alpha tolerates this.

Tests

9 new tests in tests/inputs.test.ts (each git inits a real repo in the fixture):

  • Nested .gitignore patterns correctly anchored (the v13 footgun).
  • Untracked-but-not-ignored files included.
  • Workspace-root .gitignore excludes via git.
  • .git/info/exclude honored.
  • Deleted-but-tracked files skipped (existsSync guard).
  • Declared outputs still excluded under the git path.
  • Nested-project boundary still excludes.
  • Negation in inputs.files still strips.
  • node_modules always-ignored even when force-added to git.

All 23 pre-existing FS-walker tests continue to pass — they don't git init the fixture, so they exercise the fallback path.

414 pass / 0 fail (was 405).

Test plan

  • bun src/bin.ts run lint — clean
  • bun src/bin.ts run format-check — clean
  • bun test — 414 pass, 0 fail
  • CACHE_VERSION bumped + history entry added
  • CI green

Generated by Claude Code

`src/cache/inputs.ts` now uses `git ls-files --cached --others
--exclude-standard` when the project is inside a git repo, falling
back to the pre-v14 `Bun.Glob` + `ignore`-library walker when it
isn't. Matches what Turborepo and Nx both do at the bottom of their
hash pipelines.

## What changes for users

1. **Nested `.gitignore` is correctly anchored.** Pre-v14, a project-
   level pattern like `pkg/.gitignore: src/skip.ts` was evaluated
   against workspace-relative paths and never matched. v14 defers to
   git, which gets nested anchoring right.
2. **`.git/info/exclude` and global excludes are honored** — they
   were invisible to the v13 walker.
3. **Untracked-but-not-ignored files enter inputs immediately.** No
   `git add` required; matches user intuition.

## Fallback

When there's no `.git` directory, no git binary, or git fails for
any reason, we fall back to the previous walker. Behavior is
unchanged for non-git workspaces.

## Implementation

- New private helper `listGitTrackedFiles(projectDir)` in
  `cache/inputs.ts` — synchronous `Bun.spawnSync` of `git ls-files
  --cached --others --exclude-standard -z .`, returns project-relative
  paths or `null`.
- `resolveFiles` branches on the result: git path matches the
  user's globs against the git-tracked set; FS-walker path keeps
  the existing behavior.
- `existsSync` guard filters deleted-but-tracked entries so the
  hasher doesn't throw ENOENT.
- Always-ignored + nested-project boundary + declared-outputs
  exclusions apply on BOTH paths (defense in depth).

## CACHE_VERSION

Bumped v13 → v14. The file set for `inputs.files: ['**/*']` can
differ when a project had mis-handled nested gitignores; bumping
forces a one-time cache invalidation so users don't pick up stale
entries written under the old enumeration. Pre-alpha tolerates this.

## Tests

9 new git-path tests in `tests/inputs.test.ts` — each init's a real
git repo in the fixture and verifies:
- Nested .gitignore patterns correctly anchored (the v13 footgun).
- Untracked-but-not-ignored files included.
- Workspace-root .gitignore exclusion.
- .git/info/exclude honored.
- Deleted-but-tracked files skipped (existsSync guard).
- Declared outputs still excluded.
- Nested-project boundary still excludes.
- Negation in inputs.files still strips.
- node_modules always-ignored even when force-added to git.

All 23 pre-existing FS-walker tests continue to pass (they don't
git-init the fixture → fallback path).

414 pass, 0 fail (was 405).

## Docs

- docs/modules/inputs.md — rewrote "File resolution rules" for v14.
- docs/caching.md — CACHE_VERSION sentinel + history entry.
- docs/modules/cache.md — CACHE_VERSION sentinel.
- CLAUDE.md — decision log entry.
@Exelord
Exelord merged commit 330687e into main May 13, 2026
1 check passed
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.

2 participants