Git hooks are a very useful feature in the git. Git hooks are scripts that you can place in a hooks directory. They are triggered every time an specific event occurs in a Git repository. They let you customize Git’s internal behavior and trigger customizable actions at key points in your CI/CD and git workflow.
Some of the common use cases include to encourage a commit policy, altering the project environment depending on the state of the repository, to trigger continuous integration workflows before and after commits, etc. However since scripts can be written as per the requirements at hand, you can use Git hooks to automate or optimize virtually any aspect of your development workflow.
Location of Git Hooks
Every Git repository has a .git/hooks folder with a script for each hook you can bind to. More strictly, the default hooks directory is $GIT_DIR/hooks, but that can be changed via the core.hooksPath configuration variable. You’re free to change or update these scripts as necessary, and Git will execute them when those events occur.
Hooks can reside either in local machine or server-side repositories, and they are only executed to the actions in that repository (whether an action is occurring at local level or server level). The rest of the blog post applies to both local and server-side hooks, unless specified otherwise.
Here’s a full list of hooks you can attach scripts to:
- applypatch-msg
- pre-applypatch
- post-applypatch
- pre-commit
- prepare-commit-msg
- commit-msg
- post-commit
- pre-rebase
- post-checkout
- post-merge
- pre-receive
- update
- post-receive
- post-update
- pre-auto-gc
- post-rewrite
- pre-push
Installing Git Hooks
As discussed above, by default, hooks are stored in the .git/hooks directory. If you navigate to the directory and do a list, you would see out of the box available hooks:
These represent most of the available hooks, but the .sample extension prevents them from executing by default. To install a hook, all you have to do is remove the .sample extension. Or, if you’re writing a new script from scratch, you can simply add a new file matching one of the above filenames, minus the .sample extension.
For example, we can install commit-msg hook using below command:
Also to be noted that git hooks need to be executable. So one needs to add executable bit +x
to the hook using chmod
. Since it is already assigned executable permission in our case, we need not perform it again.
The built-in sample scripts are very useful references, as they document the parameters that are passed in to each hook (they vary from hook to hook). Hooks can get their arguments via the environment, command-line arguments, and stdin.
Scripting Languages
Most of the built-in scripts uses Shell and PERL, but you can use any scripting language as long as it can be run as executable. The shebang operator (#!) for the script, defines the scripting engine to be used for execution of the script. So in order to use a different language, all you have to do is to change it to the path of your language engine.
For instance, we can write an executable Python script for pre-commit hook by using below code:
#!/usr/bin/env python import sys, os commit_msg_filepath = sys.argv[1] with open(commit_msg_filepath, 'w') as f: f.write("# Please include a useful commit message!")
The flexibility of being able to choose an scripting engine of one’s choice, makes it very powerful.
Scope of Hooks
Hooks are generally local to any given Git repository and they are not copied over to the new repository when one runs git clone
. That is why we were using terms like client side hooks or server side hooks till now. Also, since hooks are local, they can be altered by anybody with access to the repository.
So this is an important point to consider when configuring git hooks. First we need to find a way to make sure hooks stay up to date among your development team members. Second, developers should not need to manage git hooks by themselves.
Since directory for git hooks is not cloned with the rest of the repository and nor it is under version control, it poses a problem. A simple solution, therefore, is to store your hooks in the actual project directory (apart from .git section), and then create a script which creates symlinks to install the hooks. Or you can ask developers to do it by themselves by copying and pasting into hooks directory. Both of these are not viable in the long run and prone to human errors.
As an alternative, git also provides a Template Directory mechanism that makes it easier to install hooks automatically. All of the files and directories contained in this template directory are copied into the .git directory every time you use git init
or git clone
.
That said, it is still upto the owner of the repository to manage git hooks and alter/install/remove the same. So most of the organizations prefer to maintain server side hooks where they can be managed and enforced.
Local Hooks
Local hooks affect only the repository in which they reside. Below are some of the most common local hooks:
- pre-commit
- prepare-commit-msg
- commit-msg
- post-commit
- post-checkout
- pre-rebase
The first 4 hooks let you define the entire commit life cycle and the other 2 let you perform some extra actions or safety checks. Note that all of the pre-hooks control the action that is about to take place while the post- hooks allow to control what happens after actions. Generally, post- hooks are to trigger some api calls or send notifications.
Pre Commit git hook
pre-commit is invoked before obtaining the proposed commit log message and making a commit. You can use this hook to obtain snapshot of what is being committed and perform some action. Exiting with a non-zero status from this script causes the git commit
command to abort before creating a commit and therefore failure to create commit. Also this hook does not accept any arguments.
Let’s take a look at default pre-commit script:
https://github.com/git/git/blob/master/templates/hooks–pre-commit.sample
The first few lines check whether this is an first commit in the repository or not. Next it checks whether Non-ASCII characters can be used as filenames or not. Next it checks if there are whitespace errors, print the offending file names and fail.
The git diff-index --cached
command compares a commit against the index. By passing the ––check option, we’re asking it to warn us if the changes introduces whitespace errors. If it does, we abort the commit by returning an exit status of 1, otherwise we exit with 0 and the commit workflow continues as normal.
Also note that this hook can be bypassed by using ––no–verify option
Prepare Commit git hook
The prepare-commit-msg hook is called after the pre-commit hook to populate the text editor with a commit message. Before calling this hook, the default message has already been prepared by git. So this is a good place to alter the default message to be used for commit.
This hook takes from one to three arguments as below:
1. The name of a temporary file that contains the message. You change the commit message by altering this file.
2. The type of commit. This can be message (–m or –F option), template (–t option), merge (if the commit is a merge commit), or squash (if the commit is squashing other commits).
3. The SHA1 hash of the relevant commit. Only given if –c, –C, or ––amend option was given.
Same as with pre-commit
hook, exiting with a non-zero status aborts the commit.
Let’s take a look at default prepare commit message script:
https://github.com/git/git/blob/master/templates/hooks–prepare-commit-msg.sample
There is plenty of comment mentioned in the script itself to explain its working. However we can make it more useful by including custom actions. Let’s say if commit is being made in an issue- branch or feature- branch, we can detect and include the same in our commit message along with some instructions:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
import sys, os, re | |
from subprocess import check_output | |
# Collect the parameters | |
commit_msg_filepath = sys.argv[1] | |
if len(sys.argv) > 2: | |
commit_type = sys.argv[2] | |
else: | |
commit_type = '' | |
if len(sys.argv) > 3: | |
commit_hash = sys.argv[3] | |
else: | |
commit_hash = '' | |
print "prepare-commit-msg: File: %s\nType: %s\nHash: %s" % (commit_msg_filepath, commit_type, commit_hash) | |
# Figure out which branch we're on | |
branch = check_output(['git', 'symbolic-ref', '–short', 'HEAD']).strip() | |
print "prepare-commit-msg: On branch '%s'" % branch | |
# Populate the commit message with the issue #, if there is one | |
if branch.startswith('issue-'): | |
print "prepare-commit-msg: Oh hey, it's an issue branch." | |
result = re.match('issue-(.*)', branch) | |
issue_number = result.group(1) | |
with open(commit_msg_filepath, 'r+') as f: | |
content = f.read() | |
f.seek(0, 0) | |
f.write("ISSUE-%s %s" % (issue_number, content)) | |
# Populate the commit message with the feature #, if there is one | |
if branch.startswith('feature-'): | |
print "prepare-commit-msg: Oh hey, it's an feature branch." | |
result = re.match('feature-(.*)', branch) | |
feature_number = result.group(1) | |
with open(commit_msg_filepath, 'r+') as f: | |
content = f.read() | |
f.seek(0, 0) | |
f.write("FEATURE-%s %s" % (feature__number, content)) |
A sample git commit would look like below:
Commit Message git hook
This hook is invoked by both git-commit
and git-merge
, and can be bypassed with the ––no–verify option. It takes a single parameter, the name of the file that holds the proposed commit log message.
Same as with above two scripts, exiting with a non-zero status causes the command to abort.
This hook can be used to warn developers by printing a message that their commit message does not adhere to certain standards or even modify the message entered. However, it should not be used for something like triggering a continuous integration workflow. This hook can also be used to refuse the commit after inspecting the message file.
For example, below code makes sure that the user didn’t delete the ISSUE-[#] or FEATURE- [#] string that was automatically generated by the prepare-commit-msg
hook in the previous section:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
import sys, os, re | |
from subprocess import check_output | |
# Collect the parameters | |
commit_msg_filepath = sys.argv[1] | |
# Figure out which branch we're on | |
branch = check_output(['git', 'symbolic-ref', '–short', 'HEAD']).strip() | |
print "commit-msg: On branch '%s'" % branch | |
# Check the commit message if we're on an issue branch | |
if branch.startswith('issue-'): | |
print "commit-msg: Oh hey, it's an issue branch." | |
result = re.match('issue-(.*)', branch) | |
issue_number = result.group(1) | |
required_message = "ISSUE-%s" % issue_number | |
with open(commit_msg_filepath, 'r') as f: | |
content = f.read() | |
if not content.startswith(required_message): | |
print "commit-msg: ERROR! The commit message must start with '%s'" % required_message | |
sys.exit(1) | |
# Check the commit message if we're on an feature branch | |
if branch.startswith('feature-'): | |
print "commit-msg: Oh hey, it's an feature branch." | |
result = re.match('feature-(.*)', branch) | |
feature_number = result.group(1) | |
required_message = "FEATURE-%s" % feature_number | |
with open(commit_msg_filepath, 'r') as f: | |
content = f.read() | |
if not content.startswith(required_message): | |
print "commit-msg: ERROR! The commit message must start with '%s'" % required_message | |
sys.exit(1) |
Post Commit git hook
The post-commit hook is called immediately after the commit-msg
hook. It can not change the outcome of the git commit operation, so it is used primarily for notification purposes.
The script takes no parameters and its exit status does not affect the commit in any way.
This hook is not present by default in the .git/hooks directory and needs to be created first.
For most post-commit scripts, you will want access to the commit that was just created and send out some notification or trigger a continuous integration build. For example, below code can be used to sent an email to entire team that an commit has been received:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
import smtplib | |
from email.mime.text import MIMEText | |
from subprocess import check_output | |
# Get the git log –stat entry of the new commit | |
log = check_output(['git', 'log', '-1', '–stat', 'HEAD']) | |
# Create a plaintext email message | |
msg = MIMEText("Look, I'm actually doing some work:\n\n%s" % log) | |
msg['Subject'] = 'Git post-commit hook notification' | |
msg['From'] = 'git@example.com' | |
msg['To'] = 'dev-team@example.com' | |
# Send the message | |
SMTP_SERVER = 'smtp.example.com' | |
SMTP_PORT = 587 | |
session = smtplib.SMTP(SMTP_SERVER, SMTP_PORT) | |
session.ehlo() | |
session.starttls() | |
session.ehlo() | |
session.login(msg['From'], 'secretPassword') | |
session.sendmail(msg['From'], msg['To'], msg.as_string()) | |
session.quit() |
Although it is possible to use post-commit to trigger a local continuous integration system, however this is not an good practice unless this is a server side hook. You should ideally be initiating builds in the post-receive
hook which is a server side hook.
We’ll discuss other generally used hooks in upcoming posts.
[…] our previous post, we discussed what are git hooks, how to install git hooks ,few of the local git hooks and […]
LikeLike