Source Controlled Git Hooks With Phing

The other day I was experimenting with Git hooks. These are scripts that you can execute before certain actions are run in Git. For example, you might want to ensure that forced updates are not run, ensuring respository files have the correct permissions after merging, or that the files have ASCII standard names before being committed.

To use a hook in Git you just need to add them to the .git/hooks directory in your respository and to change the mode of the file so that it is executable. A new Git repository will create several sample hook files that can be used by removing the '.sample' from the end and making them executable. For more information on Git hooks and how to use them see the Git hooks manual page in the Git documentation.

My primary reason for creating a Git hook was to ensure that there were no syntax errors on my code before it was even committed. I had previously been either checking manually or using Jenkins to download the repository and check the code for errors, but then I realised that it would be far easier to stop them at the source.

To this end I created a hook file called pre-commit and added this to the hooks directory. As you can probably guess this hook is run before the commit action is executed in Git. I have talked previously about using Phing to syntax check all PHP and JavaScript files in a repository so I just used the same Phing target here. The only addition was that I also needed the commit action to stop if the Phing syntax check failed, which could be done in the hook file itself. There would be enough information about what failed in the Phing output to show why the commit had failed so I didn't need to print anything else out here. This is the hook file in full.

#!/bin/sh

echo "Parsing PHP and JavaScript files for lint errors."

phing -f scripts/build.xml syntaxcheck || exit 1

This worked as expected and I was unable to commit and files that contained syntax errors to the repository. The only problem was that if I checked the repository out again in a different location I would lose this hook as the files in the .git directory (and therefore .git/hooks) are not source controlled. This also meant that I couldn't get other people working on the project to follow the same practice as they would be missing the hook on their repositories as well. After a bit of searching for a solution to this I found the best course of action was to include the hook file in the repository and then write a script to automate the inclusion of the hook into the .git/hooks directory.

I created a directory in my repository called githooks and moved the hook I had created into it so that it could be source controlled. Rather than create a script from scratch I decided to add a githook target to the already existing Phing build file in my repository. This target has the following actions:

  • Define a couple of properties that point to the .git/hooks directory and the githooks directory in my repository.
  • Delete the existing .git/hooks direcory.
  • Symlink the githooks directory to .git/hooks, thereby replacing the original hooks directory.
  • Create a fileset that contains all of the possible hooks files in the githooks directory.
  • Loop through the files in the fileset and ensure that they are all set to be executable. This is tricky and requires the use of a secondary target called chmodsetexecute that has the single action of setting the filename passed to it to be executable.

Here are the two Phing targets used to set up the repository with custom Git hooks.

 <target name="githook">
   <resolvepath propertyName="git_hooks_path" file="${project.basedir}/../.git/hooks"/>
   <resolvepath propertyName="repo_hooks_path" file="${project.basedir}/../githooks"/>

   <!-- Delete existing .git/hooks directory -->
   <delete dir="${git_hooks_path}" quiet="true" failonerror="false" />
   
   <!-- link the git hooks directories together -->
   <symlink target="${repo_hooks_path}" link="${git_hooks_path}" />
   
   <fileset dir="${repo_hooks_path}" id="hookfiles">
     <include name="applypatch-msg" />
     <include name="post-commit" />
     <include name="post-update" />
     <include name="pre-commit" />
     <include name="update" />
     <include name="commit-msg" />
     <include name="post-receive" />
     <include name="pre-applypatch" />
     <include name="pre-rebase" />
   </fileset>
   
   <foreach param="hookfilename" target="chmodsetexecute">
     <fileset refid="hookfiles" />
   </foreach>
 </target>

 <target name="chmodsetexecute">
   <exec command="chmod +x ${repo_hooks_path}/${hookfilename}" />
 </target>

With all this in place it is just a case of making sure that this target is ran every time a new checkout is done and that existing repositories are updated with this change. This sets up the correct Git hooks in place and makes sure that no one can commit code that has syntax errors.

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
5 + 1 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.