Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Last updated: March 5, 2024
Similar to other hook mechanisms, Git hooks are a method of running given code around the execution of Git actions such as committing and pushing. Effectively, this enables a stripped-down and often simpler version of the functionality offered by implementations such as Jenkins and Concourse.
In this tutorial, we explore Git hooks and ways to customize them. First, we explain what a Git hook is and what types of hooks exist. After that, we delve into hook management. Finally, we create client and server hooks to demonstrate their function.
We tested the code in this tutorial on Debian 12 (Bookworm) with GNU Bash 5.2.15. Unless otherwise specified, it should work in most POSIX-compliant environments.
Git hooks are specific executable files that automatically run commands for certain Git actions. They can be basic shell scripts or whole binary executables.
There are two main types of hooks:
Let’s see an example diagram with some common hook types and where they run:
Remote Repository
+---------------------------------+
| [+] post-receive |
| ^ |
| | |
| +------+ |
| |Remote| |
| |Origin| |
| +------+ |
| ^ |
| | |
| [+] update |
| [+] pre-receive |
| ^ |
+--|Push|-------------------------+
| ^ |
| | |
| [+] pre-push |
| ^ |
| | |
| ... |
| [+] post-commit |
| ^ |
| | |
| Commit [+] commit-msg |
| ^ |
| | |
| [+] prepare-commit-msg |
| [+] pre-commit |
| ^ |
| | |
| +------+ |
| |Commit| |
| | Data | |
| +------+ |
+---------------------------------+
Local Repository
In addition, there are many other hooks as well.
Several main hook types can run during regular operations on the client side:
Separately, we have hooks for specific and more rare actions:
Further, the pre-auto-gc garbage collection hook runs on git gc –auto.
When it comes to the remote repository, the server mainly reacts to pushes:
Since hooks are related to the Git actions, clients may have to wait for a hook to finish its run before receiving a response. Because of this, it’s good practice to make hooks short in terms of runtime and start detached tasks when possible.
Notably, we can get a full list of hooks and their parameters via man githooks.
Git hooks reside under the repository directory, usually .git, in a subdirectory called hooks:
$ ls -1 .git/hooks/
applypatch-msg.sample
commit-msg.sample
fsmonitor-watchman.sample
post-update.sample
pre-applypatch.sample
pre-commit.sample
pre-merge-commit.sample
prepare-commit-msg.sample
pre-push.sample
pre-rebase.sample
pre-receive.sample
push-to-checkout.sample
update.sample
Notably, each new repository usually contains a number of templates in the form of .sample files
Some hooks receive stdin data in the form of parameters when called. For example, we can distinguish between a commit –amend and a rebase for a post-rewrite hook by reading the standard input.
Further, certain hook types have preset environment variables accessible within the context of the hook executable. They enable additional information to pass from the Git activity to the hook-handling commands.
Finally, we can add options for some hook types. For instance, –push-option can be read on the server side during hooks that process pushed data.
Configured hooks run on any related operation, regardless of whether any data transfer or modification occurs.
Critically, the main hook file must be executable and is expected to conclude with a 0 exit code for the Git action to continue and be successful. Of course, we can make the hook and respective Git operation fail if the command fails.
Thus, it’s a good idea to use the relevant hook file as a central point to call any related scripts and other executables, ensuring critical actions cause the main hook to return non-zero. In addition, hook executables usually perform any necessary checks to avoid redundant runs.
To enable a hook, we perform several actions:
Of course, we can use any of the .sample files by copying it and removing the extension.
To demonstrate, let’s create two basic hooks from scratch.
As an example, let’s create a basic pre-commit hook that performs tests on the code and prompts the user whether they want a commit to go through if the tests fail:
$ cat .git/hooks/pre-commit
#!/usr/bin/env bash
exec < /dev/tty
npm test
failed=$?
if [ $failed ]; then
while true; do
read -p 'Continue with failing tests [Yn]: ' yn
case $yn in
[Yy]* ) exit 0;;
[Nn]* ) exit 1;;
* ) echo 'Y or N';;
esac
done
fi
$ chmod +x .git/hooks/pre-commit
Here, the first line of the pre-commit hook changes the stdin handle to point to /dev/tty, which is usually the keyboard. This way, we can prompt the user and receive a reply. Critically, we lose the data in stdin from before the change.
In this case, npm test performs tests on the repository code, after which we save the exit status in the $failed variable. If the tests do fail, the if conditional expression is true. Then, we run an endless loop that uses read to [-p]rompt the user whether they want to continue, accepting only y, Y, n, and N.
In short, based on the test results and the user reply, if any, the commit is accepted or denied.
Now, we can create a Git server hook that runs a deployment script upon receipt of a big push:
$ git init --bare
[...]
$ cat hooks/post-receive
#!/usr/bin/env bash
refs=$(cat | wc --lines)
if [ $refs -gt 4 ]; then
fly --target=node trigger-job --job nodetasks/deploy 2>&1 >/dev/null &
disown
echo 'deployed'
else
echo 'not deployed'
fi
$ chmod +x hooks/post-receive
In this basic post-receive hook, we trigger a Concourse job if there are changes to more than 4 refs. In this case, the exit code isn’t of consequence, since the hook can’t interrupt a process.
Notably, we & background, 2>&1 >/dev/null silence, and disown the Concourse job run. This enables the client to continue working instead of waiting for the process to complete.
At this point, we can configure the respective remote and trigger both hooks from the local repository.
First, we make and stage changes:
$ echo $(date) | tee --append file{1..5} >/dev/null
$ git add file{1..5}
After that, we trigger the local hook with a commit:
$ git commit --all --message 'upDate'
[...]
X 1 problem (1 error, 0 warnings)
Continue with failing tests [Yn]: Y
[master 13f7540] upDate
5 files changed, 5 insertions(+)
create mode 100644 file1
create mode 100644 file2
create mode 100644 file3
create mode 100644 file4
create mode 100644 file5
Despite the error, we decide to submit the commit.
Next, we add the origin:
$ git remote add origin https://gerganov.com/repository
Finally, the remote post-receive hook triggers after a successful push:
$ git push --all
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 4 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 309 bytes | 666.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
remote: deployed
To /root/xremo
775778b..13f7540 master -> master
Thus, we see the hooks run and the code is deployed.
In this article, we talked about Git hooks and how to manage and create them.
In conclusion, although fairly rudimentary when compared with full automation systems, Git hooks enable many activities like testing, updates, checks, standardizing, and similar.