Skip to content

Commit 9deffc4

Browse files
committed
Merge pull request #863 from thomasboyt/fast-exclude-paths
Don't descend into ignored directories when building context
2 parents 1dae8ef + a49166a commit 9deffc4

File tree

3 files changed

+62
-27
lines changed

3 files changed

+62
-27
lines changed

docker/utils/utils.py

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -108,38 +108,68 @@ def exclude_paths(root, patterns, dockerfile=None):
108108

109109
exclude_patterns = list(set(patterns) - set(exceptions))
110110

111-
all_paths = get_paths(root)
112-
113-
# Remove all paths that are matched by any exclusion pattern
114-
paths = [
115-
p for p in all_paths
116-
if not any(match_path(p, pattern) for pattern in exclude_patterns)
117-
]
118-
119-
# Add back the set of paths that are matched by any inclusion pattern.
120-
# Include parent dirs - if we add back 'foo/bar', add 'foo' as well
121-
for p in all_paths:
122-
if any(match_path(p, pattern) for pattern in include_patterns):
123-
components = p.split('/')
124-
paths += [
125-
'/'.join(components[:end])
126-
for end in range(1, len(components) + 1)
127-
]
111+
paths = get_paths(root, exclude_patterns, include_patterns,
112+
has_exceptions=len(exceptions) > 0)
128113

129114
return set(paths)
130115

131116

132-
def get_paths(root):
117+
def should_include(path, exclude_patterns, include_patterns):
118+
"""
119+
Given a path, a list of exclude patterns, and a list of inclusion patterns:
120+
121+
1. Returns True if the path doesn't match any exclusion pattern
122+
2. Returns False if the path matches an exclusion pattern and doesn't match
123+
an inclusion pattern
124+
3. Returns true if the path matches an exclusion pattern and matches an
125+
inclusion pattern
126+
"""
127+
for pattern in exclude_patterns:
128+
if match_path(path, pattern):
129+
for pattern in include_patterns:
130+
if match_path(path, pattern):
131+
return True
132+
return False
133+
return True
134+
135+
136+
def get_paths(root, exclude_patterns, include_patterns, has_exceptions=False):
133137
paths = []
134138

135-
for parent, dirs, files in os.walk(root, followlinks=False):
139+
for parent, dirs, files in os.walk(root, topdown=True, followlinks=False):
136140
parent = os.path.relpath(parent, root)
137141
if parent == '.':
138142
parent = ''
143+
144+
# If exception rules exist, we can't skip recursing into ignored
145+
# directories, as we need to look for exceptions in them.
146+
#
147+
# It may be possible to optimize this further for exception patterns
148+
# that *couldn't* match within ignored directores.
149+
#
150+
# This matches the current docker logic (as of 2015-11-24):
151+
# https://github.com/docker/docker/blob/37ba67bf636b34dc5c0c0265d62a089d0492088f/pkg/archive/archive.go#L555-L557
152+
153+
if not has_exceptions:
154+
155+
# Remove excluded patterns from the list of directories to traverse
156+
# by mutating the dirs we're iterating over.
157+
# This looks strange, but is considered the correct way to skip
158+
# traversal. See https://docs.python.org/2/library/os.html#os.walk
159+
160+
dirs[:] = [d for d in dirs if
161+
should_include(os.path.join(parent, d),
162+
exclude_patterns, include_patterns)]
163+
139164
for path in dirs:
140-
paths.append(os.path.join(parent, path))
165+
if should_include(os.path.join(parent, path),
166+
exclude_patterns, include_patterns):
167+
paths.append(os.path.join(parent, path))
168+
141169
for path in files:
142-
paths.append(os.path.join(parent, path))
170+
if should_include(os.path.join(parent, path),
171+
exclude_patterns, include_patterns):
172+
paths.append(os.path.join(parent, path))
143173

144174
return paths
145175

tests/integration/build_test.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def test_build_with_dockerignore(self):
6565
'ignored',
6666
'Dockerfile',
6767
'.dockerignore',
68+
'!ignored/subdir/excepted-file',
6869
'', # empty line
6970
]))
7071

@@ -76,6 +77,9 @@ def test_build_with_dockerignore(self):
7677
with open(os.path.join(subdir, 'file'), 'w') as f:
7778
f.write("this file should be ignored")
7879

80+
with open(os.path.join(subdir, 'excepted-file'), 'w') as f:
81+
f.write("this file should not be ignored")
82+
7983
tag = 'docker-py-test-build-with-dockerignore'
8084
stream = self.client.build(
8185
path=base_dir,
@@ -84,7 +88,7 @@ def test_build_with_dockerignore(self):
8488
for chunk in stream:
8589
pass
8690

87-
c = self.client.create_container(tag, ['ls', '-1A', '/test'])
91+
c = self.client.create_container(tag, ['find', '/test', '-type', 'f'])
8892
self.client.start(c)
8993
self.client.wait(c)
9094
logs = self.client.logs(c)
@@ -93,8 +97,9 @@ def test_build_with_dockerignore(self):
9397
logs = logs.decode('utf-8')
9498

9599
self.assertEqual(
96-
list(filter(None, logs.split('\n'))),
97-
['not-ignored'],
100+
sorted(list(filter(None, logs.split('\n')))),
101+
sorted(['/test/ignored/subdir/excepted-file',
102+
'/test/not-ignored']),
98103
)
99104

100105
@requires_api_version('1.21')

tests/unit/utils_test.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -671,17 +671,17 @@ def test_directory_with_trailing_slash(self):
671671

672672
def test_directory_with_single_exception(self):
673673
assert self.exclude(['foo', '!foo/bar/a.py']) == self.all_paths - set([
674-
'foo/a.py', 'foo/b.py',
674+
'foo/a.py', 'foo/b.py', 'foo', 'foo/bar'
675675
])
676676

677677
def test_directory_with_subdir_exception(self):
678678
assert self.exclude(['foo', '!foo/bar']) == self.all_paths - set([
679-
'foo/a.py', 'foo/b.py',
679+
'foo/a.py', 'foo/b.py', 'foo'
680680
])
681681

682682
def test_directory_with_wildcard_exception(self):
683683
assert self.exclude(['foo', '!foo/*.py']) == self.all_paths - set([
684-
'foo/bar', 'foo/bar/a.py',
684+
'foo/bar', 'foo/bar/a.py', 'foo'
685685
])
686686

687687
def test_subdirectory(self):

0 commit comments

Comments
 (0)