pre-commit: How to create hooks for unsupported tools

A melange of ancient tools that may interest you.

The pre-commit framework lists hundreds of hook repositories on its hooks page. You can drop these into your configuration file and get a tool running in seconds. But there are many more tools out there that you might want to use, which you can run with custom configuration.

Custom hooks use the special local repository name, meaning they’re local to your project. Then you need to configure the hook with the fields listed in the docs, including language which defines how to install the tool. Let’s look at the options for language and a couple of examples setting up different tools.

Languages, lawful and chaotic

pre-commit hooks use a “language”, selecting from the supported list.

Most of these represent a programming language with an associated package manager, for which pre-commit will manage an isolated environment. For example, the python language creates a Python virtual environment directory, installing dependencies with pip.

But some are more like “pseudo-languages”, using some other method to run the hook. For running arbitrary tools, there is the system language, which can run any program.

It’s easiest to use the managed programming languages, since pre-commit can handle isolation and version management for you. But if needs must, you can use system, with the drawback that your team will need to install and update the relevant tool by hand.

The following examples cover running a tool in a fully managed lanaguage, node (Node.js), and then a pre-installed tool with system.

node hook: stylelint, a CSS linter

stylelint is a CSS linter that can help you write valid, consistent CSS. At time of writing its repository does not have a pre-commit hooks file. But since it is available on npm, you can configure pre-commit to install and run it with a local node hook.

Here’s an example hook for running stylelint:

repos:
-   repo: local
    hooks:
    -   id: stylelint
        name: stylelint CSS linter
        language: node
        additional_dependencies:
        -   stylelint@14.16.1
        -   stylelint-config-standard@29.0.0
        entry: stylelint
        args: [--formatter, unix, --fix]
        types_or: [css]
        exclude: '.*\.min\.css'

A thorough dissection:

That’s it for configuring pre-commit. stylelint also needs a configuration file. To use the stylelint-config-standard package as-is, drop this JSON into .stylelintrc in the repository root:

{
  "extends": "stylelint-config-standard"
}

After adding any new hook, check it against all applicable files:

$ pre-commit run stylelint --all-files
stylelint CSS linter.....................................................Failed
- hook id: stylelint
- exit code: 2
- files were modified by this hook

/.../example.css:3:16: Unexpected invalid hex color "#0" (color-no-invalid-hex) [error]

1 problem (1 error, 0 warnings)

In this example, stylelint found several issues, some of which it auto-fixed, and one which it reported as an error. The auto-fixes standardize the formatting:

@@ -1,4 +1,3 @@
-:root
-{
-    --darkest: #0;
+:root {
+  --darkest: #0;
 }

The error covers an invalid hex colour code, #0, which needs manually correcting:

@@ -1,3 +1,3 @@
 :root {
-  --darkest: #0;
+  --darkest: #000;
 }

After those fixes, the hook passes:

$ pre-commit run stylelint --all-files
stylelint CSS linter.....................................................Passed

Bri-lint-iant.

When adding a hook, you can commit the hook and initial fixes in one. This way, pre-commit will always pass, no matter which commit you are on.

system hook: jpegoptim, a JPEG optimizer

jpegoptim is a JPEG optimization tool. When passed a JPEG, it optimizes it to shrink file size without affecting quality, an easy way of saving disk space and bandwidth.

Since jpegoptim is written in C, there is no platform-independent package manager to install it with. So, you need to use the system language to run it in pre-commit, and rely on setting it up manually.

Here’s an example hook for running jpegoptim:

repos:
-   repo: local
    hooks:
    -   id: jpegoptim
        name: Optimize JPEGs
        language: system
        entry: jpegoptim
        types_or: [jpeg]

The fields similar to above, though fewer are required. Some notes:

After adding the hook, you can again optimize all JPEGs in your repository with:

$ pre-commit run jpegoptim --all-files

Let’s see what the hook looks like when it runs during commit. Add a new JPEG and try to commit:

$ git status -s
A  donut.jpeg

$ git commit -m "Mmm, donut."
Optimize JPEGs...........................................................Failed
- hook id: jpegoptim
- files were modified by this hook

donut.jpeg 550x541 24bit N Exif IPTC JFIF  [OK] 66520 --> 56017 bytes (15.79%), optimized.

jpegoptim found 15% savings, sweet. Because the file changed, pre-commit blocked the commit.

Check it looks fine, and then continue:

$ open donut.jpeg

$ git status -s
Found existing alias for "git status". You should use: "gst"
AM donut.jpeg

$ git add donut.jpeg
Found existing alias for "git add". You should use: "ga"

$ git commit -m "Mmm, donut."
Optimize JPEGs...........................................................Passed
[main cc4e434] Mmm, donut.
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 donut.jpeg

And that’s how to run a system tool within pre-commit.

Push your hook definitions upstream

You can adapt custom configuration for a tool in a supported language into a .pre-commit-hooks.yaml file in the repository. Adding configuration into a tool’s repository makes it more discoverable and allows pre-commit autoupdate to keep you on the latest version. See the documentation and existing repositories for details.

Most projects are more than happy to add the .pre-commit-hooks.yaml file, along with a little documentation. After you’ve done so, add the repository to the all-repos.yaml file for pre-commit.com, to make it show up on the website.

Fin

May you be hooked on code quality!

—Adam


Learn more about pre-commit in my Git DX book.


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: