Skip to content

Instantly share code, notes, and snippets.

@skliarpawlo
Created November 18, 2015 15:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save skliarpawlo/42c51b9798e7bf88b3e1 to your computer and use it in GitHub Desktop.
Save skliarpawlo/42c51b9798e7bf88b3e1 to your computer and use it in GitHub Desktop.
git hooks
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import re
import sys
EMO = os.environ.get('TUBULAR_COMMIT_EMOJI', '👮 ')
SHORT_DESCRIPTION_LINE_REGEX = re.compile('^((?:[A-Z]+-\d+)|FIX|HOTFIX|IMP) (.{1,50})$')
EMOJI_REGEX = re.compile('^:.*:$')
def green(msg):
return '\033[92m{}\033[0m'.format(msg)
def yellow(msg):
return '\033[93m{}\033[0m'.format(msg)
def red(msg):
return '\033[91m{}\033[0m'.format(msg)
def cyan(msg):
return '\033[96m{}\033[0m'.format(msg)
def read_commit_message():
msg_file = sys.argv[1]
with open(msg_file) as f:
msg = f.readlines()
return msg
def check_commit_message(msg):
# filter comments
msg = [line for line in msg if not line.startswith('#')]
bump_match = EMOJI_REGEX.search(msg[0])
if bump_match is not None:
return True
match = SHORT_DESCRIPTION_LINE_REGEX.search(msg[0])
if match is None:
sys.stdout.write(red('ERROR: first line is not formatted correctly\n'))
sys.stdout.write(yellow('Hint: check if it starts with XXXX-1234<space> or '
'FIX|HOTFIX|IMP<space>\n'))
sys.stdout.write(yellow('Hint: check if short message is in lowercase and '
'contains less then 50 symbols\n'))
return False
groups = match.groups()
short_message = groups[1]
if not short_message[0].islower():
sys.stdout.write(red('ERROR: short description is not in lowercase\n'))
return False
if len(msg) > 1 and msg[1].strip() != '':
sys.stdout.write(red('ERROR: second line is not empty\n'))
return False
return True
def main():
sys.stdin = open('/dev/tty')
msg = read_commit_message()
sys.stdout.write(cyan('{} Checking commit message\n'.format(EMO)))
if not check_commit_message(msg):
quit = input('Commit message does not conform TEP-35, do you want '
'to fix that? [Y/n] ').strip().lower()
if quit != 'n':
return 1
else:
sys.stdout.write(green('SUCCESS - '))
sys.stdout.write(cyan('Ready to commit\n\n'))
return 0
sys.exit(main())
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import re
import subprocess
import sys
NON_EXISTENT_MOCK_METHODS = [
'assert_calls',
'assert_not_called',
'assert_called',
'assert_called_once',
'not_called',
'called_once',
]
EMO = os.environ.get('TUBULAR_COMMIT_EMOJI', '👮 ')
MODIFIED = re.compile(r'^(?P<status>\w)\s+(?P<name>.*)')
COMMIT_CHECKS = (
{
'output': 'Checking for console.log()... ',
'command': 'grep -n -e "[^\/\/]console.log" {0}',
'match_files': ['static\/.*\.(js|coffee)$'],
'print_filename': True,
'only_warn': False
},
{
'output': 'Checking for alert()... ',
'command': 'grep -n alert\( {0}',
'match_files': ['.*\.[js|coffee]$'],
'print_filename': True,
'only_warn': False
},
{
'output': 'Checking for toxic whitespace... ',
'command': 'file {0} | grep -q text && grep -n -e " $" {0}',
'match_files': ['.*'],
'ignore_files': ['.*\.whl$', '.*\.(GET|POST|PUT|DELETE|HEAD)$', '.*\.(sql|json)'],
'print_filename': True,
'only_warn': False
},
{
'output': 'Checking for print statements... ',
'command': "grep -n -E '(^|\s)print[ \(]' {0} | sed -n '/\ file=/!p'",
'match_files': ['.*\.py$'],
'print_filename': True,
'only_warn': True
},
{
'output': 'Checking for non existent mock methods(... ',
'command': "grep -n -e '\(\." + '\|\.'.join(NON_EXISTENT_MOCK_METHODS) + "\)(' {0} | sed -n '/\ file=/!p'",
'match_files': ['.*\.py$'],
'print_filename': True,
'only_warn': False
},
{
'output': 'Checking PyFlakes... ',
'command': 'pyflakes {0}',
'match_files': ['.*\.py$'],
'print_filename': True,
'only_warn': False
},
# PEP8 is the canonical Python style guide. We're going to be using
# it with a few exceptions:
# * E501 which suggests 79 character line length can be ignored
# * E221 which wants no extra spaces in assignment will be ignored
# * E712 comparison to True (this is useful but peewee borks it)
# * E126 which wants a hanging indent to be visually aligned
#
# For everything else, see:
# https://www.python.org/dev/peps/pep-0008
{
'output': 'Checking pep8... ',
'command': 'pep8 {0} --ignore=E501,E221,E712,E126',
'match_files': ['.*\.py$'],
'print_filename': True,
'only_warn': True
},
# Readability counts. While pep8 suggests 79 characters, a quick
# poll suggests that number is too austere for most. We're going
# with a 99 character max. Please note that matching 100 characters
# means an effective line length of 99, which gives room on your screen
# for 1 column with a line break or wrapper glyph (if any) when you
# set your editor at 100.
#
# See a well written explanation here:
# https://www.python.org/dev/peps/pep-0008/#maximum-line-length
{
'output': 'Checking for line length... ',
'command': "egrep -n '.{{100}}' {0}",
'match_files': ['.*\.py$'],
'print_filename': True,
'only_warn': True
}
)
def green(msg):
return '\033[92m{}\033[0m'.format(msg)
def yellow(msg):
return '\033[93m{}\033[0m'.format(msg)
def red(msg):
return '\033[91m{}\033[0m'.format(msg)
def cyan(msg):
return '\033[96m{}\033[0m'.format(msg)
def matches_file(file_name, match_files):
return any(re.compile(match_file).match(file_name) for match_file in match_files)
def kick_offending_file_out_of_staging(file_name):
subprocess.call(['git', 'reset', 'HEAD', file_name],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
def list_submodules():
shell_cmd = "cd $(git rev-parse --show-toplevel) && " \
"git submodule summary | grep \"^\*\" | cut -f2 -d' ' && " \
"cd - > /dev/null"
out, err = subprocess.Popen([shell_cmd], stdout=subprocess.PIPE, shell=True).communicate()
submodules = out.decode().strip().split('\n')
return submodules
def check_files(files, check, end_result):
result = 0
warnings = False
sys.stdout.write(check['output'])
for status, file_name in files:
if status in ['D']:
continue
if file_name in list_submodules():
continue
if 'match_files' not in check or matches_file(file_name, check['match_files']):
if 'ignore_files' not in check or not matches_file(file_name, check['ignore_files']):
process = subprocess.Popen(check['command'].format(file_name),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE, shell=True)
out, err = process.communicate()
if err:
result = 1
sys.stdout.write(yellow(err))
if out:
if check['print_filename']:
prefix = '\t%s:' % file_name
else:
prefix = '\t'
output_lines = ['%s%s' % (prefix, line) for line in out.splitlines()]
if check['only_warn']:
warnings = True
sys.stdout.write(yellow('\n' + '\n'.join(output_lines) + '\n'))
else:
result = 1
sys.stdout.write(red('\n' + '\n'.join(output_lines) + '\n'))
kick_offending_file_out_of_staging(file_name)
if warnings and end_result == 0:
quit = input(
'Would you like to quit and handle the found warnings? '
'Press \'a\' - to run autopep and retry [Y/n/a] ').strip().lower()
if quit == 'a':
sys.stdout.write(cyan('Running pre-commit hook autopep8\n'))
pep8ify(files)
main()
sys.exit(0)
if quit != 'n':
sys.exit(-1) # Need a non-zero exit to not commit
if result == 0 and not warnings:
sys.stdout.write(green('OK\n'))
return result
def pep8ify(files):
with_errors = False
for mode, file_path in files:
if not os.path.exists(file_path):
continue
if not file_path.endswith('.py'):
continue
sys.stdout.write(cyan('Auto pep8ifing files in staging'))
proc = subprocess.Popen(
'autopep8 --in-place --aggressive --ignore=E501,E221,E712,E126 {file_path}'.format(
file_path=file_path,
),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True)
out, err = proc.communicate()
if err:
sys.stdout.write(red('FAIL - '))
sys.stdout.write(cyan('Could not autopep8ify file {file_path}'.format(
file_path=file_path
)))
with_errors = True
else:
sys.stdout.write(green('OK - '))
sys.stdout.write(cyan('{file_path}'.format(
file_path=file_path
)))
sys.stdout.write(cyan('Adding reformatted file to staging'))
git_proc = subprocess.Popen(
'git add {file_path}'.format(
file_path=file_path,
),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True)
out, err = git_proc.communicate()
if err:
sys.stdout.write(red('FAIL - '))
sys.stdout.write(cyan('could not add file {file_path} to staging'.format(
file_path=file_path
)))
with_errors = True
if with_errors:
sys.stdout.write(red('\nFINISHED WITH ERRORS\n'))
else:
sys.stdout.write(green('\nSUCCESS\n'))
def run_tests():
sys.stdout.write('Running unittests... ')
sys.stdout.flush()
proc = subprocess.Popen(
'TUBULAR_SETTINGS_ENV=test nosetests .',
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True)
out, err = proc.communicate()
if proc.returncode:
sys.stdout.write(red('FAIL - '))
sys.stdout.write(cyan('Tests failed\n'))
sys.stdout.write(err.decode('utf-8'))
return proc.returncode
sys.stdout.write(green('OK\n'))
return 0
def main():
"""Main entry point for programme to lint stdin lines or staged/cached git files."""
end_result = 0
# For the Jenkins test we need to be able to pipe to this linter
stdin_lines = sys.stdin.readlines()
if stdin_lines:
assert hasattr(MODIFIED.match(stdin_lines[0]), 'groups'), 'Input does not match pattern'
files = [MODIFIED.match(line).groups() for line in stdin_lines]
# Get list of files that are staged/cached in GIT (doesn't include untracked or modified files)
else:
out = subprocess.check_output([
'git', 'diff-index', '--cached', '--name-status', 'HEAD'
])
files = [MODIFIED.match(line.decode('utf8')).groups() for line in out.splitlines()]
sys.stdin = open('/dev/tty') # Default fd=0 is /dev/null
if files:
sys.stdout.write(
cyan('{} Running pre-commit hook code validation on staged files\n'.format(EMO))
)
for check in COMMIT_CHECKS:
end_result = check_files(files, check, end_result) or end_result
if os.environ.get('TUBULAR_PRECOMMIT_RUNTESTS', False):
end_result = end_result or run_tests()
if end_result == 0:
sys.stdout.write(green('SUCCESS - '))
sys.stdout.write(cyan('Ready to commit\n\n'))
else:
sys.stdout.write(red('FAIL - '))
sys.stdout.write(cyan('Not committing. Unstaged offending files. '
'Don\'t forget to `git add` these files after fixing them.\n'))
return end_result
if __name__ == '__main__':
sys.exit(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment