From ebddd9bb7fa6155668f239fff5cbc5dd5bfafdb8 Mon Sep 17 00:00:00 2001 From: Jacob Kiers Date: Tue, 12 May 2015 12:07:08 +0200 Subject: [PATCH] Add git pre-commit hook The pre-commit hook will lint-check the added PHP files and also check their CS. Signed-off-by: Jacob Kiers --- resources/git-hooks/README.txt | 1 + resources/git-hooks/pre-commit | 266 ++++++++++++++++++++++++++++++++ resources/git-template/.gitkeep | 0 resources/git-template/hooks | 1 + tilde/gitconfig | 6 +- 5 files changed, 271 insertions(+), 3 deletions(-) create mode 100644 resources/git-hooks/README.txt create mode 100755 resources/git-hooks/pre-commit create mode 100644 resources/git-template/.gitkeep create mode 120000 resources/git-template/hooks diff --git a/resources/git-hooks/README.txt b/resources/git-hooks/README.txt new file mode 100644 index 0000000..809b52c --- /dev/null +++ b/resources/git-hooks/README.txt @@ -0,0 +1 @@ +You can add global git hooks in this directory. \ No newline at end of file diff --git a/resources/git-hooks/pre-commit b/resources/git-hooks/pre-commit new file mode 100755 index 0000000..2c6950f --- /dev/null +++ b/resources/git-hooks/pre-commit @@ -0,0 +1,266 @@ +#!/usr/bin/env php + /dev/null')) { + $against = 'HEAD'; +} + +// Only run when we're on a branch (to avoid rebase hell) +// http://git-blame.blogspot.nl/2013/06/checking-current-branch-programatically.html +$branch = run('git symbolic-ref --short -q HEAD'); +if (!$branch) { + writeln('Not on any branch'); + exit(0); +} + +/* + * collect all files which have been added, copied or + * modified and store them in an array called output + */ +$diffLines = array(); +exec('git diff-index --cached --full-index --diff-filter=ACM '.$against, $diffLines); + +writeln(); + +// Filter files that don't need a check. +foreach ($diffLines as $line) { + $partList = preg_split('#\s+#', $line, 6); + $hash = $partList[3]; + $status = $partList[4]; + $fileName = $partList[5]; + if ('D' === $status) { + // deleted file; do nothing + continue; + } + + $type = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); + $validator = 'validator_'.$type; + if (!$type || !function_exists($validator)) { + $type = run("git cat-file -p ".$hash." | head -n1 | awk -F/ '/^#\!/ {print \$NF}' | sed 's/^env //g'"); + $validator = 'validator_'.$type; + if (!function_exists($validator)) { + // No validator + writeln(' Skipping "'.format($fileName, 'green').'" no validator available.'); + continue; + } + } + + write(' Checking "'.format($fileName, 'green').'" with validator '.format($type, 'green').'.'); + + $output = ''; + if (!$validator($hash, $fileName, $output)) { + writeln(PHP_EOL.'X ERROR '.implode(PHP_EOL.' ', explode(PHP_EOL, $output)).PHP_EOL, 'red'); + $exit = 1; + continue; + } + writeln(' OK', 'green'); +} + +if ($exit > 0) { + writeln(PHP_EOL."Please fix the above errors and run 'git add'.", 'gray'); +} + +exit($exit); + +function validator_php($hash, $fileName, &$output) +{ + if (validator_php_syntax($hash, $fileName, $output)) { + return validator_php_cs($hash, $fileName, $output); + } + + return false; +} + +function validator_php_syntax($hash, $fileName, &$output) +{ + $output = ''; + $exitCode = 0; + + $result = run('git cat-file -p '.escapeshellarg($hash).' | php -l', $output, $exitCode, "purge", "default"); + if ($result) { + return true; + } + + $output = 'Syntax Error'.PHP_EOL.$output; + + return false; +} + +function validator_php_cs($hash, $fileName, &$output) +{ + // Use .php_cs config if project has one. + $configFile = ''; + if (file_exists('.php_cs')) { + $configFile = ' --config-file='.escapeshellarg(realpath('.php_cs')); + } + + $tmpDir = '/tmp/cs-check/'.$hash; + $tmp = $tmpDir.'/'.$fileName; + run('mkdir -p '.dirname($tmp)); + run('git cat-file -p '.escapeshellarg($hash).' > '.$tmp); + + $return = null; + run('php-cs-fixer fix --dry-run --verbose --level=symfony'.$configFile.' '.escapeshellarg($tmp), $currentOutput, $return, 'default', 'default'); + + run('rm -rf '.escapeshellarg($tmpDir)); + + // Check output + if ($return !== 0) { + $out = explode(PHP_EOL, $currentOutput); + + $rule = null; + foreach ($out as $line) { + if (preg_match('#\s+[0-9]+\)\s#', $line)) { + $rule = $line; + break; + } + } + + if ($rule !== null && preg_match('#\((.*)\)#', $rule, $matches)) { + $output = 'Code Style errors'.PHP_EOL.$matches[1]; + } else { + $output = 'Code Style errors'.PHP_EOL.implode(PHP_EOL, $out).PHP_EOL; + } + + return false; + } + + return true; +} + +/** + * Runs like exec with a few changes: + * - Output is returned as a string. + * - Output is NOT appended. + * - STDERR is also added to the output. + * - STDERR and/or STDOUT can be disabled by passing purge. + * - Returns the first output line if successful and false when failed. + * - If no output is generated and the exit status equals 0 then true is returned. + * + * @param string $command + * @param string &$output + * @param int &$exitCode + * @param string $stdout + * @param string $stderr + * + * @return boolean + */ +function run($command, &$output = '', &$exitCode = 0, $stdout = 'default', $stderr = 'purge') +{ + $descriptors = array( + 0 => array("pipe", "r"), // stdin + 1 => array("pipe", "w"), // stdout + 2 => array("pipe", "w"), // stderr + ); + + $pipes = array(); + + $out = array(); + $process = proc_open($command, $descriptors, $pipes); + fclose($pipes[0]); + unset($pipes[0]); + + do { + $read = $pipes; + $write = $except = array(); + if (!stream_select($read, $write, $except, 5)) { + writeln('Timeout on process: '.$command, 'red'); + break; + } + + foreach ($read as $pipe) { + $pipeId = array_search($pipe, $pipes); + if ($pipeId === false) { + writeln('Unable to determine where the output came from.', 'red'); + } + + if (feof($pipe)) { + fclose($pipe); + if ($pipeId !== false) { + unset($pipes[$pipeId]); + } + continue; + } + + $line = fgets($pipe); + if ($line === false) { + continue; + } + + $color = $stderr; + if ($pipeId == 1) { + $color = $stdout; + } + + if ($color != 'purge') { + $out[] = format(rtrim($line), $color); + } + } + } while (count($pipes) > 0); + + $exitCode = proc_close($process); + $output = implode(PHP_EOL, $out); + + if ($exitCode == 0) { + if (!isset($out[0]) || $out[0] == '') { + return true; + } + + return $out[0]; + } + + return false; +} + +function format($string, $color = 'default') +{ + if ($color == 'default') { + return $string; + } + + if ($color == 'purge') { + return ''; + } + + $colors = array( + 'gray' => 37, + 'green' => 32, + 'red' => 31, + ); + + if (!isset($colors[$color])) { + writeln($color.' is not a valid color.'); + exit(1); + } + + return chr(0x1B).'['.$colors[$color].'m'.$string.chr(0x1B).'[m'; +} + +function writeln($write = '', $color = 'default') +{ + write($write.PHP_EOL, $color); +} + +function write($write = '', $color = 'default') +{ + echo format($write, $color); + flush(); +} diff --git a/resources/git-template/.gitkeep b/resources/git-template/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resources/git-template/hooks b/resources/git-template/hooks new file mode 120000 index 0000000..3380b4a --- /dev/null +++ b/resources/git-template/hooks @@ -0,0 +1 @@ +/Users/jacobkiers/dotfiles/resources/git-hooks \ No newline at end of file diff --git a/tilde/gitconfig b/tilde/gitconfig index ef448f2..18f5259 100644 --- a/tilde/gitconfig +++ b/tilde/gitconfig @@ -45,9 +45,6 @@ [url "git@github.com:jacobkiers/"] insteadOf = "git://github.com/jacobkiers/" -[url "git@bitbucket.org:alphacomm/"] - insteadOf = "https://bitbucket.org/alphacomm/" - [url "git@bitbucket.org:jacobkiers/"] insteadOf = "https://bitbucket.org/jacobkiers/" @@ -91,3 +88,6 @@ [fetch] prune = true + +[init] + templatedir = ~/dotfiles/resources/git-template