Enhance your commits with Git hooks!
By Delicious Insights • Published on Jan 3, 2019

This post is also available in French.

When I have to share my work on projects, I want to feel confortable and ensure that what I share is clear and optimal.

A few years ago, when I looked at my VCS history I found it to be sometimes hard to read and analyze. I moved on and tried to make better commits, avoiding the “What the commit?” effect with generic or weird unusable messages like fix stuff.

Let’s sum this up. When speaking of good commits, what I mean is:

  • I want my content and code to be optimal;
  • the resulting history has to be precise and meaningful.

Because I am lazy (like most developers I met in my career, which is not a bad thing 😅), I don’t want to think about this every time I create a commit. I want it to be automated.

Here comes our savior: Git and its hooks.

  • pre-commit: check and sometimes rewrite parts of my (non-optimal) code/content;
  • commit-msg: check my commit messages.

Setup and share

Sadly Git has no efficient process to share hooks inside a project (despite Git 2.9 and its git config core.hooksPath…).

When scouring the web for better solutions you can find some alternatives. My preferred one is husky. Because I work mostly on web projects or Node.js scripts I use npm, and husky is an npm module we can install as a dev dependency and share through our package.json file inside our project.

How does it work? Husky is a Git hooks wrapper. It means that when installing your project with its dependencies (through npm install), it will “hook Git hooks”, inserting its own callbacks inside ./.git/hooks/ scripts.

In the terminal:

npm install --save-dev husky

Then in the package.json we’re ready to define our hooks:

"husky": {
  "hooks": {
    "commit-msg": "…",
    "pre-commit": "…"
  }
},

A significant advantage is that we can now call scripts built into the project. Suppose I created a custom script in a git-hooks subdirectory. I could then call my script from my husky configuration:

"pre-commit": "./git-hooks/check-stage.js"

We can also chain our script calls:

"pre-commit": "./git-hooks/check-stage.js && ./git-hooks/another-script"

Note: you don’t have to work with JavaScript to use npm and husky; it’s just a convenient way of making it work everywhere 😁.

Check the code before committing

I’m trying to be a super-hero developer but I still make mistakes. I leave things in that shouldn’t appear in my code. I also forget the conventions I should be using in my projects (for instance: how to format my code).

When asking other developers it appears that I am not the only one facing these problems. Because we are human we are prone to fatigue, distraction, laziness… 😅

Therefore we’d better find some tools to guide us and fix our mistakes.

My second brain (aka Christophe, my boss) already found a wonderful tool for code auto-formatting that works with many languages (JS, CSS, HTML, SCSS, Markdown, JSX…): Prettier. That tool is already configured to work with our VSCode editor. But if VSCode fails to run prettier or if we want to edit some file with another editor, we’d like Prettier to run nonetheless.

Therefore, Prettier must run on the updated or created code that gets committed, whoever is contributing it to our project. Many npm modules are available for that but only one can process only our staged work (the one that is going to be committed): precise-commits.

In the terminal:

npm install --save-dev precise-commits

Then in the package.json:

"husky": {
  "hooks": {
    "commit-msg": "…",
    "pre-commit": "precise-commit"
  }
},

I still have to check for undesirable content. Once again I looked on the mighty Internet for a suitable tool but nothing matched my needs as I wanted a customizable tool. I ended up building my own 🤘: git-precommit-checks.

The goal is to setup some rules to be run on what’s being committed. A rule can match a file pattern (otherwise all the updated/created files are targeted). Then a regex runs on each file content; if a match is found it will print a message on the terminal as an error or a warning. An error is a blocking rule and will therefore stop the commit.

For instance I don’t want to leave some console.log in my JS files and I want to prevent failed merge to pass through (no conflict markers left in the code). I also want to be warned when I forget FIXME or TODO keywords, but without stopping my commit.

In the terminal:

npm install --save-dev git-precommit-checks

Then in the package.json:

"husky": {
  "hooks": {
    "commit-msg": "…",
    "pre-commit": "git-precommit-checks && precise-commit"
  }
},
"hooks": {
  "pre-commit": [
    {
      "message": "You’ve got leftover conflict markers",
      "regex": "/^[<>|=]{4,}/m"
    },
    {
      "filter": "\\.js$",
      "message": "You’ve got leftover `console.log`",
      "regex": "console\\.log"
    },
    {
      "message": "You have unfinished devs",
      "nonBlocking": "true",
      "regex": "(?:FIXME|TODO)"
    }
  ]
},

Here is an example of what it could look like in your terminal:

$ git commit -m 'feat(demo): display pre-commit checks'

  husky > pre-commit (node v10.14.1)
  ✔  contents checks: there may be something to improve or fix!

  === You have unfinished devs ===
  src/App.js
  src/utils/song.js

  ✖  contents checks: oops, something’s wrong!  😱

  === You’ve got leftover conflict markers ===
  src/components/Player.js

Ensure commit messages are well written

This is only possible when you’re using a commit message convention.

At Delicious Insights we’re using the conventional changelog guidelines.

We only add a small extra: using a text ellipsis at the end of our messages when there is more then one line for the description (apart from issue reference), that is, when the description has a “body.”

Once again, we found a useful module on npm that can help us with our message formatting: commitlint. It checks the text we wrote and stops the commit if the expected rules are broken.

npm install --save-dev @commitlint/cli @commitlint/config-conventional

We must update our package.json to tell commitlint that we’re using the conventional-changelog and then tell husky to use it:

"commitlint": {
  "extends": [
    "@commitlint/config-conventional"
  ]
},
"husky": {
  "hooks": {
    "commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
    "pre-commit": "git-precommit-checks && precise-commit"
  }
},

Here is an example:

# First try: bad format
$ git commit -m 'Bad message format'

  husky > commit-msg (node v10.14.1)
  ⧗   input:
  Bad message format

  ✖   message may not be empty [subject-empty]type may not be empty [type-empty]
  ✖   found 2 problems, 0 warnings
  husky > commit-msg hook failed (add --no-verify to bypass)


# Second try: good structure, "type" key unknown
$ git commit -m 'type(context): message type is unknow'

  husky > commit-msg (node v10.14.1)
  ⧗   input:
  type(context): message type is unknow

  ✖   type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test] [type-enum]
  ✖   found 1 problems, 0 warnings
  husky > commit-msg hook failed (add --no-verify to bypass)


# Last try: good message
$  git commit -m 'feat(context): this one is well formatted'

  husky > commit-msg (node v10.14.1)
  ⧗   input: feat(context): this one is well formatted
  ✔   found 0 problems, 0 warnings

Checking downstream is good, but checking ahead is better!

Maybe you’re like me and you have a goldfish memory, you can’t remember how to write your messages. So you’ll be happy to get some help.

commitlint comes with a built-in wizard (@commitlint/prompt-cli), but I prefer git commitizen that lets us write git cz instead of git commit and launch a wizard.

In our terminal:

npm install --save-dev commitizen cz-conventional-changelog

Then update our package.json to tell commitizen that we’re using the conventional-changelog:

  "config": {
    "commitizen": {
      "path": "cz-conventional-changelog"
    }
  }

Here is an example:

$ git cz
  cz-cli@2.9.6, cz-conventional-changelog@1.2.0


  Line 1 will be cropped at 100 characters. All other lines will be wrapped after 100 characters.

  ? Select the type of change that you're committing:
    docs:     Documentation only changes
    style:    Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
    refactor: A code change that neither fixes a bug nor adds a feature
  ❯ perf:     A code change that improves performance
    test:     Adding missing tests or correcting existing tests
    build:    Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
    ci:       Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
  (Move up and down to reveal more choices)

  ? Denote the scope of this change ($location, $browser, $compile, etc.):
  context

  ? Write a short, imperative tense description of the change:
  this is a good description

  ? Provide a longer description of the change:

  ? List any breaking changes or issues closed by this change:
  Close #42

  husky > commit-msg (node v10.14.1)
  ⧗   input: feat(context): this is a good description
  ✔   found 0 problems, 0 warnings

  [master 72015cc] feat(context): this is a good description

Voila, better safe than sorry!

As time passes we see how automation is becoming ever more present. I really like the way we can share these small automations inside our projects thanks to Git and npm.

You can now try and go further, and figure out wayts to automate commit messages with spellcheckers, use speech synthesis to notify the user when there is a problem, live translate the message, etc. It’s now up to you to identify your needs and use Git hooks to automate as much as possible!

If you want a wider description of Git hooks behavior you can check our previous article about it.