Skip to content

Instantly share code, notes, and snippets.

@bdlangton
Last active June 1, 2022 19:13
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save bdlangton/a8b3c2f81ae167fd44e28ee31f0ee21b to your computer and use it in GitHub Desktop.
Save bdlangton/a8b3c2f81ae167fd44e28ee31f0ee21b to your computer and use it in GitHub Desktop.
Git hooks

Install required packages

Install the packages that you'll need. Drupal/coder includes phpcs. If you're not wanting to use a specific hook, then you don't need to install the associated packages.

composer global require drupal/coder phpmd/phpmd sebastian/phpcpd

Set path

Set the path in your shell startup script (ex: .zshrc or .bashrc).

export PATH="$PATH:$HOME/.composer/vendor/bin"

Configure phpcs

Set Drupal code sniffer.

phpcs --config-set installed_paths ~/.composer/vendor/drupal/coder/coder_sniffer

Set Drupal as the default sniffer (optional).

phpcs --config-set default_standard Drupal

Copy all files in this gist

Set up the git template directory.

git config --global init.templatedir '~/.git-templates'`
mkdir -p ~/.git-templates/hooks

Remove any files now that you don't want to use. Perhaps you want to remove the prepare-commit hooks, or just remove one or two pre-commit hooks. Unless you are getting rid of all pre-commit hooks, you want to keep the pre-commit file.

Copy all files into ~/.git-templates/hooks.

Make sure all of the scripts are executable.

chmod a+x ~/.git-templates/hooks/*

Modify files

You may want to modify phpmd-ruleset.xml. This file is intended as a starting point, but there are many rules that you may or may not want included. Configure it to your liking.

Every pre-commit hook has the ability to prevent a commit from happening when errors are encountered. For several of them, I commented out the code that would prevent the commit. If you want to be more strict, then uncomment those lines.

Most of the commit hooks have arrays defined for file extensions to include, file patterns to ignore, and directories to ignore. Modify these as appropriate for your own projects. These are configured for common Drupal 7/8 patterns.

Copy git hooks into specific repositories

Any new repository inited will get all of the hooks in the templates dir.

For existing repos, running git init will copy in the hooks as well, but it won't override hooks that already exist in the local repo. To override hooks that already exist, you need to delete the hook, then run git init. You could also use this script that deletes existing hooks for you, then runs git init: https://gist.github.com/bdlangton/a03686bbe0387f5b2539350bd3a3c453

#!/usr/bin/php
<?php
/**
* @file
* Runs the commit message against Git standards.
*/
// Initial values for some variables.
$exit_status = 0;
// Get the commit message.
$message = file_get_contents($argv[1]);
$line_num = 1;
foreach (preg_split('/\v/', $message, -1) as $line) {
// Once we reach commented out lines auto generated by git then we are done.
if (substr($line, 0, 1) == '#') {
break;
}
// The first line.
if ($line_num == 1) {
// The subject should be 50 characters or less.
// This is not required, just recommended.
if (strlen($line) > 50) {
echo "The commit subject should be 50 characters or less. Yours is " . strlen($line) . " characters.", "\n";
}
// The subject must begin with a capital letter.
if (!ctype_upper(substr($line, 0, 1))) {
echo "The commit subject must start with a capital letter.", "\n";
$exit_status = 1;
}
// The subject must not end in a period.
if (substr($line, -1, 1) == '.') {
echo "The commit subject must not end with a period.", "\n";
$exit_status = 1;
}
}
// The second line (if it exists) must be blank.
if ($line_num == 2 && !empty($line)) {
echo "The second line of the commit message must be blank.", "\n";
$exit_status = 1;
}
// The third line: start of the body.
if ($line_num == 3) {
// The body must begin with a capital letter.
if (!ctype_upper(substr($line, 0, 1))) {
echo "The commit body must start with a capital letter.", "\n";
$exit_status = 1;
}
}
// Every line must be 72 characters or less.
if (strlen($line) > 72) {
echo "Every line of the commit message must be 72 characters or less. Line $line_num is " . strlen($line) . " characters.", "\n";
$exit_status = 1;
}
$line_num++;
}
exit($exit_status);
<?xml version="1.0"?>
<ruleset name="PHPMD ruleset"
xmlns="http://pmd.sf.net/ruleset/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0
http://pmd.sf.net/ruleset_xml_schema.xsd"
xsi:noNamespaceSchemaLocation="
http://pmd.sf.net/ruleset_xml_schema.xsd">
<description>
My custom ruleset.
</description>
<!-- Import rulesets -->
<rule ref="rulesets/cleancode.xml">
<exclude name="ElseExpression" />
</rule>
<rule ref="rulesets/codesize.xml" />
<rule ref="rulesets/controversial.xml">
<exclude name="CamelCaseParameterName" />
<exclude name="CamelCaseVariableName" />
</rule>
<rule ref="rulesets/design.xml" />
<rule ref="rulesets/naming.xml" />
<rule ref="rulesets/unusedcode.xml" />
</ruleset>
#!/bin/sh
# Check that the git directory exists.
GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)
if [ -z "$GIT_DIR" ]; then
echo >&2 "Fatal: GIT_DIR not set"
exit 1
fi
# Loop through every pre-commit_* file.
for i in `ls $GIT_DIR/hooks/pre-commit_*`;
do $i;
if [ "$?" -ne "0" ]; then
exit 1
fi
done;
exit 0;
#!/usr/bin/php
<?php
/**
* @file
* Checks if contrib modules were updated that build.make was also updated.
*/
// Initial values for some variables.
$files = [];
$rev = [];
$return = 0;
$exit_status = 0;
$root_dir = '/';
// Get the top level directory.
exec('git rev-parse --show-toplevel', $root_dir, $return);
// If the contrib path doesn't exist, bail.
$contrib_path = 'docroot/sites/all/modules/contrib';
if (!file_exists($root_dir . '/' . $contrib_path)) {
return;
}
// Find contrib files that have been changed in this commit.
exec('git rev-parse --verify HEAD 2> /dev/null', $rev, $return);
$against = $return == 0 ? 'HEAD' : '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
exec("git diff-index --cached --name-only {$against} {$contrib_path}", $files);
// If no contrib files were changed, then there is no issue.
if (empty($files)) {
return;
}
// If the build make file doesn't exist, bail.
$build_make = 'build.make';
if (!file_exists($root_dir . '/' . $build_make)) {
return;
}
// Check if the build.make file was changed.
$build_make_changed = FALSE;
exec("git diff-index --cached --name-only {$against} {$build_make}", $build_make_changed);
// If the build make was changed, then there is no issue.
if (!empty($build_make_changed)) {
return;
}
$exit_status = 1;
echo "\nCommit was aborted because the following file(s) were updated but build.make wasn't:\n" . implode("\n", $files), "\n";
exit($exit_status);
#!/usr/bin/php
<?php
/**
* @file
* Checks for debugging code in modified files.
*/
// Initial values for some variables.
$files = [];
$rev = [];
$return = 0;
$exit_status = 0;
// Find files that have been changed in this commit.
exec('git rev-parse --verify HEAD 2> /dev/null', $rev, $return);
$against = $return == 0 ? 'HEAD' : '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
exec("git diff-index --cached --name-only {$against}", $files);
// File extensions to check. If a modified file doesn't contain one of these
// extensions, then it will be skipped.
$file_exts = [
'php',
'module',
'inc',
'install',
'test',
'profile',
'theme',
'txt',
'class',
];
// Ignore filenames that contain these strings.
$ignore_filename_strings = [
'_default.inc',
'context.inc',
'ds.inc',
'features',
'field_group.inc',
'rules_defaults',
'strongarm.inc',
];
// Ignore file paths that contain these strings.
$ignore_file_path_strings = [
'contrib',
'core',
'vendor',
];
// Debugging code segments to make sure they weren't committed.
$debugging_searches = [
'dpm(',
'dvm(',
'dsm(',
'dpr(',
'kpr(',
'ksm(',
'kint(',
'dvr(',
'print_r(',
'var_dump(',
'var_export(',
'console\.log',
];
// Loop through each file that has been modified in this commit.
foreach ($files as $file) {
// Skip files that don't exist.
if (!file_exists($file)) {
continue;
}
// Get the filename and extension.
$filename = pathinfo($file, PATHINFO_BASENAME);
$ext = pathinfo($file, PATHINFO_EXTENSION);
// Skip over the file if it matches an ignored filename or an ignored file
// path, or does not match one of the included file extensions.
$ignore_filenames = array_filter($ignore_filename_strings, function ($item) use ($filename) {
return strpos($filename, $item) !== FALSE;
});
$ignore_file_paths = array_filter($ignore_file_path_strings, function ($item) use ($file) {
return strpos($file, $item) !== FALSE;
});
if (!in_array($ext, $file_exts) || !empty($ignore_filenames) || !empty($ignore_file_paths)) {
continue;
}
// Check for debugging code that was committed.
foreach ($debugging_searches as $search) {
$debugging_output = [];
exec("git diff --cached --unified=0 " . escapeshellarg($file) . " | grep '\+.*" . $search . "'", $debugging_output);
if (!empty($debugging_output)) {
echo "Commit aborted. Debugging code found:\n" . implode("\n", $debugging_output), "\n";
$exit_status = 1;
}
}
}
exit($exit_status);
#!/usr/bin/php
<?php
/**
* @file
* Runs modified files against PHPLint.
*/
// Initial values for some variables.
$files = [];
$rev = [];
$return = 0;
$exit_status = 0;
// Find files that have been changed in this commit.
exec('git rev-parse --verify HEAD 2> /dev/null', $rev, $return);
$against = $return == 0 ? 'HEAD' : '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
exec("git diff-index --cached --name-only {$against}", $files);
// File extensions to check. If a modified file doesn't contain one of these
// extensions, then it will be skipped.
$file_exts = [
'php',
'module',
'inc',
'install',
'test',
'profile',
'theme',
'txt',
'class',
];
// Ignore filenames that contain these strings.
$ignore_filename_strings = [
'_default.inc',
'context.inc',
'ds.inc',
'features',
'field_group.inc',
'rules_defaults',
'strongarm.inc',
];
// Ignore file paths that contain these strings.
$ignore_file_path_strings = [
'contrib',
'core',
'vendor',
];
// Loop through each file that has been modified in this commit.
foreach ($files as $file) {
// Skip files that don't exist.
if (!file_exists($file)) {
continue;
}
// Get the filename and extension.
$filename = pathinfo($file, PATHINFO_BASENAME);
$ext = pathinfo($file, PATHINFO_EXTENSION);
// Skip over the file if it matches an ignored filename or an ignored file
// path, or does not match one of the included file extensions.
$ignore_filenames = array_filter($ignore_filename_strings, function ($item) use ($filename) {
return strpos($filename, $item) !== FALSE;
});
$ignore_file_paths = array_filter($ignore_file_path_strings, function ($item) use ($file) {
return strpos($file, $item) !== FALSE;
});
if (!in_array($ext, $file_exts) || !empty($ignore_filenames) || !empty($ignore_file_paths)) {
continue;
}
// Run lint on the file.
$lint_output = [];
exec("php -l " . escapeshellarg($file), $lint_output, $return);
if ($return == 0) {
continue;
}
echo implode("\n", $lint_output), "\n";
$exit_status = 1;
}
exit($exit_status);
#!/usr/bin/php
<?php
/**
* @file
* Runs phpcpd against the repo.
*/
// Initial values for some variables.
$return = 0;
$exit_status = 0;
// Run phpcpd on the file.
$phpcpd_output = [];
$phpcpd_cmd = "phpcpd --exclude=vendor --exclude=core --exclude=contrib --exclude=sites/default .";
$file = escapeshellarg($file);
exec($phpcpd_cmd, $phpcpd_output, $return);
if (empty($phpcpd_output)) {
return;
}
echo implode("\n", $phpcpd_output), "\n";
// If any errors were returned, then prevent commit.
if ($return != 0) {
// $exit_status = $return;
// echo "\nCommit was aborted because of duplicates. You must fix the errors.", "\n";
}
exit($exit_status);
#!/usr/bin/php
<?php
/**
* @file
* Runs modified files against phpcs using the Drupal coding standards.
*/
// Initial values for some variables.
$standard = 'Drupal';
$files = [];
$rev = [];
$return = 0;
$exit_status = 0;
// Find files that have been changed in this commit.
exec('git rev-parse --verify HEAD 2> /dev/null', $rev, $return);
$against = $return == 0 ? 'HEAD' : '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
exec("git diff-index --cached --name-only {$against}", $files);
// File extensions to check. If a modified file doesn't contain one of these
// extensions, then it will be skipped.
$file_exts = [
'php',
'module',
'inc',
'install',
'test',
'profile',
'theme',
'txt',
'class',
];
// Ignore filenames that contain these strings.
$ignore_filename_strings = [
'_default.inc',
'context.inc',
'ds.inc',
'features',
'field_group.inc',
'rules_defaults',
'strongarm.inc',
];
// Ignore file paths that contain these strings.
$ignore_file_path_strings = [
'contrib',
'core',
'vendor',
];
// Loop through each file that has been modified in this commit.
foreach ($files as $file) {
// Skip files that don't exist.
if (!file_exists($file)) {
continue;
}
// Get the filename and extension.
$filename = pathinfo($file, PATHINFO_BASENAME);
$ext = pathinfo($file, PATHINFO_EXTENSION);
// Skip over the file if it matches an ignored filename or an ignored file
// path, or does not match one of the included file extensions.
$ignore_filenames = array_filter($ignore_filename_strings, function ($item) use ($filename) {
return strpos($filename, $item) !== FALSE;
});
$ignore_file_paths = array_filter($ignore_file_path_strings, function ($item) use ($file) {
return strpos($file, $item) !== FALSE;
});
if (!in_array($ext, $file_exts) || !empty($ignore_filenames) || !empty($ignore_file_paths)) {
continue;
}
// Run phpcs on the file.
$phpcs_output = [];
$extensions = implode(',', $file_exts);
$phpcs_cmd = "phpcs --standard=$standard --extensions=$extensions $file";
$file = escapeshellarg($file);
exec($phpcs_cmd, $phpcs_output, $return);
if (empty($phpcs_output)) {
continue;
}
echo implode("\n", $phpcs_output), "\n";
// If any errors were returned, then prevent commit.
if ($return != 0) {
#$exit_status = $return;
#echo "\nCommit was aborted because of errors. You must fix the errors (warnings are optional).", "\n";
}
}
exit($exit_status);
#!/usr/bin/php
<?php
/**
* @file
* Runs modified files against phpmd.
*/
// Initial values for some variables.
$files = [];
$rev = [];
$return = 0;
$exit_status = 0;
// Find files that have been changed in this commit.
exec('git rev-parse --verify HEAD 2> /dev/null', $rev, $return);
$against = $return == 0 ? 'HEAD' : '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
exec("git diff-index --cached --name-only {$against}", $files);
// File extensions to check. If a modified file doesn't contain one of these
// extensions, then it will be skipped.
$file_exts = [
'php',
'module',
'inc',
'install',
'test',
'profile',
'theme',
'txt',
'class',
];
// Ignore filenames that contain these strings.
$ignore_filename_strings = [
'_default.inc',
'context.inc',
'ds.inc',
'features',
'field_group.inc',
'rules_defaults',
'strongarm.inc',
];
// Ignore file paths that contain these strings.
$ignore_file_path_strings = [
'contrib',
'core',
'vendor',
];
// Loop through each file that has been modified in this commit.
foreach ($files as $file) {
// Skip files that don't exist.
if (!file_exists($file)) {
continue;
}
// Get the filename and extension.
$filename = pathinfo($file, PATHINFO_BASENAME);
$ext = pathinfo($file, PATHINFO_EXTENSION);
// Skip over the file if it matches an ignored filename or an ignored file
// path, or does not match one of the included file extensions.
$ignore_filenames = array_filter(
$ignore_filename_strings, function ($item) use ($filename) {
return strpos($filename, $item) !== FALSE;
}
);
$ignore_file_paths = array_filter(
$ignore_file_path_strings, function ($item) use ($file) {
return strpos($file, $item) !== FALSE;
}
);
if (!in_array($ext, $file_exts) || !empty($ignore_filenames) || !empty($ignore_file_paths)) {
continue;
}
// Run phpmd on the file.
$phpmd_output = [];
$phpmd_cmd = "phpmd $file text .git/hooks/phpmd-ruleset.xml --suffixes php,inc,module,install,test,profile";
$file = escapeshellarg($file);
exec($phpmd_cmd, $phpmd_output, $return);
if (empty($phpmd_output)) {
continue;
}
echo implode("\n", $phpmd_output), "\n";
// If any errors were returned, then prevent commit.
if ($return != 0) {
// $exit_status = $return;
// echo "\nCommit was aborted because of errors. You must fix the errors.", "\n";
}
}
exit($exit_status);
#!/bin/sh
# Check that the git directory exists.
GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)
if [ -z "$GIT_DIR" ]; then
echo >&2 "Fatal: GIT_DIR not set"
exit 1
fi
# Loop through every prepare-commit-msg_* file.
for i in `ls $GIT_DIR/hooks/prepare-commit-msg_*`;
do $i;
if [ "$?" -ne "0" ]; then
exit 1
fi
done;
exit 0;
#!/usr/bin/php
<?php
/**
* @file
* Starts the commit message off with the branch name in all caps.
*/
$branch = '';
$messageFile = '.git/COMMIT_EDITMSG';
$message = file_get_contents($messageFile);
// Get the branch name.
exec('git rev-parse --abbrev-ref HEAD', $branch);
$branch = strtoupper(reset($branch));
// Prepend the branch name only if the branch name was not already included in
// the original message.
if (!empty($branch)) {
if (strpos(strtoupper($message), $branch) !== 0) {
file_put_contents($messageFile, $branch . " " . $message);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment