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);
}
}
}
}
@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