Skip links

Git commit hooks using PHP

05 July 2011 20:18 - by Freek Lijten - 1 comment

Tags: ,

Git has a flexible plugin system which lets you hook into various moments of the git flow. This can be very useful for checking code for syntax errors for instance. But also checking for conventions, refusing new files and other options are viable. In this article I'll describe the various hooks and give some PHP examples which will show how to use these hooks.

Git hooks are usually found inside the .git/hooks folder of your git repository. Git tends to provide sample hook files there which are postfixed with a .sample extension. These examples are written as shell scripts. Take a look at them if you want, but today we're talking PHP!

Types of hooks

There are different types of hooks. The two main categories are client side and server side hooks. The client side hooks deal with operations like committing and merging, the server side hooks deal with operations like pushing. I will focus on client side hooks, and more specifically on the commit workflow hooks, in this article. For a complete overview of both check this chapter of ProGit.

Triggering a hook

Now how does a hook get triggered? This is very simple actually. All you have to do is place a file inside the right directory, make it executable and give it a correct name. In the commit workflow case, any of the following filenames will have effect:

  • pre-commit (triggers before you type a commit message, useful for checking for syntax errors and the likes)
  • prepare-commit-msg (runs before the commit message editor is fired up but after the default message is created)
  • commit-msg runs when you trigger the commit but before the commit actually happens. I use this to validate the commit messages for certain patterns)
  • post-commit (runs  after everything finished succesfully)

Do make sure a commit hook file is executable. If if it isn't, it will not be executed (no really) and the checks will effectively be skipped. So if your files aren't executable yet, chmod +x them before you try committing.

An example

The two commit hooks we use at Procurios (right now at least) are the pre-commit and the commit-msg hooks. Let's have a look at our pre-commit hook file (adapted to keep the code short):

#!/usr/bin/php
<?php
//collect all files which have been added, copied or 
//modified and store them in an array called output
exec('git diff --cached --name-status --diff-filter=ACM', $output);

foreach ($output as $line) {
    $action = trim($line[0]);
    $fileName = trim( substr($line, 1) );
    ...
    checkSyntax($fileName);
}

exit(0);

function checkSyntax($fileName)
{
    $op = array();    
    exec('cat ' . $fileName . ' | /usr/bin/php -l 2>/dev/null', $output, $failed);
 
    if (!$failed) {
        return;
    }

    echo "Syntax error in $fileName: " . $output[1];
    exit(1);
}

Quite a lot seems to be going on here. The first line is the shebang and after that a common php open tag follows. The first real action starts now. The command inside the exec function returns a list of all staged files which are either added, copied or modified (after all why syntax check deleted files). Example output could look like this:

M       dirX/fileZ
M       fileX
M       fileY

The output of this command is stored inside the variable called $output. This is an array built from lines of the output. After some operations a $fileName is acquired and passed to a function where the syntax checking takes place. We do this by catting the file and piping it to php with the -l option (lint) set. This option has PHP run in syntax check mode only.

Again we store the output of the command inside exec, but I've added something as well. The return value of the executed command is stored inside the $failed variable. I call this $failed because a successful operation returns a 0 and a failed operations returns a 1. This way !$failed would trigger if $failed has a value of 0, thus having an if statement which makes sense if you read it.

So if there actually is a syntax error $failed will contain a value of 1 and a notice stating the error and the filename is echoed to the user. It will be clear that very little code has just ensured you can never commit a syntax error to your git repository!

Checking on your message

At Procurios we have a convention for commit messages. Every line should start with a three letter code (CHG for change, ADD  for a new file, etc) followed by an (optional) space, a - or :, another (optional) space and the actual message. The file below is (again modified for readability) is how we check for the message conventions.

#!/usr/bin/php
<?php

$message = file_get_contents($argv[1]);
checkMessage($message);
exit(0);

function checkMessage($message)
{
	if (strlen($message) < 10) {
		echo 'A commit must be annotated by a prefixed message of at least ten characters';
		exit(1);
	}
	foreach (preg_split('/\v/', $message, -1, PREG_SPLIT_NO_EMPTY) as $line) {
		// Read first 3 chars of line
		if ($line[0] == '#') {
			continue;
		}
		$verb = substr($line, 0, 3);

		$allowed = array('ADD', 'FIX', 'CHG', 'OPT', 'DOC', 'REM', 'MRG', 'MOV', 'CPY');

		if (!in_array($verb, $allowed)) {
			echo = '"' . $verb . '" is not a valid prefix for commit messages. Use only ADD, FIX, CHG, OPT, DOC, REM, MRG, MOV or CPY';
			exit(1);			
		}

		$message = substr($line, 3);
		preg_match('/^\s*[\-:]/', $message, $matches);
		if (empty($matches)) {
			echo 'A commit message must exist of a three letter code, a - or : followed by the actual message. The - or : is missing while it is required';
			exit(1);
		}
	}
}

The script called commit-msg gets one command line argument, the file where the commit message can be found. Since $argv[0] is the filepath of the called script, $argv[1] contains what we need. What happens next is pretty straightforward. We check whether the commit message line meets a minimum length, we skip comments and finally we check whether the pattern of the line matches the demands I described earlier. All in all, it's not that hard.

As I said before the code is simplified a good deal for readability. I would not stop execution at the first found error for instance, but store it in a place and output all found errors after execution finishes. I also took some shortcuts in the code itself so forgive me if you don't like it :)

Closing off

If you're a git user I would really encourage you to look into these commit hooks. Apart from them being fun, they can ensure conventions and prevent errors. The fact that you can write them in a language of choice makes it that much better. I will most likely return with an article on the "server side" hooks soon, so if you liked this article, keep an eye out for the follow-up!

Share this post!

Related posts

Comments

  1. greg0ire greg0ire wrote on 12 September 2013 22:31

    Nice post! If you're into git hooks for php, have a look at this project I made, maybe you'll find something useful, maybe you'll want to contribute...

Leave a comment!

Italic and bold

*This is italic*, and _so is this_.
**This is bold**, and __so is this__.

Links

This is a link to [Procurios](http://www.procurios.nl).

Lists

A bulleted list can be made with:
- Minus-signs,
+ Add-signs,
* Or an asterisk.

A numbered list can be made with:
1. List item number 1.
2. List item number 2.

Quote

The text below creates a quote:
> This is the first line.
> This is the second line.

Code

A text block with code can be created. Prefix a line with four spaces and a code-block will be made.