Add IgnoreFilterManager.find_matching and IgnoreFilter.find_matching, to find actual...
authorJelmer Vernooij <jelmer@jelmer.uk>
Sat, 15 Jul 2017 14:59:30 +0000 (14:59 +0000)
committerJelmer Vernooij <jelmer@jelmer.uk>
Sat, 15 Jul 2017 15:04:25 +0000 (15:04 +0000)
dulwich/ignore.py
dulwich/tests/test_ignore.py

index a093ee4790f7fc5f71a90dca5570d17b4eb9e87e..1e789b3c473a422d9f6344344faf47f282769ff0 100644 (file)
@@ -127,6 +127,29 @@ def match_pattern(path, pattern):
     return re.match(re_pattern, path)
 
 
+class Pattern(object):
+
+    def __init__(self, pattern):
+        self.pattern = pattern
+        if pattern[0:1] == b'!':
+            self.is_exclude = False
+            pattern = pattern[1:]
+        else:
+            if pattern[0:1] == b'\\':
+                pattern = pattern[1:]
+            self.is_exclude = True
+        self._re = re.compile(translate(pattern))
+
+    def __bytes__(self):
+        return self.pattern
+
+    def __eq__(self, other):
+        return (type(self) == type(other) and self.pattern == other.pattern)
+
+    def match(self, path):
+        return self._re.match(path)
+
+
 class IgnoreFilter(object):
 
     def __init__(self, patterns):
@@ -136,16 +159,19 @@ class IgnoreFilter(object):
 
     def append_pattern(self, pattern):
         """Add a pattern to the set."""
-        pattern_str = pattern
-        if pattern[0:1] == b'!':
-            is_exclude = False
-            pattern = pattern[1:]
-        else:
-            if pattern[0:1] == b'\\':
-                pattern = pattern[1:]
-            is_exclude = True
-        self._patterns.append(
-            (is_exclude, re.compile(translate(pattern)), pattern_str))
+        self._patterns.append(Pattern(pattern))
+
+    def find_matching(self, path):
+        """Yield all matching patterns for path.
+
+        :param path: Path to match
+        :return: Iterator over  iterators
+        """
+        if not isinstance(path, bytes):
+            path = path.encode(sys.getfilesystemencoding())
+        for pattern in self._patterns:
+            if pattern.match(path):
+                yield pattern
 
     def is_ignored(self, path):
         """Check whether a path is ignored.
@@ -155,13 +181,9 @@ class IgnoreFilter(object):
         :return: status is None if file is not mentioned, True if it is
             included, False if it is explicitly excluded.
         """
-        if not isinstance(path, bytes):
-            path = path.encode(sys.getfilesystemencoding())
         status = None
-        matched = None
-        for (is_exclude, compiled, pattern_str) in self._patterns:
-            if compiled.match(path):
-                status = is_exclude
+        for pattern in self.find_matching(path):
+            status = pattern.is_exclude
         return status
 
     @classmethod
@@ -242,12 +264,13 @@ class IgnoreFilterManager(object):
             self._path_filters[path] = None
         return self._path_filters[path]
 
-    def is_ignored(self, path):
-        """Check whether a path is explicitly included or excluded in ignores.
+    def find_matching(self, path):
+        """Find matching patterns for path.
+
+        Stops after the first ignore file with matches.
 
         :param path: Path to check
-        :return: None if the file is not mentioned, True if it is included,
-            False if it is explicitly excluded.
+        :return: Iterator over Pattern instances
         """
         if os.path.isabs(path):
             path = os.path.relpath(path, self._top_path)
@@ -257,12 +280,24 @@ class IgnoreFilterManager(object):
             dirname = '/'.join(parts[:i])
             for s, f in filters:
                 relpath = '/'.join(parts[s:i])
-                status = f.is_ignored(relpath)
-                if status is not None:
-                    return status
+                matches = list(f.find_matching(relpath))
+                if matches:
+                    return iter(matches)
             ignore_filter = self._load_path(dirname)
             if ignore_filter is not None:
                 filters.insert(0, (i, ignore_filter))
+        return iter([])
+
+    def is_ignored(self, path):
+        """Check whether a path is explicitly included or excluded in ignores.
+
+        :param path: Path to check
+        :return: None if the file is not mentioned, True if it is included,
+            False if it is explicitly excluded.
+        """
+        matches = list(self.find_matching(path))
+        if matches:
+            return matches[-1].is_exclude
         return None
 
     @classmethod
index 34f76992d68aa1f414ceac4b4077bbfd4247547f..57531d1ff8b8e8d5c2f7e818037101cb3b857a4c 100644 (file)
@@ -31,6 +31,7 @@ from dulwich.ignore import (
     IgnoreFilter,
     IgnoreFilterManager,
     IgnoreFilterStack,
+    Pattern,
     match_pattern,
     read_ignore_patterns,
     translate,
@@ -135,15 +136,28 @@ class IgnoreFilterTests(TestCase):
         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_excluded(self):
         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):
         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')))
 
 
 class IgnoreFilterStackTests(TestCase):