7f18dfee1d3022bcca8c319ca087d29db5169046
[jelmer/dulwich.git] / dulwich / tests / test_ignore.py
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
21 """Tests for ignore files."""
22
23 from io import BytesIO
24 import os
25 import re
26 import shutil
27 import tempfile
28 from dulwich.tests import TestCase
29
30 from dulwich.ignore import (
31     IgnoreFilter,
32     IgnoreFilterManager,
33     IgnoreFilterStack,
34     match_pattern,
35     read_ignore_patterns,
36     translate,
37     )
38 from dulwich.repo import Repo
39
40
41 POSITIVE_MATCH_TESTS = [
42     (b"foo.c", b"*.c"),
43     (b"foo/foo.c", b"*.c"),
44     (b"foo/foo.c", b"foo.c"),
45     (b"foo.c", b"/*.c"),
46     (b"foo.c", b"/foo.c"),
47     (b"foo.c", b"foo.c"),
48     (b"foo.c", b"foo.[ch]"),
49     (b"foo/bar/bla.c", b"foo/**"),
50     (b"foo/bar/bla/blie.c", b"foo/**/blie.c"),
51     (b"foo/bar/bla.c", b"**/bla.c"),
52     (b"bla.c", b"**/bla.c"),
53     (b"foo/bar", b"foo/**/bar"),
54     (b"foo/bla/bar", b"foo/**/bar"),
55     (b"foo/bar/", b"bar/"),
56     (b"foo/bar/", b"bar"),
57 ]
58
59 NEGATIVE_MATCH_TESTS = [
60     (b"foo.c", b"foo.[dh]"),
61     (b"foo/foo.c", b"/foo.c"),
62     (b"foo/foo.c", b"/*.c"),
63     (b"foo/bar/", b"/bar/"),
64 ]
65
66
67 TRANSLATE_TESTS = [
68     (b"*.c", b'(?ms)(.*/)?[^/]+\\.c/?\\Z'),
69     (b"foo.c", b'(?ms)(.*/)?foo\\.c/?\\Z'),
70     (b"/*.c", b'(?ms)[^/]+\\.c/?\\Z'),
71     (b"/foo.c", b'(?ms)foo\\.c/?\\Z'),
72     (b"foo.c", b'(?ms)(.*/)?foo\\.c/?\\Z'),
73     (b"foo.[ch]", b'(?ms)(.*/)?foo\\.[ch]/?\\Z'),
74     (b"bar/", b'(?ms)(.*/)?bar\\/\\Z'),
75     (b"foo/**", b'(?ms)foo(/.*)?/?\\Z'),
76     (b"foo/**/blie.c", b'(?ms)foo(/.*)?\\/blie\\.c/?\\Z'),
77     (b"**/bla.c", b'(?ms)(.*/)?bla\\.c/?\\Z'),
78     (b"foo/**/bar", b'(?ms)foo(/.*)?\\/bar/?\\Z'),
79 ]
80
81
82 class TranslateTests(TestCase):
83
84     def test_translate(self):
85         for (pattern, regex) in TRANSLATE_TESTS:
86             if re.escape(b'/') == b'/':
87                 # Slash is no longer escaped in Python3.7, so undo the escaping
88                 # in the expected return value..
89                 regex = regex.replace(b'\\/', b'/')
90             self.assertEqual(
91                 regex, translate(pattern),
92                 "orig pattern: %r, regex: %r, expected: %r" %
93                 (pattern, translate(pattern), regex))
94
95
96 class ReadIgnorePatterns(TestCase):
97
98     def test_read_file(self):
99         f = BytesIO(b"""
100 # a comment
101
102 # and an empty line:
103
104 \#not a comment
105 !negative
106 with trailing whitespace 
107 with escaped trailing whitespace\ 
108 """)
109         self.assertEqual(list(read_ignore_patterns(f)), [
110             b'\\#not a comment',
111             b'!negative',
112             b'with trailing whitespace',
113             b'with escaped trailing whitespace '
114         ])
115
116
117 class MatchPatternTests(TestCase):
118
119     def test_matches(self):
120         for (path, pattern) in POSITIVE_MATCH_TESTS:
121             self.assertTrue(
122                 match_pattern(path, pattern),
123                 "path: %r, pattern: %r" % (path, pattern))
124
125     def test_no_matches(self):
126         for (path, pattern) in NEGATIVE_MATCH_TESTS:
127             self.assertFalse(
128                 match_pattern(path, pattern),
129                 "path: %r, pattern: %r" % (path, pattern))
130
131
132 class IgnoreFilterTests(TestCase):
133
134     def test_included(self):
135         filter = IgnoreFilter([b'a.c', b'b.c'])
136         self.assertTrue(filter.is_ignored(b'a.c'))
137         self.assertIs(None, filter.is_ignored(b'c.c'))
138
139     def test_excluded(self):
140         filter = IgnoreFilter([b'a.c', b'b.c', b'!c.c'])
141         self.assertFalse(filter.is_ignored(b'c.c'))
142         self.assertIs(None, filter.is_ignored(b'd.c'))
143
144     def test_include_exclude_include(self):
145         filter = IgnoreFilter([b'a.c', b'!a.c', b'a.c'])
146         self.assertTrue(filter.is_ignored(b'a.c'))
147
148
149 class IgnoreFilterStackTests(TestCase):
150
151     def test_stack_first(self):
152         filter1 = IgnoreFilter([b'[a].c', b'[b].c', b'![d].c'])
153         filter2 = IgnoreFilter([b'[a].c', b'![b],c', b'[c].c', b'[d].c'])
154         stack = IgnoreFilterStack([filter1, filter2])
155         self.assertIs(True, stack.is_ignored(b'a.c'))
156         self.assertIs(True, stack.is_ignored(b'b.c'))
157         self.assertIs(True, stack.is_ignored(b'c.c'))
158         self.assertIs(False, stack.is_ignored(b'd.c'))
159         self.assertIs(None, stack.is_ignored(b'e.c'))
160
161
162 class IgnoreFilterManagerTests(TestCase):
163
164     def test_load_ignore(self):
165         tmp_dir = tempfile.mkdtemp()
166         self.addCleanup(shutil.rmtree, tmp_dir)
167         repo = Repo.init(tmp_dir)
168         with open(os.path.join(repo.path, '.gitignore'), 'w') as f:
169             f.write('/foo/bar\n')
170         os.mkdir(os.path.join(repo.path, 'dir'))
171         with open(os.path.join(repo.path, 'dir', '.gitignore'), 'w') as f:
172             f.write('/blie\n')
173         with open(os.path.join(repo.path, 'dir', 'blie'), 'w') as f:
174             f.write('IGNORED')
175         with open(os.path.join(repo.controldir(), 'info', 'exclude'), 'w') as f:
176             f.write('/excluded\n')
177         m = IgnoreFilterManager.from_repo(repo)
178         self.assertTrue(m.is_ignored(os.path.join(repo.path, 'dir', 'blie')))
179         self.assertIs(None, m.is_ignored(os.path.join(repo.path, 'dir', 'bloe')))
180         self.assertIs(None, m.is_ignored(os.path.join(repo.path, 'dir')))
181         self.assertTrue(m.is_ignored(os.path.join(repo.path, 'foo', 'bar')))
182         self.assertTrue(m.is_ignored(os.path.join(repo.path, 'excluded')))