Introduction
Like many other Version Control Systems, Git has a way to fire off custom scripts when certain important actions occur.
Git hook scripts are useful for identifying simple issues before submission to code review. We run our hooks on every commit to automatically point out issues in code such as missing semicolons, trailing whitespace, and debug statements. By pointing these issues out before code review, this allows a code reviewer to focus on the architecture of a change while not wasting time with trivial style nitpicks. Also it helps protect the depo from devs who push changes without any attention!
There are two groups of these hooks: client-side and server-side. You can use these hooks for all sorts of reasons.
- Client-side hooks are triggered by operations such as committing and merging.
- Server-side hooks run on network operations such as receiving pushed commits.
Installing a Hook
The hooks are all stored in the hooks subdirectory of the Git directory. In most projects, that’s .git/hooks
To enable a hook script, put a file in the hooks subdirectory of your .git directory that is named appropriately (without any extension) and is executable. From that point forward, it will be called.
There is 4 files that can be executed before and after your commit:
pre-commit
prepare-commit-msg
commit-msg
post-commit
(after the commit)
The pre-commit
hook is run first, before you even type in a commit message. It’s used to inspect the snapshot that’s about to be committed, to see if you’ve forgotten something, to make sure tests run, or to examine whatever you need to inspect in the code. Exiting non-zero from this hook aborts the commit, although you can bypass it with git commit –no-verify. You can do things like check for code style (run lint or something equivalent), check for trailing whitespace (the default hook does exactly this), or check for appropriate documentation on new methods.
The prepare-commit-msg
hook is run before the commit message editor is fired up but after the default message is created. It lets you edit the default message before the commit author sees it. This hook takes a few parameters: the path to the file that holds the commit message so far, the type of commit, and the commit SHA-1 if this is an amended commit. This hook generally isn’t useful for normal commits; rather, it’s good for commits where the default message is auto-generated, such as templated commit messages, merge commits, squashed commits, and amended commits. You may use it in conjunction with a commit template to programmatically insert information.
The commit-msg
hook takes one parameter, which again is the path to a temporary file that contains the commit message written by the developer. If this script exits non-zero, Git aborts the commit process, so you can use it to validate your project state or commit message before allowing a commit to go through. In the last section of this chapter, we’ll demonstrate using this hook to check that your commit message is conformant to a required pattern.
After the entire commit process is completed, the post-commit
hook runs. It doesn’t take any parameters, but you can easily get the last commit by running git log -1 HEAD. Generally, this script is used for notification or something similar.
Example
To add a pre-commit hook in Git, follow these steps:
- Navigate to the
.git/hooks
directory in your repository:cd path/to/your/repo/.git/hooks
- Create or modify the
pre-commit
script:- If the
pre-commit
file exists, edit it. - If it doesn’t exist, create a new file named
pre-commit
:touch pre-commit
- If the
- Make the
pre-commit
script executable:chmod +x pre-commit
- Write your script inside the
pre-commit
file: Here’s an example of a simple pre-commit hook that prevents commits with unformatted Python code usingblack
:#!/bin/sh # Run black to format Python files black --check . if [ $? -ne 0 ]; then echo "Python files are not formatted properly. Run 'black .' to format them." exit 1 fi
- Test the hook: Now, try committing some changes. If the conditions in the hook aren’t met (e.g., Python files aren’t formatted), the commit will be blocked, and you’ll see the error message from the hook.
Using pre-commit
framework:
If you want a more sophisticated setup with multiple hooks or linters, you can use the pre-commit
framework:
- Install
pre-commit
:pip install pre-commit
- Create a
.pre-commit-config.yaml
file in the root of your repository: ```yaml repos:- repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.0.1 hooks:
- id: trailing-whitespace
- id: end-of-file-fixer ```
- repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.0.1 hooks:
- Install the pre-commit hook:
pre-commit install
Now, every time you commit, it will automatically check for the conditions defined in .pre-commit-config.yaml
.
Global hooks
To make a Git pre-commit hook global for all users on a system or across multiple repositories, you can configure a global or shared Git hooks directory. Here’s how to do it:
Option 1: Set up a Global Hooks Directory
-
Create a Global Hooks Directory: Choose a directory where you want to store the global hooks, e.g.,
/usr/local/share/git-core/hooks
, or in the home directory for user-specific hooks, e.g.,~/.git-hooks
.mkdir -p ~/.git-hooks
-
Tell Git to Use the Global Hooks Directory: Set the
core.hooksPath
in your global Git configuration to point to this directory:git config --global core.hooksPath ~/.git-hooks
This command updates your
~/.gitconfig
file to tell Git to look in~/.git-hooks
for hooks by default. -
Create the Pre-commit Hook: Now, inside your global hooks directory, create a
pre-commit
script:touch ~/.git-hooks/pre-commit chmod +x ~/.git-hooks/pre-commit
Add your desired hook logic to this script. For example, here’s a simple script that checks for uncommitted changes:
#!/bin/sh if git diff-index --quiet HEAD --; then echo "Pre-commit checks passed." else echo "Uncommitted changes detected. Please commit or stash changes first." exit 1 fi
-
Verify: Now, whenever any Git repository you manage attempts to commit, Git will use this global
pre-commit
hook.
Option 2: Create a Template for New Repositories
-
Set up a Global Template Directory: Git provides the ability to set up a template directory that is used when initializing new repositories. Create a directory that will serve as a template for hooks:
mkdir -p ~/.git-template/hooks
-
Create the Pre-commit Hook in the Template: Inside the template directory, create the
pre-commit
hook as before:touch ~/.git-template/hooks/pre-commit chmod +x ~/.git-template/hooks/pre-commit
Add your script to this hook.
-
Set the Template Globally: Configure Git to use this template for all new repositories:
git config --global init.templateDir ~/.git-template
-
Apply to Existing Repositories: This template will only affect newly created repositories. To apply it to existing repositories, you’ll need to manually copy the hooks to their
.git/hooks/
directories.
Notes:
- Existing Repositories: The global hooks will not automatically affect existing repositories unless you reinitialize them or manually copy the hook.
-
Global Hooks Across Users: If you’re managing multiple users on a system, you can set the global hooks directory at the system level using
git config --system
, but it requires admin privileges:sudo git config --system core.hooksPath /path/to/global/hooks
Example of Another Pre-Commit Hook (local.conf changed)
Edit the pre-commit
file and add the following script:
#!/bin/sh
# Pre-commit hook to prevent changes to 'prod/local.conf' file
# Check if 'prod/local.conf' is staged for commit
if git diff --cached --name-only | grep -q '^prod/local.conf$'; then
echo "Error: Changes to 'prod/local.conf' are not allowed."
exit 1
fi
# Allow commit if no changes are found
exit 0
How the Script Works:
- The script uses
git diff --cached --name-only
to list all the files that are staged for commit. - It then checks if
prod/local.conf
is among the files that are staged. - If
prod/local.conf
is detected, the hook will output an error message and prevent the commit by exiting withexit 1
. - If
prod/local.conf
is not found in the list of staged files, the commit will proceed as normal (the script exits withexit 0
).
Example of Another Pre-Commit Hook (Checking for TODO Comments)
Here’s an example of a pre-commit
hook that blocks commits if they contain TODO
comments:
#!/bin/sh
# A pre-commit hook to block commits with TODO comments.
# Check for TODOs in the staged files
if git diff --cached | grep -i 'TODO'; then
echo "Commit contains TODO comments. Please resolve them before committing."
exit 1
fi
Summary of Steps:
- Navigate to your repository’s
.git/hooks
directory. - Create or edit the
pre-commit
file. - Write your hook script inside the
pre-commit
file. - Make the
pre-commit
script executable withchmod +x pre-commit
. - Test your hook by making a commit.
Now your Git project will run the pre-commit hook every time a commit is attempted.
Bypass pre-commit hooks
To bypass a pre-commit hook for a specific commit, you can use the --no-verify
option when committing. This will prevent Git from running the pre-commit hooks for that particular commit.
Example Command to Bypass a Pre-commit Hook
To make a commit without running the pre-commit hooks, use:
git commit -m "Your commit message" --no-verify
Important Notes:
- The
--no-verify
flag skips all hooks, includingpre-commit
,commit-msg
, andpre-push
hooks. - Use this option sparingly since it bypasses important checks or validations that the pre-commit hook may be enforcing.
- You can still push to the repository normally, but be mindful that server-side hooks (like
pre-receive
orupdate
hooks) will not be bypassed by this method, so your push might still be blocked at the server level if additional validations are in place.
This is useful when you need to make an emergency commit or are aware that a specific pre-commit check is unnecessary for a particular case.
Server-side hooks
A server-side Git pre-receive hook is used to enforce certain rules on a Git server before accepting pushed commits from clients. This hook can be particularly useful to enforce policies across all repositories or ensure code quality checks, such as verifying commit messages, preventing certain file types, or ensuring code passes tests. By setting up a pre-receive hook on the Git server, you can enforce custom policies and prevent unwanted commits from being pushed to the repository. The hook scripts are highly flexible and can be adapted to enforce any specific checks or conditions you require for your project.
Steps to Add a Server-Side Pre-Receive Hook:
1. Access the Git Server
You need access to the Git server where the repository is hosted.
- SSH into your Git server:
ssh your-user@your-server
2. Navigate to the Git Repository on the Server
Find the Git repository directory on the server. This will typically be inside a directory like /var/git
or /home/git
, depending on your server setup.
cd /path/to/repository.git
Ensure you’re in the bare repository (which ends with .git
). Bare repositories are the ones that clients push to directly.
3. Go to the hooks
Directory
Inside the .git
folder (for non-bare repositories) or at the root of the bare repository, there is a hooks
directory where server-side hooks reside.
cd hooks
4. Create or Edit the pre-receive
Hook
If a pre-receive
hook doesn’t exist, create one:
touch pre-receive
Open the file in a text editor:
nano pre-receive
5. Add Your Hook Logic
The pre-receive
hook script is executed every time a git push
is attempted. It reads input through standard input (stdin) and expects three fields:
- The old commit reference (old SHA1 hash)
- The new commit reference (new SHA1 hash)
- The refname (e.g.,
refs/heads/master
)
You can implement checks and enforce rules based on these fields.
Here’s a basic example of a pre-receive hook that prevents force pushes to the master
branch:
#!/bin/sh
# Prevent force pushes to the master branch
while read oldrev newrev refname
do
if [ "$refname" = "refs/heads/master" ]; then
# If the old revision is not zero (which means the branch exists),
# and new revision is zero (which indicates deletion), reject.
if ! git merge-base --is-ancestor $oldrev $newrev; then
echo "Force pushing to master is not allowed!"
exit 1
fi
fi
done
exit 0
This hook blocks any non-fast-forward changes (i.e., force pushes) to the master
branch.
6. Make the Hook Executable
After adding the logic to the pre-receive
hook, make the file executable:
chmod +x pre-receive
7. Test the Hook
To test the hook, try pushing to the repository from a client. The hook will run each time a git push
is made to the server.
For example, try force-pushing to the master
branch from a client:
git push --force origin master
If the hook is set up correctly, the push should be rejected, and you should see the message Force pushing to master is not allowed!
.
8. Hook Logic Examples
Here are a few more examples of what you can do with a server-side pre-receive hook:
-
Reject pushes with invalid commit messages:
#!/bin/sh # Ensure commit messages contain a JIRA ticket ID (e.g., JIRA-123) while read oldrev newrev refname do commits=$(git rev-list $oldrev..$newrev) for commit in $commits do message=$(git log --format=%B -n 1 $commit) if ! echo "$message" | grep -q "JIRA-[0-9]\+"; then echo "Commit $commit is missing a JIRA ticket ID!" exit 1 fi done done exit 0
-
Reject pushes with large files:
#!/bin/sh # Block pushes if they contain files larger than 10MB MAX_SIZE=10485760 while read oldrev newrev refname do for file in $(git diff-tree --no-commit-id --name-only -r $newrev) do if [ $(git cat-file -s "$newrev:$file") -gt $MAX_SIZE ]; then echo "File $file is larger than 10MB! Push rejected." exit 1 fi done done exit 0
9. Apply the Hook Globally (Optional)
If you want this pre-receive hook to apply to all repositories on the server, you can configure a global hooks directory:
-
Set the
core.hooksPath
globally in the system-wide Git config file (/etc/gitconfig
or similar):sudo git config --system core.hooksPath /path/to/global/hooks
-
Place your
pre-receive
hook in this global hooks directory.
This will apply the hook to all repositories on the server that use the global hook configuration.
GitLab
Please see this page for info on setting up Gitlab server-side pre-receive hooks.
References
- Pre-Commit : A framework for managing and maintaining multi-language pre-commit hooks
- Customizing Git: Git Hooks
- pre-commit-hooks: Some out-of-the-box hooks for pre-commit.