diff --git a/.travis.yml b/.travis.yml index b4e742d..6da3cd8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,9 +22,10 @@ matrix: - python2.6 -m virtualenv env - pyenv local --unset - source env/bin/activate + - python -c "import sys; print(sys.platform)" - os: osx language: generic - env: NAME="Python 2.7" + env: NAME="Python 2 Homebrew" before_install: - brew update - brew upgrade python2 || true @@ -33,7 +34,7 @@ matrix: - source env/bin/activate - os: osx language: generic - env: NAME="Python pypy" + env: NAME="Python pypy Homebrew" before_install: - brew update - brew upgrade pypy || brew install pypy || true @@ -58,8 +59,8 @@ matrix: before_install: - brew update - brew upgrade pyenv || brew install pyenv || true - - pyenv install 3.5.6 - - pyenv local 3.5.6 + - pyenv install 3.5.9 + - pyenv local 3.5.9 - eval "$(pyenv init -)" - python3.5 -m venv env - pyenv local --unset @@ -70,8 +71,8 @@ matrix: before_install: - brew update - brew upgrade pyenv || brew install pyenv || true - - pyenv install 3.6.7 - - pyenv local 3.6.7 + - pyenv install 3.6.9 + - pyenv local 3.6.9 - eval "$(pyenv init -)" - python3.6 -m venv env - pyenv local --unset @@ -81,26 +82,38 @@ matrix: env: NAME="Python 3.7" before_install: - brew update - - brew upgrade python || true - - python3.7 -m venv env + - brew upgrade pyenv || brew install pyenv || true + - pyenv install 3.7.5 + - pyenv local 3.7.5 + - eval "$(pyenv init -)" + - python3.6 -m venv env + - pyenv local --unset - source env/bin/activate - os: osx language: generic - env: NAME="Python HEAD" + env: NAME="Python 3 Homebrew" before_install: - brew update - - brew unlink python - - brew install python --HEAD - - python3 -m venv env + - brew upgrade python || true + - python3.7 -m venv env - source env/bin/activate - os: osx language: generic - env: NAME="Python pypy3" + env: NAME="Python pypy3 Homebrew" before_install: - brew update - brew upgrade pypy3 || brew install pypy3 || true - pypy3 -m venv env - source env/bin/activate + - os: osx + language: generic + env: NAME="Python HEAD" + before_install: + - brew update + - brew unlink python + - brew install python --HEAD + - python3 -m venv env + - source env/bin/activate - os: linux env: - PIP_URL="https://bootstrap.pypa.io/2.6/get-pip.py" @@ -127,12 +140,19 @@ matrix: language: python python: 3.7 sudo: true + - os: linux + language: python + python: 3.8 + sudo: true - os: linux language: python python: 3.6-dev - os: linux language: python python: 3.7-dev + - os: linux + language: python + python: 3.8-dev - os: linux language: python python: nightly @@ -146,6 +166,8 @@ matrix: python: 3.6-dev - os: linux python: 3.7-dev + - os: linux + python: 3.8-dev - os: linux python: nightly diff --git a/README.rst b/README.rst index 3dd7f6c..b0811d1 100644 --- a/README.rst +++ b/README.rst @@ -48,6 +48,9 @@ Questions & Answers | Q: How to exclude files? | A: Mark paths you want to exclude in the .gitattributes file with the export-ignore attribute. Read more on `git-scm.com `_. +| Q: What about non-unicode filenames? +| A: All filenames that particular version of Python can represent and handle are supported. Extra [en|de]coding is done where appropriate. + Support ------- If functional you need is missing but you're ready to pay for it, feel free to `contact me `_. If not, create an issue anyway, I'll take a look as soon as I can. diff --git a/git_archive_all.py b/git_archive_all.py index e3e4922..47675ff 100755 --- a/git_archive_all.py +++ b/git_archive_all.py @@ -52,6 +52,86 @@ def quote(s): return "'" + s.replace("'", "'\"'\"'") + "'" +try: + # Python 3.2+ + from os import fsdecode +except ImportError: + def fsdecode(filename): + if not isinstance(filename, unicode): + return filename.decode(sys.getfilesystemencoding(), 'strict') + else: + return filename + + +def git_fsdecode(filename): + """ + Decode filename from git output into str. + """ + if sys.platform.startswith('win32'): + return filename.decode('utf-8') + else: + return fsdecode(filename) + + +try: + # Python 3.2+ + from os import fsencode +except ImportError: + def fsencode(filename): + if not isinstance(filename, bytes): + return filename.encode(sys.getfilesystemencoding(), 'strict') + else: + return filename + + +def git_fsencode(filename): + """ + Encode filename from str into git input. + """ + if sys.platform.startswith('win32'): + return filename.encode('utf-8') + else: + return fsencode(filename) + + +try: + # Python 3.6+ + from os import fspath as _fspath + + def fspath(filename, decoder=fsdecode, encoder=fsencode): + # Python 3.6+: str can represent any path (PEP 383) + # str is not required on Windows (PEP 529) + # Decoding is still applied for consistency and to follow PEP 519 recommendation. + return decoder(_fspath(filename)) +except ImportError: + def fspath(filename, decoder=fsdecode, encoder=fsencode): + """ + Python 3.4 and 3.5: str can represent any path (PEP 383), + but str is required on Windows (no PEP 529) + + Python 2.6 and 2.7: str cannot represent any path (no PEP 383), + str is required on Windows (no PEP 529) + bytes is required on POSIX (no PEP 383) + """ + if sys.version_info > (3,): + import pathlib + if isinstance(filename, pathlib.PurePath): + return str(filename) + else: + return decoder(filename) + elif sys.platform.startswith('win32'): + return decoder(filename) + else: + return encoder(filename) + + +def git_fspath(filename): + """ + fspath representation of git output. + """ + return fspath(filename, git_fsdecode, git_fsencode) + + class GitArchiver(object): """ GitArchiver @@ -85,7 +165,6 @@ def __init__(self, prefix='', exclude=True, force_sub=False, extra=None, main_re baz foo/ bar - @type prefix: str @param exclude: Determines whether archiver should follow rules specified in .gitattributes files. @type exclude: bool @@ -100,39 +179,33 @@ def __init__(self, prefix='', exclude=True, force_sub=False, extra=None, main_re If given path is path to a subdirectory (but not a submodule directory!) it will be replaced with abspath to top-level directory of the repository. If None, current cwd is used. - @type main_repo_abspath: str @param git_version: Version of Git that determines whether various workarounds are on. If None, tries to resolve via Git's CLI. @type git_version: tuple or None """ + self._should_decode_path = None + self._check_attr_gens = {} + if git_version is None: git_version = self.get_git_version() if git_version is not None and git_version < (1, 6, 1): raise ValueError("git of version 1.6.1 and higher is required") - if extra is None: - extra = [] + self.git_version = git_version if main_repo_abspath is None: main_repo_abspath = path.abspath('') elif not path.isabs(main_repo_abspath): raise ValueError("main_repo_abspath must be an absolute path") - try: - main_repo_abspath = path.abspath(self.run_git_shell('git rev-parse --show-toplevel', main_repo_abspath).rstrip()) - except CalledProcessError: - raise ValueError("{0} is not part of a git repository".format(main_repo_abspath)) + self.main_repo_abspath = self.resolve_git_main_repo_abspath(main_repo_abspath) - self.prefix = prefix + self.prefix = fspath(prefix) self.exclude = exclude - self.extra = extra + self.extra = [fspath(e) for e in extra] if extra is not None else [] self.force_sub = force_sub - self.main_repo_abspath = main_repo_abspath - self.git_version = git_version - - self._check_attr_gens = {} def create(self, output_path, dry_run=False, output_format=None, compresslevel=None): """ @@ -142,7 +215,6 @@ def create(self, output_path, dry_run=False, output_format=None, compresslevel=N Supported formats are: gz, zip, bz2, xz, tar, tgz, txz @param output_path: Output file path. - @type output_path: str @param dry_run: Determines whether create should do nothing but print what it would archive. @type dry_run: bool @@ -151,6 +223,8 @@ def create(self, output_path, dry_run=False, output_format=None, compresslevel=N of output_file_path. @type output_format: str """ + output_path = fspath(output_path) + if output_format is None: file_name, file_ext = path.splitext(output_path) output_format = file_ext[len(extsep):].lower() @@ -195,13 +269,13 @@ def add_file(file_path, arcname): raise ValueError("unknown format: {0}".format(output_format)) def archiver(file_path, arcname): - self.LOG.debug("{0} => {1}".format(file_path, arcname)) + self.LOG.debug(fspath("{0} => {1}").format(file_path, arcname)) add_file(file_path, arcname) else: archive = None def archiver(file_path, arcname): - self.LOG.info("{0} => {1}".format(file_path, arcname)) + self.LOG.info(fspath("{0} => {1}").format(file_path, arcname)) self.archive_all_files(archiver) @@ -213,17 +287,15 @@ def is_file_excluded(self, repo_abspath, repo_file_path): Checks whether file at a given path is excluded. @param repo_abspath: Absolute path to the git repository. - @type repo_abspath: str @param repo_file_path: Path to a file relative to repo_abspath. - @type repo_file_path: str @return: True if file should be excluded. Otherwise False. @rtype: bool """ next(self._check_attr_gens[repo_abspath]) attrs = self._check_attr_gens[repo_abspath].send(repo_file_path) - return attrs['export-ignore'] == 'set' + return attrs['export-ignore'] == b'set' def archive_all_files(self, archiver): """ @@ -239,7 +311,7 @@ def archive_all_files(self, archiver): for file_path in self.walk_git_files(): archiver(path.join(self.main_repo_abspath, file_path), path.join(self.prefix, file_path)) - def walk_git_files(self, repo_path=''): + def walk_git_files(self, repo_path=fspath('')): """ An iterator method that yields a file path relative to main_repo_abspath for each file that should be included in the archive. @@ -249,20 +321,25 @@ def walk_git_files(self, repo_path=''): Recurs into submodules as well. @param repo_path: Path to the git submodule repository relative to main_repo_abspath. - @type repo_path: str @return: Iterator to traverse files under git control relative to main_repo_abspath. @rtype: Iterable """ - repo_abspath = path.join(self.main_repo_abspath, repo_path) + repo_abspath = path.join(self.main_repo_abspath, fspath(repo_path)) assert repo_abspath not in self._check_attr_gens - self._check_attr_gens[repo_abspath] = self.check_attr(repo_abspath, ['export-ignore']) + self._check_attr_gens[repo_abspath] = self.check_git_attr(repo_abspath, ['export-ignore']) try: repo_file_paths = self.run_git_shell( 'git ls-files -z --cached --full-name --no-empty-directory', - repo_abspath - ).split('\0')[:-1] + cwd=repo_abspath + ) + repo_file_paths = repo_file_paths.split(b'\0')[:-1] + + if sys.platform.startswith('win32'): + repo_file_paths = (git_fspath(p.replace(b'/', b'\\')) for p in repo_file_paths) + else: + repo_file_paths = map(git_fspath, repo_file_paths) for repo_file_path in repo_file_paths: repo_file_abspath = path.join(repo_abspath, repo_file_path) # absolute file path @@ -282,7 +359,7 @@ def walk_git_files(self, repo_path=''): self.run_git_shell('git submodule update', repo_abspath) try: - repo_gitmodules_abspath = path.join(repo_abspath, ".gitmodules") + repo_gitmodules_abspath = path.join(repo_abspath, fspath(".gitmodules")) with open(repo_gitmodules_abspath) as f: lines = f.readlines() @@ -291,7 +368,7 @@ def walk_git_files(self, repo_path=''): m = re.match("^\\s*path\\s*=\\s*(.*)\\s*$", l) if m: - repo_submodule_path = m.group(1) # relative to repo_path + repo_submodule_path = fspath(m.group(1)) # relative to repo_path main_repo_submodule_path = path.join(repo_path, repo_submodule_path) # relative to main_repo_abspath if self.is_file_excluded(repo_abspath, repo_submodule_path): @@ -309,20 +386,19 @@ def walk_git_files(self, repo_path=''): self._check_attr_gens[repo_abspath].close() del self._check_attr_gens[repo_abspath] - def check_attr(self, repo_abspath, attrs): + def check_git_attr(self, repo_abspath, attrs): """ - Generator that returns attributes for given paths relative to repo_abspath. + Generator that returns git attributes for received paths relative to repo_abspath. - >>> g = GitArchiver.check_attr('repo_path', ['export-ignore']) + >>> archiver = GitArchiver(...) + >>> g = archiver.check_git_attr('repo_path', ['export-ignore']) >>> next(g) >>> attrs = g.send('relative_path') >>> print(attrs['export-ignore']) @param repo_abspath: Absolute path to a git repository. - @type repo_abspath: str - @param attrs: Attributes to check. - @type attrs: [str] + @param attrs: Attributes to check @rtype: generator """ @@ -332,7 +408,7 @@ def make_process(): return Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, cwd=repo_abspath, env=env) def read_attrs(process, repo_file_path): - process.stdin.write(repo_file_path.encode('utf-8') + b'\0') + process.stdin.write(repo_file_path + b'\0') process.stdin.flush() # For every attribute check-attr will output: NUL NUL NUL @@ -349,7 +425,8 @@ def read_attrs(process, repo_file_path): nuls_count += 1 if nuls_count % 3 == 0: - yield map(self.decode_git_output, (path, attr, info)) + yield path, attr, info + path, attr, info = b'', b'', b'' elif nuls_count % 3 == 0: path += b @@ -362,7 +439,7 @@ def read_attrs_old(process, repo_file_path): """ Compatibility with versions 1.8.5 and below that do not recognize -z for output. """ - process.stdin.write(repo_file_path.encode('utf-8') + b'\0') + process.stdin.write(repo_file_path + b'\0') process.stdin.flush() # For every attribute check-attr will output: : : \n @@ -383,11 +460,11 @@ def read_attrs_old(process, repo_file_path): if attr_start == -1: raise RuntimeError("unexpected output of check-attr: {0}".format(line)) - info = line[info_start + 2:len(line) - 1] # trim leading ": " and trailing \n - attr = line[attr_start + 2:info_start] # trim leading ": " path = line[:attr_start] + attr = line[attr_start + 2:info_start] # trim leading ": " + info = line[info_start + 2:len(line) - 1] # trim leading ": " and trailing \n + yield path, attr, info - yield map(self.decode_git_output, (path, attr, info)) lines_count += 1 if not attrs: @@ -395,17 +472,19 @@ def read_attrs_old(process, repo_file_path): process = make_process() + if self.git_version is None or self.git_version > (1, 8, 5): + reader = read_attrs + else: + reader = read_attrs_old + try: while True: repo_file_path = yield + repo_file_path = git_fsencode(fspath(repo_file_path)) repo_file_attrs = {} - if self.git_version is None or self.git_version > (1, 8, 5): - reader = read_attrs - else: - reader = read_attrs_old - for path, attr, value in reader(process, repo_file_path): + attr = attr if attr in attrs else attr.decode('utf-8') # use str or bytes depending on what user passed repo_file_attrs[attr] = value yield repo_file_attrs @@ -413,36 +492,33 @@ def read_attrs_old(process, repo_file_path): process.stdin.close() process.wait() - @classmethod - def decode_git_output(cls, output): + def resolve_git_main_repo_abspath(self, abspath): """ - Decode Git's binary output handeling the way it escapes unicode characters. - - @type output: bytes - - @rtype: str + Return absolute path to the repo for a given path. """ - return output.decode('unicode_escape').encode('raw_unicode_escape').decode('utf-8') + try: + main_repo_abspath = self.run_git_shell('git rev-parse --show-toplevel', cwd=abspath).rstrip() + return path.abspath(git_fspath(main_repo_abspath)) + except CalledProcessError as e: + raise ValueError("{0} is not part of a git repository ({1})".format(abspath, e.returncode)) @classmethod def run_git_shell(cls, cmd, cwd=None): """ - Runs git shell command, reads output and decodes it into unicode string. + Run git shell command, read output and decode it into a unicode string. @param cmd: Command to be executed. @type cmd: str - @type cwd: str @param cwd: Working directory. - @rtype: str @return: Output of the command. + @rtype: bytes @raise CalledProcessError: Raises exception if return code of the command is non-zero. """ p = Popen(cmd, shell=True, stdout=PIPE, cwd=cwd) output, _ = p.communicate() - output = cls.decode_git_output(output) if p.returncode: if sys.version_info > (2, 6): @@ -474,7 +550,7 @@ def get_git_version(cls): return None try: - return tuple(int(v) for v in version.split('.')) + return tuple(int(v) if v.isdigit() else 0 for v in version.split(b'.')) except ValueError: cls.LOG.warning("Unable to parse Git version \"%s\".", version) return None diff --git a/requirements.txt b/requirements.txt index a77637b..bc36c9b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ -pytest==3.7.1; python_version>"2.6" -pytest==3.2.5; python_version<="2.6" +pytest==3.7.1; python_version > "2.6" +pytest==3.2.5; python_version <= "2.6" pytest-cov==2.5.1 +pytest-mock; python_version > "2.6" codecov==2.0.15 pycodestyle==2.4.0 diff --git a/setup.cfg b/setup.cfg index b747ba4..35dc238 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,6 +24,7 @@ classifiers = Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 Topic :: Software Development :: Version Control Topic :: System :: Archiving platforms = any @@ -32,6 +33,11 @@ long_description = file: README.rst [options] zip_safe = 1 python_requires = >=2.6,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*, +test_require = + pytest + pytest-cov + pycodestyle + pytest-mock; python_version > "2.6" [tool:pytest] addopts= --cov=git_archive_all --cov-report=term --cov-report=html --cov-branch --showlocals -vv diff --git a/setup.py b/setup.py index c6ad103..4b68dbf 100644 --- a/setup.py +++ b/setup.py @@ -35,6 +35,5 @@ def run_tests(self): version=verstr, py_modules=['git_archive_all'], entry_points={'console_scripts': 'git-archive-all=git_archive_all:main'}, - tests_require=['pytest', 'pytest-cov', 'pycodestyle'], cmdclass={"test": PyTest}, ) diff --git a/test_git_archive_all.py b/test_git_archive_all.py index 9d3f09a..c5df30e 100644 --- a/test_git_archive_all.py +++ b/test_git_archive_all.py @@ -15,7 +15,7 @@ import pytest import git_archive_all -from git_archive_all import GitArchiver +from git_archive_all import GitArchiver, fspath def makedirs(p): @@ -27,15 +27,17 @@ def makedirs(p): def as_posix(p): - if sys.version_info < (3,): - str_p = unicode(p) + if sys.platform.startswith('win32'): + return p.replace(b'\\', b'/') if isinstance(p, bytes) else p.replace('\\', '/') else: - str_p = str(p) + return p - if sys.platform.startswith('win32'): - str_p = str_p.replace('\\', '/') - return str_p +def os_path_join(*args): + """ + Ensure that all path components are uniformly encoded. + """ + return os.path.join(*(fspath(p) for p in args)) @pytest.fixture @@ -49,13 +51,13 @@ def git_env(tmpdir_factory): """ e = { 'GIT_CONFIG_NOSYSTEM': 'true', - 'HOME': str(tmpdir_factory.getbasetemp()) + 'HOME': tmpdir_factory.getbasetemp().strpath } with tmpdir_factory.getbasetemp().join('.gitconfig').open('w+') as f: f.writelines([ '[core]\n', - 'attributesfile = {0}\n'.format(as_posix(tmpdir_factory.getbasetemp().join('.gitattributes'))), + 'attributesfile = {0}\n'.format(as_posix(tmpdir_factory.getbasetemp().join('.gitattributes').strpath)), '[user]\n', 'name = git-archive-all\n', 'email = git-archive-all@example.com\n', @@ -89,7 +91,7 @@ def __setitem__(self, key, value): class Repo: def __init__(self, path): - self.path = os.path.abspath(path) + self.path = os.path.abspath(fspath(path)) def init(self): os.mkdir(self.path) @@ -106,7 +108,7 @@ def add(self, rel_path, record): raise ValueError def add_file(self, rel_path, contents): - file_path = os.path.join(self.path, rel_path) + file_path = os_path_join(self.path, rel_path) with open(file_path, 'w') as f: f.write(contents) @@ -115,17 +117,17 @@ def add_file(self, rel_path, contents): return file_path def add_dir(self, rel_path, contents): - dir_path = os.path.join(self.path, rel_path) + dir_path = os_path_join(self.path, rel_path) makedirs(dir_path) for k, v in contents.items(): - self.add(as_posix(os.path.normpath(os.path.join(dir_path, k))), v) + self.add(as_posix(os.path.normpath(os_path_join(dir_path, k))), v) check_call(['git', 'add', dir_path], cwd=self.path) return dir_path def add_submodule(self, rel_path, contents): - submodule_path = os.path.join(self.path, rel_path) + submodule_path = os_path_join(self.path, rel_path) r = Repo(submodule_path) r.init() r.add_dir('.', contents) @@ -149,7 +151,8 @@ def make_expected_tree(contents): e[k] = v.contents elif v.kind in ('dir', 'submodule') and not v.excluded: for nested_k, nested_v in make_expected_tree(v.contents).items(): - e[as_posix(os.path.join(k, nested_k))] = nested_v + nested_k = as_posix(os_path_join(k, nested_k)) + e[nested_k] = nested_v return e @@ -159,11 +162,7 @@ def make_actual_tree(tar_file): for m in tar_file.getmembers(): if m.isfile(): - name = m.name - - if sys.version_info < (3,): - name = m.name.decode('utf-8') - + name = fspath(m.name) a[name] = tar_file.extractfile(m).read().decode() else: raise NotImplementedError @@ -291,9 +290,47 @@ def make_actual_tree(tar_file): '\'\".dat\'': FileRecord('Although practicality beats purity.'), }) +nonunicode_base = deepcopy(base) +nonunicode_base['data'] = DirRecord({ + b'test.\xc2': FileRecord('Special cases aren\'t special enough to break the rules.'), +}) + +nonunicode_quoted = deepcopy(base) +nonunicode_quoted['data'] = DirRecord({ + b'\'test.\xc2\'': FileRecord('Special cases aren\'t special enough to break the rules.'), + b'\"test.\xc2\"': FileRecord('Although practicality beats purity.'), +}) + +backslash_base = deepcopy(base) +backslash_base['data'] = DirRecord({ + '\\.dat': FileRecord('Special cases aren\'t special enough to break the rules.'), +}) + +backslash_quoted = deepcopy(base) +backslash_quoted['data'] = DirRecord({ + '\'\\.dat\'': FileRecord('Special cases aren\'t special enough to break the rules.'), + '\"\\.dat\"': FileRecord('Although practicality beats purity.') +}) + +non_unicode_backslash_base = deepcopy(base) +non_unicode_backslash_base['data'] = DirRecord({ + b'\\\xc2.dat': FileRecord('Special cases aren\'t special enough to break the rules.'), +}) + +non_unicode_backslash_quoted = deepcopy(base) +non_unicode_backslash_quoted['data'] = DirRecord({ + b'\'\\\xc2.dat\'': FileRecord('Special cases aren\'t special enough to break the rules.'), + b'\"\\\xc2.dat\"': FileRecord('Although practicality beats purity.') +}) + + +skipif_file_darwin = pytest.mark.skipif(sys.platform.startswith('darwin'), reason='Invalid macOS filename.') +skipif_file_win32 = pytest.mark.skipif(sys.platform.startswith('win32'), reason="Invalid Windows filename.") + + @pytest.mark.parametrize('contents', [ pytest.param(base, id='No Ignore'), - pytest.param(base_quoted, id='No Ignore (Quoted)', marks=pytest.mark.skipif(sys.platform.startswith('win32'), reason="Invalid Windows filename.")), + pytest.param(base_quoted, id='No Ignore (Quoted)', marks=skipif_file_win32), pytest.param(ignore_in_root, id='Ignore in Root'), pytest.param(ignore_in_submodule, id='Ignore in Submodule'), pytest.param(ignore_in_nested_submodule, id='Ignore in Nested Submodule'), @@ -301,12 +338,18 @@ def make_actual_tree(tar_file): pytest.param(ignore_in_nested_submodule_from_root, id='Ignore in Nested Submodule from Root'), pytest.param(ignore_in_nested_submodule_from_submodule, id='Ignore in Nested Submodule from Submodule'), pytest.param(unset_export_ignore, id='-export-ignore'), - pytest.param(unicode_base, id='No Ignore (Unicode)'), - pytest.param(unicode_quoted, id='No Ignore (Quoted Unicode)', marks=pytest.mark.skipif(sys.platform.startswith('win32'), reason="Invalid Windows filename.")), + pytest.param(unicode_base, id='Unicode'), + pytest.param(unicode_quoted, id='Unicode (Quoted)', marks=skipif_file_win32), pytest.param(brackets_base, id='Brackets'), - pytest.param(brackets_quoted, id="Brackets (Quoted)", marks=pytest.mark.skipif(sys.platform.startswith('win32'), reason="Invalid Windows filename.")), - pytest.param(quote_base, id="Quote", marks=pytest.mark.skipif(sys.platform.startswith('win32'), reason="Invalid Windows filename.")), - pytest.param(quote_quoted, id="Quote (Quoted)", marks=pytest.mark.skipif(sys.platform.startswith('win32'), reason="Invalid Windows filename.")) + pytest.param(brackets_quoted, id="Brackets (Quoted)", marks=skipif_file_win32), + pytest.param(quote_base, id="Quote", marks=skipif_file_win32), + pytest.param(quote_quoted, id="Quote (Quoted)", marks=skipif_file_win32), + pytest.param(nonunicode_base, id="Non-Unicode", marks=[skipif_file_win32, skipif_file_darwin]), + pytest.param(nonunicode_quoted, id="Non-Unicode (Quoted)", marks=[skipif_file_win32, skipif_file_darwin]), + pytest.param(backslash_base, id='Backslash', marks=skipif_file_win32), + pytest.param(backslash_quoted, id='Backslash (Quoted)', marks=skipif_file_win32), + pytest.param(non_unicode_backslash_base, id='Non-Unicode Backslash', marks=[skipif_file_win32, skipif_file_darwin]), + pytest.param(non_unicode_backslash_quoted, id='Non-Unicode Backslash (Quoted)', marks=[skipif_file_win32, skipif_file_darwin]) ]) def test_ignore(contents, tmpdir, git_env, monkeypatch): """ @@ -315,13 +358,13 @@ def test_ignore(contents, tmpdir, git_env, monkeypatch): for name, value in git_env.items(): monkeypatch.setenv(name, value) - repo_path = os.path.join(str(tmpdir), 'repo') + repo_path = os_path_join(tmpdir.strpath, 'repo') repo = Repo(repo_path) repo.init() repo.add_dir('.', contents) repo.commit('init') - repo_tar_path = os.path.join(str(tmpdir), 'repo.tar') + repo_tar_path = os_path_join(tmpdir.strpath, 'repo.tar') repo.archive(repo_tar_path) repo_tar = TarFile(repo_tar_path, format=PAX_FORMAT, encoding='utf-8') @@ -337,13 +380,13 @@ def test_cli(tmpdir, git_env, monkeypatch): for name, value in git_env.items(): monkeypatch.setenv(name, value) - repo_path = os.path.join(str(tmpdir), 'repo') + repo_path = os_path_join(tmpdir.strpath, 'repo') repo = Repo(repo_path) repo.init() repo.add_dir('.', contents) repo.commit('init') - repo_tar_path = os.path.join(str(tmpdir), 'repo.tar') + repo_tar_path = os_path_join(tmpdir.strpath, 'repo.tar') git_archive_all.main(['git_archive_all.py', '--prefix', '', '-C', repo_path, repo_tar_path]) repo_tar = TarFile(repo_tar_path, format=PAX_FORMAT, encoding='utf-8') @@ -353,6 +396,16 @@ def test_cli(tmpdir, git_env, monkeypatch): assert actual == expected +@pytest.mark.skipif(sys.version_info < (2, 7), reason="No mocking on Python 2.6") +@pytest.mark.parametrize('version', [ + b'git version 2.21.0.0.1', + b'git version 2.21.0.windows.1' +]) +def test_git_version_parse(version, mocker): + mocker.patch.object(GitArchiver, 'run_git_shell', return_value=version) + assert GitArchiver.get_git_version() == (2, 21, 0, 0, 1) + + def test_pycodestyle(): style = pycodestyle.StyleGuide(repeat=True, max_line_length=240) report = style.check_files(['git_archive_all.py'])