magnifying glass over an old book

$ git log

commit f9d601108fd2a19ff7b5dfc62a790083b9af500c
Author: CF Frost <cf.frost@example.com>
Date:   Wed Jan 17 14:08:36 2018 -0500

    fixed that bug

commit 253b7e2f445e65222ef81f33cf4070660084a3dc
Author: CF Frost <cf.frost@example.com>
Date:   Wed Jan 17 14:07:05 2018 -0500

    tweaks to button styles

commit 7e12e77eae5e4ca80e7b3fb55db5c6d954d2cbe4
Author: CF Frost <cf.frost@example.com>
Date:   Wed Jan 17 14:06:16 2018 -0500

    added a new action

We’ve all seen Git histories like this. To some it’s no big deal, but on my team this will get your pull request (PR) declined regardless of how your code looks. In fact we won’t merge your PR unless your commit messages conform to a very specific standard: Conventional Commits. We chose this standard for its readability and its ability to automate our semantic versioning & changelog.

Conventional commits are structured as <type>(<scope>):<subject>. (I’ll let you read the above document for further details on the convention.) Adhering to this structure with detailed subjects and message bodies when appropriate allows us to create a truly meaningful history. When browsing this history we can really see the progression of development, and when reviewing PRs we have a brief summary of changes right in front of us. However, the real value of a meaningful commit message may not come for months down the road.

A Case Study

The following is based on actual events.

Let’s say we have a software engineer named Dorcas. Dorcas’s product owner, Throckmorton, came to her to ask, “Why doesn’t this user see the same links that I do on the homepage?”. Dorcas didn’t know, so she told him that she would find out and get back to him. Dorcas only took a few minutes to find the code that was causing the issue Throckmorton had called out. She looked through the commit history on BitBucket and saw this commit:

commit 3b85fbfebac285745e15da57e89d591dac21a3fe
Author: Myrtle Carter <myrtle.carter@example.com>
Date:   Fri Aug 4 15:58:12 2017 -0400

    feat(links): filtered out links for non-premium users

    Only premium users should see links to premium features.
    JIRA-1337

Myrtle left Dorcas a gold mine of information in this commit message. Now she can see that the reason for the filtering has to do with premium features. She even included a message body with a bit of reasoning behind the change. There is something else really important here that we haven’t discussed yet: a JIRA task ID. With this, Dorcas was able to look up the JIRA task and learn even more details about the change and why it was done. As it turns out, the given user was not a premium user. She is able to write up a quick instant message with an explanation and a link to the JIRA task and send it off to Throckmorton.

Thanks to Myrtle’s diligence in her commit messages, a great deal of time searching is saved for Dorcas and anyone she would have had to otherwise engage to find the answer Throckmorton needed.

Squashing commits

Another key that helped Dorcas find the answer so quickly, was her team’s practice of squashing commits before merging PRs and keeping PRs small (limited to a single feature). This keeps “work in progress” commits out of the history, so that each commit is more meaningful and the history as a whole is easier to read and understand. Most hosted git solutions have a built-in option for squashing commits when merging PRs, but it can also be done manually through a soft reset and commit or an interactive rebase. For the following example, we’ll walk through a soft reset and commit assuming that the user is making a pull request from feature/user-alert to main.

  1. First, make sure we are up-to-date by fetch the latest from our origin repo.
    $ git fetch origin
    
  2. Then we will do a soft reset to main which will make our git history identical to that of main, and stage our changes to be committed.
    $ git reset --soft origin/main
    
  3. Now we are ready to commit.
    $ git commit
    
  4. After committing we’ll need to force push since we are rewriting history.
    $ git push origin feature/user-alert -f
    

It is worth noting that there are rare occasions when it makes sense to have multiple commits in a single PR, as long as each of those commits represents an individual unit of work. Like the pirate code, this is more of a guideline than an actual rule.

Tooling

There are some great open source couple tools for JavaScript projects that can set us up for success with our commit messages. commitlint provides a CLI for validating commit messages. We’ll need to install this CLI and our config before continuing.

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

To add our config we’ll create a commitlint.config.js in the root of our project:

module.exports = {
  extends: ['@commitlint/config-conventional']
};

Using githook-scripts we can easily add this into our workflow with the following package.json configuration:

{
  ...
  "scripts": {
    "githook:commit-msg": "commitlint --edit $GIT_PARAMS"
  }
  ...
}

A contributor could try to get past that without a valid commit message by using the --no-verify flag when they commit, so we’ll add a test for the git history, which we can use in our PR build process to validate all commits in the branch that aren’t in main yet. This is recommended to be run in your test script like so:

{
  ...
  "scripts": {
    "test:spec": "jest",
    "test:git-history": "commitlint --from main --to HEAD",
    "test": "npm run test:spec && npm run test:git-history",
    "githook:pre-commit": "npm test"
  }
  ...
}

Now you are all set and ready to rock with your detailed & meaningful commit messages. Though it may take some time to get used to for a team that hasn’t enforced commit message standards before, it’s sure to save you some headaches down the line. While this may not be for every team, it certainly has been beneficial for mine. Many other teams within American Express have differing approaches with their histories and still deliver quality software to production.