December 8, 2019

Enforcing Commit Message Format Using Husky

TL;DR: Github repo of the demo.

In the past, I've implemented githooks to check for the commit message format – mainly that it had to begin with a Jira ID. It works well, but the initial setup is a bit clunky – I had a separate repo to store githooks, and a batch file in the main repo to set core.hooksPath to the path of that other repo. Each developer had to pull down the githooks repo in a well-known folder path relative to the main repo and run the batch file...

I've recently introduced Husky to force linting locally before developers can commit (and push) their changes. It meant that I couldn't use the githooks I had for the commit message check, since Husky overwrites them during its installation, so I created a simple script to check for the commit message format and then added as a Husky task.

The demo repo is in Github. I could've used a shell script, or a PowerShell script, but since my team members have Windows and Mac, decided to write it in JavaScript and run it via node, since each front-end developer is guaranteed to have it for running npm. The script uses terminal color codes to highlight errors so that developers can quickly notice it.

  /*
    Assumptions:
    1. Only working with commit messages (tiny commit message files), so no need to worry about large file handling such as using a buffer.
    2. Assumed utf8 format, which is the git default unless changed explicitly (https://git-scm.com/docs/git-commit/#_discussion).
  */

  // Used for console color coding.
  const colorReset = "\x1b[0m";
  const colorBrightRed = "\x1b[1;31m";
  const colorGreen = "\x1b[32m";

  const fs = require("fs");

  // Customize the regex pattern below to match your desired commit message format.  The example pattern below checks
  // that commit messages begin with a Jira ID where the Jira Project Key is "JIRA".
  const pattern = /^JIRA-[0-9]+: .*/;

  // The environment variable is set by Husky.  The value is: .git/COMMIT_EDITMSG
  // Refer to https://git-scm.com/docs/githooks#_hooks for information on how git changes the working directory before
  // executing hooks.
  fs.readFile(process.env.HUSKY_GIT_PARAMS, "utf8", (err, data) => {
    if (err) throw err;

    if (data.match(pattern)) {
      console.log(colorGreen + "check-commit-msg: Passed." + colorReset);
    } else {
      console.log(
        colorBrightRed +
          'check-commit-msg: Failed - please begin your commit message with a Jira ID, followed by ":" and space e.g., "JIRA-123: Added commit message check."' +
          colorReset
      );
      process.exit(1);
    }
  });

One thing I've noticed is that the commit message hook runs after the pre-commit hook. Since I have pre-commit hook that does the linting, it means I have to wait for the linting to finish, then the commit message hook will kick in to let me know whether the commit message is good or not. The official reference doesn't specifically call out the order of hooks, though it looks like it's documented in a top-down order. The official book does say "The pre-commit hook is run first, before you even type in a commit message." This is probably due to how git works internally, since the commit message is actually part of the commit object and not some metadata (as in other traditional source control systems).

Of course, this husky solution only works for front-end (JavaScript) projects. I'll be keeping the standard githooks for other back-end projects (C#, C++, etc.) we have for now.

Also, while working on this, found commitlint. Might be worth a look...

No comments:

Post a Comment