You want to know what’s cool? Not running one Git hook when creating a commit, but
N number of Git hooks. Um.
There are many ways to do this, but my preferred way is to add N number of entries for a particular Git lifecycle event in
.gitconfig that will be called in order when the event is triggered.
Let’s dive in.
Hopefully, everyone is familiar with Git hooks, so I’m not going to spend time on defining them. In this post, I’ll be talking specifically about a specific client-side hook, the
pre-commit hook, although the information here will apply to any of the hooks.
Every time a repository is initialized or cloned, a
.git hidden directory is created in the root of the repository that contains information about all of the internals of the project, such as
Another of the directories that is created is the
hooks directory, and within it templates for all of Git’s client-side lifecycle events. Here are a few:
All of the files are templates with the
.sample file extension and can be used as-is by simply removing the
.sample extension. Although they are mostly shell scripts, they can be written in any scripting language that is present on the system. As long as the files are executable and are properly named, they’ll be executed by Git as the same-named event is fired.
Note that, for security reasons, client-side hooks are not copied when cloned. Instead, the directory will be newly-created with the usual hook templates.
From the official site:
pre-commithook 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.
Now, if you only have one hook, then simply name it
pre-commit, make it executable, and drop it into the
hooks directory. However, if multiple scripts should be invoked when this event occurs, then things can get a bit dicey (ok, not really, as we’ll see in a moment).
For example, let’s say that I not only want to lint, but I want to ensure that all text files end with a newline character and that it has the most recent version of the product’s license at the top of the file.
For this purpose, I’ll want the following three scripts to run every time I commit:
Let’s look at how to implement this.
The first thing to do is to update the appropriate Git config file with the hooks to be run. They will should be defined in either the global config file (usually
$HOME/.gitconfig) or a repo’s local config file (
.git/config) and will be called in the order in which they are defined:
[hooks "pre-commit"] hook = lint.bash hook = EOF.bash hook = license.bash
To set them, you can open the config file and edit manually as above, or you can set them at the command line:
$ git config --global --add hooks.pre-commit.hook "lint.bash" $ git config --global --add hooks.pre-commit.hook "EOF.bash" $ git config --global --add hooks.pre-commit.hook "license.bash"
For local configs, simply replace
The second thing to do is to replace the
pre-commit hook in
.git/hooks, if present, with one that will execute each hook against the individual files that make up the snapshot:
#!/bin/bash # Try for local hooks first. HOOKS=$(git config --get-all --local hooks.pre-commit.hook) if [ -z "$HOOKS" ]; then HOOKS=$(git config --get-all --global hooks.pre-commit.hook) fi if [ -n "$HOOKS" ]; then for HOOK in $HOOKS; do bash ./.git/hooks/pre-commit.d/"$HOOK" if [ "$?" -eq 1 ]; then exit 1 else # Separate the hooks by an empty line. echo fi done fi
The Bash shell script will first look for any locally-defined pre-commit hooks in a repository’s
.git/config before searching for globally-defined hooks (again, usually in
If it finds any, it will push them into the
HOOKS variable that can be iterated over in the
for loop, calling each shell script respectively and doing a hard fail on the first script to return a failure value of 1.
Any script to be run must be placed within
.git/hooks/pre-commit.d/ in this example. Of course, you can name that whatever you’d like.
Git relies upon your scripts returning the appropriate return value to know how to proceed.
A return value of zero will have Git continue with the commit, while a non-zero return value will abort the commit.
Other Git Hooks
As alluded to earlier, this same method can be used for any Git event.
For example, in addition to the
pre-commit event, let’s also add some hooks for the
$ git config --local --add hooks.pre-rebase.hook "foo.bash" $ git config --local --add hooks.pre-rebase.hook "bar.bash"
From the official site:
pre-rebasehook runs before you rebase anything and can halt the process by exiting non-zero. You can use this hook to disallow rebasing any commits that have already been pushed. The example
pre-rebasehook that Git installs does this, although it makes some assumptions that may not match with your workflow.
Now, the local config looks like the following:
[hooks "pre-rebase"] hook = foo.bash hook = bar.bash [hooks "pre-commit"] hook = lint.bash hook = EOF.bash hook = license.bash
Then, simply perform the following:
- Copy the current
- Rename the
pre-committext strings in the new
pre-rebasehook script to
- Create a
.git/hooksdirectory and put your
bar.bashscripts inside it.
- Pet a goat.
You’re done-zo. Sweet.
That’s it! It’s pretty simple, and the implementation leverages Git’s own config definitions, which I like. I found this implementation years ago in a blog post written by one of the authors of Git (I think it was Junio Hamano), but the link now results in a 404, and I can’t find another.