Adding a compiled version.
This commit is contained in:
parent
83e9e5cd64
commit
7605727aea
|
@ -0,0 +1,36 @@
|
|||
# pre-commit
|
||||
|
||||
This program is a git pre-commit hook that runs an arbitrary set of
|
||||
syntax, style, and complexity checks against files about to be checked
|
||||
in.
|
||||
|
||||
pre-commit is a git-hook; you install it into your project's .git
|
||||
directory in .git/hooks and make it executable. It will run every time
|
||||
you attempt to commit a collection of files, running the configured list
|
||||
of linters against those files, and will terminate the check-in if any
|
||||
of the files fails.
|
||||
|
||||
pre-commit uses the git-stash to temporarily store any changes you may
|
||||
have made between your "git-add" and your "git-commit"; it therefore
|
||||
checks against your *staged* files, not your *workspace* files. Most
|
||||
hooks do the wrong thing and assume your stage and workspace are the
|
||||
same. This is not necessarily so.
|
||||
|
||||
pre-commit is written in Hy, a Lisp-like dialect of Python. I find Hy's
|
||||
support for "cond", complex anonymous functions, and complex return
|
||||
values highly appealing. The UTF-8 handling in this script means it is
|
||||
compatible only with Hy running atop Python3.
|
||||
|
||||
pre-commit is based on the pre-commit recommendations in Steve Pulec's
|
||||
"Why you need a git-hook and why most are wrong" available at:
|
||||
http://css.dzone.com/articles/why-your-need-git-pre-commit The changes
|
||||
I've made reflect a different set of needs, different possible ways of
|
||||
receiving error conditions, and a slightly nicer output.
|
||||
|
||||
If, while installing this, you encounter a problem, you must return your
|
||||
git repository to its original pre-stash state. Learn to use the
|
||||
following commands correctly:
|
||||
|
||||
git stash list
|
||||
git stash pop
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
#!/usr/bin/env hy
|
||||
|
||||
(def *version* "0.0.2")
|
||||
(import os re subprocess sys)
|
||||
|
||||
; pccs (pre-commit configs) should be a directory under your .git
|
||||
; where you store the RCs for your various linters. If you want to
|
||||
; use a global one, you'll have to edit the configuration entries
|
||||
; below.
|
||||
|
||||
(def *config-path* (os.path.join (.get os.environ "GIT_DIR" "./.git") "pccs"))
|
||||
(def *modified* (.compile re "^[MA]\s+(?P<name>.*)$"))
|
||||
|
||||
(def *checks*
|
||||
[
|
||||
; {
|
||||
; "output" "Checking for debugger commands in Javascript..."
|
||||
; "command" "grep -n debugger {filename}"
|
||||
; "match_files" [".*\.js$"]
|
||||
; "print_filename" True
|
||||
; "error_condition" "output"
|
||||
; }
|
||||
{
|
||||
"output" "Running Jshint..."
|
||||
"command" "jshint -c {config_path}/jshint.rc {filename}"
|
||||
"match_files" [".*\.js$"]
|
||||
"print_filename" False
|
||||
"error_condition" "error"
|
||||
}
|
||||
{
|
||||
"output" "Running Coffeelint..."
|
||||
"command" "coffeelint {filename}"
|
||||
"match_files" [".*\.coffee$"]
|
||||
"print_filename" False
|
||||
"error_condition" "error"
|
||||
}
|
||||
{
|
||||
"output" "Running JSCS..."
|
||||
"command" "jscs -c {config_path}/jscs.rc {filename}"
|
||||
"match_files" [".*\.js$"]
|
||||
"print_filename" False
|
||||
"error_condition" "error"
|
||||
}
|
||||
{
|
||||
"output" "Running pep8..."
|
||||
"command" "pep8 -r --ignore=E501,W293,W391 {filename}"
|
||||
"match_files" [".*\.py$"]
|
||||
"print_filename" False
|
||||
"error_condition" "error"
|
||||
}
|
||||
{
|
||||
"output" "Running xmllint..."
|
||||
"command" "xmllint {filename}"
|
||||
"match_files" [".*\.xml"]
|
||||
"print_filename" False
|
||||
"error_condition" "error"
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
(defn get-git [cmd]
|
||||
(let [[fullcmd (+ ["git"] cmd)]
|
||||
[process (subprocess.Popen fullcmd
|
||||
:stdout subprocess.PIPE
|
||||
:stderr subprocess.PIPE)]
|
||||
[(, out err) (.communicate process)]]
|
||||
(, out err process.returncode)))
|
||||
|
||||
(defn call-git [cmd]
|
||||
(let [[fullcmd (+ ["git"] cmd)]]
|
||||
(subprocess.call fullcmd
|
||||
:stdout subprocess.PIPE
|
||||
:stderr subprocess.PIPE)))
|
||||
|
||||
(defn get-cmd [cmd]
|
||||
(let [[process (subprocess.Popen cmd
|
||||
:stdout subprocess.PIPE
|
||||
:stderr subprocess.PIPE
|
||||
:shell True)]
|
||||
[(, out err) (.communicate process)]]
|
||||
(, out err process.returncode)))
|
||||
|
||||
(defn max-code [code-pairs]
|
||||
(reduce (fn [m i] (if
|
||||
(> (abs (get i 0)) (abs m))
|
||||
(get i 0)
|
||||
m))
|
||||
code-pairs 0))
|
||||
|
||||
(defn message-bodies [code-pairs]
|
||||
(lmap (fn [i] (get i 1)) code-pairs))
|
||||
|
||||
(defn matches-file [filename match-files]
|
||||
(any (map (fn [match-file] (-> (.compile re match-file)
|
||||
(.match filename)))
|
||||
match-files)))
|
||||
|
||||
; Hy is overeager to return an iterators which is consumed during
|
||||
; later traversal. lmap returns a concrete list instead.
|
||||
|
||||
(defn lmap (pred iter) (list (map pred iter)))
|
||||
|
||||
(defn run-external-checker [filename check]
|
||||
(let [[cmd (-> (get check "command") (.format
|
||||
:filename filename
|
||||
:config_path *config-path*))]
|
||||
[(, out err returncode) (get-cmd cmd)]]
|
||||
(if (or (and out (= (.get check "error_condition" "error") "output"))
|
||||
err
|
||||
(not (= returncode 0)))
|
||||
(let [[prefix (if (get check "print_filename")
|
||||
(.format "\t{}:" filename)
|
||||
"\t")]
|
||||
[output (+
|
||||
(lmap (fn [line] (.format "{}{}" prefix (.decode line "utf-8")))
|
||||
(.splitlines out))
|
||||
(if err [err] []))]]
|
||||
[(or returncode 1) output])
|
||||
[0 []])))
|
||||
|
||||
(defn check-file [filename check]
|
||||
(cond [(and (in "match_files" check)
|
||||
(not (matches-file filename (get check "match_files")))) [0 []] ]
|
||||
[(and (in "ignore_files" check)
|
||||
(matches-file filename (get check "ignore_files"))) [0 []] ]
|
||||
[true (run-external-checker filename check)]))
|
||||
|
||||
(defn check-files [filenames check]
|
||||
(let [[scan-results (lmap
|
||||
(fn [filename] (check-file filename check)) filenames)]
|
||||
[messages (+ [(get check "output")] (message-bodies scan-results))]]
|
||||
[(max-code scan-results) messages]))
|
||||
|
||||
(defn get-all-files []
|
||||
(let [[build-filenames
|
||||
(fn [filenames]
|
||||
(map
|
||||
(fn [f] (os.path.join (get filenames 0) f)) (get filenames 2)))]]
|
||||
(flatten (list-comp (build-filenames o) [o (.walk os ".")]))))
|
||||
|
||||
; I removed the originally recommended "-u" command from stash; it was
|
||||
; "cleaning up" far too zealously, deleting my node_modules directory
|
||||
|
||||
(defn get-some-files [against]
|
||||
(let [[(, out err returncode)
|
||||
(get-git ["diff-index" "--name-status" against])]
|
||||
[lines (.splitlines out)]
|
||||
[matcher (fn [line] (.match *modified* (.decode line "utf-8")))]]
|
||||
(filter
|
||||
(fn [x] (not (= x "")))
|
||||
(list-comp (.group match "name") [match (map matcher lines)] match))))
|
||||
|
||||
(defn scan [all-files against]
|
||||
(do
|
||||
(call-git ["stash" "--keep-index"])
|
||||
(let [[toscan
|
||||
(list (if all-files (get-all-files) (get-some-files against)))]
|
||||
[check-results
|
||||
(lmap (fn [check] (check-files toscan check)) *checks*)]
|
||||
[exit-code (max-code check-results)]
|
||||
[messages (flatten (message-bodies check-results))]]
|
||||
(do
|
||||
(for [line messages] (print line))
|
||||
(call-git ["reset" "--hard"])
|
||||
(call-git ["stash" "pop" "--quiet" "--index"])
|
||||
exit-code))))
|
||||
|
||||
; That magic number below is what git requires when the repository is
|
||||
; completely empty.
|
||||
|
||||
(defn get-head-tag []
|
||||
(let [[(, out err returncode) (get-git ["rev-parse" "--verify HEAD"])]]
|
||||
(if err "4b825dc642cb6eb9a060e54bf8d69288fbee4904" "HEAD")))
|
||||
|
||||
(defmain [&rest args]
|
||||
(sys.exit
|
||||
(scan
|
||||
(and (> (len args) 1)
|
||||
(= (get args 2) "--all-files"))
|
||||
(get-head-tag))))
|
|
@ -0,0 +1,153 @@
|
|||
from hy.core.language import filter, flatten, is_integer, map, reduce
|
||||
VERSION = '0.0.2'
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
CONFIG_PATH = os.path.join(os.environ.get('GIT_DIR', './.git'), 'pccs')
|
||||
MODIFIED = re.compile('^[MA]\\s+(?P<name>.*)$')
|
||||
CHECKS = [{'output': 'Running Jshint...', 'command': 'jshint -c {config_path}/jshint.rc {filename}', 'match_files': ['.*\\.js$'], 'print_filename': False, 'error_condition': 'error', }, {'output': 'Running Coffeelint...', 'command': 'coffeelint {filename}', 'match_files': ['.*\\.coffee$'], 'print_filename': False, 'error_condition': 'error', }, {'output': 'Running JSCS...', 'command': 'jscs -c {config_path}/jscs.rc {filename}', 'match_files': ['.*\\.js$'], 'print_filename': False, 'error_condition': 'error', }, {'output': 'Running pep8...', 'command': 'pep8 -r --ignore=E501,W293,W391 {filename}', 'match_files': ['.*\\.py$'], 'print_filename': False, 'error_condition': 'error', }, {'output': 'Running xmllint...', 'command': 'xmllint {filename}', 'match_files': ['.*\\.xml'], 'print_filename': False, 'error_condition': 'error', }]
|
||||
|
||||
def get_git(cmd):
|
||||
|
||||
def _hy_anon_fn_1():
|
||||
fullcmd = (['git'] + cmd)
|
||||
process = subprocess.Popen(fullcmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(out, err) = process.communicate()
|
||||
(out, err)
|
||||
return (out, err, process.returncode)
|
||||
return _hy_anon_fn_1()
|
||||
|
||||
def call_git(cmd):
|
||||
|
||||
def _hy_anon_fn_3():
|
||||
fullcmd = (['git'] + cmd)
|
||||
return subprocess.call(fullcmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
return _hy_anon_fn_3()
|
||||
|
||||
def get_cmd(cmd):
|
||||
|
||||
def _hy_anon_fn_5():
|
||||
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
|
||||
(out, err) = process.communicate()
|
||||
(out, err)
|
||||
return (out, err, process.returncode)
|
||||
return _hy_anon_fn_5()
|
||||
|
||||
def max_code(code_pairs):
|
||||
|
||||
def _hy_anon_fn_7(m, i):
|
||||
return (i[0] if (abs(i[0]) > abs(m)) else m)
|
||||
return reduce(_hy_anon_fn_7, code_pairs, 0)
|
||||
|
||||
def message_bodies(code_pairs):
|
||||
|
||||
def _hy_anon_fn_9(i):
|
||||
return i[1]
|
||||
return lmap(_hy_anon_fn_9, code_pairs)
|
||||
|
||||
def matches_file(filename, match_files):
|
||||
|
||||
def _hy_anon_fn_11(match_file):
|
||||
return re.compile(match_file).match(filename)
|
||||
return any(map(_hy_anon_fn_11, match_files))
|
||||
|
||||
def lmap(pred, iter):
|
||||
return list(map(pred, iter))
|
||||
|
||||
def run_external_checker(filename, check):
|
||||
|
||||
def _hy_anon_fn_16():
|
||||
cmd = check['command'].format(filename=filename, config_path=CONFIG_PATH)
|
||||
(out, err, returncode) = get_cmd(cmd)
|
||||
(out, err, returncode)
|
||||
if ((out and (check.get('error_condition', 'error') == 'output')) or err or (not (returncode == 0))):
|
||||
|
||||
def _hy_anon_fn_15():
|
||||
prefix = ('\t{}:'.format(filename) if check['print_filename'] else '\t')
|
||||
|
||||
def _hy_anon_fn_14(line):
|
||||
return '{}{}'.format(prefix, line.decode('utf-8'))
|
||||
output = (lmap(_hy_anon_fn_14, out.splitlines()) + ([err] if err else []))
|
||||
return [(returncode or 1), output]
|
||||
_hy_anon_var_1 = _hy_anon_fn_15()
|
||||
else:
|
||||
_hy_anon_var_1 = [0, []]
|
||||
return _hy_anon_var_1
|
||||
return _hy_anon_fn_16()
|
||||
|
||||
def check_file(filename, check):
|
||||
return ([0, []] if (('match_files' in check) and (not matches_file(filename, check['match_files']))) else ([0, []] if (('ignore_files' in check) and matches_file(filename, check['ignore_files'])) else (run_external_checker(filename, check) if True else None)))
|
||||
|
||||
def check_files(filenames, check):
|
||||
|
||||
def _hy_anon_fn_20():
|
||||
|
||||
def _hy_anon_fn_19(filename):
|
||||
return check_file(filename, check)
|
||||
scan_results = lmap(_hy_anon_fn_19, filenames)
|
||||
messages = ([check['output']] + message_bodies(scan_results))
|
||||
return [max_code(scan_results), messages]
|
||||
return _hy_anon_fn_20()
|
||||
|
||||
def get_all_files():
|
||||
|
||||
def _hy_anon_fn_24():
|
||||
|
||||
def build_filenames(filenames):
|
||||
|
||||
def _hy_anon_fn_22(f):
|
||||
return os.path.join(filenames[0], f)
|
||||
return map(_hy_anon_fn_22, filenames[2])
|
||||
return flatten([build_filenames(o) for o in os.walk('.')])
|
||||
return _hy_anon_fn_24()
|
||||
|
||||
def get_some_files(against):
|
||||
|
||||
def _hy_anon_fn_28():
|
||||
(out, err, returncode) = get_git(['diff-index', '--name-status', against])
|
||||
(out, err, returncode)
|
||||
lines = out.splitlines()
|
||||
|
||||
def matcher(line):
|
||||
return MODIFIED.match(line.decode('utf-8'))
|
||||
|
||||
def _hy_anon_fn_27(x):
|
||||
return (not (x == ''))
|
||||
return filter(_hy_anon_fn_27, [match.group('name') for match in map(matcher, lines) if match])
|
||||
return _hy_anon_fn_28()
|
||||
|
||||
def scan(all_files, against):
|
||||
call_git(['stash', '--keep-index'])
|
||||
|
||||
def _hy_anon_fn_31():
|
||||
toscan = list((get_all_files() if all_files else get_some_files(against)))
|
||||
|
||||
def _hy_anon_fn_30(check):
|
||||
return check_files(toscan, check)
|
||||
check_results = lmap(_hy_anon_fn_30, CHECKS)
|
||||
exit_code = max_code(check_results)
|
||||
messages = flatten(message_bodies(check_results))
|
||||
for line in messages:
|
||||
print(line)
|
||||
call_git(['reset', '--hard'])
|
||||
call_git(['stash', 'pop', '--quiet', '--index'])
|
||||
return exit_code
|
||||
return _hy_anon_fn_31()
|
||||
|
||||
def get_head_tag():
|
||||
|
||||
def _hy_anon_fn_33():
|
||||
(out, err, returncode) = get_git(['rev-parse', '--verify HEAD'])
|
||||
(out, err, returncode)
|
||||
return ('4b825dc642cb6eb9a060e54bf8d69288fbee4904' if err else 'HEAD')
|
||||
return _hy_anon_fn_33()
|
||||
|
||||
def main(*args):
|
||||
return sys.exit(scan(((len(args) > 1) and (args[2] == '--all-files')), get_head_tag()))
|
||||
if (__name__ == '__main__'):
|
||||
import sys
|
||||
:G_1235 = main(*sys.argv)
|
||||
_hy_anon_var_2 = (sys.exit(:G_1235) if is_integer(:G_1235) else None)
|
||||
else:
|
||||
_hy_anon_var_2 = None
|
Loading…
Reference in New Issue