Răsfoiți Sursa

Initial support for ignores.

Jelmer Vernooij 7 ani în urmă
părinte
comite
f575fffcf3
4 a modificat fișierele cu 226 adăugiri și 0 ștergeri
  1. 8 0
      NEWS
  2. 115 0
      dulwich/ignore.py
  3. 1 0
      dulwich/tests/__init__.py
  4. 102 0
      dulwich/tests/test_ignore.py

+ 8 - 0
NEWS

@@ -14,6 +14,14 @@
    add from current working directory rather than repository root.
    (Jelmer Vernooij, #521)
 
+ IMPROVEMENTS
+
+  * Add basic support for reading ignore files in ``dulwich.ignore``.
+    Note that this is not yet hooked into the other parts of Dulwich,
+    and not all gitignore patterns are currently supported. See
+    the TODOs at the top of dulwich/ignore.py.
+    (Jelmer Vernooij, #524)
+
  DOCUMENTATION
 
   * Clarified docstrings for Client.{send_pack,fetch_pack} implementations.

+ 115 - 0
dulwich/ignore.py

@@ -0,0 +1,115 @@
+# Copyright (C) 2017 Jelmer Vernooij <jelmer@jelmer.uk>
+#
+# 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
+# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
+# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
+# License, Version 2.0.
+#
+
+"""Parsing of gitignore files."""
+
+# TODO(jelmer): Handle ignore files in subdirectories
+# TODO(jelmer): Handle ** in patterns
+# TODO(jelmer): Compile patterns
+
+import fnmatch
+import posixpath
+
+
+def read_ignore_patterns(f):
+    """Read a git ignore file.
+
+    :param f: File-like object to read from
+    :return: List of patterns
+    """
+
+    for l in f:
+        l = l.rstrip(b"\n")
+
+        # Ignore blank lines, they're used for readability.
+        if not l:
+            continue
+
+        if l[0:1] == b'#':
+            # Comment
+            continue
+
+        # Trailing spaces are ignored unless they are quoted with a backslash.
+        while l.endswith(b' ') and not l.endswith(b'\\ '):
+            l = l[:-1]
+        l = l.replace(b'\\ ', b' ')
+
+        yield l
+
+
+def match_pattern(path, pattern):
+    """Match a gitignore-style pattern against a path.
+
+    :param path: Path to match
+    :param pattern: Pattern to match
+    :return: bool indicating whether the pattern matched
+    """
+    if b'/' not in pattern:
+        return fnmatch.fnmatch(posixpath.basename(path), pattern)
+    pattern = pattern.lstrip(b'/')
+    return fnmatch.fnmatch(path, pattern)
+
+
+class IgnoreFilter(object):
+
+    def __init__(self, patterns):
+        self._patterns = []
+        for pattern in patterns:
+            self.append_pattern(pattern)
+
+    def append_pattern(self, pattern):
+        """Add a pattern to the set."""
+        self._patterns.append(pattern)
+
+    def is_ignored(self, path):
+        """Check whether a path is ignored.
+
+        For directories, include a trailing slash.
+
+        :return: None if file is not mentioned, True if it is included, False
+            if it is explicitly excluded.
+        """
+        status = None
+        for pattern in self._patterns:
+            if pattern[0:1] == b'!':
+                if match_pattern(pattern[1:], path):
+                    # Explicitly excluded.
+                    return False
+            else:
+                if pattern[0:1] == b'\\':
+                    pattern = pattern[1:]
+                if match_pattern(pattern, path):
+                    status = True
+        return status
+
+
+class IgnoreFilterStack(object):
+    """Check for ignore status in multiple filters."""
+
+    def __init__(self, filters):
+        self._filters = filters
+
+    def is_ignored(self, path):
+        """Check whether a path is explicitly included or excluded in ignores."""
+        status = None
+        for filter in self._filters:
+            status = filter.is_ignored(path)
+            if status is not None:
+                return status
+        return status

+ 1 - 0
dulwich/tests/__init__.py

@@ -101,6 +101,7 @@ def self_test_suite():
         'grafts',
         'greenthreads',
         'hooks',
+        'ignore',
         'index',
         'lru_cache',
         'objects',

+ 102 - 0
dulwich/tests/test_ignore.py

@@ -0,0 +1,102 @@
+# test_ignore.py -- Tests for ignore files.
+# Copyright (C) 2017 Jelmer Vernooij <jelmer@jelmer.uk>
+#
+# 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
+# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
+# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
+# License, Version 2.0.
+#
+
+"""Tests for ignore files."""
+
+from io import BytesIO
+import unittest
+
+from dulwich.ignore import (
+    IgnoreFilter,
+    match_pattern,
+    read_ignore_patterns,
+    )
+
+
+POSITIVE_MATCH_TESTS = [
+    ("foo.c", "*.c"),
+    ("foo/foo.c", "*.c"),
+    ("foo/foo.c", "foo.c"),
+    ("foo.c", "/*.c"),
+    ("foo.c", "/foo.c"),
+    ("foo.c", "foo.c"),
+    ("foo.c", "foo.[ch]"),
+    ("foo/bar/bla.c", "foo/**"),
+    ("foo/bar/bla/blie.c", "foo/**/blie.c"),
+    ("foo/bar/bla.c", "**/bla.c"),
+]
+
+NEGATIVE_MATCH_TESTS = [
+    ("foo.c", "foo.[dh]"),
+    ("foo/foo.c", "/foo.c"),
+]
+
+
+KNOWNFAIL_NEGATIVE_MATCH_TESTS = [
+    ("foo/foo.c", "/*.c"),
+    ]
+
+
+class ReadIgnorePatterns(unittest.TestCase):
+
+    def test_read_file(self):
+        f = BytesIO(b"""
+# a comment
+
+# and an empty line:
+
+\#not a comment
+!negative
+with trailing whitespace 
+with escaped trailing whitespace\ 
+""")
+        self.assertEqual(list(read_ignore_patterns(f)), [
+            '\\#not a comment',
+            '!negative',
+            'with trailing whitespace',
+            'with escaped trailing whitespace '
+        ])
+
+
+class MatchPatternTests(unittest.TestCase):
+
+    def test_matches(self):
+        for (path, pattern) in POSITIVE_MATCH_TESTS:
+            self.assertTrue(
+                match_pattern(path, pattern),
+                "path: %r, pattern: %r" % (path, pattern))
+
+    def test_no_matches(self):
+        for (path, pattern) in NEGATIVE_MATCH_TESTS:
+            self.assertFalse(
+                match_pattern(path, pattern),
+                "path: %r, pattern: %r" % (path, pattern))
+
+
+class IgnoreFilterTests(unittest.TestCase):
+
+    def test_included(self):
+        filter = IgnoreFilter(['a.c', 'b.c'])
+        self.assertTrue(filter.is_ignored('a.c'))
+        self.assertIs(None, filter.is_ignored('c.c'))
+
+    def test_excluded(self):
+        filter = IgnoreFilter(['a.c', 'b.c', '!c.c'])
+        self.assertFalse(filter.is_ignored('c.c'))