A Git Pre-Commit Hook for Running Checkstyle via Maven
At Black Pepper we use Checkstyle by default on any new Java project to enforce code style. Integrated with Maven, this will fail the build in the event of any violations. This frees up developers enormously, allowing them to focus on important decisions rather than arguing about where to put their opening braces, and prevents the intent of Git commits from being lost in whole-file IDE reformatting.
Unfortunately, unlike in Eclipse, Checkstyle integration with IDEA is pretty clunky. Violations are almost invisible unless you have the culprit file open in the editor, and as code refactorings often create violations in other files, there’s a good chance these won’t be spotted.
In fact, it sometimes seems that the majority of CI failures are caused by Checkstyle errors. Developers often have the discipline to run any tests they think may have broken before pushing back a new feature; what they’re less good at is visually verifying that the maximum line length hasn’t been exceeded in a file due to some refactoring operation, or manually running the IDEA Checkstyle tool.
Running Checkstyle Automatically
It would be nice to run Checkstyle automatically in our workflow somehow, to catch violations before they occur on CI, thus saving ourselves a tedious context switch when we break the build. I’ve got very simple requirements for what I’d like here:
- It must run before every commit. I don’t want any commits in my local repository with violations, or else I feel like I have to go back in history and edit them to prevent them being pushed back broken.
- It must be quick. Really quick. I’ve heard horror stories about extensive build jobs being run in Git hooks, and how these would inevitably get disabled to prevent developers’ utter exasperation. You should be committing all the time. Anything over a few seconds would be unacceptable.
A Git Hook for running Checkstyle
First off, the simplest possible content of a file at ${GIT_DIR}/hooks/pre-commit
:
mvn checkstyle:check
This works, but completely fails on requirement 2):
- It executes Maven even if Java files haven’t changed, and so no violations could possibly occur; and
- It executes on every submodule of the top-level project POM, even if nothing has changed within them.
It takes about ten seconds on my current project, on every single commit. We can do better.
Doing Better
#!/bin/bash -e
function get_module() {
local path=$1
while true; do
path=$(dirname $path)
if [ -f "$path/pom.xml" ]; then
echo "$path"
return
elif [[ "./" =~ "$path" ]]; then
return
fi
done
}
modules=()
for file in $(git diff --name-only --cached \*.java); do
module=$(get_module "$file")
if [ "" != "$module" ] \
&& [[ ! " ${modules[@]} " =~ " $module " ]]; then
modules+=("$module")
fi
done
if [ ${#modules[@]} -eq 0 ]; then
exit
fi
modules_arg=$(printf ",%s" "${modules[@]}")
modules_arg=${modules_arg:1}
export MAVEN_OPTS="-client
-XX:+TieredCompilation
-XX:TieredStopAtLevel=1
-Xverify:none"
mvn -q -pl "$modules_arg" checkstyle:check
Now this is a big improvement:
- We only consider staged
.java
files; - For each of these, we walk up the project filesystem tree looking for any
pom.xml
files, and if found, assume this is a module we need to run Checkstyle on; and - We add some JVM options to the Maven invocation to encourage the JVM to start up a bit quicker.
On my current project (greenfield, 8 weeks of development to date and 8 Maven submodules) this hook runs in 2-3 seconds for most commits – and it’s already saved me from breaking the build numerous times. Latest source on GitHub – a big thanks to Nick “*nix Wizard” Holloway for the code review.