Cussing-free git commits

Posted on - last update on

A few months ago I got a message from a random engineer on the company slack; he told me that he reviewed one of my pull request, and I might want to go through my code again, since I left some “inappropriate” debug logs there. I immediately checked what he could’ve mean, and I found the println() with a content that was unmistakably created out of sheer rage and desperation caused by hours of an unsuccessful bug hunting session. Unless you have a temper of a zen-master, I bet you know what kind of bug I’m talking about.

I prefer to prevent “accidents” like this one - after all, this is what agile is about, right? So I started to look for a solution - then ended up at git hooks pretty fast. However, with git hooks we have basically two options:

  • run the same pre-commit hook for every repository and skip the repo-defined hook (follow the red arrow)

or

  • copy my pre-commit hook to every repository I work, worked and ever will work with (go as the green arrow). Neither is a good choice. 😕

running-both-local-and-global-git-hooks

What I want instead is to run both, basically to go through the dashed arrow. And there is a fairly simple way to achieve this.

Configuring core.hooksPath

A local hook is something that lives in the repository; it is getting executed on everyone’s machine who works on the repo.

A global hook lives on the machine instead - if I have global hooks then they will will be triggered for every repository I work on my machine, no matter what the repository is.

The issue is that git has only a single config entry for setting the hook path, and this is a setting for the machine. We can set an absolute path here (like /home/foo/bar) or a path relative to $GIT_DIR (like the default value, $GIT_DIR/hooks). It is intentional that we have a single directory; git does not want to take up the responsibility to deal with execution orders of multiple triggers. Fair.

What we could do instead is to write a global hook that executes the local hook as well 💡 and since the work directory of a running hook is the root of the repository, it is easy to call the local hook from the global one. 🎉

Execute global hooks THEN local hooks

Lets set the core.hooksPath to an absolute path then

git config --global core.hooksPath $HOME/.git/hooks

…And now we can have a pre-commit file in that directory:

#!/bin/bash

# execute global functions here...

# ... then call the local hook
if [ -f .git/hooks/pre-commit ]; then
	.git/hooks/./pre-commit
fi

Since executing the local hook is the last step, the global hook will exit with the same code as the local hook did.

Swear-word filtering logic

We can abort the commit process with exiting with a non-0 code from the hook.

We will use git diff --cached to get all the changes since the last commit and grep -if FILE_WITH_EXCEPTIONS to collect all occurrences popping up in the input:

CUSS_WORDS="$HOME/.git/hooks/no-no.txt"

if git diff --cached | grep -if $CUSS_WORDS >/dev/null; then
	echo "cuss words detected! abort commit"
	exit 1
fi

This same solution could be used to filter out any kind of dangerous strings; you can even add your API keys, just as a second line of defense for not committing and pushing them onto a public repository.

Conclusion

In this post we saw how to

  • create a simple git hook
  • execute multiple hooks with from different paths
  • prevent unwanted strings commited and pushed upstream

You can find the whole pre-commit hook in this gist.