Git hooks, practical uses (yes, even on Windows)
What are Git hooks? Can you do anything useful with them? Also, since Git hooks come from Linux, is there anything special you need to do to get them working on Windows?
What are Git hooks?
Git hooks allow you to run custom scripts whenever certain important events occur in the Git life-cycle, such as committing, merging, and pushing. Git ships with a number of sample hook scripts in the repo\.git\hooks
directory, but they are disabled by default. For instance, if you open that folder you’ll find a file called pre-commit.sample
. To enable it, just rename it to pre-commit
by removing the .sample
extension and make the script executable (chmod +x pre-commit
if you’re on Linux, or just check your NTFS execute rights if you’re on Windows). When you attempt to commit using git commit
, the script is found and executed. If your pre-commit script exits with a 0 (zero), you commit successfully, otherwise the commit fails.
If you take a look at the default sample pre-commit script , it does a few helpful things by default, like disallowing non-ascii filenames since they cause issues on some platforms, and checking for whitespace errors. This means that if you enable this script and have “whitespace errors”, like lines that end in spaces or tabs, you’ll fail to commit and have to remove the whitespace before you can commit.
What cool stuff can I do with Git hooks?
Since you’re working with scripts, you can do pretty much anything with Git hooks. That being said, just because you can do something… Yeah, you know. Git hooks can make the behavior of common Git tasks like committing, pushing and pulling nonstandard, which can annoy people, especially if they’re just trying to get used to the glories of Git. But here are a few examples of what you might accomplish with Git hooks.
Make sure your name and email are set properly with a pre-commit hook
It’s really annoying when you’re pushing to a public repository hosting service like GitHub and you have the wrong name or email in your Git config.
# (pre-commit or pre-push)
# Make sure my email is set properly
useremail=$(git config user.email)
if [ "$useremail" != "[email protected]" ]
then
cat <<\EOF
Error: user.email not set to "[email protected]"
EOF
exit 1
fi
Here’s the same script, but this time it makes sure Git knows my name before it lets me push my stuff:
# (pre-commit or pre-push)
# Make sure my name is set properly
username=$(git config user.name)
if [ "$username" != "Ty Walls" ]
then
cat <<\EOF
Error: user.name not set to "Ty Walls"
EOF
exit 1
fi
You can add these code snippets to either pre-commit
or pre-push
, but I recommend pre-commit
in this case. Otherwise you may have to edit your commit history
to correct your name and email.
Don’t push something you’ll regret to a public Git repo
You’ve heard the horror stories of people accidentally pushing sensitive information such as passwords, email addresses, API keys, etc to their public repo. Ryan Hellyer accidentally leaked his Amazon AWS access keys
to GitHub and woke up to a $6,000 bill the next morning. Here’s a simple pre-commit
or pre-push
script snippet that will fail to commit or push if the outgoing diff matches one of the blacklisted keywords.
# (pre-commit or pre-push)
# Fail if any matching words are present in the diff
matches=$(git diff-index --patch HEAD | grep '^+' | grep -Pi 'Word1|Word2|Word3')
if [ ! -z "$matches" ]
then
cat <<\EOT
Error: Words from the blacklist were present in the diff:
EOT
echo $matches
exit 1
fi
Of course, if you include the words here, you’ll have to make sure that the hook script doesn’t somehow end up getting pushed to your repo. Perhaps rather than keeping the blacklist in plain text right there in the script, you could store it in another encrypted format somewhere else on your computer. The script could be adjusted to parse that file to obtain the blacklisted words.
Use pre-push
to only push a green build
You may have read my article on writing a Psake build script
to facilitate continuous integration and general awesomeness on your project. If your Psake script already builds and tests everything, why not run it in pre-push
hook? Then you’ll save yourself the embarrassment of pushing a broken build.
# (pre-push)
# Make sure we can build and all tests are passing
exec powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\build.ps1" Tests
Do Git hooks have any limitations?
Well, if you’re thinking you can use Git hooks to impose some Machiavellian workflows on your team, you may want to reconsider that. Client side hooks can often be bypassed, either by using low-level “plumbing” commands instead of the high-level “porcelain” commands, and often by passing the --no-verify
option to the command. For example, git commit --no-verify
will not run the pre-commit
hook. On the server side, you might have more success with your draconian schemes. But you have been warned. :)
Sharing your hooks
Another limitation is that client-side Git hooks aren’t checked into the repo, and thus aren’t shared amongst team members. If you want to share handy hooks with your fellow code meisters, you’ll have to dream up some distribution scheme. Some people create a directory in their repo which contains the Git hooks and is under source control. Then they create a link from .git\hooks\*
to that folder, perhaps via a shared update script of some kind.
Making use of Git’s new hooksPath
option
[UPDATE June 18, 2016]: As of Git 2.9.0, it’s now possible to specify the location of your Git hooks with a configuration option - <code>hooksPath</code> !
What if you have some Git hooks that you want to use by default with all of your repositories? Aside from installing Git 2.9 or better, all you have to do is copy them to a central location and configure Git to look there for your hooks. For example, create a folder in your home directory called .githooks
. Then run the following Git command:
git config --global core.hooksPath '~/.githooks'
(If you’re not familiar with Unix-like systems, the ~
represents your home directory. On Windows it would evaluate to %userprofile%.)
But what if you don’t want to use these global hooks for a particular repo? Just configure Git to use the default location for that repo, like this:
git config core.hooksPath '.git/hooks'
Note the lack of the --global
option, indicating that you are setting a configuration option for the current repository.
Word of caution
Git hooks adds a layer of complexity to your repository and make its behavior non-standard. That’s why many experts recommend not using git hooks in the first place. I’m no expert, but I agree. Avoid Git hooks unless they provide some real quantifiable value.
Why won’t Git hooks work on windows?
The first time I tried using Git hooks on Windows, they did absolutely nothing. They didn’t even fail with an error. I made sure that they were named correctly, without the .sample
extension, and I verified that my account had execute permissions to the hook script.
The problem was lurking in the first line of the script, the “shebang” declaration:
#!/bin/sh
On Unix-like OS’s, the #!
tells the program loader that this is a script to be interpreted, and /bin/sh
is the path to the interpreter you want to use, sh
in this case. Windows is definitely not a Unix-like OS. Git for Windows
supports Bash commands and shell scripts via Cygwin
. By default, what does it find when it looks for sh.exe
at /bin/sh
? Yup, nothing; nothing at all. Fix it by providing the path to the sh
executable on your system. I’m using the 64-bit version of Git for Windows, so my shebang line looks like this.
#!C:/Program\ Files/Git/usr/bin/sh.exe
Don’t forget to escape the space (’ ‘) with a backslash. After you correct the path to sh.exe, your script should run okay.
One thing you’ll realize though: Bash and Windows aren’t super compatible. So your hook script will work okay as long as you stick to the basics, but if you need anything more advanced and you’re using Windows, you’ll need to use PowerShell instead of sh
. Fortunately this isn’t difficult, and I’ve actually already shown you how to do it in the part about running your Psake script in the pre-push
hook. All you have to do is call the PowerShell script from within your bash script. For example, to write a fancy pre-commit hook in PowerShell, create a PowerShell script with all of your lovely pre-commit logic and call it, say, pre-commit.ps1
. Then add the following to your pre-commit
script.
#!C:/Program\ Files/Git/usr/bin/sh.exe
exec powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\.git\hooks\pre-commit.ps1"
If you want to prevent the commit, make sure that pre-commit.ps1
exits with a value other than zero.
Have you discovered some handy uses for Git hooks? Post your comments below.