When working with multiple people on the same code base, simultaneously or over time, it is important to maintain a consistent code style. During code reviews, it is important to focus on e.g. the correct application of the SOLID principles and design patterns. However, I often find myself (rightfully) nitpicking about that one closing bracket that is indented with five spaces instead of four. This takes up valuable processing power (of my brain!) and might lead to more serious issues being overlooked.
Enter linting. Big in the worlds of JavaScript and Python, linting generally serves two purposes. First, enforcing code style, including white space, bracket placement and indentation. Second, automatically analyzing the code for potential bugs and weaknesses. I’m exclusively focusing on enforcing code style here, as many enterprise Java projects are using SonarQube or a similar tool to analyze the code for potential issues.
Installing pre-commit
We’ll use Git pre-commit hooks to run out linters before every commit, and pre-commit to manage our hooks. Get started by installing pre-commit on your machine by running e.g. pip install pre-commit
or brew install pre-commit
.
Configuring pre-commit
Create a file named pre-commit-config.yaml
in the root of your repository that will contain the configuration for this project. A good starting point for a configuration for a Java project would be the following:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
hooks:
- id: check-merge-conflict
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
rev: v2.0.0
hooks:
- id: pretty-format-java
args: [--aosp,--autofix]
This configuration uses hooks from two repositories, three default pre-commit hooks and one custom linter:
check-merge-conflict
checks for merge conflict markers in your code, so you will not accidentally commit thoseend-of-file-fixer
makes sure that a file always ends with a newlinetrailing-whitespace
trims any trailing whitespacepretty-format-java
is the most important one here, as it runs the Google Java Formatter over your code. The argument--aosp
tells the linter to use the Android Open Source Project code style rules, and--autofix
tells it to automatically fix any formatting issues
Activating pre-commit
To install the pre-commit hooks from your configuration file into the .git/hooks
folder, run pre-commit install
. Also make sure to commit your pre-commit-config.yaml
file.
Pre-commit is now activated and will check any subsequent commits with the linters you have configured. Note that the --autofix
option of pretty-format-java
linter makes changes in your Java files, but does not stage them for commit. So the commit will fail and you will have to stage (git add
) the modified files and re-run the commit.
Run on all files
Normally, pre-commit hooks only run on modified files staged for commit. For an existing repository, this can become annoying, because even a small functional change in a class will lead to the linter reformatting the entire file, drowning out the actual change in the code review. So did we actually end up in an even worse state now? No, there is just one additional step to take, and it is pretty simple. Make sure your working directory is clean and run the command:
pre-commit run --all-files
This will apply all the hooks you configured to all the files in your repository, and potentially stage any changes for commit. Review the changes and commit them. Surely now we are done, right? Almost, but not quite, because there is still one important step.
Enforce the pre-commit hooks
Anyone working on this project needs to retrieve the pre-commit configuration from Git and run pre-commit install
for the changes to take effect on their machines locally. Now obviously for your current team it seems doable to make everybody do that right away, but what if someone new starts in the team and you forget about this?
The best way to enforce that code only makes it into the repository if it passes the pre-commit hooks is to add a stage in your CI/CD pipeline in e.g. Bamboo or Jenkins that simply spins up a Docker image with pre-commit installed, checks out your repository, and runs pre-commit run --all-files
. That way, the build will fail if any of the pre-commit hooks fail.
0 Comments