test_check_ignore.py 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201
  1. # test_check_ignore.py -- Compatibility tests for git check-ignore
  2. # Copyright (C) 2025 Jelmer Vernooij <jelmer@jelmer.uk>
  3. #
  4. # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
  5. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  6. # General Public License as published by the Free Software Foundation; version 2.0
  7. # or (at your option) any later version. You can redistribute it and/or
  8. # modify it under the terms of either of these two licenses.
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. #
  16. # You should have received a copy of the licenses; if not, see
  17. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  18. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  19. # License, Version 2.0.
  20. #
  21. """Compatibility tests for git check-ignore functionality."""
  22. import os
  23. import tempfile
  24. from collections.abc import Sequence
  25. from dulwich import porcelain
  26. from dulwich.repo import Repo
  27. from .utils import CompatTestCase, run_git_or_fail
  28. class CheckIgnoreCompatTestCase(CompatTestCase):
  29. """Test git check-ignore compatibility between dulwich and git."""
  30. min_git_version = (1, 8, 5) # git check-ignore was added in 1.8.5
  31. def setUp(self) -> None:
  32. super().setUp()
  33. self.test_dir = tempfile.mkdtemp()
  34. self.addCleanup(self._cleanup_test_dir)
  35. self.repo = Repo.init(self.test_dir)
  36. self.addCleanup(self.repo.close)
  37. def _cleanup_test_dir(self) -> None:
  38. import shutil
  39. shutil.rmtree(self.test_dir)
  40. def _write_gitignore(self, content: str) -> None:
  41. """Write .gitignore file with given content."""
  42. gitignore_path = os.path.join(self.test_dir, ".gitignore")
  43. with open(gitignore_path, "w") as f:
  44. f.write(content)
  45. def _create_file(self, path: str, content: str = "") -> None:
  46. """Create a file with given content."""
  47. full_path = os.path.join(self.test_dir, path)
  48. os.makedirs(os.path.dirname(full_path), exist_ok=True)
  49. with open(full_path, "w") as f:
  50. f.write(content)
  51. def _create_dir(self, path: str) -> None:
  52. """Create a directory."""
  53. full_path = os.path.join(self.test_dir, path)
  54. os.makedirs(full_path, exist_ok=True)
  55. def _git_check_ignore(self, paths: Sequence[str]) -> set[str]:
  56. """Run git check-ignore and return set of ignored paths."""
  57. try:
  58. output = run_git_or_fail(
  59. ["-c", "core.quotePath=false", "check-ignore", *paths],
  60. cwd=self.test_dir,
  61. )
  62. # git check-ignore returns paths separated by newlines
  63. return set(
  64. line.decode("utf-8") for line in output.strip().split(b"\n") if line
  65. )
  66. except AssertionError:
  67. # git check-ignore returns non-zero when no paths are ignored
  68. return set()
  69. def _dulwich_check_ignore(self, paths: Sequence[str]) -> set[str]:
  70. """Run dulwich check_ignore and return set of ignored paths."""
  71. # Convert to absolute paths relative to the test directory
  72. abs_paths = [os.path.join(self.test_dir, path) for path in paths]
  73. ignored = set(
  74. porcelain.check_ignore(self.test_dir, abs_paths, quote_path=False)
  75. )
  76. # Convert back to relative paths and preserve original path format
  77. result = set()
  78. path_mapping = {}
  79. for orig_path, abs_path in zip(paths, abs_paths):
  80. path_mapping[abs_path] = orig_path
  81. for path in ignored:
  82. # Normalize the path to use forward slashes for comparison
  83. normalized_path = path.replace("\\", "/")
  84. test_dir_normalized = self.test_dir.replace("\\", "/")
  85. if normalized_path.startswith(test_dir_normalized + "/"):
  86. rel_path = normalized_path[len(test_dir_normalized) + 1 :]
  87. # Find the original path format that was requested
  88. orig_path = None
  89. for requested_path in paths:
  90. if requested_path.rstrip("/") == rel_path.rstrip("/"):
  91. orig_path = requested_path
  92. break
  93. result.add(orig_path if orig_path else rel_path)
  94. else:
  95. # For relative paths, normalize to forward slashes
  96. result.add(path.replace("\\", "/"))
  97. return result
  98. def _assert_ignore_match(self, paths: Sequence[str]) -> None:
  99. """Assert that dulwich and git return the same ignored paths."""
  100. git_ignored = self._git_check_ignore(paths)
  101. dulwich_ignored = self._dulwich_check_ignore(paths)
  102. self.assertEqual(
  103. git_ignored,
  104. dulwich_ignored,
  105. f"Mismatch for paths {paths}: git={git_ignored}, dulwich={dulwich_ignored}",
  106. )
  107. def test_issue_1203_directory_negation(self) -> None:
  108. """Test issue #1203: directory negation patterns with data/**,!data/*/."""
  109. self._write_gitignore("data/**\n!data/*/\n")
  110. self._create_file("data/test.dvc", "content")
  111. self._create_dir("data/subdir")
  112. # Based on dulwich's own test for issue #1203, the expected behavior is:
  113. # data/test.dvc: ignored, data/: not ignored, data/subdir/: not ignored
  114. # But git check-ignore might behave differently...
  115. # Test the core case that issue #1203 was about
  116. self._assert_ignore_match(["data/test.dvc"])
  117. def test_basic_patterns(self) -> None:
  118. """Test basic gitignore patterns."""
  119. self._write_gitignore("*.tmp\n*.log\n")
  120. self._create_file("test.tmp")
  121. self._create_file("debug.log")
  122. self._create_file("readme.txt")
  123. paths = ["test.tmp", "debug.log", "readme.txt"]
  124. self._assert_ignore_match(paths)
  125. def test_directory_patterns(self) -> None:
  126. """Test directory-specific patterns."""
  127. self._write_gitignore("build/\nnode_modules/\n")
  128. self._create_dir("build")
  129. self._create_dir("node_modules")
  130. self._create_file("build.txt")
  131. paths = ["build/", "node_modules/", "build.txt"]
  132. self._assert_ignore_match(paths)
  133. def test_issue_972_directory_pattern_with_slash(self) -> None:
  134. """Test issue #972: /data/ pattern should match both 'data' and 'data/'."""
  135. self._write_gitignore("/data/\n")
  136. self._create_dir("data")
  137. self._create_file("data/file.txt")
  138. # Both 'data' and 'data/' should be matched by /data/ pattern
  139. paths = ["data", "data/", "data/file.txt"]
  140. self._assert_ignore_match(paths)
  141. def test_wildcard_patterns(self) -> None:
  142. """Test wildcard patterns."""
  143. self._write_gitignore("*.py[cod]\n__pycache__/\n*.so\n")
  144. self._create_file("test.pyc")
  145. self._create_file("test.pyo")
  146. self._create_file("test.pyd")
  147. self._create_file("test.py")
  148. self._create_dir("__pycache__")
  149. paths = ["test.pyc", "test.pyo", "test.pyd", "test.py", "__pycache__/"]
  150. self._assert_ignore_match(paths)
  151. def test_negation_patterns(self) -> None:
  152. """Test negation patterns with !."""
  153. self._write_gitignore("*.log\n!important.log\n")
  154. self._create_file("debug.log")
  155. self._create_file("error.log")
  156. self._create_file("important.log")
  157. paths = ["debug.log", "error.log", "important.log"]
  158. self._assert_ignore_match(paths)
  159. def test_double_asterisk_patterns(self) -> None:
  160. """Test double asterisk ** patterns."""
  161. self._write_gitignore("**/temp\nvendor/**/cache\n")
  162. self._create_file("temp")
  163. self._create_file("src/temp")
  164. self._create_file("deep/nested/temp")
  165. self._create_file("vendor/lib/cache")
  166. self._create_file("vendor/gem/deep/cache")
  167. paths = [
  168. "temp",
  169. "src/temp",
  170. "deep/nested/temp",
  171. "vendor/lib/cache",
  172. "vendor/gem/deep/cache",
  173. ]
  174. self._assert_ignore_match(paths)
  175. def test_subdirectory_gitignore(self) -> None:
  176. """Test .gitignore files in subdirectories."""
  177. # Root .gitignore
  178. self._write_gitignore("*.tmp\n")
  179. # Subdirectory .gitignore
  180. self._create_dir("subdir")
  181. subdir_gitignore = os.path.join(self.test_dir, "subdir", ".gitignore")
  182. with open(subdir_gitignore, "w") as f:
  183. f.write("*.local\n!important.local\n")
  184. self._create_file("test.tmp")
  185. self._create_file("subdir/test.tmp")
  186. self._create_file("subdir/config.local")
  187. self._create_file("subdir/important.local")
  188. paths = [
  189. "test.tmp",
  190. "subdir/test.tmp",
  191. "subdir/config.local",
  192. "subdir/important.local",
  193. ]
  194. self._assert_ignore_match(paths)
  195. def test_complex_directory_negation(self) -> None:
  196. """Test complex directory negation patterns."""
  197. self._write_gitignore("dist/\n!dist/assets/\ndist/assets/*.tmp\n")
  198. self._create_dir("dist/assets")
  199. self._create_file("dist/main.js")
  200. self._create_file("dist/assets/style.css")
  201. self._create_file("dist/assets/temp.tmp")
  202. paths = [
  203. "dist/",
  204. "dist/main.js",
  205. "dist/assets/",
  206. "dist/assets/style.css",
  207. "dist/assets/temp.tmp",
  208. ]
  209. self._assert_ignore_match(paths)
  210. def test_leading_slash_patterns(self) -> None:
  211. """Test patterns with leading slash."""
  212. self._write_gitignore("/root-only.txt\nsubdir/specific.txt\n")
  213. self._create_file("root-only.txt")
  214. self._create_file("deep/root-only.txt") # Should not be ignored
  215. self._create_file("subdir/specific.txt")
  216. self._create_file("deep/subdir/specific.txt") # Should also be ignored
  217. paths = [
  218. "root-only.txt",
  219. "deep/root-only.txt",
  220. "subdir/specific.txt",
  221. "deep/subdir/specific.txt",
  222. ]
  223. self._assert_ignore_match(paths)
  224. def test_empty_directory_edge_case(self) -> None:
  225. """Test edge case with empty directories."""
  226. self._write_gitignore("empty/\n!empty/keep\n")
  227. self._create_dir("empty")
  228. self._create_file("empty/keep", "keep this")
  229. paths = ["empty/", "empty/keep"]
  230. self._assert_ignore_match(paths)
  231. def test_nested_wildcard_negation(self) -> None:
  232. """Test nested wildcard patterns with negation."""
  233. self._write_gitignore("docs/**\n!docs/*/\n!docs/**/*.md\n")
  234. self._create_file("docs/readme.txt") # Should be ignored
  235. self._create_file("docs/guide.md") # Should not be ignored
  236. self._create_dir("docs/api") # Should not be ignored
  237. self._create_file("docs/api/index.md") # Should not be ignored
  238. self._create_file("docs/api/temp.txt") # Should be ignored
  239. paths = [
  240. "docs/readme.txt",
  241. "docs/guide.md",
  242. "docs/api/",
  243. "docs/api/index.md",
  244. "docs/api/temp.txt",
  245. ]
  246. self._assert_ignore_match(paths)
  247. def test_case_sensitivity(self) -> None:
  248. """Test case sensitivity in patterns."""
  249. self._write_gitignore("*.TMP\nREADME\n")
  250. self._create_file("test.tmp") # Lowercase
  251. self._create_file("test.TMP") # Uppercase
  252. self._create_file("readme") # Lowercase
  253. self._create_file("README") # Uppercase
  254. paths = ["test.tmp", "test.TMP", "readme", "README"]
  255. self._assert_ignore_match(paths)
  256. def test_unicode_filenames(self) -> None:
  257. """Test unicode filenames in patterns."""
  258. try:
  259. self._write_gitignore("тест*\n*.测试\n")
  260. self._create_file("тест.txt")
  261. self._create_file("файл.测试")
  262. self._create_file("normal.txt")
  263. paths = ["тест.txt", "файл.测试", "normal.txt"]
  264. self._assert_ignore_match(paths)
  265. except (UnicodeEncodeError, OSError):
  266. # Skip test if filesystem doesn't support unicode
  267. self.skipTest("Filesystem doesn't support unicode filenames")
  268. def test_double_asterisk_edge_cases(self) -> None:
  269. """Test edge cases with ** patterns."""
  270. self._write_gitignore("**/afile\ndir1/**/b\n**/*.tmp\n")
  271. # Test **/afile pattern
  272. self._create_file("afile") # Root level
  273. self._create_file("dir/afile") # One level deep
  274. self._create_file("deep/nested/afile") # Multiple levels deep
  275. # Test dir1/**/b pattern
  276. self._create_file("dir1/b") # Direct child
  277. self._create_file("dir1/subdir/b") # One level deep in dir1/
  278. self._create_file("dir1/deep/nested/b") # Multiple levels deep in dir1/
  279. self._create_file("other/dir1/b") # Should not match (dir1/ not at start)
  280. # Test **/*.tmp pattern
  281. self._create_file("test.tmp") # Root level
  282. self._create_file("dir/test.tmp") # One level deep
  283. self._create_file("deep/nested/test.tmp") # Multiple levels deep
  284. paths = [
  285. "afile",
  286. "dir/afile",
  287. "deep/nested/afile",
  288. "dir1/b",
  289. "dir1/subdir/b",
  290. "dir1/deep/nested/b",
  291. "other/dir1/b",
  292. "test.tmp",
  293. "dir/test.tmp",
  294. "deep/nested/test.tmp",
  295. ]
  296. self._assert_ignore_match(paths)
  297. def test_double_asterisk_with_negation(self) -> None:
  298. """Test ** patterns combined with negation."""
  299. self._write_gitignore(
  300. "**/build/**\n!**/build/assets/**\n**/build/assets/*.tmp\n"
  301. )
  302. # Create build directories at different levels
  303. self._create_file("build/main.js")
  304. self._create_file("build/assets/style.css")
  305. self._create_file("build/assets/temp.tmp")
  306. self._create_file("src/build/app.js")
  307. self._create_file("src/build/assets/logo.png")
  308. self._create_file("src/build/assets/cache.tmp")
  309. self._create_file("deep/nested/build/lib.js")
  310. self._create_file("deep/nested/build/assets/icon.svg")
  311. self._create_file("deep/nested/build/assets/debug.tmp")
  312. paths = [
  313. "build/main.js",
  314. "build/assets/style.css",
  315. "build/assets/temp.tmp",
  316. "src/build/app.js",
  317. "src/build/assets/logo.png",
  318. "src/build/assets/cache.tmp",
  319. "deep/nested/build/lib.js",
  320. "deep/nested/build/assets/icon.svg",
  321. "deep/nested/build/assets/debug.tmp",
  322. ]
  323. self._assert_ignore_match(paths)
  324. def test_double_asterisk_middle_patterns(self) -> None:
  325. """Test ** patterns in the middle of paths."""
  326. self._write_gitignore("src/**/test/**\nlib/**/node_modules\n**/cache/**/temp\n")
  327. # Test src/**/test/** pattern
  328. self._create_file("src/test/unit.js")
  329. self._create_file("src/components/test/unit.js")
  330. self._create_file("src/deep/nested/test/integration.js")
  331. self._create_file("other/src/test/unit.js") # Should not match
  332. # Test lib/**/node_modules pattern
  333. self._create_file("lib/node_modules/package.json")
  334. self._create_file("lib/vendor/node_modules/package.json")
  335. self._create_file("lib/deep/path/node_modules/package.json")
  336. self._create_file("other/lib/node_modules/package.json") # Should not match
  337. # Test **/cache/**/temp pattern
  338. self._create_file("cache/temp")
  339. self._create_file("cache/data/temp")
  340. self._create_file("app/cache/temp")
  341. self._create_file("app/cache/nested/temp")
  342. self._create_file("deep/cache/very/nested/temp")
  343. paths = [
  344. "src/test/unit.js",
  345. "src/components/test/unit.js",
  346. "src/deep/nested/test/integration.js",
  347. "other/src/test/unit.js",
  348. "lib/node_modules/package.json",
  349. "lib/vendor/node_modules/package.json",
  350. "lib/deep/path/node_modules/package.json",
  351. "other/lib/node_modules/package.json",
  352. "cache/temp",
  353. "cache/data/temp",
  354. "app/cache/temp",
  355. "app/cache/nested/temp",
  356. "deep/cache/very/nested/temp",
  357. ]
  358. self._assert_ignore_match(paths)
  359. def test_multiple_double_asterisks(self) -> None:
  360. """Test patterns with multiple ** segments."""
  361. self._write_gitignore("**/**/test/**/*.js\n**/src/**/build/**/dist\n")
  362. # Test **/**/test/**/*.js pattern (multiple ** in one pattern)
  363. self._create_file("test/file.js")
  364. self._create_file("a/test/file.js")
  365. self._create_file("a/b/test/file.js")
  366. self._create_file("test/c/file.js")
  367. self._create_file("test/c/d/file.js")
  368. self._create_file("a/b/test/c/d/file.js")
  369. self._create_file("a/b/test/c/d/file.txt") # Different extension
  370. # Test **/src/**/build/**/dist pattern
  371. self._create_file("src/build/dist")
  372. self._create_file("app/src/build/dist")
  373. self._create_file("src/lib/build/dist")
  374. self._create_file("src/build/prod/dist")
  375. self._create_file("app/src/lib/build/prod/dist")
  376. paths = [
  377. "test/file.js",
  378. "a/test/file.js",
  379. "a/b/test/file.js",
  380. "test/c/file.js",
  381. "test/c/d/file.js",
  382. "a/b/test/c/d/file.js",
  383. "a/b/test/c/d/file.txt",
  384. "src/build/dist",
  385. "app/src/build/dist",
  386. "src/lib/build/dist",
  387. "src/build/prod/dist",
  388. "app/src/lib/build/prod/dist",
  389. ]
  390. self._assert_ignore_match(paths)
  391. def test_double_asterisk_directory_traversal(self) -> None:
  392. """Test ** patterns with directory traversal edge cases."""
  393. self._write_gitignore("**/.*\n!**/.gitkeep\n**/.git/**\n")
  394. # Test **/.* pattern (hidden files at any level)
  395. self._create_file(".hidden")
  396. self._create_file("dir/.hidden")
  397. self._create_file("deep/nested/.hidden")
  398. self._create_file(".gitkeep") # Should be negated
  399. self._create_file("dir/.gitkeep") # Should be negated
  400. # Test **/.git/** pattern
  401. self._create_file(".git/config")
  402. self._create_file(".git/objects/abc123")
  403. self._create_file("submodule/.git/config")
  404. self._create_file("deep/submodule/.git/refs/heads/master")
  405. paths = [
  406. ".hidden",
  407. "dir/.hidden",
  408. "deep/nested/.hidden",
  409. ".gitkeep",
  410. "dir/.gitkeep",
  411. ".git/config",
  412. ".git/objects/abc123",
  413. "submodule/.git/config",
  414. "deep/submodule/.git/refs/heads/master",
  415. ]
  416. self._assert_ignore_match(paths)
  417. def test_double_asterisk_empty_segments(self) -> None:
  418. """Test ** patterns with edge cases around empty path segments."""
  419. self._write_gitignore("a/**//b\n**//**/test\nc/**/**/\n")
  420. # These patterns test edge cases with path separator handling
  421. self._create_file("a/b")
  422. self._create_file("a/x/b")
  423. self._create_file("a/x/y/b")
  424. self._create_file("test")
  425. self._create_file("dir/test")
  426. self._create_file("dir/nested/test")
  427. self._create_file("c/file")
  428. self._create_file("c/dir/file")
  429. self._create_file("c/deep/nested/file")
  430. paths = [
  431. "a/b",
  432. "a/x/b",
  433. "a/x/y/b",
  434. "test",
  435. "dir/test",
  436. "dir/nested/test",
  437. "c/file",
  438. "c/dir/file",
  439. "c/deep/nested/file",
  440. ]
  441. self._assert_ignore_match(paths)
  442. def test_double_asterisk_root_patterns(self) -> None:
  443. """Test ** patterns at repository root with complex negations."""
  444. self._write_gitignore("/**\n!/**/\n!/**/*.md\n/**/*.tmp\n")
  445. # Pattern explanation:
  446. # /** - Ignore everything at any depth
  447. # !/**/ - But don't ignore directories
  448. # !/**/*.md - And don't ignore .md files
  449. # /**/*.tmp - But do ignore .tmp files (overrides .md negation for .tmp.md files)
  450. self._create_file("file.txt")
  451. self._create_file("readme.md")
  452. self._create_file("temp.tmp")
  453. self._create_file("backup.tmp.md") # Edge case: both .tmp and .md
  454. self._create_dir("dir")
  455. self._create_file("dir/file.txt")
  456. self._create_file("dir/guide.md")
  457. self._create_file("dir/cache.tmp")
  458. self._create_file("deep/nested/doc.md")
  459. self._create_file("deep/nested/log.tmp")
  460. paths = [
  461. "file.txt",
  462. "readme.md",
  463. "temp.tmp",
  464. "backup.tmp.md",
  465. "dir/",
  466. "dir/file.txt",
  467. "dir/guide.md",
  468. "dir/cache.tmp",
  469. "deep/nested/doc.md",
  470. "deep/nested/log.tmp",
  471. ]
  472. self._assert_ignore_match(paths)
  473. def test_single_asterisk_patterns(self) -> None:
  474. """Test single asterisk * patterns in various positions."""
  475. self._write_gitignore("src/*/build\n*.log\ntest*/\n*_backup\nlib/*\n*/temp/*\n")
  476. # Test src/*/build pattern
  477. self._create_file("src/app/build")
  478. self._create_file("src/lib/build")
  479. self._create_file("src/nested/deep/build") # Should not match (only one level)
  480. self._create_file("other/src/app/build") # Should not match
  481. # Test *.log pattern
  482. self._create_file("app.log")
  483. self._create_file("error.log")
  484. self._create_file("logs/debug.log") # Should match
  485. self._create_file("app.log.old") # Should not match
  486. # Test test*/ pattern (directories starting with test)
  487. self._create_dir("test")
  488. self._create_dir("testing")
  489. self._create_dir("test_data")
  490. self._create_file("test_file") # Should not match (not a directory)
  491. # Test *_backup pattern
  492. self._create_file("db_backup")
  493. self._create_file("config_backup")
  494. self._create_file("old_backup_file") # Should not match (backup not at end)
  495. # Test lib/* pattern
  496. self._create_file("lib/module.js")
  497. self._create_file("lib/utils.py")
  498. self._create_file("lib/nested/deep.js") # Should not match (only one level)
  499. # Test */temp/* pattern
  500. self._create_file("app/temp/cache")
  501. self._create_file("src/temp/logs")
  502. self._create_file("deep/nested/temp/file") # Should not match (nested too deep)
  503. self._create_file("temp/file") # Should not match (temp at root)
  504. paths = [
  505. "src/app/build",
  506. "src/lib/build",
  507. "src/nested/deep/build",
  508. "other/src/app/build",
  509. "app.log",
  510. "error.log",
  511. "logs/debug.log",
  512. "app.log.old",
  513. "test/",
  514. "testing/",
  515. "test_data/",
  516. "test_file",
  517. "db_backup",
  518. "config_backup",
  519. "old_backup_file",
  520. "lib/module.js",
  521. "lib/utils.py",
  522. "lib/nested/deep.js",
  523. "app/temp/cache",
  524. "src/temp/logs",
  525. "deep/nested/temp/file",
  526. "temp/file",
  527. ]
  528. self._assert_ignore_match(paths)
  529. def test_single_asterisk_edge_cases(self) -> None:
  530. """Test edge cases with single asterisk patterns."""
  531. self._write_gitignore("*\n!*/\n!*.txt\n*.*.*\n")
  532. # Pattern explanation:
  533. # * - Ignore everything
  534. # !*/ - But don't ignore directories
  535. # !*.txt - And don't ignore .txt files
  536. # *.*.* - But ignore files with multiple dots
  537. self._create_file("file")
  538. self._create_file("readme.txt")
  539. self._create_file("config.json")
  540. self._create_file("archive.tar.gz") # Multiple dots
  541. self._create_file("backup.sql.old") # Multiple dots
  542. self._create_dir("folder")
  543. self._create_file("folder/nested.txt")
  544. self._create_file("folder/data.json")
  545. paths = [
  546. "file",
  547. "readme.txt",
  548. "config.json",
  549. "archive.tar.gz",
  550. "backup.sql.old",
  551. "folder/",
  552. "folder/nested.txt",
  553. "folder/data.json",
  554. ]
  555. self._assert_ignore_match(paths)
  556. def test_single_asterisk_with_character_classes(self) -> None:
  557. """Test single asterisk with character classes and special patterns."""
  558. self._write_gitignore("*.[oa]\n*~\n.*\n!.gitignore\n[Tt]emp*\n")
  559. # Test *.[oa] pattern (object and archive files)
  560. self._create_file("main.o")
  561. self._create_file("lib.a")
  562. self._create_file("app.so") # Should not match
  563. self._create_file("test.c") # Should not match
  564. # Test *~ pattern (backup files)
  565. self._create_file("file~")
  566. self._create_file("config~")
  567. self._create_file("~file") # Should not match (~ at start)
  568. # Test .* pattern with negation
  569. self._create_file(".hidden")
  570. self._create_file(".secret")
  571. self._create_file(".gitignore") # Should be negated
  572. # Test [Tt]emp* pattern (case variations)
  573. self._create_file("temp_file")
  574. self._create_file("Temp_data")
  575. self._create_file("TEMP_LOG") # Should not match (not T or t)
  576. self._create_file("temporary")
  577. paths = [
  578. "main.o",
  579. "lib.a",
  580. "app.so",
  581. "test.c",
  582. "file~",
  583. "config~",
  584. "~file",
  585. ".hidden",
  586. ".secret",
  587. ".gitignore",
  588. "temp_file",
  589. "Temp_data",
  590. "TEMP_LOG",
  591. "temporary",
  592. ]
  593. self._assert_ignore_match(paths)
  594. def test_mixed_single_double_asterisk_patterns(self) -> None:
  595. """Test patterns that mix single (*) and double (**) asterisks."""
  596. self._write_gitignore(
  597. "src/**/test/*.js\n**/build/*\n*/cache/**\nlib/*/vendor/**/*.min.*\n"
  598. )
  599. # Test src/**/test/*.js - double asterisk in middle, single at end
  600. self._create_file("src/test/unit.js")
  601. self._create_file("src/components/test/spec.js")
  602. self._create_file("src/deep/nested/test/integration.js")
  603. self._create_file(
  604. "src/test/nested/unit.js"
  605. ) # Should not match (nested after test)
  606. self._create_file(
  607. "src/components/test/unit.ts"
  608. ) # Should not match (wrong extension)
  609. # Test **/build/* - double asterisk at start, single at end
  610. self._create_file("build/app.js")
  611. self._create_file("src/build/main.js")
  612. self._create_file("deep/nested/build/lib.js")
  613. self._create_file("build/dist/app.js") # Should not match (nested after build)
  614. # Test */cache/** - single at start, double at end
  615. self._create_file("app/cache/temp")
  616. self._create_file("src/cache/data/file")
  617. self._create_file("lib/cache/deep/nested/item")
  618. self._create_file(
  619. "nested/deep/cache/file"
  620. ) # Should not match (cache not at second level)
  621. self._create_file("cache/file") # Should not match (cache at root)
  622. # Test lib/*/vendor/**/*.min.* - complex mixed pattern
  623. self._create_file("lib/app/vendor/jquery.min.js")
  624. self._create_file("lib/ui/vendor/bootstrap.min.css")
  625. self._create_file("lib/core/vendor/deep/nested/lib.min.map")
  626. self._create_file("lib/app/vendor/jquery.js") # Should not match (not .min.)
  627. self._create_file(
  628. "lib/nested/deep/vendor/lib.min.js"
  629. ) # Should not match (too deep before vendor)
  630. paths = [
  631. "src/test/unit.js",
  632. "src/components/test/spec.js",
  633. "src/deep/nested/test/integration.js",
  634. "src/test/nested/unit.js",
  635. "src/components/test/unit.ts",
  636. "build/app.js",
  637. "src/build/main.js",
  638. "deep/nested/build/lib.js",
  639. "build/dist/app.js",
  640. "app/cache/temp",
  641. "src/cache/data/file",
  642. "lib/cache/deep/nested/item",
  643. "nested/deep/cache/file",
  644. "cache/file",
  645. "lib/app/vendor/jquery.min.js",
  646. "lib/ui/vendor/bootstrap.min.css",
  647. "lib/core/vendor/deep/nested/lib.min.map",
  648. "lib/app/vendor/jquery.js",
  649. "lib/nested/deep/vendor/lib.min.js",
  650. ]
  651. self._assert_ignore_match(paths)
  652. def test_asterisk_pattern_overlaps(self) -> None:
  653. """Test overlapping single and double asterisk patterns with negations."""
  654. self._write_gitignore(
  655. "**/*.tmp\n!src/**/*.tmp\nsrc/*/cache/*.tmp\n**/test/*\n!**/test/*.spec.*\n"
  656. )
  657. # Pattern explanation:
  658. # **/*.tmp - Ignore all .tmp files anywhere
  659. # !src/**/*.tmp - But don't ignore .tmp files under src/
  660. # src/*/cache/*.tmp - But do ignore .tmp files in src/*/cache/ (overrides negation)
  661. # **/test/* - Ignore everything directly in test directories
  662. # !**/test/*.spec.* - But don't ignore spec files in test directories
  663. # Test tmp file patterns with src/ negation
  664. self._create_file("temp.tmp") # Should be ignored
  665. self._create_file("build/cache.tmp") # Should be ignored
  666. self._create_file("src/app.tmp") # Should not be ignored (src negation)
  667. self._create_file("src/lib/utils.tmp") # Should not be ignored (src negation)
  668. self._create_file(
  669. "src/app/cache/data.tmp"
  670. ) # Should be ignored (cache override)
  671. self._create_file(
  672. "src/lib/cache/temp.tmp"
  673. ) # Should be ignored (cache override)
  674. # Test test directory patterns with spec negation
  675. self._create_file("test/unit.js") # Should be ignored
  676. self._create_file("src/test/helper.js") # Should be ignored
  677. self._create_file("test/app.spec.js") # Should not be ignored (spec negation)
  678. self._create_file(
  679. "src/test/lib.spec.ts"
  680. ) # Should not be ignored (spec negation)
  681. self._create_file(
  682. "test/nested/file.js"
  683. ) # Should not be ignored (not direct child)
  684. paths = [
  685. "temp.tmp",
  686. "build/cache.tmp",
  687. "src/app.tmp",
  688. "src/lib/utils.tmp",
  689. "src/app/cache/data.tmp",
  690. "src/lib/cache/temp.tmp",
  691. "test/unit.js",
  692. "src/test/helper.js",
  693. "test/app.spec.js",
  694. "src/test/lib.spec.ts",
  695. "test/nested/file.js",
  696. ]
  697. self._assert_ignore_match(paths)
  698. def test_asterisk_boundary_conditions(self) -> None:
  699. """Test boundary conditions between single and double asterisk patterns."""
  700. self._write_gitignore("a/**/b/*\nc/**/**/d\n*/e/**/*\nf/*/g/**\n")
  701. # Test a/**/b/* - ** in middle, * at end
  702. self._create_file("a/b/file") # Direct path
  703. self._create_file("a/x/b/file") # One level between a and b
  704. self._create_file("a/x/y/b/file") # Multiple levels between a and b
  705. self._create_file("a/b/nested/file") # Should not match (nested after b)
  706. # Test c/**/**/d - multiple ** separated by single level
  707. self._create_file("c/d") # Minimal match
  708. self._create_file("c/x/d") # One level before d
  709. self._create_file("c/x/y/d") # Multiple levels before d
  710. self._create_file("c/x/y/z/d") # Even more levels
  711. # Test */e/**/* - * at start, ** in middle, * at end
  712. self._create_file("a/e/file") # Minimal match
  713. self._create_file("x/e/nested/file") # Nested after e
  714. self._create_file("y/e/deep/nested/file") # Deep nesting after e
  715. self._create_file(
  716. "nested/path/e/file"
  717. ) # Should not match (path before e too deep)
  718. # Test f/*/g/** - * in middle, ** at end
  719. self._create_file("f/x/g/file") # Basic match
  720. self._create_file("f/y/g/nested/file") # Nested after g
  721. self._create_file("f/z/g/deep/nested/file") # Deep nesting after g
  722. self._create_file(
  723. "f/nested/path/g/file"
  724. ) # Should not match (path between f and g too deep)
  725. paths = [
  726. "a/b/file",
  727. "a/x/b/file",
  728. "a/x/y/b/file",
  729. "a/b/nested/file",
  730. "c/d",
  731. "c/x/d",
  732. "c/x/y/d",
  733. "c/x/y/z/d",
  734. "a/e/file",
  735. "x/e/nested/file",
  736. "y/e/deep/nested/file",
  737. "nested/path/e/file",
  738. "f/x/g/file",
  739. "f/y/g/nested/file",
  740. "f/z/g/deep/nested/file",
  741. "f/nested/path/g/file",
  742. ]
  743. self._assert_ignore_match(paths)
  744. def test_asterisk_edge_case_combinations(self) -> None:
  745. """Test really tricky edge cases with asterisk combinations."""
  746. self._write_gitignore("***\n**/*\n*/**\n*/*/\n**/*/*\n*/*/**\n")
  747. # Test *** pattern (should behave like **)
  748. self._create_file("file1")
  749. self._create_file("dir/file2")
  750. self._create_file("deep/nested/file3")
  751. # Test **/* pattern (anything with at least one path segment)
  752. self._create_file("path1/item1")
  753. self._create_file("path2/sub/item2")
  754. # Test */** pattern (anything under a single-level directory)
  755. self._create_file("single/file4")
  756. self._create_file("single/nested/deep")
  757. # Test */*/ pattern (directories exactly two levels deep)
  758. self._create_dir("level1/level2")
  759. self._create_dir("dir1/dir2")
  760. self._create_dir("path3/sub1/sub2") # Should not match (too deep)
  761. # Test **/*/* pattern (at least two path segments after any prefix)
  762. self._create_file("test1/test2/test3")
  763. self._create_file("deep/nested/item3/item4")
  764. self._create_file(
  765. "simple/item"
  766. ) # Should not match (only one segment after any prefix at root)
  767. # Test */*/** pattern (single/single/anything)
  768. self._create_file("part1/part2/anything")
  769. self._create_file("seg1/seg2/deep/nested")
  770. paths = [
  771. "file1",
  772. "dir/file2",
  773. "deep/nested/file3",
  774. "path1/item1",
  775. "path2/sub/item2",
  776. "single/file4",
  777. "single/nested/deep",
  778. "level1/level2/",
  779. "dir1/dir2/",
  780. "path3/sub1/sub2/",
  781. "test1/test2/test3",
  782. "deep/nested/item3/item4",
  783. "simple/item",
  784. "part1/part2/anything",
  785. "seg1/seg2/deep/nested",
  786. ]
  787. self._assert_ignore_match(paths)
  788. def test_asterisk_consecutive_patterns(self) -> None:
  789. """Test patterns with consecutive asterisks and weird spacing."""
  790. self._write_gitignore("a*/b*\n*x*y*\n**z**\n**/.*/**\n*.*./*\n")
  791. # Test a*/b* pattern
  792. self._create_file("a/b") # Minimal match
  793. self._create_file("app/build") # Both have suffixes
  794. self._create_file("api/backup") # Both have suffixes
  795. self._create_file("a/build") # a exact, b with suffix
  796. self._create_file("app/b") # a with suffix, b exact
  797. self._create_file("x/a/b") # Should not match (a not at start)
  798. # Test *x*y* pattern
  799. self._create_file("xy") # Minimal
  800. self._create_file("axby") # x and y in middle
  801. self._create_file("prefixsuffyend") # x and y with text around
  802. self._create_file("xyz") # Should not match (no y after x)
  803. self._create_file("axy") # x and y consecutive
  804. # Test **z** pattern
  805. self._create_file("z") # Just z
  806. self._create_file("az") # z at end
  807. self._create_file("za") # z at start
  808. self._create_file("aza") # z in middle
  809. self._create_file("dir/z") # z at any depth
  810. self._create_file("deep/nested/prefix_z_suffix") # z anywhere in name
  811. # Test **/.*/** pattern (hidden files in any directory structure)
  812. self._create_file("dir/.hidden/file")
  813. self._create_file("deep/nested/.secret/data")
  814. self._create_file(".visible/file") # At root level
  815. self._create_file("other/.config") # Should not match (no trailing path)
  816. # Test *.*./* pattern (files with dots in specific structure)
  817. self._create_file("app.min.js/file") # Two dots, then directory
  818. self._create_file("lib.bundle.css/asset") # Two dots, then directory
  819. self._create_file("simple.js") # Should not match (only one dot, no directory)
  820. self._create_file("no.dots.here") # Should not match (no trailing directory)
  821. paths = [
  822. "a/b",
  823. "app/build",
  824. "api/backup",
  825. "a/build",
  826. "app/b",
  827. "x/a/b",
  828. "xy",
  829. "axby",
  830. "prefixsuffyend",
  831. "xyz",
  832. "axy",
  833. "z",
  834. "az",
  835. "za",
  836. "aza",
  837. "dir/z",
  838. "deep/nested/prefix_z_suffix",
  839. "dir/.hidden/file",
  840. "deep/nested/.secret/data",
  841. ".visible/file",
  842. "other/.config",
  843. "app.min.js/file",
  844. "lib.bundle.css/asset",
  845. "simple.js",
  846. "no.dots.here",
  847. ]
  848. self._assert_ignore_match(paths)
  849. def test_asterisk_escaping_and_special_chars(self) -> None:
  850. """Test asterisk patterns with special characters and potential escaping."""
  851. import sys
  852. self._write_gitignore(
  853. "\\*literal\n**/*.\\*\n[*]bracket\n*\\[escape\\]\n*.{tmp,log}\n"
  854. )
  855. # Test \*literal pattern (literal asterisk)
  856. # Skip files with asterisks on Windows as they're invalid filenames
  857. if sys.platform != "win32":
  858. self._create_file("*literal") # Literal asterisk at start
  859. self._create_file("prefix*literal") # Literal asterisk in middle
  860. self._create_file("xliteral") # Should not match (no literal asterisk)
  861. # Test **/*.* pattern (files with .* extension)
  862. # Skip files with asterisks on Windows
  863. if sys.platform != "win32":
  864. self._create_file("file.*") # Literal .* extension
  865. self._create_file("dir/test.*") # At any depth
  866. self._create_file("file.txt") # Should not match (not .* extension)
  867. # Test [*]bracket pattern (bracket containing asterisk)
  868. if sys.platform != "win32":
  869. self._create_file("*bracket") # Literal asterisk from bracket
  870. self._create_file("xbracket") # Should not match
  871. self._create_file("abracket") # Should not match
  872. # Test *\[escape\] pattern (literal brackets)
  873. self._create_file("test[escape]") # Literal brackets
  874. self._create_file("prefix[escape]") # With prefix
  875. self._create_file("test[other]") # Should not match (wrong brackets)
  876. # Test *.{tmp,log} pattern (brace expansion - may not work in gitignore)
  877. self._create_file("file.{tmp,log}") # Literal braces
  878. self._create_file("test.tmp") # Might match if braces are expanded
  879. self._create_file("test.log") # Might match if braces are expanded
  880. self._create_file("test.{other}") # Should not match
  881. paths = [
  882. "xliteral",
  883. "file.txt",
  884. "xbracket",
  885. "abracket",
  886. "test[escape]",
  887. "prefix[escape]",
  888. "test[other]",
  889. "file.{tmp,log}",
  890. "test.tmp",
  891. "test.log",
  892. "test.{other}",
  893. ]
  894. # Add files with asterisks only on non-Windows platforms
  895. if sys.platform != "win32":
  896. paths.extend(
  897. [
  898. "*literal",
  899. "prefix*literal",
  900. "file.*",
  901. "dir/test.*",
  902. "*bracket",
  903. ]
  904. )
  905. self._assert_ignore_match(paths)
  906. def test_quote_path_true_unicode_filenames(self) -> None:
  907. """Test quote_path=True functionality with unicode filenames."""
  908. try:
  909. self._write_gitignore("тест*\n*.测试\n")
  910. self._create_file("тест.txt")
  911. self._create_file("файл.测试")
  912. self._create_file("normal.txt")
  913. paths = ["тест.txt", "файл.测试", "normal.txt"]
  914. # Test that dulwich with quote_path=True matches git's quoted output
  915. git_ignored = self._git_check_ignore_quoted(paths)
  916. dulwich_ignored = self._dulwich_check_ignore_quoted(paths)
  917. self.assertEqual(
  918. git_ignored,
  919. dulwich_ignored,
  920. f"Mismatch for quoted paths {paths}: git={git_ignored}, dulwich={dulwich_ignored}",
  921. )
  922. except (UnicodeEncodeError, OSError):
  923. # Skip test if filesystem doesn't support unicode
  924. self.skipTest("Filesystem doesn't support unicode filenames")
  925. def test_quote_path_consistency(self) -> None:
  926. """Test that quote_path=True and quote_path=False are consistent."""
  927. try:
  928. self._write_gitignore("тест*\n*.测试\nmixed_тест*\n")
  929. self._create_file("тест.txt")
  930. self._create_file("файл.测试")
  931. self._create_file("normal.txt")
  932. self._create_file("mixed_тест.log")
  933. paths = ["тест.txt", "файл.测试", "normal.txt", "mixed_тест.log"]
  934. # Get both quoted and unquoted results from dulwich
  935. quoted_ignored = self._dulwich_check_ignore_quoted(paths)
  936. unquoted_ignored = self._dulwich_check_ignore(paths)
  937. # Verify that the number of ignored files is the same
  938. self.assertEqual(
  939. len(quoted_ignored),
  940. len(unquoted_ignored),
  941. "Quote path setting should not change which files are ignored",
  942. )
  943. # Verify quoted paths contain the expected files
  944. expected_quoted = {
  945. '"\\321\\202\\320\\265\\321\\201\\321\\202.txt"',
  946. '"\\321\\204\\320\\260\\320\\271\\320\\273.\\346\\265\\213\\350\\257\\225"',
  947. '"mixed_\\321\\202\\320\\265\\321\\201\\321\\202.log"',
  948. }
  949. self.assertEqual(quoted_ignored, expected_quoted)
  950. # Verify unquoted paths contain the expected files
  951. expected_unquoted = {"тест.txt", "файл.测试", "mixed_тест.log"}
  952. self.assertEqual(unquoted_ignored, expected_unquoted)
  953. except (UnicodeEncodeError, OSError):
  954. # Skip test if filesystem doesn't support unicode
  955. self.skipTest("Filesystem doesn't support unicode filenames")
  956. def test_parent_exclusion_wildcard_directories(self) -> None:
  957. """Test parent directory exclusion with wildcard patterns (issue #1721)."""
  958. self._write_gitignore("*/build/\n!*/build/important.txt\n")
  959. self._create_dir("src/build")
  960. self._create_dir("test/build")
  961. self._create_file("src/build/important.txt")
  962. self._create_file("test/build/important.txt")
  963. self._create_file("src/build/other.txt")
  964. paths = [
  965. "src/build/",
  966. "src/build/important.txt",
  967. "src/build/other.txt",
  968. "test/build/",
  969. "test/build/important.txt",
  970. ]
  971. self._assert_ignore_match(paths)
  972. def test_parent_exclusion_double_asterisk(self) -> None:
  973. """Test parent directory exclusion with ** patterns."""
  974. self._write_gitignore("**/node_modules/\n!**/node_modules/keep.txt\n")
  975. self._create_dir("node_modules")
  976. self._create_dir("src/node_modules")
  977. self._create_dir("deep/nested/node_modules")
  978. self._create_file("node_modules/keep.txt")
  979. self._create_file("src/node_modules/keep.txt")
  980. self._create_file("deep/nested/node_modules/keep.txt")
  981. paths = [
  982. "node_modules/",
  983. "node_modules/keep.txt",
  984. "src/node_modules/",
  985. "src/node_modules/keep.txt",
  986. "deep/nested/node_modules/",
  987. "deep/nested/node_modules/keep.txt",
  988. ]
  989. self._assert_ignore_match(paths)
  990. def test_glob_pattern_base_directory(self) -> None:
  991. """Test that dir/** doesn't match dir/ itself."""
  992. self._write_gitignore("logs/**\n!logs/important.log\n!logs/keep/\n")
  993. self._create_dir("logs")
  994. self._create_dir("logs/keep")
  995. self._create_dir("logs/subdir")
  996. self._create_file("logs/important.log")
  997. self._create_file("logs/other.log")
  998. self._create_file("logs/subdir/file.txt")
  999. paths = [
  1000. "logs/",
  1001. "logs/important.log",
  1002. "logs/other.log",
  1003. "logs/keep/",
  1004. "logs/subdir/",
  1005. "logs/subdir/file.txt",
  1006. ]
  1007. self._assert_ignore_match(paths)
  1008. def _git_check_ignore_quoted(self, paths: Sequence[str]) -> set[str]:
  1009. """Run git check-ignore with default quoting and return set of ignored paths."""
  1010. try:
  1011. # Use default git settings (core.quotePath=true by default)
  1012. output = run_git_or_fail(
  1013. ["check-ignore", *paths],
  1014. cwd=self.test_dir,
  1015. )
  1016. # git check-ignore returns paths separated by newlines
  1017. return set(
  1018. line.decode("utf-8") for line in output.strip().split(b"\n") if line
  1019. )
  1020. except AssertionError:
  1021. # git check-ignore returns non-zero when no paths are ignored
  1022. return set()
  1023. def _dulwich_check_ignore_quoted(self, paths: Sequence[str]) -> set[str]:
  1024. """Run dulwich check_ignore with quote_path=True and return set of ignored paths."""
  1025. # Convert to absolute paths relative to the test directory
  1026. abs_paths = [os.path.join(self.test_dir, path) for path in paths]
  1027. ignored = set(porcelain.check_ignore(self.test_dir, abs_paths, quote_path=True))
  1028. # Convert back to relative paths and preserve original path format
  1029. result = set()
  1030. path_mapping = {}
  1031. for orig_path, abs_path in zip(paths, abs_paths):
  1032. path_mapping[abs_path] = orig_path
  1033. for path in ignored:
  1034. if path.startswith(self.test_dir + "/"):
  1035. rel_path = path[len(self.test_dir) + 1 :]
  1036. # Find the original path format that was requested
  1037. orig_path = None
  1038. for requested_path in paths:
  1039. if requested_path.rstrip("/") == rel_path.rstrip("/"):
  1040. orig_path = requested_path
  1041. break
  1042. result.add(orig_path if orig_path else rel_path)
  1043. else:
  1044. result.add(path)
  1045. return result