Created
December 10, 2018 23:05
-
-
Save voku/87f6124734e0b2eab07aeaaf7f8db3b7 to your computer and use it in GitHub Desktop.
A pre-commit-hook example with Code Sniffer + Code Fixer + PHPStan
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/php | |
<?php | |
/** | |
* // | |
* // add something like this in your "composer post-update-cmd && post-install-cmd" | |
* // | |
* echo "\n\n"; | |
* echo "Run force \"code_check_git_hook.php\" as pre-commit-hook ..."; | |
* $force_pre_commit_hook_cmd = 'ln -sf YOUR_PATH_TO_CODE_CHECK_SCRIPTS/code_check_git_hook.php YOUR_PATH_TO_PROJECT_ROOT/.git/hooks/pre-commit'; | |
* passthru($force_pre_commit_hook_cmd, $return_var); | |
* if ($return_var !== 0) { | |
* echo "\n" . 'error: ' . $force_pre_commit_hook_cmd; | |
* exit(1); | |
* } | |
*/ | |
use Composer\XdebugHandler\XdebugHandler; | |
require_once __DIR__ . '/YOUR_PATH_THE_AUTOLOADER.php'; | |
$xdebug = new XdebugHandler('your-code-check'); | |
$xdebug->check(); | |
unset($xdebug); | |
set_time_limit(0); | |
if ($argc == 1) { | |
echo "No commit hash passed, checking git repository staged area (pre-comit)" . \PHP_EOL; | |
} | |
if ($argc == 2) { | |
// get commit hash from command line argument | |
$commit_hash = $argv[1]; | |
$commit_as_range = $commit_hash . '^..' . $commit_hash; | |
echo "Commit hash passed, checking files from commit: " . $commit_hash . \PHP_EOL; | |
} | |
if ($argc == 3) { | |
// get commit hash from command line argument | |
$commit_hash = $argv[1]; | |
// also get previous (last pushed) commit hash from command line argument | |
$prev_commit_hash = $argv[2]; | |
$commit_as_range = $prev_commit_hash . '..' . $commit_hash; | |
echo "Commit hashes passed, checking files from commit range: " . $commit_as_range . \PHP_EOL; | |
} | |
if (!empty($commit_as_range)) { | |
// commit hash passed, check files from this commit | |
$new_files_cmd = 'git diff --name-only --diff-filter=ACmrtd ' . $commit_as_range . ' | grep -e "\.php$"'; | |
exec($new_files_cmd, $new_files, $return); | |
$changed_files_cmd = 'git diff --name-only --diff-filter=ACMRtd ' . $commit_as_range . ' | grep -e "\.php$"'; | |
exec($changed_files_cmd, $changed_files, $return); | |
} else { | |
// no commit hash passed, check git staged area | |
$merge_commit_cmd = 'git rev-parse -q --verify MERGE_HEAD'; | |
exec($merge_commit_cmd, $merge_commit_hash, $return); | |
if ($merge_commit_hash) { | |
// this is a merge commit, so skip the "pre-commit-check" | |
exit(0); | |
} | |
$new_files_cmd = 'git diff --cached --name-only --diff-filter=ACmrtd HEAD | grep -e "\.php$"'; | |
exec($new_files_cmd, $new_files, $return); | |
$changed_files_cmd = 'git diff --cached --name-only --diff-filter=ACMRtd HEAD | grep -e "\.php$"'; | |
exec($changed_files_cmd, $changed_files, $return); | |
} | |
if (!is_array($changed_files) || count($changed_files) === 0) { | |
// no changes files | |
echo "No changed PHP files." . \PHP_EOL; | |
exit(0); | |
} | |
/* | |
if (is_gitlab_ci() === true) { | |
echo "Changed PHP files: " . \PHP_EOL; | |
echo json_encode($changed_files, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES) . \PHP_EOL; | |
echo "New PHP files: " . \PHP_EOL; | |
echo json_encode($new_files, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES) . \PHP_EOL; | |
} | |
*/ | |
if (!empty($commit_as_range)) { | |
// commit hash passed, check files from this commit | |
echo 'Checking PHP Syntax ...' . "\n"; | |
$php_lint_cmd = 'git diff --name-only --diff-filter=ACMRtd ' . $commit_as_range . ' | grep -e "\.php$" | xargs -d \'\n\' -n1 -P' . get_processor_cores_number() . ' php -l | grep -v \'No syntax errors\'; test $? -eq 1'; | |
exec($php_lint_cmd, $php_errormsg_output, $return); | |
if ($return != 0) { | |
echo 'PHP-Syntax [' . $return . ']: (╯°□°)╯︵ ┻━┻ Please fix the php error before commit.' . "\n" . print_r($php_errormsg_output, true); | |
exit(1); | |
} | |
} else { | |
// no commit hash passed, check git staged area | |
echo 'Checking PHP Syntax ...' . "\n"; | |
$php_lint_cmd = 'git diff --cached --name-only --diff-filter=ACMRtd HEAD | grep -e "\.php$" | xargs -d \'\n\' -n1 -P' . get_processor_cores_number() . ' php -l | grep -v \'No syntax errors\'; test $? -eq 1'; | |
exec($php_lint_cmd, $php_errormsg_output, $return); | |
if ($return != 0) { | |
echo 'PHP-Syntax [' . $return . ']: (╯°□°)╯︵ ┻━┻ Please fix the php error before commit.' . "\n" . print_r($php_errormsg_output, true); | |
exit(1); | |
} | |
} | |
checkCommitChangedFiles($changed_files); | |
checkCommitNewFiles($new_files); | |
exit(0); | |
/** | |
* @return int | |
*/ | |
function get_processor_cores_number(): int { | |
$command = "grep -c processor /proc/cpuinfo"; | |
return (int)shell_exec($command); | |
} | |
/** | |
* @param string $new_file_and_path | |
* | |
* @return bool | |
*/ | |
function isValidFileForPhpStanChecks($new_file_and_path) { | |
if (\strpos($new_file_and_path, '/tests/') !== false) { | |
return false; | |
} | |
if (\strpos($new_file_and_path, '/scripts/') !== false) { | |
return false; | |
} | |
if (\strpos($new_file_and_path, 'Cest.php') !== false) { | |
return false; | |
} | |
$data = file_get_contents($new_file_and_path); | |
if (strpos($data, '@skipPhpStanCheck') !== false) { | |
return false; | |
} | |
return true; | |
} | |
/** | |
* @param string $new_file_and_path | |
* | |
* @return bool | |
*/ | |
function isValidFileForCodeChecks($new_file_and_path) { | |
return strpos($new_file_and_path, '/composer/') === false | |
&& | |
strpos($new_file_and_path, '/archiv/') === false | |
&& | |
strpos($new_file_and_path, '/vendor/') === false | |
&& | |
strpos($new_file_and_path, '/thirdparty/') === false | |
&& | |
strpos($new_file_and_path, '/generated/') === false | |
&& | |
strpos($new_file_and_path, '/_generated/') === false | |
&& | |
strpos($new_file_and_path, '/tmp/') === false | |
&& | |
file_exists($new_file_and_path); | |
} | |
/** | |
* @param string[] $changed_files | |
*/ | |
function checkCommitChangedFiles($changed_files) { | |
$changed_files_string = ''; | |
$changed_files_string_phpstan = ''; | |
$fileCounterTmp = 0; | |
$allCounterTmp = count($changed_files); | |
foreach ($changed_files as $changed_file) { | |
if ($fileCounterTmp === 0) { | |
$changed_files_string = ''; | |
$changed_files_string_phpstan = ''; | |
} | |
$allCounterTmp--; | |
$changed_file_and_path = 'YOUR_PATH_TO_PROJECT_ROOT/' . $changed_file; | |
if (isValidFileForCodeChecks($changed_file_and_path)) { | |
$fileCounterTmp++; | |
$changed_files_string .= ' ' . $changed_file_and_path; | |
if (isValidFileForPhpStanChecks($changed_file_and_path)) { | |
$changed_files_string_phpstan .= ' ' . $changed_file_and_path; | |
} | |
} | |
if ($allCounterTmp > 0 && $fileCounterTmp < 500) { | |
continue; | |
} | |
// reset | |
$fileCounterTmp = 0; | |
if (!$changed_files_string) { | |
// no real changed files | |
continue; | |
} | |
/******************************************************************** | |
* 1.0 Code Sniffer | |
********************************************************************/ | |
$code_sniffer_cmd = 'bash -c "php \ | |
YOUR_PATH_TO_CODE_CHECK_SCRIPTS/check_code_codesniffer.php \ | |
\'' . $changed_files_string . '\' \ | |
\'for changed files\' \ | |
"'; | |
passthru($code_sniffer_cmd, $return); | |
if ($return != 0) { | |
exit($return); | |
} | |
/******************************************************************** | |
* 1.1 Code Fixer | |
********************************************************************/ | |
/* | |
$code_fixer_cmd = 'bash -c "php \ | |
YOUR_PATH_TO_CODE_CHECK_SCRIPTS/check_code_codefixer.php \ | |
\'' . $changed_files_string . '\' \ | |
\'for changed files\' \ | |
"'; | |
passthru($code_fixer_cmd, $return); | |
if ($return != 0) { | |
exit($return); | |
} | |
*/ | |
/******************************************************************** | |
* 1.2 PhpStan | |
********************************************************************/ | |
if ($changed_files_string_phpstan) { | |
$phpstan_cmd = 'bash -c "php \ | |
YOUR_PATH_TO_CODE_CHECK_SCRIPTS/check_code_phpstan.php \ | |
\'' . $changed_files_string_phpstan . '\' \ | |
\'2\' \ | |
\'YOUR_PATH_TO_CODE_CHECK_SCRIPTS/phpstan_legacy.neon\' \ | |
\'for changed files\' \ | |
"'; | |
passthru($phpstan_cmd, $return); | |
if ($return != 0) { | |
exit($return); | |
} | |
} | |
} | |
} | |
/** | |
* @param string[] $new_files | |
*/ | |
function checkCommitNewFiles($new_files) { | |
$new_files_string = ''; | |
$new_files_string_phpstan = ''; | |
$fileCounterTmp = 0; | |
$allCounterTmp = count($new_files); | |
foreach ($new_files as $new_file) { | |
if ($fileCounterTmp === 0) { | |
$new_files_string = ''; | |
$new_files_string_phpstan = ''; | |
} | |
$allCounterTmp--; | |
$new_file_and_path = 'YOUR_PATH_TO_PROJECT_ROOT/' . $new_file; | |
if (isValidFileForCodeChecks($new_file_and_path)) { | |
$fileCounterTmp++; | |
$new_files_string .= ' ' . $new_file_and_path; | |
if (isValidFileForPhpStanChecks($new_file_and_path)) { | |
$new_files_string_phpstan .= ' ' . $new_file_and_path; | |
} | |
} | |
if ($allCounterTmp > 0 && $fileCounterTmp < 500) { | |
continue; | |
} | |
// reset | |
$fileCounterTmp = 0; | |
if (!$new_files_string) { | |
// no real changed files | |
continue; | |
} | |
/******************************************************************** | |
* 2.0 Code Sniffer | |
********************************************************************/ | |
$code_sniffer_cmd = 'bash -c "php \ | |
YOUR_PATH_TO_CODE_CHECK_SCRIPTS/check_code_codesniffer.php \ | |
\'' . $new_files_string . '\' \ | |
\'for new files\' \ | |
"'; | |
passthru($code_sniffer_cmd, $return); | |
if ($return != 0) { | |
exit($return); | |
} | |
/******************************************************************** | |
* 2.1 Code Fixer | |
********************************************************************/ | |
$code_fixer_cmd = 'bash -c "php \ | |
YOUR_PATH_TO_CODE_CHECK_SCRIPTS/check_code_codefixer.php \ | |
\'' . $new_files_string . '\' \ | |
\'for new files\' \ | |
"'; | |
passthru($code_fixer_cmd, $return); | |
if ($return != 0) { | |
exit($return); | |
} | |
/******************************************************************** | |
* 2.2 PhpStan | |
********************************************************************/ | |
if ($new_files_string_phpstan) { | |
$phpstan_cmd = 'bash -c "php \ | |
YOUR_PATH_TO_CODE_CHECK_SCRIPTS/check_code_phpstan.php \ | |
\'' . $new_files_string_phpstan . '\' \ | |
\'3\' \ | |
\'YOUR_PATH_TO_CODE_CHECK_SCRIPTS/phpstan.neon\' \ | |
\'for new files\' \ | |
"'; | |
passthru($phpstan_cmd, $return); | |
if ($return != 0) { | |
exit($return); | |
} | |
} | |
} | |
} |
@integer we run the check every night for all files on the CI server and I did not see any / many errors that where reported by the CI server, because the pre-commit hook prevent the developers to commit non valid code. :)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Your PHPStan checks don't work well. If you run PHPStan only on changed files it can miss some errors. PHPStan have to run over changed files and their dependent files. Check
phpstan dump-deps