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); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@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. :)