The Ops Community ⚙️

Jason Purdy
Jason Purdy

Posted on

2

Local git commit hooks

I mentioned in a previous post about how GitHub can automatically check your code for syntax errors whenever you push an update to the server, but you can also catch those on your local development box before you commit code. You can just add a script in the code repo's .git/hooks/pre-commit path. What we do is have our script in a docs directory and then in our composer.json file, when the developer is installing/updating the php libraries, there is a script that (re)establishes the commit hook:

...
        "post-install-cmd": [
            "rm -rf .git/hooks", "ln -s ../docs/hooks .git/hooks"
        ],
        "post-update-cmd": [
            "rm -rf .git/hooks", "ln -s ../docs/hooks .git/hooks"
        ],
...
Enter fullscreen mode Exit fullscreen mode

As for the script itself, I cannot take credit for it. I think it originally came from this post from Carlos Buenosvinos. It doesn't look like that post is still working, but here is the script I use:

#!/usr/bin/env php
<?php
// Credit: https://carlosbuenosvinos.com/write-your-git-hooks-in-php-and-keep-them-under-git-control/
require __DIR__ . '/../../vendor/autoload.php';
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\Process;
use Symfony\Component\Console\Application;
class CodeQualityTool extends Application
{
private $output;
private $input;
const PHP_FILES_IN_SRC = '/^src\/(.*)(\.php)$/';
const PHP_FILES_IN_CLASSES = '/^classes\/(.*)(\.php)$/';
public function __construct()
{
parent::__construct('Code Quality Tool', '1.0.0');
}
public function doRun(InputInterface $input, OutputInterface $output)
{
$this->input = $input;
$this->output = $output;
$output->writeln('<fg=white;options=bold;bg=red>Code Quality Tool</fg=white;options=bold;bg=red>');
$output->writeln('<info>Fetching files</info>');
$files = $this->extractCommitedFiles();
$output->writeln('<info>Check composer</info>');
$this->checkComposer($files);
$output->writeln('<info>Running PHPLint</info>');
if (!$this->phpLint($files)) {
throw new Exception('There are some PHP syntax errors!');
}
// $output->writeln('<info>Checking code style</info>');
// if (!$this->codeStyle($files)) {
// throw new Exception(sprintf('There are coding standards violations!'));
// }
// $output->writeln('<info>Checking code style with PHPCS</info>');
// if (!$this->codeStylePsr($files)) {
// throw new Exception(sprintf('There are PHPCS coding standards violations!'));
// }
// $output->writeln('<info>Checking code mess with PHPMD</info>');
// if (!$this->phPmd($files)) {
// throw new Exception(sprintf('There are PHPMD violations!'));
// }
// $output->writeln('<info>Running unit tests</info>');
// if (!$this->unitTests()) {
// throw new Exception('Fix the fucking unit tests!');
// }
$output->writeln('<info>Good job!</info>');
}
private function checkComposer($files)
{
$composerJsonDetected = false;
$composerLockDetected = false;
foreach ($files as $file) {
if ($file === 'composer.json') {
$composerJsonDetected = true;
}
if ($file === 'composer.lock') {
$composerLockDetected = true;
}
}
if ($composerJsonDetected && !$composerLockDetected) {
throw new Exception('composer.lock must be commited if composer.json is modified!');
}
}
private function extractCommitedFiles()
{
$output = array();
$rc = 0;
exec('git rev-parse --verify HEAD 2> /dev/null', $output, $rc);
$against = '945a25fc22a5ffd70947f98cf2f5263e664d3663';
if ($rc == 0) {
$against = 'HEAD';
}
exec("git diff-index --cached --name-status $against | egrep '^(A|M)' | awk '{print $2;}'", $output);
return $output;
}
private function phpLint($files)
{
$needle = '/(\.php)|(\.inc)|(\.module)$/';
$succeed = true;
foreach ($files as $file) {
if (!preg_match($needle, $file)) {
continue;
}
$process = new Process(array('php', '-l', $file));
$process->run();
if (!$process->isSuccessful()) {
$this->output->writeln($file);
$this->output->writeln(sprintf('<error>%s</error>', trim($process->getErrorOutput())));
if ($succeed) {
$succeed = false;
}
}
}
return $succeed;
}
private function phPmd($files)
{
$needle = self::PHP_FILES_IN_SRC;
$succeed = true;
$rootPath = realpath(__DIR__ . '/../../');
foreach ($files as $file) {
if (!preg_match($needle, $file) || preg_match('/src\/AtrapaloLib\/ORM\/Doctrine\/DBAL\/Driver\/Adodb/', $file)) {
continue;
}
$process = new Process(['php', 'bin/phpmd', $file, 'text', 'controversial']);
$process->setWorkingDirectory($rootPath);
$process->run();
if (!$process->isSuccessful()) {
$this->output->writeln($file);
$this->output->writeln(sprintf('<error>%s</error>', trim($process->getErrorOutput())));
$this->output->writeln(sprintf('<info>%s</info>', trim($process->getOutput())));
if ($succeed) {
$succeed = false;
}
}
}
return $succeed;
}
private function unitTests()
{
$process = new Process(array('php', 'bin/phpunit'));
$process->setWorkingDirectory(__DIR__ . '/../..');
$process->setTimeout(3600);
$process->run(function ($type, $buffer) {
$this->output->write($buffer);
});
return $process->isSuccessful();
}
private function codeStyle(array $files)
{
$succeed = true;
foreach ($files as $file) {
$classesFile = preg_match(self::PHP_FILES_IN_CLASSES, $file);
$srcFile = preg_match(self::PHP_FILES_IN_SRC, $file);
if (!$classesFile && !$srcFile) {
continue;
}
$fixers = '-psr0';
if ($classesFile) {
$fixers = 'eof_ending,indentation,linefeed,lowercase_keywords,trailing_spaces,short_tag,php_closing_tag,extra_empty_lines,elseif,function_declaration';
}
$process = new Process(array('php', 'bin/php-cs-fixer', '--dry-run', '--verbose', 'fix', $file, '--fixers='.$fixers));
$process->setWorkingDirectory(__DIR__ . '/../../');
$process->run();
if (!$process->isSuccessful()) {
$this->output->writeln(sprintf('<error>%s</error>', trim($process->getOutput())));
if ($succeed) {
$succeed = false;
}
}
}
return $succeed;
}
private function codeStylePsr(array $files)
{
$succeed = true;
$needle = self::PHP_FILES_IN_SRC;
foreach ($files as $file) {
if (!preg_match($needle, $file)) {
continue;
}
$process = new Process(array('php', 'bin/phpcs', '--standard=PSR2', $file));
$process->setWorkingDirectory(__DIR__ . '/../../');
$process->run();
if (!$process->isSuccessful()) {
$this->output->writeln(sprintf('<error>%s</error>', trim($process->getOutput())));
if ($succeed) {
$succeed = false;
}
}
}
return $succeed;
}
}
$console = new CodeQualityTool();
$console->run();
view raw pre-commit.php hosted with ❤ by GitHub



This is for a Drupal project, which already has Symfony installed, so you may need to require that (and there may be other requirements), if you aren't already using that.

You can see by some of the commented-out areas, I'm not doing the full verification because I just haven't had the time to work on that. I kind of want to redo it using Robo, but that's a project for another day.

There are times where you want to bypass the verification. If you're in a hurry (not a good sign ;)), or if there's something wrong with the verification hook, you can skip the verification with the --no-verify argument:

git commit -m 'gotta git this code in place! ;)' --no-verify

Top comments (0)