#!/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 *git-modified-pattern* (.compile re "^[MA]\s+(?P.*)$")) (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-response [cmd] (let [[fullcmd (+ ["git"] cmd)] [process (subprocess.Popen fullcmd :stdout subprocess.PIPE :stderr subprocess.PIPE)] [(, out err) (.communicate process)]] (, out err process.returncode))) (defn run-git-command [cmd] (let [[fullcmd (+ ["git"] cmd)]] (subprocess.call fullcmd :stdout subprocess.PIPE :stderr subprocess.PIPE))) (defn get-shell-response [fullcmd] (let [[process (subprocess.Popen fullcmd :stdout subprocess.PIPE :stderr subprocess.PIPE :shell True)] [(, out err) (.communicate process)]] (, out err process.returncode))) (defn derive-max-code [code-pairs] (reduce (fn [m i] (if (> (abs (get i 0)) (abs m)) (get i 0) m)) code-pairs 0)) (defn derive-message-bodies [code-pairs] (lmap (fn [i] (get i 1)) code-pairs)) (defn lmap (pred iter) (list (map pred iter))) (defn encode-shell-messages [prefix messages] (lmap (fn [line] (.format "{}{}" prefix (.decode line "utf-8"))) (.splitlines messages))) (defn run-external-checker [filename check] (let [[cmd (-> (get check "command") (.format :filename filename :config_path *config-path*))] [(, out err returncode) (get-shell-response 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 (+ (encode-shell-messages prefix out) (if err (encode-shell-messages prefix err) []))]] [(or returncode 1) output]) [0 []]))) (defn matches-file [filename match-files] (any (map (fn [match-file] (-> (.compile re match-file) (.match filename))) match-files))) (defn check-scan-wanted [filename check] (cond [(and (in "match_files" check) (not (matches-file filename (get check "match_files")))) false] [(and (in "ignore_files" check) (matches-file filename (get check "ignore_files"))) false] [true true])) (defn check-files [filenames check] (let [[filenames-to-check (filter (fn [filename] (check-scan-wanted filename check)) filenames)] [results-of-checks (lmap (fn [filename] (run-external-checker filename check)) filenames-to-check)] [messages (+ [(get check "output")] (derive-message-bodies results-of-checks))]] [(derive-max-code results-of-checks) messages])) (defn gather-all-filenames [] (let [[build-filenames (fn [filenames] (map (fn [f] (os.path.join (get filenames 0) f)) (get filenames 2)))]] (list (flatten (list-comp (build-filenames o) [o (.walk os ".")]))))) (defn gather-staged-filenames [against] (let [[(, out err returncode) (get-git-response ["diff-index" "--name-status" against])] [lines (.splitlines out)] [matcher (fn [line] (.match *git-modified-pattern* (.decode line "utf-8")))]] (list (filter (fn [x] (not (= x ""))) (list-comp (.group match "name") [match (map matcher lines)] match))))) (defn run-checks-for [scan-all-files against] (do (run-git-command ["stash" "--keep-index"]) (let [[filenames-to-scan (if scan-all-files (gather-all-filenames) (gather-staged-filenames against))] [results-of-scan (lmap (fn [check] (check-files filenames-to-scan check)) *checks*)] [exit-code (derive-max-code results-of-scan)] [messages (flatten (derive-message-bodies results-of-scan))]] (do (for [line messages] (print line)) (run-git-command ["reset" "--hard"]) (run-git-command ["stash" "pop" "--quiet" "--index"]) exit-code)))) (defn get-head-tag [] (let [[empty-repository-hash "4b825dc642cb6eb9a060e54bf8d69288fbee4904"] [(, out err returncode) (get-git-response ["rev-parse" "--verify HEAD"])]] (if err empty-repository-hash "HEAD"))) (defmain [&rest args] (let [[scan-all-files (and (> (len args) 1) (= (get args 2) "--all-files"))]] (sys.exit (int (run-checks-for scan-all-files (get-head-tag))))))