Skip to content

Commit

Permalink
Merge pull request #8 from mikexstudios/edge-cases
Browse files Browse the repository at this point in the history
Fixes for handling .gitignore edge cases in GitIgnorePattern.
  • Loading branch information
cpburnz committed May 16, 2015
2 parents 43d970f + ec6ceb4 commit a52ccec
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 14 deletions.
34 changes: 28 additions & 6 deletions pathspec/gitignore.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ def __init__(self, pattern):
regex = None
include = None

elif pattern == '/':
# EDGE CASE: According to git check-ignore (v2.4.1)), a single '/'
# does not match any file.
regex = None
include = None

elif pattern:

if pattern.startswith('!'):
Expand Down Expand Up @@ -68,15 +74,24 @@ def __init__(self, pattern):
# paths. So, remove empty first segment to make pattern relative
# to root.
del pattern_segs[0]
else:
# A pattern without a beginning slash ('/') will match any
# descendant path. This is equivilent to "**/{pattern}". So,
# prepend with double-asterisks to make pattern relative to
# root.
elif len(pattern_segs) == 1 or \
(len(pattern_segs) == 2 and not pattern_segs[1]):
# A **single** pattern without a beginning slash ('/') will
# match any descendant path. This is equivalent to
# "**/{pattern}". So, prepend with double-asterisks to make
# pattern relative to root.
# EDGE CASE: This also holds for a single pattern with a
# trailing slash (e.g. dir/).
if pattern_segs[0] != '**':
pattern_segs.insert(0, '**')
else:
# EDGE CASE: A pattern without a beginning slash ('/') but
# contains at least one prepended directory (e.g.
# "dir/{pattern}") should not match "**/dir/{pattern}",
# according to `git check-ignore` (v2.4.1).
pass

if not pattern_segs[-1]:
if not pattern_segs[-1] and len(pattern_segs) > 1:
# A pattern ending with a slash ('/') will match all descendant
# paths if it is a directory but not if it is a regular file.
# This is equivilent to "{pattern}/**". So, set last segment to
Expand Down Expand Up @@ -118,6 +133,13 @@ def __init__(self, pattern):
if need_slash:
regex.append('/')
regex.append(self._translate_segment_glob(seg))
if i == end and include == True:
# A pattern ending without a slash ('/') will match a file
# or a directory (with paths underneath it).
# e.g. foo matches: foo, foo/bar, foo/bar/baz, etc.
# EDGE CASE: However, this does not hold for exclusion cases
# according to `git check-ignore` (v2.4.1).
regex.append('(?:/.*)?')
need_slash = True
regex.append('$')
regex = ''.join(regex)
Expand Down
104 changes: 96 additions & 8 deletions pathspec/tests/test_gitignore.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,60 @@ def test_00_empty(self):
self.assertIsNone(spec.include)
self.assertIsNone(spec.regex)

def test_01_absolute_root(self):
"""
Tests a single root absolute path pattern.
This should NOT match any file (according to git check-ignore (v2.4.1)).
"""
spec = GitIgnorePattern('/')
self.assertIsNone(spec.include)
self.assertIsNone(spec.regex)

def test_01_absolute(self):
"""
Tests an absolute path pattern.
This should match:
an/absolute/file/path
an/absolute/file/path/foo
This should NOT match:
foo/an/absolute/file/path
"""
spec = GitIgnorePattern('/an/absolute/file/path')
self.assertTrue(spec.include)
self.assertEquals(spec.regex.pattern, '^an/absolute/file/path$')
self.assertEquals(spec.regex.pattern, '^an/absolute/file/path(?:/.*)?$')

def test_01_relative(self):
"""
Tests a relative path pattern.
This should match:
spam
spam/
foo/spam
spam/foo
foo/spam/bar
"""
spec = GitIgnorePattern('spam')
self.assertTrue(spec.include)
self.assertEquals(spec.regex.pattern, '^(?:.+/)?spam$')
self.assertEquals(spec.regex.pattern, '^(?:.+/)?spam(?:/.*)?$')

def test_01_relative_nested(self):
"""
Tests a relative nested path pattern.
This should match:
foo/spam
foo/spam/bar
This should **not** match (according to git check-ignore (v2.4.1)):
bar/foo/spam
"""
spec = GitIgnorePattern('foo/spam')
self.assertTrue(spec.include)
self.assertEquals(spec.regex.pattern, '^foo/spam(?:/.*)?$')

def test_02_comment(self):
"""
Expand All @@ -49,6 +88,9 @@ def test_02_comment(self):
def test_02_ignore(self):
"""
Tests an exclude pattern.
This should NOT match (according to git check-ignore (v2.4.1)):
temp/foo
"""
spec = GitIgnorePattern('!temp')
self.assertIsNotNone(spec.include)
Expand All @@ -59,18 +101,32 @@ def test_03_child_double_asterisk(self):
"""
Tests a directory name with a double-asterisk child
directory.
This should match:
spam/bar
This should **not** match (according to git check-ignore (v2.4.1)):
foo/spam/bar
"""
spec = GitIgnorePattern('spam/**')
self.assertTrue(spec.include)
self.assertEquals(spec.regex.pattern, '^(?:.+/)?spam/.*$')
self.assertEquals(spec.regex.pattern, '^spam/.*$')

def test_03_inner_double_asterisk(self):
"""
Tests a path with an inner double-asterisk directory.
This should match:
left/bar/right
left/foo/bar/right
left/bar/right/foo
This should **not** match (according to git check-ignore (v2.4.1)):
foo/left/bar/right
"""
spec = GitIgnorePattern('left/**/right')
self.assertTrue(spec.include)
self.assertEquals(spec.regex.pattern, '^(?:.+/)?left(?:/.+)?/right$')
self.assertEquals(spec.regex.pattern, '^left(?:/.+)?/right(?:/.*)?$')

def test_03_only_double_asterisk(self):
"""
Expand All @@ -83,38 +139,70 @@ def test_03_only_double_asterisk(self):
def test_03_parent_double_asterisk(self):
"""
Tests a file name with a double-asterisk parent directory.
This should match:
foo/spam
foo/spam/bar
"""
spec = GitIgnorePattern('**/spam')
self.assertTrue(spec.include)
self.assertEquals(spec.regex.pattern, '^(?:.+/)?spam$')
self.assertEquals(spec.regex.pattern, '^(?:.+/)?spam(?:/.*)?$')

def test_04_infix_wildcard(self):
"""
Tests a pattern with an infix wildcard.
This should match:
foo--bar
foo-hello-bar
a/foo-hello-bar
foo-hello-bar/b
a/foo-hello-bar/b
"""
spec = GitIgnorePattern('foo-*-bar')
self.assertTrue(spec.include)
self.assertEquals(spec.regex.pattern, '^(?:.+/)?foo\\-[^/]*\\-bar$')
self.assertEquals(spec.regex.pattern, '^(?:.+/)?foo\\-[^/]*\\-bar(?:/.*)?$')

def test_04_postfix_wildcard(self):
"""
Tests a pattern with a postfix wildcard.
This should match:
~temp-
~temp-foo
~temp-foo/bar
foo/~temp-bar
foo/~temp-bar/baz
"""
spec = GitIgnorePattern('~temp-*')
self.assertTrue(spec.include)
self.assertEquals(spec.regex.pattern, '^(?:.+/)?\\~temp\\-[^/]*$')
self.assertEquals(spec.regex.pattern, '^(?:.+/)?\\~temp\\-[^/]*(?:/.*)?$')

def test_04_prefix_wildcard(self):
"""
Tests a pattern with a prefix wildcard.
This should match:
bar.py
bar.py/
foo/bar.py
foo/bar.py/baz
"""
spec = GitIgnorePattern('*.py')
self.assertTrue(spec.include)
self.assertEquals(spec.regex.pattern, '^(?:.+/)?[^/]*\\.py$')
self.assertEquals(spec.regex.pattern, '^(?:.+/)?[^/]*\\.py(?:/.*)?$')

def test_05_directory(self):
"""
Tests a directory pattern.
This should match:
dir/
foo/dir/
foo/dir/bar
This should **not** match:
dir
"""
spec = GitIgnorePattern('dir/')
self.assertTrue(spec.include)
Expand Down

0 comments on commit a52ccec

Please sign in to comment.