diff --git a/docker/utils/fnmatch.py b/docker/utils/fnmatch.py index e95b63ce..e51bd815 100644 --- a/docker/utils/fnmatch.py +++ b/docker/utils/fnmatch.py @@ -65,19 +65,32 @@ def translate(pat): There is no way to quote meta-characters. """ - recursive_mode = False i, n = 0, len(pat) - res = '' + res = '^' while i < n: c = pat[i] i = i + 1 if c == '*': if i < n and pat[i] == '*': - recursive_mode = True + # is some flavor of "**" i = i + 1 - res = res + '.*' + # Treat **/ as ** so eat the "/" + if pat[i] == '/': + i = i + 1 + if i >= n: + # is "**EOF" - to align with .gitignore just accept all + res = res + '.*' + else: + # is "**" + # Note that this allows for any # of /'s (even 0) because + # the .* will eat everything, even /'s + res = res + '(.*/)?' + else: + # is "*" so map it to anything but "/" + res = res + '[^/]*' elif c == '?': - res = res + '.' + # "?" is any char except "/" + res = res + '[^/]' elif c == '[': j = i if j < n and pat[j] == '!': @@ -96,8 +109,6 @@ def translate(pat): elif stuff[0] == '^': stuff = '\\' + stuff res = '%s[%s]' % (res, stuff) - elif recursive_mode and c == '/': - res = res + re.escape(c) + '?' else: res = res + re.escape(c) - return res + '\Z(?ms)' + return res + '$' diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py index a2d463d7..7045d23c 100644 --- a/tests/unit/utils_test.py +++ b/tests/unit/utils_test.py @@ -639,6 +639,14 @@ class ExcludePathsTest(unittest.TestCase): 'foo', 'foo/bar', 'bar', + 'target', + 'target/subdir', + 'subdir', + 'subdir/target', + 'subdir/target/subdir', + 'subdir/subdir2', + 'subdir/subdir2/target', + 'subdir/subdir2/target/subdir' ] files = [ @@ -654,6 +662,14 @@ class ExcludePathsTest(unittest.TestCase): 'foo/bar/a.py', 'bar/a.py', 'foo/Dockerfile3', + 'target/file.txt', + 'target/subdir/file.txt', + 'subdir/file.txt', + 'subdir/target/file.txt', + 'subdir/target/subdir/file.txt', + 'subdir/subdir2/file.txt', + 'subdir/subdir2/target/file.txt', + 'subdir/subdir2/target/subdir/file.txt', ] all_paths = set(dirs + files) @@ -844,6 +860,15 @@ class ExcludePathsTest(unittest.TestCase): self.all_paths - set(['foo/bar', 'foo/bar/a.py']) ) + def test_single_and_double_wildcard(self): + assert self.exclude(['**/target/*/*']) == convert_paths( + self.all_paths - set( + ['target/subdir/file.txt', + 'subdir/target/subdir/file.txt', + 'subdir/subdir2/target/subdir/file.txt'] + ) + ) + class TarTest(unittest.TestCase): def test_tar_with_excludes(self):