Skip to content

Instantly share code, notes, and snippets.

@voku
Created December 10, 2018 23:05
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save voku/87f6124734e0b2eab07aeaaf7f8db3b7 to your computer and use it in GitHub Desktop.
Save voku/87f6124734e0b2eab07aeaaf7f8db3b7 to your computer and use it in GitHub Desktop.
A pre-commit-hook example with Code Sniffer + Code Fixer + PHPStan
#!/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
Copy link

integer commented Jun 27, 2019

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

@voku
Copy link
Author

voku commented Jul 3, 2019

@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