Skip to content

Instantly share code, notes, and snippets.

@claudiu-cristea
Forked from pfrenssen/README.md
Last active March 12, 2017 16:40
Show Gist options
  • Save claudiu-cristea/1ec87c25930861654cc94251da69f14b to your computer and use it in GitHub Desktop.
Save claudiu-cristea/1ec87c25930861654cc94251da69f14b to your computer and use it in GitHub Desktop.
Git hook to check coding standards using PHP CodeSniffer before pushing

About

This is a git pre-push hook intended to help developers keep their PHP code base clean by performing a scan with PHP CodeSniffer whenever new code is pushed to the repository. When any coding standards violations are present the push is rejected, allowing the developer to fix the code before making it public.

To increase performance only the changed files are checked when new code is pushed to an existing branch. Whenever a new branch is pushed, a full coding standards check is performed.

If your project enforces the use of a coding standard then this will help with that, but it is easy to circumvent since the pre-push hook can simply be deleted. You might want to implement similar functionality in a pre-receive hook on the server side, or include a coding standards check in your continuous integration pipeline.

Usage

Add PHP CodeSniffer and this gist to your composer.json:

composer.json

{
  "require-dev": {
    "squizlabs/php_codesniffer": "~2.3",
    "pfrenssen/phpcs-pre-push": "1.0"
  },
  "repositories": [
    {
      "type": "package",
      "package": {
        "name": "pfrenssen/phpcs-pre-push",
        "version": "1.0",
        "source": {
          "url": "https://gist.github.com/498fc52fea3f965f6640.git",
          "type": "git",
          "reference": "master"
        }
      }
    }
  ],
  "scripts": {
    "post-install-cmd": "scripts/composer/post-install.sh"
  }
}

Create a post-install script that will symlink the pre-push inside your git repository whenever you do a composer install:

scripts/composer/post-install.sh

#!/bin/sh

# Symlink the git pre-push hook to its destination.
if [ ! -h ".git/hooks/pre-push" ] ; then
  ln -s "../../vendor/pfrenssen/phpcs-pre-push/pre-push" ".git/hooks/pre-push"
fi

Create a PHP CodeSniffer ruleset containing your custom coding standard:

phpcs.xml

<?xml version="1.0" encoding="UTF-8"?>
<!-- See http://pear.php.net/manual/en/package.php.php-codesniffer.annotated-ruleset.php -->
<ruleset name="MyRuleset">
  <description>Custom coding standard for my project</description>

  <rule ref="PSR2" />

  <!--
    Scan the entire root folder by default. If your application code is
    contained in a subfolder such as `lib/` or `src/` you can use that instead.
   -->
  <file>.</file>

  <!-- Minified files don't have to comply with coding standards. -->
  <exclude-pattern>*.min.css</exclude-pattern>
  <exclude-pattern>*.min.js</exclude-pattern>

  <!-- Exclude files that do not contain PHP, Javascript or CSS code. -->
  <exclude-pattern>*.json</exclude-pattern>
  <exclude-pattern>*.sh</exclude-pattern>
  <exclude-pattern>*.xml</exclude-pattern>
  <exclude-pattern>*.yml</exclude-pattern>
  <exclude-pattern>composer.lock</exclude-pattern>

  <!-- Exclude the `vendor` folder. -->
  <exclude-pattern>vendor/</exclude-pattern>

  <!-- PHP CodeSniffer command line options -->
  <arg name="extensions" value="php,inc,css,js"/>
  <arg name="report" value="full"/>
  <arg value="p"/>
</ruleset>

Now run make the post-install.sh script executable and run composer update and you're set! This will download the required packages, and will put the git pre-push hook in place.

$ chmod u+x scripts/composer/post-install.sh
$ composer update
#!/usr/bin/env php
<?php
/**
* @file
* Git pre-push hook to check coding standards before pushing.
*/
/**
* The SHA1 ID of an empty branch.
*/
define ('SHA1_EMPTY', '0000000000000000000000000000000000000000');
$file_list = [];
$full_check = FALSE;
// Loop over the commits.
while ($commit = trim(fgets(STDIN))) {
list ($local_ref, $local_sha, $remote_ref, $remote_sha) = explode(' ', $commit);
// Skip the coding standards check if we are deleting a branch or if there is
// no local branch.
if ($local_ref === '(delete)' || $local_sha === SHA1_EMPTY) {
exit(0);
}
// Do a full check if this is a new branch.
if ($remote_sha === SHA1_EMPTY) {
$full_check = TRUE;
break;
}
// Escape shell command arguments. These should normally be safe since they
// only contain SHA numbers, but you never know.
foreach (['local_sha', 'remote_sha'] as $argument) {
$$argument = escapeshellcmd($$argument);
}
$command = "git diff-tree --no-commit-id --name-only -r '$local_sha' '$remote_sha'";
$file_list = array_merge($file_list, explode("\n", `$command`));
}
// Remove duplicates, empty lines and files that no longer exist in the branch.
$file_list = array_unique(array_filter($file_list, function ($file) {
return !empty($file) && file_exists($file);
}));
// If a phpcs.xml file is present and contains a list of extensions, remove all
// files that do not match the extensions from the list.
if (file_exists('phpcs.xml')) {
$configuration = simplexml_load_file('phpcs.xml');
$extensions = [];
foreach ($configuration->xpath('/ruleset/arg[@name="extensions"]') as $argument) {
// The list of extensions is comma separated.
foreach (explode(',', (string) $argument['value']) as $extension) {
// The type of file can be specified using a slash (e.g. 'module/php') so
// only keep the part before the slash.
if (($position = strpos($extension, '/')) !== FALSE) {
$extension = substr($extension, 0, $position);
}
$extensions[$extension] = $extension;
}
}
if (!empty($extensions)) {
$file_list = array_filter($file_list, function ($file) use ($extensions) {
return array_key_exists(pathinfo($file, PATHINFO_EXTENSION), $extensions);
});
}
// Check white-listed paths.
$allowed_files = array();
foreach ($configuration->xpath('/ruleset/file') as $file) {
$allowed_files[] = (string) $file;
}
if (!empty($allowed_files)) {
$file_list = array_filter($file_list, function ($file) use ($allowed_files) {
$realpath = realpath($file);
$keep = FALSE;
foreach ($allowed_files as $allowed_file) {
// Allow if files is under the path or match exactly.
if (strpos($realpath, $allowed_file) === 0) {
$keep = TRUE;
break;
}
}
return $keep;
});
}
// Check exclude patterns.
foreach ($configuration->xpath('/ruleset/exclude-pattern') as $argument) {
$exclude_pattern = (string) $argument;
// If last char is a slash, that means it is a folder, so everything should
// be excluded.
if (substr($exclude_pattern, -1) == '/') {
$exclude_pattern .= '*';
}
foreach ($file_list as $key => $file) {
if (fnmatch($exclude_pattern, $file)) {
unset($file_list[$key]);
}
}
}
}
if (empty($file_list) && !$full_check) {
exit(0);
}
// Get the path to the PHP CodeSniffer binary from composer.json.
$command = getcwd() . '/vendor/bin/phpcs';
if ($composer_json = json_decode(file_get_contents(getcwd() . '/composer.json'))) {
if (!empty($composer_json->config->{'bin-dir'})) {
$bin_dir = escapeshellcmd(trim($composer_json->config->{'bin-dir'}, '/'));
$command = getcwd() . '/' . $bin_dir . '/phpcs';
}
}
// Check if the PHP CodeSniffer binary is present.
if (!is_executable($command)) {
echo "error: PHP CodeSniffer binary not found at $command\n";
exit(1);
}
// Run PHP CodeSniffer and exit.
$file_filter = $full_check ? '' : " '" . implode("' '", $file_list) . "'";
passthru($command . $file_filter, $return_value);
exit($return_value);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment