Skip to content

Instantly share code, notes, and snippets.

@ciarand
Last active August 29, 2015 14:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ciarand/1d2596b58d39c371ae57 to your computer and use it in GitHub Desktop.
Save ciarand/1d2596b58d39c371ae57 to your computer and use it in GitHub Desktop.
the really cool PHP pre-commit hook by @ciarand
#!/usr/bin/env php
<?php
/*
.---._----------------------------------+
___________/ ._____) WELCOME TO |
) __| the really cool |
__| PHP pre-commit hook |
..---------.._____| by @ciarand |
+---------------------------------+
ASCII art message board thing credit goes to WelcomeMat.co.
*/
/*
Copyright (c) 2015, Ciaran Downey <code@ciarand.me>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
// we go hard
error_reporting(E_ALL);
set_error_handler("error_handler");
set_exception_handler("exception_handler");
// let's define us some functions. we're going old school here, no classes or other object
// shenanigans.
/**
* Returns a callable which returns the result of a method call corresponding
* to the provided string and args
*
* Idea taken shamelessly from @nikic's fantastic iter library.
* Source: https://github.com/nikic/iter
*
* @param $name
*
* @return callable
*/
function method($name)
{
return function ($object) use ($name) {
return $object->$name();
};
}
/**
* Dies with the provided exit message AND exit code.
*
* @param $string
*/
function dief($string)
{
echo $string . PHP_EOL;
exit(1);
}
/**
* Stashes all your unstaged changes.
*
* @return bool success
*/
function setup()
{
passthru("git stash -q --keep-index", $ret);
return $ret === 0;
}
/**
* Unstashes all your stashed changes.
*
* @return bool success
*/
function tear_down()
{
passthru("git stash pop -q", $ret);
return $ret === 0;
}
/**
* Takes a $func that is both a callable AND a "stringable" (i.e. offers a __toString() or is a
* string or some other trickery. It puts the error from the check (either a RuntimeException or
* null), into the array.
*
* @param $check
* @param array $errors
*/
function run_check($check, array &$errors)
{
try {
// run the check
$check();
// if we got this far, we passed. Set it to null.
$errors[$check] = null;
} catch (RuntimeException $e) {
// uh oh, the test failed. Store the reason why.
$errors[$check] = $e;
}
// print that the test passed or failed
print_result($errors, $check);
}
function fail($message)
{
throw new RuntimeException($message);
}
/**
* Prints the result. A unicode checkmark or an X, followed by a pretty name for the check and a
* newline.
*
* @param array $errors
* @param string $title
*/
function print_result(array $errors, $title)
{
$icon = $errors[$title] === null ? "✓" : "✘";
echo $icon . " " . str_replace("_", " ", $title) . PHP_EOL;
}
/**
* Determines whether the filename ends with ".php" and that the file exists.
*
* @param $file
*
* @return bool
*/
function is_php_file($file)
{
return file_exists($file) && pathinfo($file, PATHINFO_EXTENSION) === "php";
}
/**
* Lints (via php -l) the given file (name).
*
* @param string $file the name of the file to lint
*
* @throws RuntimeException
*/
function lint_file($file)
{
if (!is_php_file($file)) {
return;
}
$last = exec("php -l {$file}", $output, $ret);
$ret === 0 or fail(implode("\n", $output) . $last);
}
/**
* Gets an array of the file names that have changed.
*
* @return array
*/
function get_changed()
{
$changed = `git diff --cached --name-only`;
if (trim($changed) === "") {
return [];
}
return explode("\n", $changed);
}
/**
* Lints all the changed files.
*
* @throws RuntimeException
*/
function syntax_check()
{
array_map("lint_file", get_changed());
}
/**
* Runs the project unit tests.
*
* @throws RuntimeException
*/
function unit_tests()
{
$last = exec("./bin/phpunit", $output, $ret);
$ret === 0 or fail(implode("\n", $output) . $last);
}
/**
* Prints the summary of the errors.
*
* @param array $errors
*/
function print_summary(array $errors)
{
$failed = array_filter($errors);
if (!$failed) {
echo PHP_EOL . "All checks passed! I'll let you commit now." . PHP_EOL;
return;
}
$totalcount = count($errors);
$failcount = count($failed);
$noun = $failcount === 1 ? "check" : "checks";
echo implode(PHP_EOL, [
"{$failcount} {$noun} failed! Aborting commit!",
"",
] + array_map(method("getMessage"), $failed));
}
/**
* Custom error handler.
*
* @param $errno
* @param $errstr
* @param $errfile
* @param $errline
* @param $errcontext
*/
function error_handler($errno, $errstr, $errfile, $errline, $errcontext)
{
tear_down();
dief(implode("\n", [
"A problem with the precommit hook (" . __FILE__ . ") occurred!",
"",
"A problem on line {$errline} of {$errfile} caused the error {$errstr} (no {$errno}).",
"Here's the full context:",
""
] + $errcontext));
}
/**
* Custom exception handler.
*
* @param \Exception $e
*/
function exception_handler(Exception $e)
{
tear_down();
dief(implode("\n", [
"A problem with the precommit hook (" . __FILE__ . ") occurred!",
"",
"A problem on line {$e->getLine()} of {$e->getFile()} caused the error {$e->getMessage()}",
"(no {$e->getCode()}).",
"",
"Here's the full context:",
$e->getTraceAsString(),
]));
}
/**
* Main entry point.
*
* @param array $argv
* @param $argc
*/
function main(array $argv, $argc)
{
$errors = [];
setup() or dief("couldn't stash unstaged changes");
run_check("syntax_check", $errors);
run_check("unit_tests", $errors);
print_summary($errors);
tear_down() or dief("couldn't unstash unstaged changes");
exit(count(array_filter($errors)));
}
// let's go
main($argv, $argc);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment