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:
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.
Every repository including those you clone by default will have example hooks in the .git/hooks directory:
<span>git clone git@github.com:symfony/symfony.git </span><span>cd symfony </span><span>ls .git/hooks</span>
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.
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:
<span>git clone git@github.com:symfony/symfony.git </span><span>cd symfony </span><span>ls .git/hooks</span>
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.
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
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:
<span>cd .git/hooks </span><span>touch pre-commit </span><span>chmod +x pre-commit</span>
Explanation:
Now we can verify each file with a for loop:
<span>#!/usr/bin/env bash </span><span># Hook that checks PHP syntax </span> <span># Override IFS so that spaces do not count as delimiters </span><span>old_ifs=$IFS </span><span><span>IFS</span>=$'<span title="\n">\n'</span> </span> <span># Always fail </span><span>exit 1</span>
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:
<span>php_files=<span>$(git status --short | grep -E '^(A|M)' | awk '{ print }' | grep -E '\.php$')</span></span>
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:
<span>for file in $php_files; do </span> <span>if ! php -l "<span>$i"</span>; then </span> <span>exit 1 </span> <span>fi </span><span>done</span>
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:
<span>foreach ($php_files as $file) { </span> <span>$retval = 0; </span> <span>$escapedFile = escapeshellarg($file); </span> <span>exec('php -l ' . $escapedFile, $retval); // $retval passed in as out parameter reference </span> <span>if ($retval !== 0) { </span> <span>exit(1); </span> <span>} </span><span>}</span>
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:
<span>git clone git@github.com:symfony/symfony.git </span><span>cd symfony </span><span>ls .git/hooks</span>
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.
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):
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
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.
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):
<span>cd .git/hooks </span><span>touch pre-commit </span><span>chmod +x pre-commit</span>
For commit-msg you may want to ensure all commit messages conform to a standard, like [subproject] Message. Here is one in PHP:
<span>#!/usr/bin/env bash </span><span># Hook that checks PHP syntax </span> <span># Override IFS so that spaces do not count as delimiters </span><span>old_ifs=$IFS </span><span><span>IFS</span>=$'<span title="\n">\n'</span> </span> <span># Always fail </span><span>exit 1</span>
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!
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
The above is the detailed content of An Introduction to Git Hooks. For more information, please follow other related articles on the PHP Chinese website!