Git commit hooks using PHP
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 (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!
Comments
-
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...
-
You actually make it seem really easy with your presentation but I find this
topic to be actually one thing that I think I'd never understand.
It seems too complicated and very extensive for me. I am taking a look forward for your subsequent post, I will try to get the hold of it! -
Paris, the capital of France and the city of love.
-
GENUINE Vehicle SPARE Parts IN SHARJAH.
-
I constantly spent my half an hour to read this website's content every day along with
a cup of coffee. -
Establish your baseline of search engine site visitors.
-
I have read so many articles concerning the blogger lovers however this
post is really a good paragraph, keep it up. -
Hello just wanted to give you a quick heads up
and let you know a few of the images aren't loading properly.
I'm not sure why but I think its a linking issue. I've tried it in two different internet browsers and both show the same results.