My Git Hooks, Part 2 - Making a Git Hook
This is Part 2 of a series of posts on my git hooks. Part 1 described why I did this, and why it’s in Ruby. For reference, here’s a link to some git hook documentation.
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…
<<<<
,>>>>
, or====
2, which usually mean an incomplete git merge.console.log
calls3, 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!), andalert
(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.
No “Fast-Fail”
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
--no-verify
.
Boom! Bugs galore. Instead, I report all problems, to let the user make informed decisions.
Next
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.
Footnotes
-
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
<<<<
and>>>>
is enough. ↩ -
console.log makes sense in a Node project, but I can’t ignore it based on file extension since both Node and client-side JavaScript use .js. I plan to detect project type using Heroku-like heuristics, and then ignore console.log checks in Node projects. See my GitHub issue regarding this. ↩