Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes for handling .gitignore edge cases in GitIgnorePattern. #8

Merged
merged 1 commit into from
May 16, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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