Skip to content

Instantly share code, notes, and snippets.

@apexskier
Last active August 18, 2016 15:44
Show Gist options
  • Save apexskier/82dce8f071e88a87a5e1 to your computer and use it in GitHub Desktop.
Save apexskier/82dce8f071e88a87a5e1 to your computer and use it in GitHub Desktop.
pylint code review git pre-commit hook
#!/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