Creating a Git Hook
Git Hooks are executable files with certain names in your project’s
/.git/hooks directory. Initializing your git repository creates that directory with a bunch of sample files with bogus names. Rename them, or great you own, and you’ve got a hook.
I want a client-side hook that runs on the local machine, before I get a chance to push changes to a remote repo. In particular I want
pre-commit, the hook that runs first on every commit attempt. So I create a
.git/hooks/pre-commit file, make sure that it’s executable, and write it so that it look at a commit to decide whether or not to allow it.
How do I do that?
git passes no arguments to the hook; it doesn’t need to. A pre-commit hook gleans everything it needs from the
git command and other commands. It’s an executable file running in the git repo, just have it inspect the repo!
Examining the Commit
I’ll skip over how to use
git to get the relevant changes. All of that is in the pre-commit hook file in GitHub. What’s important is that I get references to the changed blocks of code. Then I start looping over the blocks, along with the path to the file.1
Now that I have the changes, I can start looping over them. At its heart, the pre-commit hook is just a big regular expression matcher, checking to see if the file contains any “forbidden” strings. Things like…
====2, which usually mean an incomplete git merge.
console.logcalls3, which will fail in IE.
PRIVATE KEY, which is a tipoff that you’re about to commit a private key for all the world to see to GitHub (oops!), and
alert(Really? Plain alert boxes?)
Even if I find an error immediately I loop over all of the changes and print error messages for all of the suspect lines with file names. Only then do I fail the commit with a nonzero exit code.
Overriding the Checks
The hook doesn’t have to take every edge case into account, so sometimes I play a little fast and loose. Again, the point is to prevent embarrassing accidents, not to be foolproof!
If a user thinks that there was a false positive, or just wants to ignore my hard-won advice (“I like bare alert boxes!”), it’s easy to ignore it. Git has a
--no-verify flag. Just run the commit again with that flag, and the
pre-commit hook won’t be run that time. That’s a pretty big hammer, though; it forces git to skip the
pre-commit check entirely.
To help the committer ignore the checks when he needs to, I always add the following message to the bottom of every failed commit:
-------------- To commit anyway, use --no-verify
Tools and IDEs don’t make it easy to pass that
--no-verify flag. I’m OK with that. Learn to do easy git tasks (like commits) from the command line. You’ll thank me later.
This is why it’s important to not “fail-fast,” but to run through all changes and display all problems. Fail-fast checking can lead to really bad
--no-verify calls. Consider a commit with one “harmless” failure followed by many “bad” calls. In a “fast-fail” implementation, the user could…
- See only the first problem,
- Decide to ignore it, and
- Commit with
Boom! Bugs galore. Instead, I report all problems, to let the user make informed decisions.
In the next few posts I’ll get into…
- Special cases for certain file types,
- Per-project customization,
- Distribution ideas and best practices, and
- Setting up git to “guarantee” that this becomes part of your workflow.
The code examines only the changed lines of code without context. This could miss forbidden code constructs across line boundaries, but they’re rare. If I notice that context around a change is needed frequently to avoid problems, I’ll deal with it. ↩
A few equals signs in a Markdown file makes perfect sense, so I ignore them in .md and .mmd files. I figure that looking for
>>>>is enough. ↩