Git hooks are simple scripts that run before or after certain actions. They are useful for a variety of tasks, but primarily I find them useful for client-side validation so simple mistakes can be prevented. For example, you can test syntax on files being commited, you can even have tests run. I have written hooks that validate Twig syntax, run JSHint to a standard, and a lot more.
Git hooks are also extremely simple by design. Git will run these hooks if the script is executable and Git will allow the action (e.g. commit or push) to occur as long as the hook exits with no errors (status 0). Hooks can be written in any language the environment can work with.
There are two types of hooks:
- Client-side – These run on the developer’s system
- Server-side – These run on the server hosting the Git repository
Server-side hooks will not be covered in this article. However, do note that if your project is on a service like GitHub, server-side hooks are generally not applicable. On GitHub, the equivalent to server-side hooks is to use services and Webhooks which can be found in your project settings.
The Hook Files
Every repository including those you clone by default will have example hooks in the .git/hooks
directory:
git clone git@github.com:symfony/symfony.git
cd symfony
ls .git/hooks
In that directory, you will see something like:
applypatch-msg.sample
commit-msg.sample
post-update.sample
pre-applypatch.sample
pre-commit
pre-commit.sample
prepare-commit-msg.sample
pre-push.sample
pre-rebase.sample
update.sample
We will focus on the pre-commit
hook which runs prior to allowing a commit.
An Example Hook: Validating PHP Syntax
We will begin with a very simple hook, written in Bash, that validates PHP code being committed has valid syntax. This is to prevent a “quick” but broken commit from happening. Of course I discourage “simple commits” that have little to no testing, but that does not mean they will not happen.
In .git/hooks
we can start a new file called pre-commit
. It must have permissions to execute:
cd .git/hooks
touch pre-commit
chmod +x pre-commit
You can use your favourite editor to begin writing. First we need the shebang. My favoured way is to use /usr/bin/env
as this uses the correct path to the application we want rather than a hard-coded and possibly invalid path. For now we will have it continuously fail so we can easily test.
#!/usr/bin/env bash
# Hook that checks PHP syntax
# Override IFS so that spaces do not count as delimiters
old_ifs=$IFS
IFS=$'\n'
# Always fail
exit 1
PHP has a useful option for syntax validation: -l
. It takes a single file argument, so we will have to loop through whatever PHP files are being changed. For simplicity we’ll assume any PHP files being committed always end in .php
. Since the hook is run from the root of the repository, we can use standard Git commands to get information about the changes, like git status
.
Above the #Always fail
line we can use the following to get all PHP files being modified:
php_files=$(git status --short | grep -E '^(A|M)' | awk '{ print $2 }' | grep -E '\.php$')
Explanation:
php_files=
In Bash assignment is done with no delimiter but note that referencing a variable requires the$
delimiter$()
is syntax for ‘get output’. Quotes are not required to use this.grep
is being used to check for added (A
) and modified files (M
)- Awk is being used here to print
$2
. A completegit status --short
line has extra space and extra data at the beginning, so we want to remove that. Awk also performs automatic stripping. grep
is again being used, but now is checking to make sure the lines end in.php
Now we can verify each file with a for
loop:
for file in $php_files; do
if ! php -l "$i"; then
exit 1
fi
done
This may seem a bit strange but ! php -l "$i"
(note the quotes to avoid issues with spaces) is actually checking for a return value of 0
, not true
or any of the sort of values we normally expect in other languages. Just for reference, the approximately equivalent PHP code would be:
foreach ($php_files as $file) {
$retval = 0;
$escapedFile = escapeshellarg($file);
exec('php -l ' . $escapedFile, $retval); // $retval passed in as out parameter reference
if ($retval !== 0) {
exit(1);
}
}
I made a bad change to src/Symfony/Component/Finder/Glob.php
on purpose to test this and the output from git commit -m 'Test'
is like so:
PHP Parse error: syntax error, unexpected 'namespace' (T_NAMESPACE) in src/Symfony/Component/Finder/Glob.php on line 12
Parse error: syntax error, unexpected 'namespace' (T_NAMESPACE) in src/Symfony/Component/Finder/Glob.php on line 12
Errors parsing src/Symfony/Component/Finder/Glob.php
I made the loop exit the entire script as early as possible and this ultimately may not be what we want. We may in fact want a summary of things to fix as opposed to having to continue to try to commit. Anyone would easily get frustrated eventually and might even learn to use git commit --no-verify
to bypass the hook altogether.
So instead, let’s not exit on the error with php -l
but I still would like to keep things easy to read:
for file in $php_files; do
php_out=$(php -l "$file" 2>&1)
if ! [ $? -eq 0 ]; then
echo "Syntax error with ${file}:"
echo "$php_out" | grep -E '^Parse error'
echo
fi
done
Here we capture the output for php -l
(and force standard error output to standard output). We check the exit status of php -l
using the special variable $?
(which is the exit status code) and the operator -eq
. We state that a syntax error occurred (note the use of ${}
for a variable in a string). Finally, we give the relevant line for error to keep output a little more brief (grepping for '^Parse error'
), and we give one blank line to keep this a little more readable.
I made two bad modifications and the output for an attempt at a commit looks like this:
Syntax error with src/Symfony/Component/Finder/Finder.php:
Parse error: syntax error, unexpected '}' in src/Symfony/Component/Finder/Finder.php on line 118
Syntax error with src/Symfony/Component/Finder/Glob.php:
Parse error: syntax error, unexpected 'namespace' (T_NAMESPACE) in src/Symfony/Component/Finder/Glob.php on line 12
Now the course of action is to fix these problems, test, and try to commit again.
To complete the hook script, remove the exit 1
at the bottom of the script. Try to commit valid PHP files and it should work as normal.
Sharing Hooks
Hooks are not distributed with your project nor can they be automatically installed. So your best course of action is to create a place for you hooks to live (could be in the same repository) and tell your collaborators to use them. If you make this easy for them, they are more likely to do so.
One simple way to do this would be to create a hooks
directory and a simple installer install-hooks.sh
that links them (rather than copying):
#!/usr/bin/env bash
for i in hooks/*; do ln -s "${i}" ".git/hooks/${i}"; done
Anyone who clones your project can simply run bash install-hooks.sh
after cloning.
This also has the benefit of keeping your hooks under version control.
Other Hooks
prepare-commit-msg
– Provide a default commit message if one is not given.commit-msg
– Commit message validation.post-commit
– Runs after a successful commit.pre-push
– Runs beforegit push
after the remote is verified to be working. It takes 2 arguments: the name of the remote, and the URL to it.pre-rebase
– Runs beforegit rebase
.post-checkout
– Runs after a successful checkout.post-merge
– Runs after a successful merge.
These hooks generally work the same as pre-commit
although they take in arguments. One use case for post-checkout
is to ensure that a file always gets proper permissions (because Git only tracks executable, not executable and symbolic link):
#!/usr/bin/env bash
# Make sure only I can read this file
chmod 0600 my-file-with-secrets
For commit-msg
you may want to ensure all commit messages conform to a standard, like [subproject] Message. Here is one in PHP:
#!/usr/bin/env php
<?php
// The message is passed as the the last argument.
$message = file_get_contents($argv[count($argv) - 1]);
if (!preg_match('/^\[[a-z_\-]+\]\s[A-Z]/', $message)) {
echo 'Message must be of format: [subproject] Message';
exit(1);
}
?>
Conclusion
Git hooks are a powerful means to automate the workflow of your project. You can validate code, commit messages, ensure environment is proper, and a whole lot more. Is there something interesting you are using Git hooks for? Let us know in the comments!
Frequently Asked Questions (FAQs) about Git Hooks
What are the different types of Git Hooks?
Git Hooks are divided into two main types: client-side and server-side hooks. Client-side hooks are triggered by operations such as committing and merging, while server-side hooks run on network operations like receiving pushed commits. Each hook can be customized to suit your specific operational needs.
How do I create a Git Hook?
To create a Git Hook, navigate to the .git/hooks directory in your repository. Here, you’ll find sample scripts for various hooks. To create a new hook, create a file without any extension (for example, pre-commit), make it executable, and write your script.
How can I use Git Hooks for automation?
Git Hooks can be used to automate a variety of tasks in your development workflow. For instance, you can use a pre-commit hook to automatically run tests or a linter on your code before each commit. This ensures that only tested and properly formatted code is committed to the repository.
Can I share Git Hooks with my team?
By default, Git Hooks are not included when you clone a repository. This is because they are stored in the .git directory, which is not versioned. However, you can share them with your team by storing them in a separate directory within your project and creating a script to symlink them into .git/hooks.
How can I use Git Hooks to enforce policies?
Git Hooks can be used to enforce project or company policies. For example, you can use a pre-receive hook on the server-side to reject any push that doesn’t adhere to your policy (e.g., commits that don’t follow a certain format).
What languages can I use to write Git Hooks?
Git Hooks are scripts, so you can write them in any scripting language. The default samples are written in Bash, but you can use any language you’re comfortable with, like Python or Ruby.
Can I use Git Hooks to integrate with other tools?
Yes, Git Hooks can be used to integrate Git with other tools. For example, you can use a post-commit hook to trigger a build in your continuous integration server or update a ticket in your issue tracking system.
How can I debug a Git Hook?
Debugging a Git Hook can be done by writing information to a file from the hook script. For example, you can redirect the output of commands to a log file to inspect it later.
Can I ignore Git Hooks?
Yes, if you want to bypass a Git Hook while making a commit, you can use the –no-verify option with the git commit command. This can be useful when you’re working on a minor change that doesn’t require the checks implemented in your hooks.
Are there any risks associated with using Git Hooks?
While Git Hooks are powerful, they should be used with caution. A poorly written hook can cause issues, including rejecting all commits or even data loss. Always test your hooks thoroughly before deploying them.
Andrew Udvare is a web developer based in the San Francisco Bay Area whose interests include web and mobile application development.