From 27291ffa319d0c5dc3d7eaaebd4ccbe6b44aa34b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 13 Mar 2014 08:41:35 -0700 Subject: [PATCH] Initial commit. --- .coveragerc | 19 +++ .gitignore | 10 ++ .travis.yml | 8 ++ Makefile | 40 +++++++ README.md | 6 + pre-commit.py | 225 +++++++++++++++++++++++++++++++++++ pre_commit_hooks/__init__.py | 0 requirements.txt | 10 ++ setup.py | 12 ++ tests/__init__.py | 0 10 files changed, 330 insertions(+) create mode 100644 .coveragerc create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 Makefile create mode 100644 README.md create mode 100755 pre-commit.py create mode 100644 pre_commit_hooks/__init__.py create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 tests/__init__.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..418c1f1 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,19 @@ +[report] +exclude_lines = + # Don't complain about defensive assertions + raise NotImplementedError + raise AssertionError + + # Don't complain about non-runnable code + if __name__ == .__main__.: + +omit = + /usr/* + py_env/* + */__init__.py + + # Ignore test coverage + tests/* + + # Don't complain about our pre-commit file + pre-commit.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fe21114 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.pyc +.pydevproject +.project +.coverage +/py_env +*.db +.idea +build +dist +*.egg-info diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6e98e9d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: python + +python: + - 2.6 + - 2.7 + +install: pip install virtualenv +script: make diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..62667e1 --- /dev/null +++ b/Makefile @@ -0,0 +1,40 @@ + +TEST_TARGETS = +ITEST_TARGETS = -m integration +UTEST_TARGETS = -m "not(integration)" + +all: _tests + +integration: + $(eval TEST_TARGETS := $(ITEST_TARGETS)) + +unit: + $(eval TEST_TARGETS := $(UTEST_TARGETS)) + +utests: test +utest: test +tests: test +test: unit _tests +itests: itest +itest: integration _tests + +_tests: py_env + bash -c 'source py_env/bin/activate && py.test tests $(TEST_TARGETS)' + +ucoverage: unit coverage +icoverage: integration coverage + +coverage: py_env + bash -c 'source py_env/bin/activate && \ + coverage erase && \ + coverage run `which py.test` tests $(TEST_TARGETS) && \ + coverage report -m' + +py_env: requirements.txt + rm -rf py_env + virtualenv py_env + bash -c 'source py_env/bin/activate && \ + pip install -r requirements.txt' + +clean: + rm -rf py_env diff --git a/README.md b/README.md new file mode 100644 index 0000000..f6d50bb --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +pre-commit-hooks +========== + +Some out-of-the-box hooks for pre-commit. + +See also: https://github.com/asottile/pre-commit diff --git a/pre-commit.py b/pre-commit.py new file mode 100755 index 0000000..5fd8c73 --- /dev/null +++ b/pre-commit.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python + +import collections +import optparse +import os +import os.path +import shutil +import subprocess +import sys + +def __backport_check_output(): + def check_output(*popenargs, **kwargs): + r"""Run command with arguments and return its output as a byte string. + + Backported from Python 2.7 as it's implemented as pure python on stdlib. + + >>> check_output(['/usr/bin/python', '--version']) + Python 2.6.2 + """ + process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs) + output, unused_err = process.communicate() + retcode = process.poll() + if retcode: + cmd = kwargs.get("args") + if cmd is None: + cmd = popenargs[0] + error = subprocess.CalledProcessError(retcode, cmd) + error.output = output + raise error + return output + + if not hasattr(subprocess, 'check_output'): + setattr(subprocess, 'check_output', check_output) + +__backport_check_output() +del __backport_check_output + + +FILE_LIST = 'git ls-files | egrep "\.%s$"' + +ALL_FILES = FILE_LIST % '(php|sql|py|js|htm|c|cpp|h|sh|css)' +JS_FILES = FILE_LIST % 'js' +PY_FILES = FILE_LIST % 'py' +CPP_FILES = FILE_LIST % '(cc|cpp|h)' +C_FILES = FILE_LIST % '(c|h)' +C_LIKE_FILES = FILE_LIST % '(c|cc|cpp|h)' +HEADER_FILES = FILE_LIST % 'h' + +RED = '\033[41m' +GREEN = '\033[42m' +NORMAL = '\033[0m' +COLS = int(subprocess.check_output(['tput', 'cols'])) + +Test = collections.namedtuple('Test', ['command', 'name', 'nonzero', 'config']) + +TESTS = [ + Test( + "%s | xargs pyflakes" % PY_FILES, + 'Py - Pyflakes', + False, 'testpyflakes', + ), + Test( + "%s | xargs grep 'import\sipdb'" % PY_FILES, + 'Py - ipdb', + True, 'testipdb', + ), + Test( + "%s | grep 'tests' | grep -v '_test.py$' | grep -v '__init__.py' | grep -v '/conftest.py'" % PY_FILES, + 'Py - Test files should end in _test.py', + True, 'testtestnames', + ), + Test( + "%s | xargs egrep 'split\(.\\\\n.\)'" % PY_FILES, + 'Py - Use s.splitlines over s.split', + True, 'testsplitlines', + ), + Test( + "%s | xargs grep -H -n -P '\t'" % ALL_FILES, + "All - No tabs", + True, 'testtabs', + ), + Test( + "make test", + "Py - Tests", + False, 'testtests', + ), +] + +def get_git_config(config_name): + config_result = '' + try: + config_result = subprocess.check_output([ + 'git', 'config', config_name + ]) + except subprocess.CalledProcessError: pass + + return config_result.strip() + +def get_pre_commit_path(): + git_top = subprocess.check_output( + ['git', 'rev-parse', '--show-toplevel'] + ).strip() + return os.path.join(git_top, '.git/hooks/pre-commit') + +class FixAllBase(object): + name = None + matching_files_command = None + + def get_all_files(self): + try: + files = subprocess.check_output( + self.matching_files_command, + shell=True, + ) + files_split = files.splitlines() + return [file.strip() for file in files_split] + except subprocess.CalledProcessError: + return [] + + def fix_file(self, file): + '''Implement to fix the file.''' + raise NotImplementedError + + def run(self): + '''Runs the process to fix the files. Returns True if nothign to fix.''' + print '%s...' % self.name + all_files = self.get_all_files() + for file in all_files: + print 'Fixing %s' % file + self.fix_file(file) + return not all_files + +class FixTrailingWhitespace(FixAllBase): + name = 'Trimming trailing whitespace' + matching_files_command = '%s | xargs egrep -l "[[:space:]]$"' % ALL_FILES + + def fix_file(self, file): + subprocess.check_call(['sed', '-i', '-e', 's/[[:space:]]*$//', file]) + +class FixLineEndings(FixAllBase): + name = 'Fixing line endings' + matching_files_command = "%s | xargs egrep -l $'\\r'\\$" % ALL_FILES + + def fix_file(self, file): + subprocess.check_call(['dos2unix', file]) + subprocess.check_call(['mac2unix', file]) + +FIXERS = [ + FixTrailingWhitespace, + FixLineEndings, +] + +def run_tests(): + passed = True + for test in TESTS: + run_test = get_git_config('hooks.%s' % test.config) + if run_test == 'false': + print 'Skipping "%s" due to git config.' % test.name + continue + + try: + retcode = 0 + output = subprocess.check_output( + test.command, shell=True, stderr=subprocess.STDOUT + ) + except subprocess.CalledProcessError as e: + retcode = e.returncode + output = e.output + + pass_fail = '%sSuccess%s' % (GREEN, NORMAL) + failed_test = False + if (retcode and not test.nonzero) or (not retcode and test.nonzero): + pass_fail = '%sFailure(%s)%s' % (RED, retcode, NORMAL) + failed_test = True + + dots = COLS - len(pass_fail) - len(test.name) + print '%s%s%s' % (test.name, '.' * dots, pass_fail) + + if failed_test: + print + print output + passed = False + + return passed + +if __name__ == '__main__': + parser = optparse.OptionParser() + parser.add_option( + '-u', '--uninstall', + action='store_true', dest='uninstall', default=False, + help='Uninstall pre-commit script.' + ) + parser.add_option( + '-i', '--install', + action='store_true', dest='install', default=False, + help='Install pre-commit script.' + ) + opts, args = parser.parse_args() + + if opts.install: + pre_commit_path = get_pre_commit_path() + shutil.copyfile(__file__, pre_commit_path) + os.chmod(pre_commit_path, 0755) + print 'Installed pre commit to %s' % pre_commit_path + sys.exit(0) + elif opts.uninstall: + pre_commit_path = get_pre_commit_path() + if os.path.exists(pre_commit_path): + os.remove(pre_commit_path) + print 'Removed pre-commit scripts.' + + passed = True + for fixer in FIXERS: + passed &= fixer().run() + passed &= run_tests() + + if not passed: + print '%sFailures / Fixes detected.%s' % (RED, NORMAL) + print 'Please fix and commit again.' + print "You could also pass --no-verify, but you probably shouldn't." + print + print "Here's git status for convenience: " + print + os.system('git status') + sys.exit(-1) diff --git a/pre_commit_hooks/__init__.py b/pre_commit_hooks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c2671ad --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +argparse +pyyaml +simplejson + +# Testing requirements +coverage +ipdb +mock +pyflakes +pytest diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..41dc28e --- /dev/null +++ b/setup.py @@ -0,0 +1,12 @@ +from setuptools import find_packages +from setuptools import setup + +setup( + name='pre_commit_hooks', + version='0.0.0', + packages=find_packages('.', exclude=('tests*', 'testing*')), + install_requires=[ + 'argparse', + 'simplejson', + ], +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29