diff --git a/.travis.yml b/.travis.yml index 0306b61..720eec7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,10 @@ env: # These should match the tox env list - TOXENV=pypy3 install: pip install coveralls tox --use-mirrors script: tox +# Special snowflake. Our tests depend on making real commits. +before_install: + - git config --global user.name "Travis CI" + - git config --global user.email "user@example.com" after_success: - coveralls sudo: false diff --git a/hooks.yaml b/hooks.yaml index 9e48555..4e31f98 100644 --- a/hooks.yaml +++ b/hooks.yaml @@ -12,6 +12,13 @@ language: python # Match all files files: '' +- id: check-case-conflict + name: Check for case conflicts + description: Check for files that would conflict in case-insensitive filesystems + entry: check-case-conflict + language: python + # Match all files + files: '' - id: check-docstring-first name: Check docstring is first description: Checks a common error of defining a docstring after code. diff --git a/pre_commit_hooks/check_added_large_files.py b/pre_commit_hooks/check_added_large_files.py index 999e9c1..973b958 100644 --- a/pre_commit_hooks/check_added_large_files.py +++ b/pre_commit_hooks/check_added_large_files.py @@ -8,15 +8,13 @@ import math import os import sys -from plumbum import local +from pre_commit_hooks.util import added_files def find_large_added_files(filenames, maxkb): # Find all added files that are also in the list of files pre-commit tells # us about - filenames = set(local['git']( - 'diff', '--staged', '--name-only', '--diff-filter', 'A', - ).splitlines()) & set(filenames) + filenames = added_files() & set(filenames) retv = 0 for filename in filenames: diff --git a/pre_commit_hooks/check_case_conflict.py b/pre_commit_hooks/check_case_conflict.py new file mode 100644 index 0000000..6a837d9 --- /dev/null +++ b/pre_commit_hooks/check_case_conflict.py @@ -0,0 +1,58 @@ +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +import argparse + +from plumbum import local + +from pre_commit_hooks.util import added_files + + +def lower_set(iterable): + return set(x.lower() for x in iterable) + + +def find_conflicting_filenames(filenames): + repo_files = set(local['git']('ls-files').splitlines()) + relevant_files = set(filenames) | added_files() + repo_files -= relevant_files + retv = 0 + + # new file conflicts with existing file + conflicts = lower_set(repo_files) & lower_set(relevant_files) + + # new file conflicts with other new file + lowercase_relevant_files = lower_set(relevant_files) + for filename in set(relevant_files): + if filename.lower() in lowercase_relevant_files: + lowercase_relevant_files.remove(filename.lower()) + else: + conflicts.add(filename.lower()) + + if conflicts: + conflicting_files = [ + x for x in repo_files | relevant_files + if x.lower() in conflicts + ] + for filename in sorted(conflicting_files): + print('Case-insensitivity conflict found: {0}'.format(filename)) + retv = 1 + + return retv + + +def main(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument( + 'filenames', nargs='*', + help='Filenames pre-commit believes are changed.' + ) + + args = parser.parse_args(argv) + + return find_conflicting_filenames(args.filenames) + + +if __name__ == '__main__': + exit(main()) diff --git a/pre_commit_hooks/util.py b/pre_commit_hooks/util.py new file mode 100644 index 0000000..eedf7b6 --- /dev/null +++ b/pre_commit_hooks/util.py @@ -0,0 +1,11 @@ +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +from plumbum import local + + +def added_files(): + return set(local['git']( + 'diff', '--staged', '--name-only', '--diff-filter', 'A', + ).splitlines()) diff --git a/pylintrc b/pylintrc index ee7e3d6..ce9ebdb 100644 --- a/pylintrc +++ b/pylintrc @@ -17,3 +17,6 @@ ignored-classes=pytest [DESIGN] min-public-methods=0 +[SIMILARITIES] +min-similarity-lines=5 +ignore-imports=yes diff --git a/setup.py b/setup.py index 26991de..3addc42 100644 --- a/setup.py +++ b/setup.py @@ -38,6 +38,7 @@ setup( 'console_scripts': [ 'autopep8-wrapper = pre_commit_hooks.autopep8_wrapper:main', 'check-added-large-files = pre_commit_hooks.check_added_large_files:main', + 'check-case-conflict = pre_commit_hooks.check_case_conflict:main', 'check-docstring-first = pre_commit_hooks.check_docstring_first:main', 'check-json = pre_commit_hooks.check_json:check_json', 'check-yaml = pre_commit_hooks.check_yaml:check_yaml', diff --git a/testing/util.py b/testing/util.py index 8e468d6..0bc8142 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,3 +1,4 @@ +import io import os.path @@ -6,3 +7,9 @@ TESTING_DIR = os.path.abspath(os.path.dirname(__file__)) def get_resource_path(path): return os.path.join(TESTING_DIR, 'resources', path) + + +def write_file(filename, contents): + """Hax because coveragepy chokes on nested context managers.""" + with io.open(filename, 'w') as file_obj: + file_obj.write(contents) diff --git a/tests/check_added_large_files_test.py b/tests/check_added_large_files_test.py index b0ae4cd..b72a315 100644 --- a/tests/check_added_large_files_test.py +++ b/tests/check_added_large_files_test.py @@ -1,26 +1,11 @@ from __future__ import absolute_import from __future__ import unicode_literals -import io - -import pytest from plumbum import local from pre_commit_hooks.check_added_large_files import find_large_added_files from pre_commit_hooks.check_added_large_files import main - - -@pytest.yield_fixture -def temp_git_dir(tmpdir): - git_dir = tmpdir.join('gits').strpath - local['git']('init', git_dir) - yield git_dir - - -def write_file(filename, contents): - """Hax because coveragepy chokes on nested context managers.""" - with io.open(filename, 'w') as file_obj: - file_obj.write(contents) +from testing.util import write_file def test_nothing_added(temp_git_dir): diff --git a/tests/check_case_conflict_test.py b/tests/check_case_conflict_test.py new file mode 100644 index 0000000..344725d --- /dev/null +++ b/tests/check_case_conflict_test.py @@ -0,0 +1,66 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +from plumbum import local + +from pre_commit_hooks.check_case_conflict import find_conflicting_filenames +from pre_commit_hooks.check_case_conflict import main +from testing.util import write_file + + +def test_nothing_added(temp_git_dir): + with local.cwd(temp_git_dir): + assert find_conflicting_filenames(['f.py']) == 0 + + +def test_adding_something(temp_git_dir): + with local.cwd(temp_git_dir): + write_file('f.py', "print('hello world')") + local['git']('add', 'f.py') + + assert find_conflicting_filenames(['f.py']) == 0 + + +def test_adding_something_with_conflict(temp_git_dir): + with local.cwd(temp_git_dir): + write_file('f.py', "print('hello world')") + local['git']('add', 'f.py') + write_file('F.py', "print('hello world')") + local['git']('add', 'F.py') + + assert find_conflicting_filenames(['f.py', 'F.py']) == 1 + + +def test_added_file_not_in_pre_commits_list(temp_git_dir): + with local.cwd(temp_git_dir): + write_file('f.py', "print('hello world')") + local['git']('add', 'f.py') + + assert find_conflicting_filenames(['g.py']) == 0 + + +def test_file_conflicts_with_committed_file(temp_git_dir): + with local.cwd(temp_git_dir): + write_file('f.py', "print('hello world')") + local['git']('add', 'f.py') + local['git']('commit', '--no-verify', '-m', 'Add f.py') + + write_file('F.py', "print('hello world')") + local['git']('add', 'F.py') + + assert find_conflicting_filenames(['F.py']) == 1 + + +def test_integration(temp_git_dir): + with local.cwd(temp_git_dir): + assert main(argv=[]) == 0 + + write_file('f.py', "print('hello world')") + local['git']('add', 'f.py') + + assert main(argv=['f.py']) == 0 + + write_file('F.py', "print('hello world')") + local['git']('add', 'F.py') + + assert main(argv=['F.py']) == 1 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..157da6b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,13 @@ +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +import pytest +from plumbum import local + + +@pytest.yield_fixture +def temp_git_dir(tmpdir): + git_dir = tmpdir.join('gits').strpath + local['git']('init', git_dir) + yield git_dir