Skip to content

Honor .gitignore and accept gitignore syntax in ignored_paths - #366

Open
steve-downey wants to merge 1 commit into
bemanproject:mainfrom
steve-downey:feature/gitignore-ignore-list
Open

Honor .gitignore and accept gitignore syntax in ignored_paths#366
steve-downey wants to merge 1 commit into
bemanproject:mainfrom
steve-downey:feature/gitignore-ignore-list

Conversation

@steve-downey

Copy link
Copy Markdown
Member

Summary

beman-tidy ignored only what .beman-tidy.yaml listed, matching exactly and by prefix. A repository had to repeat in its configuration what its .gitignore already said, and could not express a pattern at all.

This change routes ignore matching through an IgnoreMatcher built on pathspec, so every ignore source speaks .gitignore pattern syntax:

  • the repository's .gitignore files are honored, including nested ones, each relative to its own directory
  • ignored_paths entries accept wildcards, **, leading / anchoring, trailing / for directories, and ! to re-include
  • the built-in ignores (build/, __pycache__/, IDE directories, ...) are patterns too

Sources are consulted from ignored_paths, through the .gitignore files deepest-first, down to the built-in ignores; the first matching pattern decides. A ! entry in the configuration therefore re-includes a path that a .gitignore covers.

Behavior notes

  • Opt out with use_gitignore: false in .beman-tidy.yaml.
  • README.md and LICENSE stay mandatory. validate_config already rejects them in ignored_paths; a .gitignore matching them is not a configuration error, so it warns and the checks still run.
  • Git's re-inclusion rule is kept as-is: a path below an ignored directory cannot be brought back, not even from ignored_paths. With generated/ in a .gitignore, no !generated/api.hpp works — the repository has to ignore generated/* instead. Relaxing this would desynchronize is_ignored() from the directory pruning in the tree walk.
  • Two widenings follow from gitignore semantics: a slashless ignored_paths entry such as docs now matches at any depth rather than only at the root, and the built-in build/ now matches nested build directories.
  • .venv/ added to the built-in ignores. The defaults now also bound how much of the tree is walked when collecting .gitignore files.

Verification

  • Full suite: 137 passed, 24 skipped. ruff check clean.
  • Run against a fresh clone of bemanproject/exemplar: reported checks and coverage are byte-identical before and after this change.
  • Positive control on the same clone: a .cache/scratch.cpp file — matched by exemplar's /.cache entry — is reported before the change and skipped after it.

Changes

  • beman_tidy/lib/utils/ignore.py (new): IgnoreMatcher, layered specs, .gitignore collection, per-root cache
  • beman_tidy/lib/utils/config.py: get_ignore_matcher() replaces get_ignores(); use_gitignore validated as a boolean
  • beman_tidy/lib/utils/file.py: the duplicate prefix matcher is gone; both file generators use the shared matcher
  • tests/lib/utils/test_config_gitignore.py (new): 23 tests covering pattern syntax, nesting, precedence, the mandatory files and the generators
  • README.md: configuration section and two FAQ entries
  • pyproject.toml / uv.lock / pylock.toml: pathspec>=0.12.1, lockfiles regenerated per docs/dev-guide.md

🤖 Generated with Claude Code

…paths

The ignore list only did exact and prefix matching, so a repository had to
repeat in .beman-tidy.yaml what its .gitignore already said, and could not
express a pattern at all.

Ignore matching now goes through an IgnoreMatcher built on pathspec, and
every source uses .gitignore pattern syntax: the built-in ignores, the
'ignored_paths' entries and the .gitignore files of the repository itself.
Nested .gitignore files apply relative to their own directory, deepest
first, and one inside an ignored directory is never read - as in git.

Sources are consulted from 'ignored_paths' down to the built-in ignores, so
a '!' entry in the configuration re-includes a path that a .gitignore
covers. As in git, that cannot rescue a path below an ignored directory.

README.md and LICENSE stay mandatory: a .gitignore matching them warns
instead of skipping the checks. Set 'use_gitignore: false' to opt out.

Checked against bemanproject/exemplar: the reported checks and coverage are
unchanged.
@ClausKlein

Copy link
Copy Markdown

A very good improvement for me.

@neatudarius neatudarius left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@steve-downey , thanks for contribution! LGTM, few small things to address in this PR or follow-up one (check next comments).

Before merge, please after your branch with latest main after #370 and put the label run-on-all-libraries. This way you will start an on-demand job across the org to make sure your changes does not break any library.

@@ -0,0 +1,253 @@
#!/usr/bin/env python3

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, non-blocking observations

  1. _as_matcher sorts sets/frozenset inputs (file.py _as_matcher) before building a spec. Sorting alphabetically would scramble negation order if a caller ever passed a set containing a !pattern (since ! sorts before letters) — negation order matters in gitignore semantics. Today this is unreachable (the only real caller passes an IgnoreMatcher directly, and the only literal set is the negation-free default ignore list), so it's not an active bug — just a footgun if someone starts passing raw patterns with negations through the old set-based API. Worth a one-line comment or narrowing the type accepted, not a blocker.

  2. .gitignore files are walked twice (once to build layers, once by each file generator's own os.walk). Fine for Beman-sized repos; not worth optimizing now.

  3. Two widening behavior changes are called out explicitly in the PR description (slashless ignored_paths entries now match at any depth, build/ now matches nested build dirs) — these are correct per gitignore semantics but are real behavior changes for existing configs. The PR already verified byte-identical output against a fresh exemplar clone, which is the right check.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorts sets/frozenset inputs (file.py _as_matcher) before building a spec.

well spotted. I did fix one sort problem, and missed this. Possibly because it's not reached. But I agree it's the sort of terrible surprise someone could find later.

real behavior changes for existing configs.

are any configs under the beman project org using that in an interesting way.

That might be something answered by the #370 PR after I rebase it.

Possibly worth a major version bump, though?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants