diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..876a061 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,198 @@ +env: + global: + - PIP_URL="https://bootstrap.pypa.io/get-pip.py" + - PIP="python -m pip" + +matrix: + fast_finish: true + include: + - os: osx + language: generic + env: + - NAME="Python 2.6" + - PIP_URL="https://bootstrap.pypa.io/2.6/get-pip.py" + - PIP=pip + before_install: + - brew update + - brew upgrade pyenv || brew install pyenv || true + - pyenv install 2.6.9 + - pyenv local 2.6.9 + - eval "$(pyenv init -)" + - pip install virtualenv + - python2.6 -m virtualenv env + - pyenv local --unset + - source env/bin/activate + - os: osx + language: generic + env: NAME="Python 2.7" + before_install: + - brew update + - brew upgrade python2 || true + - python2.7 -m pip install virtualenv + - python2.7 -m virtualenv env + - source env/bin/activate + - os: osx + language: generic + env: NAME="Python pypy" + before_install: + - brew update + - brew upgrade pypy || brew install pypy || true + - pypy -m pip install virtualenv + - pypy -m virtualenv env + - source env/bin/activate + - os: osx + language: generic + env: NAME="Python 3.4" + before_install: + - brew update + - brew upgrade pyenv || brew install pyenv || true + - pyenv install 3.4.9 + - pyenv local 3.4.9 + - eval "$(pyenv init -)" + - python3.4 -m venv env + - pyenv local --unset + - source env/bin/activate + - os: osx + language: generic + env: NAME="Python 3.5" + before_install: + - brew update + - brew upgrade pyenv || brew install pyenv || true + - pyenv install 3.5.6 + - pyenv local 3.5.6 + - eval "$(pyenv init -)" + - python3.5 -m venv env + - pyenv local --unset + - source env/bin/activate + - os: osx + language: generic + env: NAME="Python 3.6" + before_install: + - brew update + - brew upgrade pyenv || brew install pyenv || true + - pyenv install 3.6.6 + - pyenv local 3.6.6 + - eval "$(pyenv init -)" + - python3.6 -m venv env + - pyenv local --unset + - source env/bin/activate + - os: osx + language: generic + env: NAME="Python 3.7" + before_install: + - brew update + - brew upgrade python || true + - python3.7 -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: osx + language: generic + env: NAME="Python pypy3" + before_install: + - brew update + - brew upgrade pypy3 || brew install pypy3 || true + - pypy3 -m venv env + - source env/bin/activate + - os: linux + env: + - PIP_URL="https://bootstrap.pypa.io/2.6/get-pip.py" + - PIP=env/bin/pip + language: python + python: 2.6 + before_install: + - pip install virtualenv + - python2.6 -m virtualenv env + - source env/bin/activate + - os: linux + language: python + python: 2.7 + before_install: + - pip install virtualenv + - python2.7 -m virtualenv env + - source env/bin/activate + - os: linux + language: python + python: pypy + before_install: + - pip install virtualenv + - pypy -m virtualenv env + - source env/bin/activate + - os: linux + language: python + python: 3.4 + before_install: + - python3.4 -m venv env + - source env/bin/activate + - os: linux + language: python + python: 3.5 + before_install: + - python3.5 -m venv env + - source env/bin/activate + - os: linux + language: python + python: 3.6 + before_install: + - python3.6 -m venv env + - source env/bin/activate + - os: linux + language: python + python: &python_major_ver 3.7 + dist: xenial + sudo: true + before_install: + - python3.7 -m venv env + - source env/bin/activate + - os: linux + language: python + python: 3.6-dev + before_install: + - python3.6 -m venv env + - source env/bin/activate + - os: linux + language: python + python: 3.7-dev + before_install: + - python3.7 -m venv env + - source env/bin/activate + - os: linux + language: python + python: nightly + before_install: + - python3 -m venv env + - source env/bin/activate + - os: linux + language: python + python: pypy3 + before_install: + - pypy3 -m venv env + - source env/bin/activate + allow_failures: + - os: osx + env: NAME="Python HEAD" + - os: linux + python: 3.6-dev + - os: linux + python: 3.7-dev + - os: linux + python: nightly + +install: + - which python + - ls env/bin + - curl ${PIP_URL} | python + - ${PIP} install -r requirements.txt + +script: + - python setup.py test + +after_success: + - python -m codecov diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1695968 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +pytest==3.7.1; python_version>"2.6" +pytest==3.2.5; python_version<="2.6" +pytest-cov==2.5.1 +codecov==2.0.15 diff --git a/setup.cfg b/setup.cfg index 7f17644..5f41e7f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,6 +8,7 @@ author_email = kulakov.ilya@gmail.com url = https://github.com/Kentzo/git-archive-all description = Archive git repository with its submodules. license = MIT License +license_file = LICENSE.txt classifiers = Development Status :: 5 - Production/Stable Environment :: Console @@ -15,10 +16,22 @@ classifiers = Intended Audience :: System Administrators License :: OSI Approved :: MIT License Natural Language :: English + Programming Language :: Python :: Implementation :: CPython + Programming Language :: Python :: Implementation :: PyPy Programming Language :: Python :: 2.6 Programming Language :: Python :: 2.7 - Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 Topic :: Software Development :: Version Control Topic :: System :: Archiving platforms = any long_description = file: README.rst + +[options] +zip_safe = 1 +python_requires = >=2.6,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*, + +[tool:pytest] +addopts= --cov=git_archive_all --cov-report=term --cov-report=html --cov-branch diff --git a/setup.py b/setup.py index 3257e9e..fe3645c 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,8 @@ import re +import sys + from setuptools import setup +from setuptools.command.test import test as TestCommand # Parse the version from the file. verstrline = open('git_archive_all.py', "rt").read() @@ -10,8 +13,28 @@ else: raise RuntimeError("Unable to find version string in git_archive_all.py") + +class PyTest(TestCommand): + user_options = [("pytest-args=", "a", "Arguments to pass to pytest")] + + def initialize_options(self): + TestCommand.initialize_options(self) + self.pytest_args = "" + + def run_tests(self): + import shlex + + # import here, cause outside the eggs aren't loaded + import pytest + + errno = pytest.main(shlex.split(self.pytest_args)) + sys.exit(errno) + + setup( version=verstr, py_modules=['git_archive_all'], - entry_points={'console_scripts': 'git-archive-all=git_archive_all:main'} + entry_points={'console_scripts': 'git-archive-all=git_archive_all:main'}, + tests_require=['pytest', 'pytest-cov'], + cmdclass={"test": PyTest}, ) diff --git a/test_git_archive_all.py b/test_git_archive_all.py new file mode 100644 index 0000000..5214a3d --- /dev/null +++ b/test_git_archive_all.py @@ -0,0 +1,228 @@ +from copy import deepcopy +import errno +from functools import partial +import os +from subprocess import check_call +from tarfile import TarFile + +import pytest + +from git_archive_all import GitArchiver + + +def makedirs(p): + try: + os.makedirs(p) + except OSError as e: + if e.errno != errno.EEXIST: + raise + + +def pytest_param(*values, **kwargs): + try: + return pytest.param(*values, **kwargs) + except AttributeError: + return values + + +@pytest.fixture +def git_env(tmpdir_factory): + """ + Return ENV git configured for tests: + + 1. Both system and user configs are ignored + 2. Custom git user + 3. .gitmodules file is ignored by default + """ + e = deepcopy(os.environ) + e['GIT_CONFIG_NOSYSTEM'] = 'true' + e['HOME'] = str(tmpdir_factory.getbasetemp()) + + with tmpdir_factory.getbasetemp().join('.gitconfig').open('w+') as f: + f.writelines([ + '[core]\n', + 'attributesfile = {0}/.gitattributes\n'.format(tmpdir_factory.getbasetemp()), + '[user]\n', + 'name = git-archive-all\n', + 'email = git-archive-all@example.com\n', + ]) + + with tmpdir_factory.getbasetemp().join('.gitattributes').open('w+') as f: + f.writelines([ + '.gitmodules export-ignore' + ]) + + return e + + +class Record: + def __init__(self, kind, contents, excluded=False): + self.kind = kind + self.contents = contents + self.excluded = excluded + + def __getitem__(self, item): + return self.contents[item] + + def __setitem__(self, key, value): + self.contents[key] = value + + +FileRecord = partial(Record, 'file', excluded=False) +DirRecord = partial(Record, 'dir', excluded=False) +SubmoduleRecord = partial(Record, 'submodule', excluded=False) + + +class Repo: + def __init__(self, path, git_env): + self.path = os.path.abspath(path) + self.git_env = git_env + + def init(self): + os.mkdir(self.path) + check_call(['git', 'init'], cwd=self.path) + + def add(self, rel_path, record): + if record.kind == 'file': + return self.add_file(rel_path, record.contents) + elif record.kind == 'dir': + return self.add_dir(rel_path, record.contents) + elif record.kind == 'submodule': + return self.add_submodule(rel_path, record.contents) + else: + raise ValueError + + def add_file(self, rel_path, contents): + file_path = os.path.join(self.path, rel_path) + + with open(file_path, 'w') as f: + f.write(contents) + + check_call(['git', 'add', file_path], cwd=self.path, env=self.git_env) + return file_path + + def add_dir(self, rel_path, contents): + dir_path = os.path.join(self.path, rel_path) + makedirs(dir_path) + + for k, v in contents.items(): + self.add(os.path.join(dir_path, k), v) + + check_call(['git', 'add', dir_path], cwd=self.path, env=self.git_env) + return dir_path + + def add_submodule(self, rel_path, contents): + submodule_path = os.path.join(self.path, rel_path) + r = Repo(submodule_path, self.git_env) + r.init() + r.add_dir('.', contents) + r.commit('init') + check_call(['git', 'submodule', 'add', submodule_path], cwd=self.path, env=self.git_env) + return submodule_path + + def commit(self, message): + check_call(['git', 'commit', '-m', 'init'], cwd=self.path, env=self.git_env) + + def archive(self, path): + a = GitArchiver(main_repo_abspath=self.path) + a.create(path) + + +base = { + 'app': DirRecord({ + '__init__.py': FileRecord('#Beautiful is better than ugly.'), + }), + 'lib': SubmoduleRecord({ + '__init__.py': FileRecord('#Explicit is better than implicit.'), + 'extra': SubmoduleRecord({ + '__init__.py': FileRecord('#Simple is better than complex.'), + }) + }) +} + +ignore_in_root = deepcopy(base) +ignore_in_root['.gitattributes'] = FileRecord('\ntests/__init__.py export-ignore') +ignore_in_root['tests'] = DirRecord({ + '__init__.py': FileRecord('#Complex is better than complicated.', excluded=True) +}) + +ignore_in_submodule = deepcopy(base) +ignore_in_submodule['lib']['.gitattributes'] = FileRecord('\ntests/__init__.py export-ignore') +ignore_in_submodule['lib']['tests'] = DirRecord({ + '__init__.py': FileRecord('#Flat is better than nested.', excluded=True) +}) + +ignore_in_nested_submodule = deepcopy(base) +ignore_in_nested_submodule['lib']['extra']['.gitattributes'] = FileRecord('\ntests/__init__.py export-ignore') +ignore_in_nested_submodule['lib']['extra']['tests'] = DirRecord({ + '__init__.py': FileRecord('#Sparse is better than dense.', excluded=True) +}) + +ignore_in_submodule_from_root = deepcopy(base) +ignore_in_submodule_from_root['.gitattributes'] = FileRecord('\nlib/tests/__init__.py export-ignore') +ignore_in_submodule_from_root['lib']['tests'] = DirRecord({ + '__init__.py': FileRecord('#Readability counts.', excluded=True) +}) + +ignore_in_nested_submodule_from_root = deepcopy(base) +ignore_in_nested_submodule_from_root['.gitattributes'] = FileRecord('\nlib/extra/tests/__init__.py export-ignore') +ignore_in_nested_submodule_from_root['lib']['extra']['tests'] = DirRecord({ + '__init__.py': FileRecord('#Special cases aren\'t special enough to break the rules.', excluded=True) +}) + +ignore_in_nested_submodule_from_submodule = deepcopy(base) +ignore_in_nested_submodule_from_submodule['lib']['.gitattributes'] = FileRecord('\nextra/tests/__init__.py export-ignore') +ignore_in_nested_submodule_from_submodule['lib']['extra']['tests'] = DirRecord({ + '__init__.py': FileRecord('#Although practicality beats purity.', excluded=True) +}) + + +@pytest.mark.parametrize('contents', [ + pytest_param(base, id='No Ignore'), + 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'), + pytest_param(ignore_in_submodule_from_root, id='Ignore in Submodule from Root'), + pytest_param(ignore_in_nested_submodule_from_root, id='Ignore in Nested Submodule from Root'), +]) +def test_ignore(contents, tmpdir, git_env): + """ + Ensure that GitArchiver respects export-ignore. + """ + repo_path = os.path.join(str(tmpdir), 'repo') + repo = Repo(repo_path, git_env) + repo.init() + repo.add_dir('.', contents) + repo.commit('init') + + repo_tar_path = os.path.join(str(tmpdir), 'repo.tar') + repo.archive(repo_tar_path) + repo_tar = TarFile(repo_tar_path) + + def make_expected(contents): + e = {} + + for k, v in contents.items(): + if v.kind == 'file' and not v.excluded: + e[k] = v.contents + elif v.kind in ('dir', 'submodule') and not v.excluded: + for nested_k, nested_v in make_expected(v.contents).items(): + e[os.path.join(k, nested_k)] = nested_v + + return e + + def make_actual(tar_file): + a = {} + + for m in tar_file.getmembers(): + if m.isfile(): + a[m.name] = tar_file.extractfile(m).read().decode() + else: + raise NotImplementedError + + return a + + expected = make_expected(contents) + actual = make_actual(repo_tar) + + assert actual == expected