Skip to content

fix(gitignore): escape directory names before interpolating into pattern position - #1809

Open
AmirF194 wants to merge 2 commits into
oraios:mainfrom
AmirF194:fix/1806-gitignore-glob-metachar-escape
Open

fix(gitignore): escape directory names before interpolating into pattern position#1809
AmirF194 wants to merge 2 commits into
oraios:mainfrom
AmirF194:fix/1806-gitignore-glob-metachar-escape

Conversation

@AmirF194

@AmirF194 AmirF194 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Root cause

GitignoreParser._parse_gitignore_content rescopes patterns from a nested .gitignore by
joining the gitignore directory's relative path (rel_dir) onto each pattern line (three
join sites, src/serena/util/file_system.py:263,269,272 at HEAD 29d07d4f). rel_dir comes
from os.path.relpath and is a filesystem name, but it lands directly in pattern position,
where pathspec's GitWildMatchPattern reads *, ?, [, ], ! and # as syntax rather
than literal characters.

A directory whose name contains one of those characters (the concrete case in the issue: a
stray venv literally named ***, whose own .gitignore is just *) turns a scoped pattern
into a much broader one: ***/**/* reads as "any first segment, then anything at depth >= 2",
so it ends up excluding nearly the whole project. Indexing then reports 0 files with exit code
0, and nothing points at the actual cause.

Invariant

A pattern segment built from a filesystem name must match that name literally, never as glob
syntax. The fix escapes each path component of rel_dir (backslash before \ * ? [ ] ! #,
following pathspec's own escaping convention) at all three join sites before it is folded
into the pattern string, so directory names are always matched literally regardless of their
content.

Verification

  • Added three regression tests in test/serena/util/test_file_system.py, one per join site
    (anchored, **/-prefixed non-anchored, and the implicit-** non-anchored case), plus the
    issue's own *** scenario. Ran in a clean python:3.11-slim container against HEAD
    29d07d4f: all three fail on unmodified main and pass on this branch.
  • uv run poe test test/serena/util/test_file_system.py: 27/27 pass (24 existing + 3 new).
  • uv run poe lint and uv run poe type-check: clean. codespell on the changed files: clean.
  • Not run: the full CI matrix (language-server batches across ubuntu/windows/macos) and
    test/serena/util/test_exception.py's three pre-existing GUI-dependent failures, which are
    unrelated to this change and reproduce identically on unmodified main in this container.

Fixes #1806

Checklist

  • This PR follows the guidelines in CONTRIBUTING.md regarding the scope of PRs.
  • For changes that add features or fix problems, I have added an entry to CHANGELOG.md, which concisely describes the change.

Update (second commit)

The initial Windows CI run (before any review) failed 3 new tests, not the 3 unrelated
pre-existing flakes documented above: test_gitignore_dir_name_with_metachars_anchored_pattern
failed with a real AssertionError (not the platform-limitation OSError the other two
metachar tests hit for */?, which are illegal in Windows filenames). Root cause: the
escaping added above inserts literal backslash escape characters, and on Windows os.sep is
also backslash, so os.path.join(rel_dir_pattern, line) mixed path-separator backslashes with
escape backslashes; the pre-existing trailing .replace(os.sep, "/") then converted all of
them indiscriminately, turning an escaped a\\[1\\] into a/[1/], which no longer matches
the literal directory name.

Fixed by building every pattern with a literal / from the start (never os.sep/
os.path.join), and removing the now-unneeded (and on Windows actively harmful) blanket
separator replace. Verified with ntpath (Windows path semantics) on this Linux session:
simulating the old chain on a Windows-style relpath produces a/[1/]/mod.py (does not match
a[1]/mod.py via pathspec); the new construction produces a\\[1\\]/mod.py (matches).
The two tests that create a directory literally named ***/q? are marked
skipif(sys.platform == "win32") (those characters can't exist in a Windows filename, a test
limitation, not a fix limitation); added a pure-function test for
_escape_gitignore_path_component so that escaping logic keeps cross-platform coverage without
touching the filesystem.

…ern position

GitignoreParser rescopes patterns from nested .gitignore files by prefixing the
containing directory's relative path, but interpolates that path into pattern
position without escaping it. A directory whose name contains a gitignore
metacharacter (*, ?, [, ], !, #) is then read as glob syntax instead of a
literal name, so a directory like a stray "***" venv can turn a scoped
pattern into one matching most or all of the project.

Fixes oraios#1806
The escaping added in the prior commit inserts literal backslash escape
characters before pattern metacharacters. On Windows, os.sep is also
backslash, so os.path.join(rel_dir_pattern, line) mixed path-separator
backslashes with escape backslashes, and the pre-existing trailing
`.replace(os.sep, "/")` normalization step then converted ALL of them
indiscriminately -- turning an escaped 'a\[1\]' into 'a/[1/]', which no
longer matches the literal directory name.

Verified with ntpath (Windows path semantics) on this Linux session:
simulating a Windows relpath through the old os.sep/os.path.join/replace
chain produces 'a/[1/]/mod.py', which pathspec does not match against
'a[1]/mod.py'; the new '/'-only construction produces 'a\[1\]/mod.py',
which matches. Fixed by normalizing rel_dir to '/' immediately and joining
every pattern component with a literal '/', removing the now-unneeded (and
on Windows actively harmful) blanket separator replace.

Also marks the two directory-creation regression tests that use '*'/'?'
literally in a directory name as Windows-skipped (those characters are
illegal in Windows filenames, so the directories cannot be created there --
a platform limitation of the test, not the fix), and adds a pure-function
test for _escape_gitignore_path_component so the '*'/'?' escaping logic
still has cross-platform coverage.

Found via this PR's own Windows CI run (jobs 91787560091/91787560092/91787560183
on run 30843966565), which failed test_gitignore_dir_name_with_metachars_anchored_pattern
with an assertion failure (not the OSError the other two tests hit), before any
maintainer had looked at the PR.
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.

Directory names are interpolated unescaped into gitignore patterns, silently indexing 0 files

1 participant