Enhance your commits with Git hooks!
Published on 3 January 2019 • 7 min

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.
  • pre-push: last checks before sharing (pushing to the remote).

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 (version 7). 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”, putting its scripts in a .husky/ directory that it points the core.hooksPath local Git setting to.

As a bootstrap, we kindly put a small project boilerplate on GitHub for you 😘: dmbrehin/dev-automation.

Should you favor setting it up by hand, here you go:

In your terminal:

# Install
npm install --save-dev husky
# Create the `.husky` directory
npx husky install
# Spare your coworkers from having to setup hooks too.
npm set-script prepare "husky install"

Still in the terminal, depending on the tooling you wish to use, tell husky what scripts to run on which Git actions:

# Sets up .husky/pre-commit with relevant scripts
husky add .husky/pre-commit "npx --no-install lint-staged && npx --no-install git-precommit-checks"
# Sets up .husky/commit-msg with relevant scripts
npx husky add .husky/commit-msg "npx --no-install commitlint --edit $1"
# Sets up .husky/pre-push with relevant scripts
npx husky add .husky/pre-push "npx --no-install validate-branch-name"

A significant benefit of having your husky scripts right in your project is that they can now call other project-local scripts. Say I have a git-hooks subdirectory; I could then call my script from my husky configuration. Here’s an example for pre-commit:

#!/bin/sh
source "$(dirname "$0")/_/husky.sh"

npx --no-install lint-staged
npx --no-install git-precommit-checks
./git-hooks/check-stage.js

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): lint-staged (I used to got with precise-commits, but it’s not maintained anymore).

In your terminal:

# Install
npm install --save-dev lint-staged
# Set up the `lint-staged.config.js` config file so it triggers Prettier
echo "module.exports = { '*.js': 'prettier --write' }" > lint-staged.config.js

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:

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

You then need to set up git-precommit-checks.config.js which contains your settings and rules. Here’s an example:

module.exports = {
  display: {
    notifications: true,
    offendingContent: true,
    rulesSummary: false,
    shortStats: true,
    verbose: false,
  },
  rules: [
    {
      message: 'You’ve got conflict markers laying around',
      regex: /^[<>|=]{4,}/m,
    },
    {
      message:
        'Hold off the commit! You left things in explicitly marked as non-committable',
      regex: /do not commit/i,
    },
    {
      message: 'Looks like you still have some work to do?',
      nonBlocking: true,
      regex: /(?:FIXME|TODO)/,
    },
    {
      message: 'Sure looks like you left a "if (true)" somewhere',
      regex: /if\s+\(?(?:.*\|\|\s*)?true\)?/,
    },
    // JS specific
    {
      filter: /\.js$/,
      message:
        '😫 Seems that auto-imports weren’t so great on Material-UI components or styles',
      regex: /^import \{ .* \} from '@material-ui\//,
    },
    {
      filter: /\.js$/,
      message: '🤔 Hu.  There are "console.log(…)" call in there.',
      nonBlocking: true,
      regex: /^\s*console\.log/,
    },
    // Ruby/Rails specific
    {
      filter: /_spec\.rb$/,
      message: 'Your RSpec suite is pared down by "focus" markers',
      regex: /(?:focus: true|:focus => true)/,
    },
    {
      filter: /_spec\.rb$/,
      message: 'Your Ruby tests still have `save_and_open_page` calls',
      regex: /save_and_open_page/,
    },
    {
      filter: /\.rb$/,
      message:
        'Did some debugging, right?  Don’t leave that `binding.pry` in there.',
      regex: /^[^#]*\bbinding\.pry/,
    },
  ],
}

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!

  === Looks like you still have some work to do? ===
  src/App.js
  src/utils/song.js

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

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

Ensure commit messages are well-written

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

We’re using conventional commits (inspired by the conventional changelog).

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.

# Install
npm install --save-dev @commitlint/cli @commitlint/config-conventional
# Set up configuration commitlint.config.js
echo "module.exports = { extends: ['@commitlint/config-conventional'] }" > commitlint.config.js

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’re quite forgetful, 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:

# Install (globally is required to expose the Git "cz" subcommand)
npm install --global commitizen
# Launch the wizard (instead of typing `git commit`)
git cz

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

Well-named branches

Just as we like our commit messages well-formed, we want our branches to be well-named. There is not (yet) a well-established naming convention there, but we do have our own conventions, so let’s enforce them.

Once again npm’s magic hat ✨ comes to the rescue with validate branch name.

It’s quick to set up

# Install
npm install --save-dev validate-branch-name

Then you set up the .validate-branch-namerc.js file with the patterns you allow:

module.exports = {
  pattern: '^(main|staging|production)$|^(bump|feat|fix|rel(?:ease)?)/.+$',
  errorMsg:
    '🤨 You fool! The branch you’re trying to push does not Honor The Code.',
}

Here’s what would happen when trying to push an incorrectly-named branch:

$ git push -u origin beep-boop

  🤨 You fool! The branch you’re trying to push does not Honor The Code.
  Branch Name: "beep-boop"
  Pattern:"/^(main|staging|production)$|^(bump|feat|fix|rel(?:ease)?)\/.+$/g"

  husky - pre-push hook exited with code 1 (error)
  error: failed to push some refs to 'github.com:mbrehin/space-balls.git'

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.

Would you like to go one step further and fully master Git core concepts, or to get advice on how to guarantee the quality of your Git projects? We can help or train you - just tell us what you need!