Skip to content

Instantly share code, notes, and snippets.

@sjparkinson

sjparkinson/pre-commit

Last active Aug 29, 2015
Embed
What would you like to do?
#!/usr/bin/php
<?php
print(Utils::getColourString("Checking staged files for common mistakes...", "cyan") . "\n");
$errors = false;
foreach(Utils::getStagedFiles() as $file)
{
printf("Checking %s file %s.\n", $file->getFormattedGitStatus(), $file->getLocalFileLocation());
$check = new Check($file);
// Lint PHP files
$check->lintPHP();
// Check for NOCOMMIT
$check->noCommitTag();
// Check for unix line endings
$check->lineEndings();
$check->leadingLine();
// Print any errors
if ($check->hasErrors()) {
$errors = true;
$check->printErrors();
}
}
print(Utils::getColourString("Finished checking files. ", "cyan"));
if ($errors === false) {
print(Utils::getColourString("✔ Success.", "green") . "\n");
print(Utils::getColourString("Have you tested everything?", "gray") . "\n");
exit(0);
} else {
print(Utils::getColourString("✘ Please fix the errors.", "red") . "\n");
exit(1);
}
class Check
{
// The file to check for issues.
private $_file;
// A list of errors found when checking.
private $_errors;
// PHP file extensions to check.
private static $_phpExtensions = ['php', 'phtml'];
// All file extensions to check.
private static $_allExtensions = ['php', 'phtml', 'js', 'less', 'css'];
/**
* Creates a new instance of the Check class.
*
* @param File $file The file to check.
*/
public function __construct(File $file)
{
$this->_file = $file;
}
/**
* Checks PHP files for basic syntax errors.
*
* Currently uses `php -l` to capture any basic syntax
* errors.
*/
public function lintPHP()
{
if (in_array($extension, self::$_phpExtensions)) {
exec('php -l ' . $file->getFileLocation(), $output, $status);
if ($status !== 0) {
foreach (array_filter($output) as $error) {
if (strpos($error, $needle) !== false) {
$errorDetail = substr($error, strlen('Parse error: parse error, '));
$this->addError("PHP Lint Error: $errorDetail\n");
}
}
}
}
}
/**
* Checks all files for the NOCOMMIT tag.
*/
public function noCommitTag()
{
if (in_array($this->_file->getExtension(), self::$_allExtensions)) {
exec(sprintf('cat %s | grep "NOCOMMIT"', $this->_file->getFileLocation()), $output);
if ($output) {
$this->addError(sprintf("No Commit Error: NOCOMMIT found in %s\n", $this->_file->getLocalFileLocation()));
}
}
}
/**
* Checks all files for unix line endings.
*
* If the file contains any windows/dos line endings (\r\n)
* then the check adds an error.
*/
public function lineEndings()
{
if (in_array($this->_file->getExtension(), self::$_allExtensions)) {
exec(sprintf('file %s | grep "CRLF"', $this->_file->getFileLocation()), $output);
if ($output) {
$this->addError(sprintf("Line Ending Error: %s contains CRLF line endings.\n", $this->_file->getLocalFileLocation()));
}
}
}
/**
* Checks only a php file for it's leading line.
*
* If the PHP file doesn't begin with the correct first line
* the check will add an error. This should stop any whitespace
* creaping in and being returned to the browser.
*/
public function leadingLine()
{
if ($this->_file->getExtension() === 'php') {
exec(sprintf('head -n 1 %s', $this->_file->getFileLocation()), $output);
if (! in_array($output[0], ['<?php', '#!'])) {
$this->addError(sprintf("Leading Line Error: %s doesn't start with <?php or #!.\n", $this->_file->getLocalFileLocation()));
}
}
}
/**
* Checks if the check contains any errors.
*
* @return bool
*/
public function hasErrors()
{
return ($this->_errors);
}
/**
* Prints each error in the list to the console,
* colourized red.
*/
public function printErrors()
{
foreach ($this->_errors as $error) {
print(Utils::getColourString($error, 'red'));
}
}
/**
* Adds a given error to the list of errors.
*
* @param string $error The error to add.
*/
private function addError($error)
{
$this->_errors[] = $error;
}
}
class File
{
// The path of the file as provided by git.
private $_fileLocation;
// The shorthand status of the file returned by git.
private $_fileStatus;
/**
* Creates a new instance of the File class.
*
* If `$file_details` is provided it will populate the instance
* from the git status string.
*
* @param $file_details A tab seperated string of the files git status.
*/
function __construct($file_details = null)
{
if ($file_details !== null && is_string($file_details)) {
list($file_status, $file_location) = explode("\t", $file_details);
$this->_fileLocation = $file_location;
$this->_fileStatus = $file_status;
}
}
/**
* Returns the name of the file including its extension.
*
* @return string
*/
public function getFileName()
{
return pathinfo($this->_fileLocation, PATHINFO_FILENAME);
}
/**
* Returns the local path to the file
* from the base of the git repository.
*
* @return string
*/
public function getLocalFileLocation()
{
return $this->_fileLocation;
}
/**
* Returns the full path to the file.
*
* @return string
*/
public function getFileLocation()
{
return getcwd() . '/' . $this->_fileLocation;
}
/**
* Returns the files extension.
*
* @return string The extension without a period.
*/
public function getExtension()
{
return pathinfo($this->_fileLocation, PATHINFO_EXTENSION);
}
/**
* Returns the short hand git status of the file.
*
* @return string
*/
public function getGitStatus()
{
return $this->_fileStatus;
}
/**
* Returns the git status of the file as a word.
*
* @return string
*
* @throws UnexpectedValueException If the recorded status is not an expected value.
*/
public function getFormattedGitStatus()
{
switch($this->_fileStatus) {
case 'A':
return 'added';
case 'C':
return 'copied';
case 'M':
return 'modified';
case 'R':
return 'renamed';
default:
throw new UnexpectedValueException("Unknown file status: $this->_fileStatus.");
}
}
}
class Utils
{
/**
* Dictonary of bash colour prefixes.
*/
private static $_foregroundPrefix = [
'black' => '0;30', 'blue' => '0;34',
'green' => '0;32', 'cyan' => '0;36',
'red' => '0;31', 'purple' => '0;35',
'brown' => '0;33', 'gray' => '0;37',
];
/**
* Gets a list of the files currently staged under git.
*
* Returns either an empty array or a tab seperated list of staged files and
* their git status.
*
* @link http://git-scm.com/docs/git-status
*
* @return array
*/
public static function getStagedFiles()
{
exec('git diff --cached --name-status --diff-filter=ACMR', $output);
$files = [];
foreach ($output as $file) {
$files[] = new File($file);
}
return $files;
}
/**
* Formats a string for colourized outputting to the command line.
*
* @param string $string The string to colourize.
* @param string $foreground The colour to set the text to.
*
* @return string
*/
public static function getColourString($string, $foreground = null)
{
$builder = "";
// Check if given foreground color found
if (self::$_foregroundPrefix[$foreground]) {
$builder .= "\033[" . self::$_foregroundPrefix[$foreground] . "m";
}
// Add string and end coloring
$builder .= $string . "\033[0m";
return $builder;
}
}
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.