ignore.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716
  1. # Copyright (C) 2017 Jelmer Vernooij <jelmer@jelmer.uk>
  2. #
  3. # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
  4. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  5. # General Public License as published by the Free Software Foundation; version 2.0
  6. # or (at your option) any later version. You can redistribute it and/or
  7. # modify it under the terms of either of these two licenses.
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. #
  15. # You should have received a copy of the licenses; if not, see
  16. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  17. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  18. # License, Version 2.0.
  19. #
  20. """Parsing of gitignore files.
  21. For details for the matching rules, see https://git-scm.com/docs/gitignore
  22. Important: When checking if directories are ignored, include a trailing slash in the path.
  23. For example, use "dir/" instead of "dir" to check if a directory is ignored.
  24. """
  25. import os.path
  26. import re
  27. from collections.abc import Iterable, Sequence
  28. from contextlib import suppress
  29. from typing import TYPE_CHECKING, BinaryIO, Union
  30. if TYPE_CHECKING:
  31. from .repo import Repo
  32. from .config import Config, get_xdg_config_home_path
  33. def _pattern_to_str(pattern: Union["Pattern", bytes, str]) -> str:
  34. """Convert a pattern to string, handling both Pattern objects and raw patterns."""
  35. if isinstance(pattern, Pattern):
  36. pattern_data: bytes | str = pattern.pattern
  37. else:
  38. pattern_data = pattern
  39. return pattern_data.decode() if isinstance(pattern_data, bytes) else pattern_data
  40. def _check_parent_exclusion(path: str, matching_patterns: Sequence["Pattern"]) -> bool:
  41. """Check if a parent directory exclusion prevents negation patterns from taking effect.
  42. Args:
  43. path: Path to check
  44. matching_patterns: List of Pattern objects that matched the path
  45. Returns:
  46. True if parent exclusion applies (negation should be ineffective), False otherwise
  47. """
  48. # Find the last negation pattern
  49. final_negation = next(
  50. (p for p in reversed(matching_patterns) if not p.is_exclude), None
  51. )
  52. if not final_negation:
  53. return False
  54. final_pattern_str = _pattern_to_str(final_negation)
  55. # Check if any exclusion pattern excludes a parent directory
  56. return any(
  57. pattern.is_exclude
  58. and _pattern_excludes_parent(_pattern_to_str(pattern), path, final_pattern_str)
  59. for pattern in matching_patterns
  60. )
  61. def _pattern_excludes_parent(
  62. pattern_str: str, path: str, final_pattern_str: str
  63. ) -> bool:
  64. """Check if a pattern excludes a parent directory of the given path."""
  65. # Handle **/middle/** patterns
  66. if pattern_str.startswith("**/") and pattern_str.endswith("/**"):
  67. middle = pattern_str[3:-3]
  68. return f"/{middle}/" in f"/{path}" or path.startswith(f"{middle}/")
  69. # Handle dir/** patterns
  70. if pattern_str.endswith("/**") and not pattern_str.startswith("**/"):
  71. base_dir = pattern_str[:-3]
  72. if not path.startswith(base_dir + "/"):
  73. return False
  74. remaining = path[len(base_dir) + 1 :]
  75. # Special case: dir/** allows immediate child file negations
  76. if (
  77. not path.endswith("/")
  78. and final_pattern_str.startswith("!")
  79. and "/" not in remaining
  80. ):
  81. neg_pattern = final_pattern_str[1:]
  82. if neg_pattern == path or ("*" in neg_pattern and "**" not in neg_pattern):
  83. return False
  84. # Nested files with ** negation patterns
  85. if "**" in final_pattern_str and Pattern(final_pattern_str[1:].encode()).match(
  86. path.encode()
  87. ):
  88. return False
  89. return True
  90. # Directory patterns (ending with /) can exclude parent directories
  91. if pattern_str.endswith("/") and "/" in path:
  92. p = Pattern(pattern_str.encode())
  93. parts = path.split("/")
  94. return any(
  95. p.match(("/".join(parts[:i]) + "/").encode()) for i in range(1, len(parts))
  96. )
  97. return False
  98. def _translate_segment(segment: bytes) -> bytes:
  99. """Translate a single path segment to regex, following Git rules exactly."""
  100. if segment == b"*":
  101. return b"[^/]+"
  102. res = b""
  103. i, n = 0, len(segment)
  104. while i < n:
  105. c = segment[i : i + 1]
  106. i += 1
  107. if c == b"*":
  108. res += b"[^/]*"
  109. elif c == b"?":
  110. res += b"[^/]"
  111. elif c == b"\\":
  112. if i < n:
  113. res += re.escape(segment[i : i + 1])
  114. i += 1
  115. else:
  116. res += re.escape(c)
  117. elif c == b"[":
  118. j = i
  119. if j < n and segment[j : j + 1] == b"!":
  120. j += 1
  121. if j < n and segment[j : j + 1] == b"]":
  122. j += 1
  123. while j < n and segment[j : j + 1] != b"]":
  124. j += 1
  125. if j >= n:
  126. res += b"\\["
  127. else:
  128. stuff = segment[i:j].replace(b"\\", b"\\\\")
  129. i = j + 1
  130. if stuff.startswith(b"!"):
  131. stuff = b"^" + stuff[1:]
  132. elif stuff.startswith(b"^"):
  133. stuff = b"\\" + stuff
  134. res += b"[" + stuff + b"]"
  135. else:
  136. res += re.escape(c)
  137. return res
  138. def _handle_double_asterisk(segments: Sequence[bytes], i: int) -> tuple[bytes, bool]:
  139. """Handle ** segment processing, returns (regex_part, skip_next)."""
  140. # Check if ** is at end
  141. remaining = segments[i + 1 :]
  142. if all(s == b"" for s in remaining):
  143. # ** at end - matches everything
  144. return b".*", False
  145. # Check if next segment is also **
  146. if i + 1 < len(segments) and segments[i + 1] == b"**":
  147. # Consecutive ** segments
  148. # Check if this ends with a directory pattern (trailing /)
  149. remaining_after_next = segments[i + 2 :]
  150. is_dir_pattern = (
  151. len(remaining_after_next) == 1 and remaining_after_next[0] == b""
  152. )
  153. if is_dir_pattern:
  154. # Pattern like c/**/**/ - requires at least one intermediate directory
  155. return b"[^/]+/(?:[^/]+/)*", True
  156. else:
  157. # Pattern like c/**/**/d - allows zero intermediate directories
  158. return b"(?:[^/]+/)*", True
  159. else:
  160. # ** in middle - handle differently depending on what follows
  161. if i == 0:
  162. # ** at start - any prefix
  163. return b"(?:.*/)??", False
  164. else:
  165. # ** in middle - match zero or more complete directory segments
  166. return b"(?:[^/]+/)*", False
  167. def _handle_leading_patterns(pat: bytes, res: bytes) -> tuple[bytes, bytes]:
  168. """Handle leading patterns like ``/**/``, ``**/``, or ``/``."""
  169. if pat.startswith(b"/**/"):
  170. # Leading /** is same as **
  171. return pat[4:], b"(.*/)?"
  172. elif pat.startswith(b"**/"):
  173. # Leading **/
  174. return pat[3:], b"(.*/)?"
  175. elif pat.startswith(b"/"):
  176. # Leading / means relative to .gitignore location
  177. return pat[1:], b""
  178. else:
  179. return pat, b""
  180. def translate(pat: bytes) -> bytes:
  181. """Translate a gitignore pattern to a regular expression following Git rules exactly."""
  182. res = b"(?ms)"
  183. # Check for invalid patterns with // - Git treats these as broken patterns
  184. if b"//" in pat:
  185. # Pattern with // doesn't match anything in Git
  186. return b"(?!.*)" # Negative lookahead - matches nothing
  187. # Don't normalize consecutive ** patterns - Git treats them specially
  188. # c/**/**/ requires at least one intermediate directory
  189. # So we keep the pattern as-is
  190. # Handle patterns with no slashes (match at any level)
  191. if b"/" not in pat[:-1]: # No slash except possibly at end
  192. res += b"(.*/)?"
  193. # Handle leading patterns
  194. pat, prefix_added = _handle_leading_patterns(pat, res)
  195. if prefix_added:
  196. res += prefix_added
  197. # Process the rest of the pattern
  198. if pat == b"**":
  199. res += b".*"
  200. else:
  201. segments = pat.split(b"/")
  202. i = 0
  203. while i < len(segments):
  204. segment = segments[i]
  205. # Add slash separator (except for first segment)
  206. if i > 0 and segments[i - 1] != b"**":
  207. res += re.escape(b"/")
  208. if segment == b"**":
  209. regex_part, skip_next = _handle_double_asterisk(segments, i)
  210. res += regex_part
  211. if regex_part == b".*": # End of pattern
  212. break
  213. if skip_next:
  214. i += 1
  215. else:
  216. res += _translate_segment(segment)
  217. i += 1
  218. # Add optional trailing slash for files
  219. if not pat.endswith(b"/"):
  220. res += b"/?"
  221. return res + b"\\Z"
  222. def read_ignore_patterns(f: BinaryIO) -> Iterable[bytes]:
  223. """Read a git ignore file.
  224. Args:
  225. f: File-like object to read from
  226. Returns: List of patterns
  227. """
  228. for line in f:
  229. line = line.rstrip(b"\r\n")
  230. # Ignore blank lines, they're used for readability.
  231. if not line.strip():
  232. continue
  233. if line.startswith(b"#"):
  234. # Comment
  235. continue
  236. # Trailing spaces are ignored unless they are quoted with a backslash.
  237. while line.endswith(b" ") and not line.endswith(b"\\ "):
  238. line = line[:-1]
  239. line = line.replace(b"\\ ", b" ")
  240. yield line
  241. def match_pattern(path: bytes, pattern: bytes, ignorecase: bool = False) -> bool:
  242. """Match a gitignore-style pattern against a path.
  243. Args:
  244. path: Path to match
  245. pattern: Pattern to match
  246. ignorecase: Whether to do case-sensitive matching
  247. Returns:
  248. bool indicating whether the pattern matched
  249. """
  250. return Pattern(pattern, ignorecase).match(path)
  251. class Pattern:
  252. """A single ignore pattern."""
  253. def __init__(self, pattern: bytes, ignorecase: bool = False) -> None:
  254. """Initialize a Pattern object.
  255. Args:
  256. pattern: The gitignore pattern as bytes.
  257. ignorecase: Whether to perform case-insensitive matching.
  258. """
  259. self.pattern = pattern
  260. self.ignorecase = ignorecase
  261. # Handle negation
  262. if pattern.startswith(b"!"):
  263. self.is_exclude = False
  264. pattern = pattern[1:]
  265. else:
  266. # Handle escaping of ! and # at start only
  267. if (
  268. pattern.startswith(b"\\")
  269. and len(pattern) > 1
  270. and pattern[1:2] in (b"!", b"#")
  271. ):
  272. pattern = pattern[1:]
  273. self.is_exclude = True
  274. # Check if this is a directory-only pattern
  275. self.is_directory_only = pattern.endswith(b"/")
  276. flags = 0
  277. if self.ignorecase:
  278. flags = re.IGNORECASE
  279. self._re = re.compile(translate(pattern), flags)
  280. def __bytes__(self) -> bytes:
  281. """Return the pattern as bytes.
  282. Returns:
  283. The original pattern as bytes.
  284. """
  285. return self.pattern
  286. def __str__(self) -> str:
  287. """Return the pattern as a string.
  288. Returns:
  289. The pattern decoded as a string.
  290. """
  291. return os.fsdecode(self.pattern)
  292. def __eq__(self, other: object) -> bool:
  293. """Check equality with another Pattern object.
  294. Args:
  295. other: The object to compare with.
  296. Returns:
  297. True if patterns and ignorecase flags are equal, False otherwise.
  298. """
  299. return (
  300. isinstance(other, type(self))
  301. and self.pattern == other.pattern
  302. and self.ignorecase == other.ignorecase
  303. )
  304. def __repr__(self) -> str:
  305. """Return a string representation of the Pattern object.
  306. Returns:
  307. A string representation for debugging.
  308. """
  309. return f"{type(self).__name__}({self.pattern!r}, {self.ignorecase!r})"
  310. def match(self, path: bytes) -> bool:
  311. """Try to match a path against this ignore pattern.
  312. Args:
  313. path: Path to match (relative to ignore location)
  314. Returns: boolean
  315. """
  316. # For negation directory patterns (e.g., !dir/), only match directories
  317. if self.is_directory_only and not self.is_exclude and not path.endswith(b"/"):
  318. return False
  319. # Check if the regex matches
  320. if self._re.match(path):
  321. return True
  322. # For exclusion directory patterns, also match files under the directory
  323. if (
  324. self.is_directory_only
  325. and self.is_exclude
  326. and not path.endswith(b"/")
  327. and b"/" in path
  328. ):
  329. return bool(self._re.match(path.rsplit(b"/", 1)[0] + b"/"))
  330. return False
  331. class IgnoreFilter:
  332. """Filter to apply gitignore patterns.
  333. Important: When checking if directories are ignored, include a trailing slash.
  334. For example, use is_ignored("dir/") instead of is_ignored("dir").
  335. """
  336. def __init__(
  337. self,
  338. patterns: Iterable[bytes],
  339. ignorecase: bool = False,
  340. path: str | None = None,
  341. ) -> None:
  342. """Initialize an IgnoreFilter with a set of patterns.
  343. Args:
  344. patterns: An iterable of gitignore patterns as bytes.
  345. ignorecase: Whether to perform case-insensitive matching.
  346. path: Optional path to the ignore file for debugging purposes.
  347. """
  348. self._patterns: list[Pattern] = []
  349. self._ignorecase = ignorecase
  350. self._path = path
  351. for pattern in patterns:
  352. self.append_pattern(pattern)
  353. def append_pattern(self, pattern: bytes) -> None:
  354. """Add a pattern to the set."""
  355. self._patterns.append(Pattern(pattern, self._ignorecase))
  356. def find_matching(self, path: bytes | str) -> Iterable[Pattern]:
  357. """Yield all matching patterns for path.
  358. Args:
  359. path: Path to match
  360. Returns:
  361. Iterator over iterators
  362. """
  363. if not isinstance(path, bytes):
  364. path = os.fsencode(path)
  365. for pattern in self._patterns:
  366. if pattern.match(path):
  367. yield pattern
  368. def is_ignored(self, path: bytes | str) -> bool | None:
  369. """Check whether a path is ignored using Git-compliant logic.
  370. For directories, include a trailing slash.
  371. Returns: status is None if file is not mentioned, True if it is
  372. included, False if it is explicitly excluded.
  373. """
  374. matching_patterns = list(self.find_matching(path))
  375. if not matching_patterns:
  376. return None
  377. # Basic rule: last matching pattern wins
  378. last_pattern = matching_patterns[-1]
  379. result = last_pattern.is_exclude
  380. # Apply Git's parent directory exclusion rule for negations
  381. if not result: # Only applies to inclusions (negations)
  382. result = self._apply_parent_exclusion_rule(
  383. path.decode() if isinstance(path, bytes) else path, matching_patterns
  384. )
  385. return result
  386. def _apply_parent_exclusion_rule(
  387. self, path: str, matching_patterns: list[Pattern]
  388. ) -> bool:
  389. """Apply Git's parent directory exclusion rule.
  390. "It is not possible to re-include a file if a parent directory of that file is excluded."
  391. """
  392. return _check_parent_exclusion(path, matching_patterns)
  393. @classmethod
  394. def from_path(
  395. cls, path: str | os.PathLike[str], ignorecase: bool = False
  396. ) -> "IgnoreFilter":
  397. """Create an IgnoreFilter from a file path.
  398. Args:
  399. path: Path to the ignore file.
  400. ignorecase: Whether to perform case-insensitive matching.
  401. Returns:
  402. An IgnoreFilter instance with patterns loaded from the file.
  403. """
  404. with open(path, "rb") as f:
  405. return cls(read_ignore_patterns(f), ignorecase, path=str(path))
  406. def __repr__(self) -> str:
  407. """Return string representation of IgnoreFilter."""
  408. path = getattr(self, "_path", None)
  409. if path is not None:
  410. return f"{type(self).__name__}.from_path({path!r})"
  411. else:
  412. return f"<{type(self).__name__}>"
  413. class IgnoreFilterStack:
  414. """Check for ignore status in multiple filters."""
  415. def __init__(self, filters: list[IgnoreFilter]) -> None:
  416. """Initialize an IgnoreFilterStack with multiple filters.
  417. Args:
  418. filters: A list of IgnoreFilter objects to check in order.
  419. """
  420. self._filters = filters
  421. def is_ignored(self, path: str) -> bool | None:
  422. """Check whether a path is explicitly included or excluded in ignores.
  423. Args:
  424. path: Path to check
  425. Returns:
  426. None if the file is not mentioned, True if it is included,
  427. False if it is explicitly excluded.
  428. """
  429. for filter in self._filters:
  430. status = filter.is_ignored(path)
  431. if status is not None:
  432. return status
  433. return None
  434. def __repr__(self) -> str:
  435. """Return a string representation of the IgnoreFilterStack.
  436. Returns:
  437. A string representation for debugging.
  438. """
  439. return f"{type(self).__name__}({self._filters!r})"
  440. def default_user_ignore_filter_path(config: Config) -> str:
  441. """Return default user ignore filter path.
  442. Args:
  443. config: A Config object
  444. Returns:
  445. Path to a global ignore file
  446. """
  447. try:
  448. value = config.get((b"core",), b"excludesFile")
  449. assert isinstance(value, bytes)
  450. return value.decode(encoding="utf-8")
  451. except KeyError:
  452. pass
  453. return get_xdg_config_home_path("git", "ignore")
  454. class IgnoreFilterManager:
  455. """Ignore file manager with Git-compliant behavior.
  456. Important: When checking if directories are ignored, include a trailing slash.
  457. For example, use is_ignored("dir/") instead of is_ignored("dir").
  458. """
  459. def __init__(
  460. self,
  461. top_path: str,
  462. global_filters: list[IgnoreFilter],
  463. ignorecase: bool,
  464. ) -> None:
  465. """Initialize an IgnoreFilterManager.
  466. Args:
  467. top_path: The top-level directory path to manage ignores for.
  468. global_filters: List of global ignore filters to apply.
  469. ignorecase: Whether to perform case-insensitive matching.
  470. """
  471. self._path_filters: dict[str, IgnoreFilter | None] = {}
  472. self._top_path = top_path
  473. self._global_filters = global_filters
  474. self._ignorecase = ignorecase
  475. def __repr__(self) -> str:
  476. """Return string representation of IgnoreFilterManager."""
  477. return f"{type(self).__name__}({self._top_path}, {self._global_filters!r}, {self._ignorecase!r})"
  478. def _load_path(self, path: str) -> IgnoreFilter | None:
  479. try:
  480. return self._path_filters[path]
  481. except KeyError:
  482. pass
  483. p = os.path.join(self._top_path, path, ".gitignore")
  484. try:
  485. self._path_filters[path] = IgnoreFilter.from_path(p, self._ignorecase)
  486. except (FileNotFoundError, NotADirectoryError):
  487. self._path_filters[path] = None
  488. except OSError as e:
  489. # On Windows, opening a path that contains a symlink can fail with
  490. # errno 22 (Invalid argument) when the symlink points outside the repo
  491. if e.errno == 22:
  492. self._path_filters[path] = None
  493. else:
  494. raise
  495. return self._path_filters[path]
  496. def find_matching(self, path: str) -> Iterable[Pattern]:
  497. """Find matching patterns for path.
  498. Args:
  499. path: Path to check
  500. Returns:
  501. Iterator over Pattern instances
  502. """
  503. if os.path.isabs(path):
  504. raise ValueError(f"{path} is an absolute path")
  505. filters = [(0, f) for f in self._global_filters]
  506. if os.path.sep != "/":
  507. path = path.replace(os.path.sep, "/")
  508. parts = path.split("/")
  509. matches = []
  510. for i in range(len(parts) + 1):
  511. dirname = "/".join(parts[:i])
  512. for s, f in filters:
  513. relpath = "/".join(parts[s:i])
  514. if i < len(parts):
  515. # Paths leading up to the final part are all directories,
  516. # so need a trailing slash.
  517. relpath += "/"
  518. matches += list(f.find_matching(relpath))
  519. ignore_filter = self._load_path(dirname)
  520. if ignore_filter is not None:
  521. filters.insert(0, (i, ignore_filter))
  522. return iter(matches)
  523. def is_ignored(self, path: str) -> bool | None:
  524. """Check whether a path is explicitly included or excluded in ignores.
  525. Args:
  526. path: Path to check. For directories, the path should end with '/'.
  527. Returns:
  528. None if the file is not mentioned, True if it is included,
  529. False if it is explicitly excluded.
  530. """
  531. matches = list(self.find_matching(path))
  532. if not matches:
  533. return None
  534. # Standard behavior - last matching pattern wins
  535. result = matches[-1].is_exclude
  536. # Apply Git's parent directory exclusion rule for negations
  537. if not result: # Only check if we would include due to negation
  538. result = _check_parent_exclusion(path, matches)
  539. # Apply special case for issue #1203: directory traversal with ** patterns
  540. if result and path.endswith("/"):
  541. result = self._apply_directory_traversal_rule(path, matches)
  542. return result
  543. def _apply_directory_traversal_rule(
  544. self, path: str, matches: list["Pattern"]
  545. ) -> bool:
  546. """Apply directory traversal rule for issue #1203.
  547. If a directory would be ignored by a ** pattern, but there are negation
  548. patterns for its subdirectories, then the directory itself should not
  549. be ignored (to allow traversal).
  550. """
  551. # Original logic for traversal check
  552. last_excluding_pattern = None
  553. for match in matches:
  554. if match.is_exclude:
  555. last_excluding_pattern = match
  556. if last_excluding_pattern and (
  557. last_excluding_pattern.pattern.endswith(b"**")
  558. or b"**" in last_excluding_pattern.pattern
  559. ):
  560. # Check if subdirectories would be unignored
  561. test_subdir = path + "test/"
  562. test_matches = list(self.find_matching(test_subdir))
  563. if test_matches:
  564. # Use standard logic for test case - last matching pattern wins
  565. test_result = test_matches[-1].is_exclude
  566. if test_result is False:
  567. return False
  568. return True # Keep original result
  569. @classmethod
  570. def from_repo(cls, repo: "Repo") -> "IgnoreFilterManager":
  571. """Create a IgnoreFilterManager from a repository.
  572. Args:
  573. repo: Repository object
  574. Returns:
  575. A `IgnoreFilterManager` object
  576. """
  577. global_filters = []
  578. for p in [
  579. os.path.join(repo.controldir(), "info", "exclude"),
  580. default_user_ignore_filter_path(repo.get_config_stack()),
  581. ]:
  582. with suppress(OSError):
  583. global_filters.append(IgnoreFilter.from_path(os.path.expanduser(p)))
  584. config = repo.get_config_stack()
  585. ignorecase = config.get_boolean((b"core"), (b"ignorecase"), False)
  586. return cls(repo.path, global_filters, ignorecase)