Skip to content

Instantly share code, notes, and snippets.

@danielpopdan
Forked from pfrenssen/README.md
Last active April 13, 2019 07:35
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save danielpopdan/990f9521f84d693ccd1a to your computer and use it in GitHub Desktop.
Save danielpopdan/990f9521f84d693ccd1a 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 Drupal code base clean by performing a scan with Drupal Coder 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 Coder and this gist to your composer.json:

composer.json

{
  "require-dev": {
    "drupal-pfrenssen/phpcs-pre-push": "1.0"
  },
  "repositories": [
    {
      "type": "package",
      "package": {
        "name": "drupal-pfrenssen/phpcs-pre-push",
        "version": "1.0",
        "source": {
          "url": "https://gist.github.com/990f9521f84d693ccd1a.git",
          "type": "git",
          "reference": "master"
        }
      }
    }
  ],
  "scripts": {
    "post-install-cmd": "scripts/composer/post-install.sh"
  },
  "require": {
    "drupal/coder": "^7.2"
  }
}

For Drupal 8 please include "drupal/coder": "^8.2".

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/drupal-pfrenssen/phpcs-pre-push/pre-push" ".git/hooks/pre-push"
  vendor/bin/phpcs --config-set installed_paths vendor/drupal/coder/coder_sniffer
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>*.css</exclude-pattern>
  <exclude-pattern>composer.lock</exclude-pattern>

  <!-- Exclude the `vendor` folder. -->
  <exclude-pattern>vendor/</exclude-pattern>
  <!-- Exclude the drupal default folders. -->
  <exclude-pattern>includes/</exclude-pattern>
  <exclude-pattern>misc/</exclude-pattern>
  <exclude-pattern>modules/</exclude-pattern>
  <exclude-pattern>profiles/minimal/</exclude-pattern>
  <exclude-pattern>profiles/standard/</exclude-pattern>
  <exclude-pattern>profiles/testing/</exclude-pattern>
  <exclude-pattern>scripts/</exclude-pattern>
  <exclude-pattern>sites/all/modules/contrib/</exclude-pattern>
  <exclude-pattern>sites/all/libraries/</exclude-pattern>
  <exclude-pattern>sites/default/</exclude-pattern>
  <exclude-pattern>themes/</exclude-pattern>
  <exclude-pattern>authorize.php</exclude-pattern>
  <exclude-pattern>cron.php</exclude-pattern>
  <exclude-pattern>index.php</exclude-pattern>
  <exclude-pattern>install.php</exclude-pattern>
  <exclude-pattern>update.php</exclude-pattern>
  <exclude-pattern>xmlrpc.php</exclude-pattern>

  <!-- PHP CodeSniffer command line options -->
  <arg name="extensions" value="php,js,module,theme,info,install,inc"/>
  <arg name="report" value="full"/>
  <arg name="standard" value="Drupal"/>
  <arg value="p"/>
</ruleset>

Now run make the post-install.sh script executable and run composer install 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 install
#!/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 = [];
// 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);
}
// 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 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]);
}
}
}
// Add arguments to command based on xml arguments.
$other_arguments = array();
foreach ($configuration->xpath('/ruleset/arg') as $argument) {
$value = (string)$argument['value'];
$name = (string)$argument['name'];
if (!empty($name) && $name != 'extensions') {
if (!empty($value)) {
$other_arguments[$name] = $value;
} else {
$other_arguments[$name] = FALSE;
}
}
}
$custom_argumets = '';
foreach ($other_arguments as $name => $value) {
if (!empty($value)) {
$custom_argumets .= ' --' . $name . '=' . $value;
} else {
$custom_argumets .= ' --' . $name;
}
}
}
if (empty($file_list)) {
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 = " '" . implode("' '", $file_list) . "'";
passthru($command . $file_filter . $custom_argumets, $return_value);
exit($return_value);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment