Skip to content

Commit ad48d5c

Browse files
Merge pull request #253 from acsone/symlink-edge-cases
file finder: symlink edge cases
2 parents 1da3325 + a72bced commit ad48d5c

File tree

2 files changed

+64
-4
lines changed

2 files changed

+64
-4
lines changed

setuptools_scm/file_finder.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ def scm_find_files(path, scm_files, scm_dirs):
55
""" setuptools compatible file finder that follows symlinks
66
77
- path: the root directory from which to search
8-
- scm_files: set of scm controlled files
9-
- scm_files: set of scm controlled directories
8+
- scm_files: set of scm controlled files and symlinks
9+
(including symlinks to directories)
10+
- scm_dirs: set of scm controlled directories
11+
(including directories containing no scm controlled files)
1012
1113
scm_files and scm_dirs must be absolute with symlinks resolved (realpath),
1214
with normalized case (normcase)
@@ -20,10 +22,31 @@ def scm_find_files(path, scm_files, scm_dirs):
2022
for dirpath, dirnames, filenames in os.walk(realpath, followlinks=True):
2123
# dirpath with symlinks resolved
2224
realdirpath = os.path.normcase(os.path.realpath(dirpath))
23-
if realdirpath not in scm_dirs or realdirpath in seen:
25+
26+
def _link_not_in_scm(n):
27+
fn = os.path.join(realdirpath, os.path.normcase(n))
28+
return os.path.islink(fn) and fn not in scm_files
29+
30+
if realdirpath not in scm_dirs:
31+
# directory not in scm, don't walk it's content
32+
dirnames[:] = []
33+
continue
34+
if os.path.islink(dirpath) and \
35+
not os.path.relpath(realdirpath, realpath).startswith(os.pardir):
36+
# a symlink to a directory not outside path:
37+
# we keep it in the result and don't walk its content
38+
res.append(
39+
os.path.join(path, os.path.relpath(dirpath, path)))
40+
dirnames[:] = []
41+
continue
42+
if realdirpath in seen:
43+
# symlink loop protection
2444
dirnames[:] = []
2545
continue
46+
dirnames[:] = [dn for dn in dirnames if not _link_not_in_scm(dn)]
2647
for filename in filenames:
48+
if _link_not_in_scm(filename):
49+
continue
2750
# dirpath + filename with symlinks preserved
2851
fullfilename = os.path.join(dirpath, filename)
2952
if os.path.normcase(os.path.realpath(fullfilename)) in scm_files:

testing/test_file_finder.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def test_symlink_file(inwd):
9191
inwd.add_and_commit()
9292
assert set(find_files('adir')) == _sep({
9393
'adir/filea',
94-
'adir/file1link',
94+
'adir/file1link', # -> ../file1
9595
})
9696

9797

@@ -102,6 +102,7 @@ def test_symlink_loop(inwd):
102102
inwd.add_and_commit()
103103
assert set(find_files('adir')) == _sep({
104104
'adir/filea',
105+
'adir/loop', # -> ../adir
105106
})
106107

107108

@@ -145,3 +146,39 @@ def test_empty_subdir(inwd):
145146
'adir/filea',
146147
'adir/emptysubdir/subdir/xfile',
147148
})
149+
150+
151+
@pytest.mark.skipif(sys.platform == 'win32',
152+
reason="symlinks to files not supported on windows")
153+
def test_double_include_through_symlink(inwd):
154+
(inwd.cwd / 'data').ensure(dir=True)
155+
(inwd.cwd / 'data' / 'datafile').ensure(file=True)
156+
(inwd.cwd / 'adir' / 'datalink').mksymlinkto('../data')
157+
(inwd.cwd / 'adir' / 'filealink').mksymlinkto('filea')
158+
inwd.add_and_commit()
159+
assert set(find_files()) == _sep({
160+
'file1',
161+
'adir/datalink', # -> ../data
162+
'adir/filealink', # -> filea
163+
'adir/filea',
164+
'bdir/fileb',
165+
'data/datafile',
166+
})
167+
168+
169+
@pytest.mark.skipif(sys.platform == 'win32',
170+
reason="symlinks to files not supported on windows")
171+
def test_symlink_not_in_scm_while_target_is(inwd):
172+
(inwd.cwd / 'data').ensure(dir=True)
173+
(inwd.cwd / 'data' / 'datafile').ensure(file=True)
174+
inwd.add_and_commit()
175+
(inwd.cwd / 'adir' / 'datalink').mksymlinkto('../data')
176+
(inwd.cwd / 'adir' / 'filealink').mksymlinkto('filea')
177+
assert set(find_files()) == _sep({
178+
'file1',
179+
'adir/filea',
180+
# adir/datalink and adir/afilelink not included
181+
# because the symlink themselves are not in scm
182+
'bdir/fileb',
183+
'data/datafile',
184+
})

0 commit comments

Comments
 (0)