test_ignore.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. # test_ignore.py -- Tests for ignore files.
  2. # Copyright (C) 2017 Jelmer Vernooij <jelmer@jelmer.uk>
  3. #
  4. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  5. # General Public License as public 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. """Tests for ignore files."""
  21. from io import BytesIO
  22. import os
  23. import re
  24. import shutil
  25. import tempfile
  26. import unittest
  27. from dulwich.tests import TestCase
  28. from dulwich.ignore import (
  29. IgnoreFilter,
  30. IgnoreFilterManager,
  31. IgnoreFilterStack,
  32. Pattern,
  33. match_pattern,
  34. read_ignore_patterns,
  35. translate,
  36. )
  37. from dulwich.repo import Repo
  38. POSITIVE_MATCH_TESTS = [
  39. (b"foo.c", b"*.c"),
  40. (b".c", b"*.c"),
  41. (b"foo/foo.c", b"*.c"),
  42. (b"foo/foo.c", b"foo.c"),
  43. (b"foo.c", b"/*.c"),
  44. (b"foo.c", b"/foo.c"),
  45. (b"foo.c", b"foo.c"),
  46. (b"foo.c", b"foo.[ch]"),
  47. (b"foo/bar/bla.c", b"foo/**"),
  48. (b"foo/bar/bla/blie.c", b"foo/**/blie.c"),
  49. (b"foo/bar/bla.c", b"**/bla.c"),
  50. (b"bla.c", b"**/bla.c"),
  51. (b"foo/bar", b"foo/**/bar"),
  52. (b"foo/bla/bar", b"foo/**/bar"),
  53. (b"foo/bar/", b"bar/"),
  54. (b"foo/bar/", b"bar"),
  55. (b"foo/bar/something", b"foo/bar/*"),
  56. ]
  57. NEGATIVE_MATCH_TESTS = [
  58. (b"foo.c", b"foo.[dh]"),
  59. (b"foo/foo.c", b"/foo.c"),
  60. (b"foo/foo.c", b"/*.c"),
  61. (b"foo/bar/", b"/bar/"),
  62. (b"foo/bar/", b"foo/bar/*"),
  63. (b"foo/bar", b"foo?bar"),
  64. ]
  65. TRANSLATE_TESTS = [
  66. (b"*.c", b"(?ms)(.*/)?[^/]*\\.c/?\\Z"),
  67. (b"foo.c", b"(?ms)(.*/)?foo\\.c/?\\Z"),
  68. (b"/*.c", b"(?ms)[^/]*\\.c/?\\Z"),
  69. (b"/foo.c", b"(?ms)foo\\.c/?\\Z"),
  70. (b"foo.c", b"(?ms)(.*/)?foo\\.c/?\\Z"),
  71. (b"foo.[ch]", b"(?ms)(.*/)?foo\\.[ch]/?\\Z"),
  72. (b"bar/", b"(?ms)(.*/)?bar\\/\\Z"),
  73. (b"foo/**", b"(?ms)foo(/.*)?/?\\Z"),
  74. (b"foo/**/blie.c", b"(?ms)foo(/.*)?\\/blie\\.c/?\\Z"),
  75. (b"**/bla.c", b"(?ms)(.*/)?bla\\.c/?\\Z"),
  76. (b"foo/**/bar", b"(?ms)foo(/.*)?\\/bar/?\\Z"),
  77. (b"foo/bar/*", b"(?ms)foo\\/bar\\/[^/]+/?\\Z"),
  78. ]
  79. class TranslateTests(TestCase):
  80. def test_translate(self):
  81. for (pattern, regex) in TRANSLATE_TESTS:
  82. if re.escape(b"/") == b"/":
  83. # Slash is no longer escaped in Python3.7, so undo the escaping
  84. # in the expected return value..
  85. regex = regex.replace(b"\\/", b"/")
  86. self.assertEqual(
  87. regex,
  88. translate(pattern),
  89. "orig pattern: %r, regex: %r, expected: %r"
  90. % (pattern, translate(pattern), regex),
  91. )
  92. class ReadIgnorePatterns(TestCase):
  93. def test_read_file(self):
  94. f = BytesIO(
  95. b"""
  96. # a comment
  97. # and an empty line:
  98. \\#not a comment
  99. !negative
  100. with trailing whitespace
  101. with escaped trailing whitespace\\
  102. """
  103. ) # noqa: W291
  104. self.assertEqual(
  105. list(read_ignore_patterns(f)),
  106. [
  107. b"\\#not a comment",
  108. b"!negative",
  109. b"with trailing whitespace",
  110. b"with escaped trailing whitespace ",
  111. ],
  112. )
  113. class MatchPatternTests(TestCase):
  114. def test_matches(self):
  115. for (path, pattern) in POSITIVE_MATCH_TESTS:
  116. self.assertTrue(
  117. match_pattern(path, pattern),
  118. "path: %r, pattern: %r" % (path, pattern),
  119. )
  120. def test_no_matches(self):
  121. for (path, pattern) in NEGATIVE_MATCH_TESTS:
  122. self.assertFalse(
  123. match_pattern(path, pattern),
  124. "path: %r, pattern: %r" % (path, pattern),
  125. )
  126. class IgnoreFilterTests(TestCase):
  127. def test_included(self):
  128. filter = IgnoreFilter([b"a.c", b"b.c"])
  129. self.assertTrue(filter.is_ignored(b"a.c"))
  130. self.assertIs(None, filter.is_ignored(b"c.c"))
  131. self.assertEqual([Pattern(b"a.c")], list(filter.find_matching(b"a.c")))
  132. self.assertEqual([], list(filter.find_matching(b"c.c")))
  133. def test_included_ignorecase(self):
  134. filter = IgnoreFilter([b"a.c", b"b.c"], ignorecase=False)
  135. self.assertTrue(filter.is_ignored(b"a.c"))
  136. self.assertFalse(filter.is_ignored(b"A.c"))
  137. filter = IgnoreFilter([b"a.c", b"b.c"], ignorecase=True)
  138. self.assertTrue(filter.is_ignored(b"a.c"))
  139. self.assertTrue(filter.is_ignored(b"A.c"))
  140. self.assertTrue(filter.is_ignored(b"A.C"))
  141. def test_excluded(self):
  142. filter = IgnoreFilter([b"a.c", b"b.c", b"!c.c"])
  143. self.assertFalse(filter.is_ignored(b"c.c"))
  144. self.assertIs(None, filter.is_ignored(b"d.c"))
  145. self.assertEqual([Pattern(b"!c.c")], list(filter.find_matching(b"c.c")))
  146. self.assertEqual([], list(filter.find_matching(b"d.c")))
  147. def test_include_exclude_include(self):
  148. filter = IgnoreFilter([b"a.c", b"!a.c", b"a.c"])
  149. self.assertTrue(filter.is_ignored(b"a.c"))
  150. self.assertEqual(
  151. [Pattern(b"a.c"), Pattern(b"!a.c"), Pattern(b"a.c")],
  152. list(filter.find_matching(b"a.c")),
  153. )
  154. def test_manpage(self):
  155. # A specific example from the gitignore manpage
  156. filter = IgnoreFilter([b"/*", b"!/foo", b"/foo/*", b"!/foo/bar"])
  157. self.assertTrue(filter.is_ignored(b"a.c"))
  158. self.assertTrue(filter.is_ignored(b"foo/blie"))
  159. self.assertFalse(filter.is_ignored(b"foo"))
  160. self.assertFalse(filter.is_ignored(b"foo/bar"))
  161. self.assertFalse(filter.is_ignored(b"foo/bar/"))
  162. self.assertFalse(filter.is_ignored(b"foo/bar/bloe"))
  163. class IgnoreFilterStackTests(TestCase):
  164. def test_stack_first(self):
  165. filter1 = IgnoreFilter([b"[a].c", b"[b].c", b"![d].c"])
  166. filter2 = IgnoreFilter([b"[a].c", b"![b],c", b"[c].c", b"[d].c"])
  167. stack = IgnoreFilterStack([filter1, filter2])
  168. self.assertIs(True, stack.is_ignored(b"a.c"))
  169. self.assertIs(True, stack.is_ignored(b"b.c"))
  170. self.assertIs(True, stack.is_ignored(b"c.c"))
  171. self.assertIs(False, stack.is_ignored(b"d.c"))
  172. self.assertIs(None, stack.is_ignored(b"e.c"))
  173. class IgnoreFilterManagerTests(TestCase):
  174. def test_load_ignore(self):
  175. tmp_dir = tempfile.mkdtemp()
  176. self.addCleanup(shutil.rmtree, tmp_dir)
  177. repo = Repo.init(tmp_dir)
  178. with open(os.path.join(repo.path, ".gitignore"), "wb") as f:
  179. f.write(b"/foo/bar\n")
  180. f.write(b"/dir2\n")
  181. f.write(b"/dir3/\n")
  182. os.mkdir(os.path.join(repo.path, "dir"))
  183. with open(os.path.join(repo.path, "dir", ".gitignore"), "wb") as f:
  184. f.write(b"/blie\n")
  185. with open(os.path.join(repo.path, "dir", "blie"), "wb") as f:
  186. f.write(b"IGNORED")
  187. p = os.path.join(repo.controldir(), "info", "exclude")
  188. with open(p, "wb") as f:
  189. f.write(b"/excluded\n")
  190. m = IgnoreFilterManager.from_repo(repo)
  191. self.assertTrue(m.is_ignored("dir/blie"))
  192. self.assertIs(None, m.is_ignored(os.path.join("dir", "bloe")))
  193. self.assertIs(None, m.is_ignored("dir"))
  194. self.assertTrue(m.is_ignored(os.path.join("foo", "bar")))
  195. self.assertTrue(m.is_ignored(os.path.join("excluded")))
  196. self.assertTrue(m.is_ignored(os.path.join("dir2", "fileinignoreddir")))
  197. self.assertFalse(m.is_ignored("dir3"))
  198. self.assertTrue(m.is_ignored("dir3/"))
  199. self.assertTrue(m.is_ignored("dir3/bla"))
  200. @unittest.expectedFailure
  201. def test_nested_gitignores(self):
  202. tmp_dir = tempfile.mkdtemp()
  203. self.addCleanup(shutil.rmtree, tmp_dir)
  204. repo = Repo.init(tmp_dir)
  205. with open(os.path.join(repo.path, '.gitignore'), 'wb') as f:
  206. f.write(b'/*\n')
  207. f.write(b'!/foo\n')
  208. os.mkdir(os.path.join(repo.path, 'foo'))
  209. with open(os.path.join(repo.path, 'foo', '.gitignore'), 'wb') as f:
  210. f.write(b'/bar\n')
  211. with open(os.path.join(repo.path, 'foo', 'bar'), 'wb') as f:
  212. f.write(b'IGNORED')
  213. m = IgnoreFilterManager.from_repo(repo)
  214. self.assertTrue(m.is_ignored('foo/bar'))
  215. def test_load_ignore_ignorecase(self):
  216. tmp_dir = tempfile.mkdtemp()
  217. self.addCleanup(shutil.rmtree, tmp_dir)
  218. repo = Repo.init(tmp_dir)
  219. config = repo.get_config()
  220. config.set(b"core", b"ignorecase", True)
  221. config.write_to_path()
  222. with open(os.path.join(repo.path, ".gitignore"), "wb") as f:
  223. f.write(b"/foo/bar\n")
  224. f.write(b"/dir\n")
  225. m = IgnoreFilterManager.from_repo(repo)
  226. self.assertTrue(m.is_ignored(os.path.join("dir", "blie")))
  227. self.assertTrue(m.is_ignored(os.path.join("DIR", "blie")))
  228. def test_ignored_contents(self):
  229. tmp_dir = tempfile.mkdtemp()
  230. self.addCleanup(shutil.rmtree, tmp_dir)
  231. repo = Repo.init(tmp_dir)
  232. with open(os.path.join(repo.path, ".gitignore"), "wb") as f:
  233. f.write(b"a/*\n")
  234. f.write(b"!a/*.txt\n")
  235. m = IgnoreFilterManager.from_repo(repo)
  236. os.mkdir(os.path.join(repo.path, "a"))
  237. self.assertIs(None, m.is_ignored("a"))
  238. self.assertIs(None, m.is_ignored("a/"))
  239. self.assertFalse(m.is_ignored("a/b.txt"))
  240. self.assertTrue(m.is_ignored("a/c.dat"))