Last active
August 18, 2016 15:44
-
-
Save apexskier/82dce8f071e88a87a5e1 to your computer and use it in GitHub Desktop.
pylint code review git pre-commit hook
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python | |
""" | |
based on http://fitzgeraldnick.com/weblog/9/ | |
A pre-commit hook for git that provides some automated code review. | |
This script must be located at ``$REPO/.git/hooks/pre-commit`` and be | |
executable. | |
""" | |
import os | |
import os.path | |
import signal | |
import shlex | |
import sys | |
import time | |
from subprocess import Popen, PIPE | |
DEBUG = False | |
def run(command, ignore_failure=False): | |
"""Execute a command (string) and return it's output and return code""" | |
try: | |
cmd = Popen(shlex.split(command), stdout=PIPE, stderr=PIPE) | |
except OSError as e: | |
if ignore_failure: | |
return None, 1 | |
else: | |
print("OSError running command: {:s}".format(command)) | |
print(e) | |
sys.exit(1) | |
output = cmd.stdout.read().strip() | |
output += cmd.stderr.read().strip() | |
cmd.communicate() | |
return (output, cmd.returncode) | |
class Messages(object): | |
"""Colored messages for printing to the terminal""" | |
C_HEADER = '\033[90m' | |
C_OKBLUE = '\033[94m' | |
C_OKGREEN = '\033[92m' | |
C_WARNING = '\033[93m' | |
C_FAIL = '\033[91m' | |
C_END = '\033[0m' | |
_template = "[ {}{}" + C_END + " ]" | |
FAIL = _template.format(C_FAIL, "FAIL") | |
WARN = _template.format(C_WARNING, "WARN") | |
PASS = _template.format(C_OKGREEN, "PASS") | |
# Threshold for code to pass the pyflakes test. 10 is the highest score pyflakes | |
# will give to any peice of code. | |
PASS_THRESHOLD = 7 | |
# the base git directory | |
GITTOP, _ = run('git rev-parse --show-cdup') | |
GITDIR, _ = run('git rev-parse --git-dir') | |
# user running this program | |
USER = os.environ['USER'] | |
CHANGED_FILES = [] | |
MERGING = os.path.isfile(os.path.join(GITDIR, "MERGE_HEAD")) | |
NODIFF = run('git diff --quiet')[1] != 0 | |
STASHING = not MERGING and NODIFF | |
# global list of results | |
RESULTS = {} | |
ERROR = False | |
FAIL = False | |
def check_username(): | |
"""make sure your username isn't left in any files""" | |
global FAIL | |
printed_header = False | |
for filename in CHANGED_FILES: | |
output, search = run("grep -n {user} {loc}".format(user=USER, loc=filename)) | |
if search == 0: | |
if not printed_header: | |
print("Checking for username") | |
printed_header = True | |
print(" {} {}".format(Messages.WARN, filename)) | |
for line in output.splitlines(): | |
print(" {}".format(line)) | |
FAIL = True | |
def check_exception(): | |
global FAIL | |
printed_header = False | |
files_changed = [filename for filename in CHANGED_FILES if is_python(filename)] | |
for filename in files_changed: | |
# NOTE: The '...rais" + "e Ex...' below prevents self catching | |
output, search = run("grep -n \"rais" + "e Exception\" {loc}".format(loc=filename)) | |
if search == 0: | |
if not printed_header: | |
print("Checking for debugging exceptions") | |
printed_header = True | |
print(" {} {}".format(Messages.WARN, filename)) | |
for line in output.splitlines(): | |
print(" {}".format(line)) | |
FAIL = True | |
def pyflakes(): | |
"""Checks your git commit with pyflakes!""" | |
# Filter out non-python or deleted files. | |
files_changed = [filename for filename in CHANGED_FILES if is_python(filename)] | |
if files_changed: | |
print("Linting Python") | |
# check for jshint | |
_, return_code = run("pyflakes", ignore_failure=True) | |
if return_code: | |
print("{} pyflakes not found".format(Messages.WARN)) | |
return | |
# Run pyflakes on each file, collect the results, and display them for the | |
# user. | |
for filename in files_changed: | |
output, return_code = run("pyflakes {:s}".format(filename)) | |
lines = [line for line in output.splitlines() if line] | |
print_file(filename, return_code, lines) | |
def is_python(filename): | |
"""Returns True if a file is python code.""" | |
if filename.endswith(".py"): | |
return True | |
# Check if file is python executable | |
if not os.access(filename, os.X_OK): | |
return False | |
try: | |
first_line = open(filename, "r").next().strip() | |
return "#!" in first_line and "python" in first_line | |
except StopIteration: | |
return False | |
def jshint(): | |
"""Checks your git commit with jshint!""" | |
# Get javascript files | |
files_changed = [filename for filename in CHANGED_FILES if is_js(filename)] | |
if files_changed: | |
print("Linting Javascript") | |
# check for jshint | |
_, return_code = run("jshint", ignore_failure=True) | |
if return_code: | |
print("{} jshint not found".format(Messages.WARN)) | |
return | |
for filename in files_changed: | |
output, return_code = run("jshint {:s}".format(filename)) | |
name_len = len(filename) | |
lines = [line[name_len + 2:] for line in output.splitlines() if line] | |
print_file(filename, return_code, lines) | |
def is_js(filename): | |
"""Returns True if a file is javascript""" | |
if filename.endswith(".js"): | |
return True | |
# Check if file is python executable | |
if not os.access(filename, os.X_OK): | |
return False | |
try: | |
first_line = open(filename, "r").next().strip() | |
return "#!" in first_line and "node" in first_line | |
except StopIteration: | |
return False | |
def lint(): | |
"""Run various lint programs""" | |
# NOTE: A better way to do this would be to go through all the changed | |
# files and categorize them into file type, then go through each file and | |
# run it's file types specific lint program | |
pyflakes() | |
jshint() | |
def print_file(filename, return_code, lines): | |
global ERROR | |
if lines or return_code: | |
ERROR = True | |
print(" {} {}".format(Messages.WARN, filename)) | |
for line in lines: | |
print(" {}".format(line)) | |
else: | |
print(" {} {}".format(Messages.PASS, filename)) | |
def update_appcache(): | |
"""Update cache manifest files to force a cache reload""" | |
appcaches = os.path.join(GITTOP, "wwu_housing/static/appcache") | |
updatebool = any("static" in filename for filename in CHANGED_FILES) | |
if updatebool and os.path.isdir(appcaches): | |
for filename in os.listdir(appcaches): | |
if filename.endswith('.appcache'): | |
fileloc = os.path.join(appcaches, filename) | |
print(fileloc) | |
content = [] | |
# replace 2nd line with comment with current time. | |
with open(fileloc, 'r') as filename: | |
content = filename.readlines() | |
content[1] = "# {}\n".format(time.ctime()) | |
filename.close() | |
with open(fileloc, 'w') as filename: | |
filename.writelines(content) | |
filename.close() | |
gitadd = Popen("git add {}".format(appcaches).split()) | |
gitadd.communicate() | |
if gitadd.returncode != 0: | |
sys.exit(-1) | |
def cleanup(signal, frame): | |
if STASHING: | |
run("git stash pop -q") | |
print("\n {} Cancelling commit, to commit anyway use `git commit --no-verify`.".format(Messages.FAIL)) | |
sys.exit(1) # throw a non 0 return code to cancel commit | |
if __name__ == "__main__": | |
signal.signal(signal.SIGINT, cleanup) | |
import argparse | |
parser = argparse.ArgumentParser(description='Validate RestructuredText files.') | |
parser.add_argument('--all', dest='allfiles', action='store_true') | |
parser.add_argument('--nolinks', dest='nolinks', action='store_true') | |
parser.add_argument('--nostash', dest='nostash', action='store_true') | |
parser.add_argument('files', type=str, nargs=argparse.REMAINDER, default=[], help="files to check explicitly") | |
args = parser.parse_args() | |
if args.nostash: | |
STASHING = False | |
if STASHING: | |
run("git stash -q --keep-index") | |
if args.allfiles: | |
CHANGED_FILES = [os.path.join(dp, f) for dp, dn, filenames in os.walk(GITTOP) for f in filenames if os.path.splitext(f)[1] == '.rst'] | |
elif args.files: | |
CHANGED_FILES = args.files | |
else: | |
# Run the git command that gets the filenames of every file that has been | |
# locally modified since the last commit. | |
git_changed, _ = run("git diff --name-only HEAD") | |
# get all changed files | |
CHANGED_FILES = [os.path.join(GITTOP, _f.strip()) for _f in git_changed.splitlines(True) | |
if os.path.isfile(os.path.join(GITTOP, _f.strip()))] | |
if CHANGED_FILES: | |
try: | |
check_username() | |
check_exception() | |
lint() | |
except Exception as e: | |
FAIL = True | |
print(e) | |
if STASHING: | |
run("git stash pop -q") | |
if CHANGED_FILES: | |
update_appcache() | |
if ERROR: | |
print("\nTip: Once you've fixed any issues, use `git commit --amend --no-edit` to add them to your last commit!\n") | |
if FAIL: | |
print(" {} Cancelling commit, to commit anyway use `git commit --no-verify`.".format(Messages.FAIL)) | |
sys.exit(1) # throw a non 0 return code to cancel commit | |
if DEBUG: | |
sys.exit(1) | |
sys.exit(0) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment