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 <jacob@alphacomm.nl>
This commit is contained in:
parent
38a0a2fa3d
commit
ebddd9bb7f
1
resources/git-hooks/README.txt
Normal file
1
resources/git-hooks/README.txt
Normal file
@ -0,0 +1 @@
|
||||
You can add global git hooks in this directory.
|
266
resources/git-hooks/pre-commit
Executable file
266
resources/git-hooks/pre-commit
Executable file
@ -0,0 +1,266 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* .git/hooks/pre-commit
|
||||
*
|
||||
* This pre-commit hooks will check for PHP errors (lint), and make sure the
|
||||
* code is PSR-2 compliant.
|
||||
*
|
||||
* @author Reen Lokum http://github.com/reenl
|
||||
*
|
||||
* The orignal version of this file can be found at:
|
||||
* https://github.com/zendframework/zf2/blob/master/README-GIT.md
|
||||
*
|
||||
* Extended with functionality found at:
|
||||
* http://kvz.io/blog/2013/12/29/one-git-commit-hook-to-rule-them-all/
|
||||
*/
|
||||
|
||||
$exit = 0;
|
||||
|
||||
// Initial commit
|
||||
$against = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
|
||||
if (run('git rev-parse --verify HEAD > /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();
|
||||
}
|
0
resources/git-template/.gitkeep
Normal file
0
resources/git-template/.gitkeep
Normal file
1
resources/git-template/hooks
Symbolic link
1
resources/git-template/hooks
Symbolic link
@ -0,0 +1 @@
|
||||
/Users/jacobkiers/dotfiles/resources/git-hooks
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user