# test_ignore.py -- Tests for ignore files. # Copyright (C) 2017 Jelmer Vernooij # # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU # General Public License as public by the Free Software Foundation; version 2.0 # or (at your option) any later version. You can redistribute it and/or # modify it under the terms of either of these two licenses. # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # You should have received a copy of the licenses; if not, see # for a copy of the GNU General Public License # and for a copy of the Apache # License, Version 2.0. # """Tests for ignore files.""" import os import re import shutil import tempfile from io import BytesIO from dulwich.ignore import ( IgnoreFilter, IgnoreFilterManager, IgnoreFilterStack, Pattern, match_pattern, read_ignore_patterns, translate, ) from dulwich.repo import Repo from . import TestCase POSITIVE_MATCH_TESTS = [ (b"foo.c", b"*.c"), (b".c", b"*.c"), (b"foo/foo.c", b"*.c"), (b"foo/foo.c", b"foo.c"), (b"foo.c", b"/*.c"), (b"foo.c", b"/foo.c"), (b"foo.c", b"foo.c"), (b"foo.c", b"foo.[ch]"), (b"foo/bar/bla.c", b"foo/**"), (b"foo/bar/bla/blie.c", b"foo/**/blie.c"), (b"foo/bar/bla.c", b"**/bla.c"), (b"bla.c", b"**/bla.c"), (b"foo/bar", b"foo/**/bar"), (b"foo/bla/bar", b"foo/**/bar"), (b"foo/bar/", b"bar/"), (b"foo/bar/", b"bar"), (b"foo/bar/something", b"foo/bar/*"), ] NEGATIVE_MATCH_TESTS = [ (b"foo.c", b"foo.[dh]"), (b"foo/foo.c", b"/foo.c"), (b"foo/foo.c", b"/*.c"), (b"foo/bar/", b"/bar/"), (b"foo/bar/", b"foo/bar/*"), (b"foo/bar", b"foo?bar"), ] TRANSLATE_TESTS = [ (b"*.c", b"(?ms)(.*/)?[^/]*\\.c/?\\Z"), (b"foo.c", b"(?ms)(.*/)?foo\\.c/?\\Z"), (b"/*.c", b"(?ms)[^/]*\\.c/?\\Z"), (b"/foo.c", b"(?ms)foo\\.c/?\\Z"), (b"foo.c", b"(?ms)(.*/)?foo\\.c/?\\Z"), (b"foo.[ch]", b"(?ms)(.*/)?foo\\.[ch]/?\\Z"), (b"bar/", b"(?ms)(.*/)?bar\\/\\Z"), (b"foo/**", b"(?ms)foo(/.*)?/?\\Z"), (b"foo/**/blie.c", b"(?ms)foo(/.*)?\\/blie\\.c/?\\Z"), (b"**/bla.c", b"(?ms)(.*/)?bla\\.c/?\\Z"), (b"foo/**/bar", b"(?ms)foo(/.*)?\\/bar/?\\Z"), (b"foo/bar/*", b"(?ms)foo\\/bar\\/[^/]+/?\\Z"), (b"/foo\\[bar\\]", b"(?ms)foo\\[bar\\]/?\\Z"), (b"/foo[bar]", b"(?ms)foo[bar]/?\\Z"), (b"/foo[0-9]", b"(?ms)foo[0-9]/?\\Z"), ] class TranslateTests(TestCase): def test_translate(self) -> None: for pattern, regex in TRANSLATE_TESTS: if re.escape(b"/") == b"/": # Slash is no longer escaped in Python3.7, so undo the escaping # in the expected return value.. regex = regex.replace(b"\\/", b"/") self.assertEqual( regex, translate(pattern), f"orig pattern: {pattern!r}, regex: {translate(pattern)!r}, expected: {regex!r}", ) class ReadIgnorePatterns(TestCase): def test_read_file(self) -> None: f = BytesIO( b""" # a comment \x20\x20 # and an empty line: \\#not a comment !negative with trailing whitespace with escaped trailing whitespace\\ """ ) self.assertEqual( list(read_ignore_patterns(f)), [ b"\\#not a comment", b"!negative", b"with trailing whitespace", b"with escaped trailing whitespace ", ], ) class MatchPatternTests(TestCase): def test_matches(self) -> None: for path, pattern in POSITIVE_MATCH_TESTS: self.assertTrue( match_pattern(path, pattern), f"path: {path!r}, pattern: {pattern!r}", ) def test_no_matches(self) -> None: for path, pattern in NEGATIVE_MATCH_TESTS: self.assertFalse( match_pattern(path, pattern), f"path: {path!r}, pattern: {pattern!r}", ) class IgnoreFilterTests(TestCase): def test_included(self) -> None: filter = IgnoreFilter([b"a.c", b"b.c"]) self.assertTrue(filter.is_ignored(b"a.c")) self.assertIs(None, filter.is_ignored(b"c.c")) self.assertEqual([Pattern(b"a.c")], list(filter.find_matching(b"a.c"))) self.assertEqual([], list(filter.find_matching(b"c.c"))) def test_included_ignorecase(self) -> None: filter = IgnoreFilter([b"a.c", b"b.c"], ignorecase=False) self.assertTrue(filter.is_ignored(b"a.c")) self.assertFalse(filter.is_ignored(b"A.c")) filter = IgnoreFilter([b"a.c", b"b.c"], ignorecase=True) self.assertTrue(filter.is_ignored(b"a.c")) self.assertTrue(filter.is_ignored(b"A.c")) self.assertTrue(filter.is_ignored(b"A.C")) def test_excluded(self) -> None: filter = IgnoreFilter([b"a.c", b"b.c", b"!c.c"]) self.assertFalse(filter.is_ignored(b"c.c")) self.assertIs(None, filter.is_ignored(b"d.c")) self.assertEqual([Pattern(b"!c.c")], list(filter.find_matching(b"c.c"))) self.assertEqual([], list(filter.find_matching(b"d.c"))) def test_include_exclude_include(self) -> None: filter = IgnoreFilter([b"a.c", b"!a.c", b"a.c"]) self.assertTrue(filter.is_ignored(b"a.c")) self.assertEqual( [Pattern(b"a.c"), Pattern(b"!a.c"), Pattern(b"a.c")], list(filter.find_matching(b"a.c")), ) def test_manpage(self) -> None: # A specific example from the gitignore manpage filter = IgnoreFilter([b"/*", b"!/foo", b"/foo/*", b"!/foo/bar"]) self.assertTrue(filter.is_ignored(b"a.c")) self.assertTrue(filter.is_ignored(b"foo/blie")) self.assertFalse(filter.is_ignored(b"foo")) self.assertFalse(filter.is_ignored(b"foo/bar")) self.assertFalse(filter.is_ignored(b"foo/bar/")) self.assertFalse(filter.is_ignored(b"foo/bar/bloe")) def test_regex_special(self) -> None: # See https://github.com/dulwich/dulwich/issues/930#issuecomment-1026166429 filter = IgnoreFilter([b"/foo\\[bar\\]", b"/foo"]) self.assertTrue(filter.is_ignored("foo")) self.assertTrue(filter.is_ignored("foo[bar]")) class IgnoreFilterStackTests(TestCase): def test_stack_first(self) -> None: filter1 = IgnoreFilter([b"[a].c", b"[b].c", b"![d].c"]) filter2 = IgnoreFilter([b"[a].c", b"![b],c", b"[c].c", b"[d].c"]) stack = IgnoreFilterStack([filter1, filter2]) self.assertIs(True, stack.is_ignored(b"a.c")) self.assertIs(True, stack.is_ignored(b"b.c")) self.assertIs(True, stack.is_ignored(b"c.c")) self.assertIs(False, stack.is_ignored(b"d.c")) self.assertIs(None, stack.is_ignored(b"e.c")) class IgnoreFilterManagerTests(TestCase): def test_load_ignore(self) -> None: tmp_dir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, tmp_dir) repo = Repo.init(tmp_dir) with open(os.path.join(repo.path, ".gitignore"), "wb") as f: f.write(b"/foo/bar\n") f.write(b"/dir2\n") f.write(b"/dir3/\n") os.mkdir(os.path.join(repo.path, "dir")) with open(os.path.join(repo.path, "dir", ".gitignore"), "wb") as f: f.write(b"/blie\n") with open(os.path.join(repo.path, "dir", "blie"), "wb") as f: f.write(b"IGNORED") p = os.path.join(repo.controldir(), "info", "exclude") with open(p, "wb") as f: f.write(b"/excluded\n") m = IgnoreFilterManager.from_repo(repo) self.assertTrue(m.is_ignored("dir/blie")) self.assertIs(None, m.is_ignored(os.path.join("dir", "bloe"))) self.assertIs(None, m.is_ignored("dir")) self.assertTrue(m.is_ignored(os.path.join("foo", "bar"))) self.assertTrue(m.is_ignored(os.path.join("excluded"))) self.assertTrue(m.is_ignored(os.path.join("dir2", "fileinignoreddir"))) self.assertFalse(m.is_ignored("dir3")) self.assertTrue(m.is_ignored("dir3/")) self.assertTrue(m.is_ignored("dir3/bla")) def test_nested_gitignores(self) -> None: tmp_dir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, tmp_dir) repo = Repo.init(tmp_dir) with open(os.path.join(repo.path, ".gitignore"), "wb") as f: f.write(b"/*\n") f.write(b"!/foo\n") os.mkdir(os.path.join(repo.path, "foo")) with open(os.path.join(repo.path, "foo", ".gitignore"), "wb") as f: f.write(b"/bar\n") with open(os.path.join(repo.path, "foo", "bar"), "wb") as f: f.write(b"IGNORED") m = IgnoreFilterManager.from_repo(repo) self.assertTrue(m.is_ignored("foo/bar")) def test_load_ignore_ignorecase(self) -> None: tmp_dir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, tmp_dir) repo = Repo.init(tmp_dir) config = repo.get_config() config.set(b"core", b"ignorecase", True) config.write_to_path() with open(os.path.join(repo.path, ".gitignore"), "wb") as f: f.write(b"/foo/bar\n") f.write(b"/dir\n") m = IgnoreFilterManager.from_repo(repo) self.assertTrue(m.is_ignored(os.path.join("dir", "blie"))) self.assertTrue(m.is_ignored(os.path.join("DIR", "blie"))) def test_ignored_contents(self) -> None: tmp_dir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, tmp_dir) repo = Repo.init(tmp_dir) with open(os.path.join(repo.path, ".gitignore"), "wb") as f: f.write(b"a/*\n") f.write(b"!a/*.txt\n") m = IgnoreFilterManager.from_repo(repo) os.mkdir(os.path.join(repo.path, "a")) self.assertIs(None, m.is_ignored("a")) self.assertIs(None, m.is_ignored("a/")) self.assertFalse(m.is_ignored("a/b.txt")) self.assertTrue(m.is_ignored("a/c.dat"))