Repository Ignore Filter
Filter repository paths with simple ordered ignore and re-include rules.
Implement filter_repo_paths(paths, rules), a small ordered path filter inspired by repository ignore files. A later exception can restore one manifest while the rest of a generated directory stays excluded.
This exercise defines its own exact/prefix language. It doesn't reproduce .gitignore or .cursorignore syntax, and it isn't a filesystem authorization boundary.
Requirements
- Preserve the original order of kept paths.
- Preserve duplicate candidate paths when they are kept. Inputs are lists of strings.
- Match path and rule text case-sensitively; don't strip or normalize candidate paths.
- Raise
ValueErrorfor invalid paths with empty segments,.segments, or..traversal. - Strip leading and trailing whitespace from each rule before parsing. After strip, blank rules and rules starting with
#are ignored. - A rule ending in
/is a directory prefix match: after stripping the trailing/, pathpmatches whenp.startswith(dir + "/"). The bare directory path equal todir(for example pathdistunder ruledist/) does not match; only paths strictly under that prefix do. - Otherwise (after optional
!), the pattern is an exact full-path match (path == pattern), even if the pattern contains/(e.g.dist/manifest.json). - A rule starting with
!re-includes paths matched by earlier rules (exact file or directory prefix, same match rules as non-negated forms). - Later matching rules win.
- Each non-blank, non-comment rule body (after stripping a leading
!and a trailing/) must also be a valid path; raiseValueErrorwhen the rule pattern is invalid (empty segments,., or..).
Example
Directory prefixes remove descendants. A later exact exception restores only the named file:
1paths = ["src/app.py", "node_modules/lib.js", "dist/app.js"]
2rules = ["node_modules/", "dist/"]
3assert filter_repo_paths(paths, rules) == ["src/app.py"]
4
5# Exact full-path rules may contain `/`; later rules win (including reincludes).
6paths = ["dist/app.js", "dist/manifest.json"]
7rules = ["dist/", "!dist/manifest.json"]
8assert filter_repo_paths(paths, rules) == ["dist/manifest.json"]The slash matters. Under dist/, dist/app.js matches, but dist and distance/app.js don't. No glob expansion occurs: *.log matches only a candidate literally named *.log.
Track one Boolean per candidate. Start with ignored = False, update it for each matching rule, and append the original candidate only after the last rule. For dist/manifest.json, the rules dist/, !dist/manifest.json, dist/manifest.json produce ignored states True, False, True, so the file is excluded again.
Constraints
- No glob engine is required.
- Keep matching deterministic and easy to explain.
- Validate every non-comment rule even when
pathsis empty. Validate every candidate even when an earlier rule would exclude it.