Skip to content

Commit 60475bd

Browse files
Merge pull request #247 from acsone/git_file_finder-sbi
New git file finder that support symlinks.
2 parents e4f80b9 + 2fbdcff commit 60475bd

File tree

7 files changed

+210
-17
lines changed

7 files changed

+210
-17
lines changed

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def parse(root):
8282
8383
[setuptools_scm.files_command]
8484
.hg = setuptools_scm.hg:FILES_COMMAND
85-
.git = setuptools_scm.git:list_files_in_archive
85+
.git = setuptools_scm.git_file_finder:find_files
8686
8787
[setuptools_scm.version_scheme]
8888
guess-next-dev = setuptools_scm.version:guess_next_dev_version

setuptools_scm/git.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
from .version import meta
33

44
from os.path import isfile, join
5-
import subprocess
6-
import tarfile
75
import warnings
86

97

@@ -128,13 +126,3 @@ def parse(root, describe_command=DEFAULT_DESCRIBE, pre_parse=warn_on_shallow):
128126
return meta(tag, distance=number, node=node, dirty=dirty, branch=branch)
129127
else:
130128
return meta(tag, node=node, dirty=dirty, branch=branch)
131-
132-
133-
def list_files_in_archive(path):
134-
"""List the files that 'git archive' generates.
135-
"""
136-
cmd = ['git', 'archive', 'HEAD']
137-
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, cwd=path)
138-
tf = tarfile.open(fileobj=proc.stdout, mode='r|*')
139-
return [member.name for member in tf.getmembers()
140-
if member.type != tarfile.DIRTYPE]

setuptools_scm/git_file_finder.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import os
2+
import subprocess
3+
import tarfile
4+
5+
6+
def _git_toplevel(path):
7+
try:
8+
out = subprocess.check_output([
9+
'git', 'rev-parse', '--show-toplevel',
10+
], cwd=(path or '.'), universal_newlines=True)
11+
return os.path.normcase(os.path.realpath(out.strip()))
12+
except subprocess.CalledProcessError:
13+
# git returned error, we are not in a git repo
14+
return None
15+
except OSError:
16+
# git command not found, probably
17+
return None
18+
19+
20+
def _git_ls_files_and_dirs(toplevel):
21+
# use git archive instead of git ls-file to honor
22+
# export-ignore git attribute
23+
cmd = ['git', 'archive', '--prefix', toplevel + os.path.sep, 'HEAD']
24+
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, cwd=toplevel)
25+
tf = tarfile.open(fileobj=proc.stdout, mode='r|*')
26+
git_files = set()
27+
git_dirs = set([toplevel])
28+
for member in tf.getmembers():
29+
name = os.path.normcase(member.name).replace('/', os.path.sep)
30+
if member.type == tarfile.DIRTYPE:
31+
git_dirs.add(name)
32+
else:
33+
git_files.add(name)
34+
return git_files, git_dirs
35+
36+
37+
def find_files(path=''):
38+
""" setuptools compatible git file finder that follows symlinks
39+
40+
Spec here: http://setuptools.readthedocs.io/en/latest/setuptools.html#\
41+
adding-support-for-revision-control-systems
42+
"""
43+
toplevel = _git_toplevel(path)
44+
if not toplevel:
45+
return []
46+
git_files, git_dirs = _git_ls_files_and_dirs(toplevel)
47+
realpath = os.path.normcase(os.path.realpath(path))
48+
assert realpath.startswith(toplevel)
49+
assert realpath in git_dirs
50+
seen = set()
51+
res = []
52+
for dirpath, dirnames, filenames in os.walk(realpath, followlinks=True):
53+
# dirpath with symlinks resolved
54+
realdirpath = os.path.normcase(os.path.realpath(dirpath))
55+
if realdirpath not in git_dirs or realdirpath in seen:
56+
dirnames[:] = []
57+
continue
58+
for filename in filenames:
59+
# dirpath + filename with symlinks preserved
60+
fullfilename = os.path.join(dirpath, filename)
61+
if os.path.normcase(os.path.realpath(fullfilename)) in git_files:
62+
res.append(
63+
os.path.join(path, os.path.relpath(fullfilename, path)))
64+
seen.add(realdirpath)
65+
return res

testing/conftest.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ def _reason(self, given_reason):
4242
else:
4343
return given_reason
4444

45+
def add_and_commit(self, reason=None):
46+
self(self.add_command)
47+
self.commit(reason)
48+
4549
def commit(self, reason=None):
4650
reason = self._reason(reason)
4751
self(self.commit_command, reason=reason)

testing/test_git.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from setuptools_scm import git
44
import pytest
55
from datetime import date
6+
from os.path import join as opj
67

78

89
@pytest.fixture
@@ -123,7 +124,8 @@ def test_git_archive_export_ignore(wd):
123124
'/test1.txt -export-ignore\n/test2.txt export-ignore')
124125
wd('git add test1.txt test2.txt')
125126
wd.commit()
126-
assert integration.find_files(str(wd.cwd)) == ['test1.txt']
127+
with wd.cwd.as_cwd():
128+
assert integration.find_files('.') == [opj('.', 'test1.txt')]
127129

128130

129131
@pytest.mark.issue(228)
@@ -132,7 +134,8 @@ def test_git_archive_subdirectory(wd):
132134
wd.write('foobar/test1.txt', 'test')
133135
wd('git add foobar')
134136
wd.commit()
135-
assert integration.find_files(str(wd.cwd)) == ['foobar/test1.txt']
137+
with wd.cwd.as_cwd():
138+
assert integration.find_files('.') == [opj('.', 'foobar', 'test1.txt')]
136139

137140

138141
def test_git_feature_branch_increments_major(wd):

testing/test_git_file_finder.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import os
2+
import sys
3+
4+
import pytest
5+
6+
from setuptools_scm.git_file_finder import find_files
7+
8+
9+
@pytest.fixture
10+
def inwd(wd):
11+
wd('git init')
12+
wd('git config user.email [email protected]')
13+
wd('git config user.name "a test"')
14+
wd.add_command = 'git add .'
15+
wd.commit_command = 'git commit -m test-{reason}'
16+
(wd.cwd / 'file1').ensure(file=True)
17+
adir = (wd.cwd / 'adir').ensure(dir=True)
18+
(adir / 'filea').ensure(file=True)
19+
bdir = (wd.cwd / 'bdir').ensure(dir=True)
20+
(bdir / 'fileb').ensure(file=True)
21+
wd.add_and_commit()
22+
with wd.cwd.as_cwd():
23+
yield wd
24+
25+
26+
def _sep(paths):
27+
return {
28+
path.replace('/', os.path.sep)
29+
for path in paths
30+
}
31+
32+
33+
def test_basic(inwd):
34+
assert set(find_files()) == _sep({
35+
'file1',
36+
'adir/filea',
37+
'bdir/fileb',
38+
})
39+
assert set(find_files('.')) == _sep({
40+
'./file1',
41+
'./adir/filea',
42+
'./bdir/fileb',
43+
})
44+
assert set(find_files('adir')) == _sep({
45+
'adir/filea',
46+
})
47+
48+
49+
def test_case(inwd):
50+
(inwd.cwd / 'CamelFile').ensure(file=True)
51+
(inwd.cwd / 'file2').ensure(file=True)
52+
inwd.add_and_commit()
53+
assert set(find_files()) == _sep({
54+
'CamelFile',
55+
'file2',
56+
'file1',
57+
'adir/filea',
58+
'bdir/fileb',
59+
})
60+
61+
62+
@pytest.mark.skipif(sys.platform == 'win32',
63+
reason="symlinks to dir not supported")
64+
def test_symlink_dir(inwd):
65+
(inwd.cwd / 'adir' / 'bdirlink').mksymlinkto('../bdir')
66+
inwd.add_and_commit()
67+
assert set(find_files('adir')) == _sep({
68+
'adir/filea',
69+
'adir/bdirlink/fileb',
70+
})
71+
72+
73+
@pytest.mark.skipif(sys.platform == 'win32',
74+
reason="symlinks to files not supported on windows")
75+
def test_symlink_file(inwd):
76+
(inwd.cwd / 'adir' / 'file1link').mksymlinkto('../file1')
77+
inwd.add_and_commit()
78+
assert set(find_files('adir')) == _sep({
79+
'adir/filea',
80+
'adir/file1link',
81+
})
82+
83+
84+
@pytest.mark.skipif(sys.platform == 'win32',
85+
reason="symlinks to dir not supported")
86+
def test_symlink_loop(inwd):
87+
(inwd.cwd / 'adir' / 'loop').mksymlinkto('../adir')
88+
inwd.add_and_commit()
89+
assert set(find_files('adir')) == _sep({
90+
'adir/filea',
91+
})
92+
93+
94+
@pytest.mark.skipif(sys.platform == 'win32',
95+
reason="symlinks to dir not supported")
96+
def test_symlink_dir_out_of_git(inwd):
97+
(inwd.cwd / 'adir' / 'outsidedirlink').\
98+
mksymlinkto(os.path.join(__file__, '..'))
99+
inwd.add_and_commit()
100+
assert set(find_files('adir')) == _sep({
101+
'adir/filea',
102+
})
103+
104+
105+
@pytest.mark.skipif(sys.platform == 'win32',
106+
reason="symlinks to files not supported on windows")
107+
def test_symlink_file_out_of_git(inwd):
108+
(inwd.cwd / 'adir' / 'outsidefilelink').mksymlinkto(__file__)
109+
inwd.add_and_commit()
110+
assert set(find_files('adir')) == _sep({
111+
'adir/filea',
112+
})
113+
114+
115+
def test_empty_root(inwd):
116+
subdir = inwd.cwd / 'cdir' / 'subdir'
117+
subdir.ensure(dir=True)
118+
(subdir / 'filec').ensure(file=True)
119+
inwd.add_and_commit()
120+
assert set(find_files('cdir')) == _sep({
121+
'cdir/subdir/filec',
122+
})
123+
124+
125+
def test_empty_subdir(inwd):
126+
subdir = inwd.cwd / 'adir' / 'emptysubdir' / 'subdir'
127+
subdir.ensure(dir=True)
128+
(subdir / 'xfile').ensure(file=True)
129+
inwd.add_and_commit()
130+
assert set(find_files('adir')) == _sep({
131+
'adir/filea',
132+
'adir/emptysubdir/subdir/xfile',
133+
})

testing/test_regressions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ def test_pip_egg_info(tmpdir, monkeypatch):
5353
def test_pip_download(tmpdir, monkeypatch):
5454
monkeypatch.chdir(tmpdir)
5555
subprocess.check_call([
56-
sys.executable, '-c',
57-
'import pip;pip.main()', 'download', 'lz4==0.9.0',
56+
sys.executable, '-m',
57+
'pip', 'download', 'lz4==0.9.0',
5858
])
5959

6060

0 commit comments

Comments
 (0)