@@ -208,30 +208,45 @@ def is_ignored(self, rel_path: str) -> bool:
208208 result = False
209209 for is_neg , rx , anchored , dir_only in self ._rules :
210210 if dir_only :
211- # Match if rel == pat or rel.startswith(pat + '/')
212- # We achieve this by matching the pattern OR pattern + '/*'
213- # Use the regex against the path and any prefix path that
214- # ends at a separator.
215- # Simpler: check the rule against every prefix of rel.
216- matched = self ._match_dir_prefix (rx , rel )
211+ matched = self ._match_dir_prefix (rx , rel , anchored )
217212 else :
218213 matched = bool (rx .match (rel ))
219214 if matched :
220215 result = not is_neg
221216 return result
222217
223218 @staticmethod
224- def _match_dir_prefix (rx : 're.Pattern' , rel : str ) -> bool :
225- """True if *rel* OR any ancestor directory matches *rx*."""
226- # Check the full path first
219+ def _match_dir_prefix (rx : 're.Pattern' , rel : str , anchored : bool = True ) -> bool :
220+ """True if *rel* is inside a directory matched by *rx*.
221+
222+ For an *anchored* pattern (``/target/``) only root-relative ancestor
223+ directories count. For a *non-anchored* pattern (``target/`` — the
224+ gitignore default) the directory may sit at ANY depth, so a whole path
225+ segment matching the pattern is enough. Segment matching (not substring)
226+ keeps ``build/`` from matching ``build-tools/`` (issue #271 / gitignore
227+ backward-compat): ``src/target/debug/x`` is ignored by ``target/`` but
228+ ``build-tools/config`` is not ignored by ``build/``.
229+ """
230+ # Check the full path first (handles patterns with wildcards/subpaths).
227231 if rx .match (rel ):
228232 return True
229- # Then check every ancestor directory
230233 parts = rel .split ('/' )
231- for i in range (1 , len (parts )):
232- prefix = '/' .join (parts [:i ])
233- if rx .match (prefix ):
234- return True
234+ if anchored :
235+ # Root-anchored: only ancestor paths measured from the root.
236+ for i in range (1 , len (parts )):
237+ if rx .match ('/' .join (parts [:i ])):
238+ return True
239+ else :
240+ # Non-anchored: the pattern (single- or multi-segment) may sit at
241+ # any depth → test every sub-path that both starts and ends on a
242+ # segment boundary. This matches `target/` against `src/target/x`
243+ # and `build/keep/` against `build/keep/x`, while whole-segment
244+ # boundaries keep `build/` from matching `build-tools/`.
245+ n = len (parts )
246+ for i in range (n ):
247+ for j in range (i + 1 , n + 1 ):
248+ if rx .match ('/' .join (parts [i :j ])):
249+ return True
235250 return False
236251
237252
0 commit comments