Fix exists: rule to match ** recursively, files-only (#95)

Summary

Fixes #95 (closed). exists: rule evaluation used Go's filepath.Glob, whose ** is not recursive — it matches exactly one directory level. As a result exists: ['**/*.php'] matched a .php file one directory deep but missed root-level files and files nested two or more levels deep, diverging both from GitLab (where ** matches at any depth) and from glci's own changes: evaluation (which already uses the recursive matchGlob matcher).

evalExists now matches each project file against the pattern with matchGlob, so ** behaves recursively and consistently across exists:/changes:.

Hardening from review

  • Files-only — like GitLab (and changes:), exists: matches regular files, never directories. Applied to both the glob and the literal fast path.
  • Path traversal — literal patterns that escape ProjectDir via .. (e.g. ../../etc/passwd) are rejected, so a .gitlab-ci.yml cannot probe the host filesystem through an exists: rule.
  • Single walkNewProjectFileCache memoizes the tree walk so a planning run with many exists: jobs walks the disk once, not once per job/rule (wired into the planner filter and the dependency planner).
  • Truncation warning — the 10,000-file cap now emits a warning instead of silently reporting "no match"; existsFileLimit is a var so tests can lower it.
  • .git skipped — GitLab matches tracked files, never repository internals.

Tests

  • Planner-level test: multiple jobs with divergent ** globs evaluated through the shared cache.
  • Unit: recursive ** at root/one/two levels, directory-no-match, path-traversal (outside + in-bounds ..), symlink-to-file match, all three input shapes (list/map/bare string), multiple globs, mixed literal+glob, nonexistent project dir, cap truncation + warning assertion, isLiteralPath table.

Verified with go build ./..., go vet ./..., make test, make test-e2e-parse, make test-e2e-golden.

Out of scope (follow-ups)

  • Brace expansion (*.{php,inc}) is unsupported in both exists: and changes: (shared matchGlob/filepath.Match limitation). Worth a separate issue.
  • A comprehensive e2e fixture job exercising recursive exists: (the existing fixture uses only a literal exists:).

🤖 Generated with Claude Code

Merge request reports

Loading
Loading