Photo by Oscar Ivan Esquivel Arteaga on Unsplash

Git is widely used across the software engineering world to build software because it helps to keep the history of changes in our codebase throughout the build of software by a team. After performing some code changes (edit file, add a new file, and delete files), we need to commit to adding our change in Git history. A commit is usually a message that summarizes what we did on the codebase.

git commit <message>

[optional description]

[optional footer(s)]
Structure of a commit message

When working in a team, the commit message can be hard for others to understand what you have done and the scope of your application changes. Let's take an example by saying we are working on a Full Stack Web Application and make a pull to the origin to new updates made by our teammates. Once done, we have a commit message like those below in the git history:

minor changes,  fix issue, wip,  test ci, small refactoring

Yeah! I see you laugh because you have already written commit messages like these. 😁

When reading this message, we would ask our self: Which changes?, Which issues? Where? Why?

The commit message is bad, and we could write them better. Let's go:

apply minor changes on the way form are validated. More details in description

fix issue which prevents user to upload a document

create form for user registration: wip

running unit and integration tests in test stage inside the pipeline

give a better variable name and delete useless code

The commit message is well written now, but they aren't read-friendly for humans. Imagine we have a bug in production, and we want to revert to the previous working commit, which was about a profile update feature. We would read all our commit messages from the top to the bottom until we found a message that matches what we are looking for.

To summarize, we need something that helps us write human-readable commits. Here is what conventional commit come in help.

What is conventional commit?

From the conventional commit specification website:

The Conventional Commits specification is a lightweight convention on top of commit messages. It provides an easy set of rules for creating an explicit commit history; which makes it easier to write automated tools on top of.

It is inspired by and based heavily on the Angular Commit Guidelines who introduce two properties.

git commit -m <type>[optional scope]: <message>

[optional description]

[optional footer(s)]
Structure of a commit message following conventional commit specification

Type

It indicates the type of commit. Is it a feature, a fix, a test, a refactoring, tooling, etc...

The recommended values are:

  • build: changes that affect build components like build tool, ci pipeline, dependencies, project version, etc...
  • chore: changes that aren't user-facing.
  • enh: changes that improve a feature.
  • docs: changes that affect the documentation.
  • feat: changes that introduce a new feature.
  • fix: changes that patch a bug.
  • perf: changes which improve performance.
  • refactor: changes which neither fix a bug nor add a feature.
  • revert: changes that revert a previous commit.
  • style: changes that don't affect code logic, such as white-spaces, formatting, missing semi-colons.
  • test: changes that add missing tests or correct existing tests.

Scope

It indicates the scope in your application concerned by the commit. It is not required, and you are responsible for the definition of values. Some examples: authentication database settings translation

Rules about the commit message

  • Use the imperative, present tense: "change" not "changed" nor "changes."
  • Don't capitalize the first letter
  • No dot (.) at the end

It should contain information indicating if it's breaking changes or the reference issues that this commit refers to, or the name of authors who contributed. Remember that the footer is optional so, feel free to provide or not this information.

When the commit is a breaking change, the footer's first sentence should start with the keyword. BREAKING CHANGE

Example of valid messages

feat(translation): add support for french language
style(database): delete useless code inside the generated migration
build: release version 2.1.0
feat(authentication): replace the authentication provider from xxx to yyy


BREAKING CHANGE: ticket enpoints no longer supports list all entites.
refers to JIRA-1337
Co-authored-by: teco, pinas, pavelito 
perf(payments): improve payment processing by 500ms

In the following part, we will see how to configure conventional commit so that our commit message will be validated before performing the commit.

Setup the project

The project example is a Node.js app, but you can follow the same process for a  project in others languages.

To start, we will start with the final result of the project covered by this tutorial. Let's clone the repository and launch it:

git clone https://github.com/tericcabrel/node-ts-starter

cd node-ts-starter

yarn install

yarn start
Setup the project example

Configure conventional commit

Now we have a working project, install packages required to add conventional commit in our project:

yarn add -D @commitlint/cli @commitlint/config-conventional

Create a file conventionalCommit.json who will contain the configuration of our types and scopes. Note that if you don't provide this file. The default configuration will be used. Paste the code below inside the file:

{
  "types": {
    "build": {
      "description": "Changes which affect CI configuration files and scripts"
    },
    "chore": {
      "description": "Changes which aren't user-facing"
    },
    "enh": {
      "description": "Changes which improve a feature"
    },
    "docs": {
      "description": "Changes which affect documentation"
    },
    "feat": {
      "description": "Changes which introduce a new feature"
    },
    "fix": {
      "description": "Changes which patch a bug"
    },
    "perf": {
    	"description": "changes which improve performance."
    },
    "refactor": {
      "description": "Changes which neither fix a bug nor add a feature"
    },
    "revert": {
      "description": "Changes which revert a previous commit"
    },
    "style": {
      "description": "Changes which don't affect code logic, such as white-spaces, formatting, missing semi-colons"
    },
    "test": {
      "description": "Changes which add missing tests or correct existing tests"
    }
  },
  "scopes": {
  	"app": {},
    "library": {},
    "authentication": {},
    "settings": {}
  }
}

Create a file commitlint.config.js then add the code below:

const conventionalCommit = require("./conventionalCommit.json");

const typesEnum = Object.keys(conventionalCommit.types);
const scopesEnum = Object.keys(conventionalCommit.scopes);

module.exports = {
  extends: ["@commitlint/config-conventional"],
  rules: {
    "type-enum": [2, "always", typesEnum],
    "scope-case": [2, "always", ["camel-case"]],
    "scope-enum": [2, "always", scopesEnum],
    "subject-empty": [2, "never"],
    "subject-case": [2, "always", ["lower-case"]],
    "header-max-length": [2, "always", 72],
  },
};

Explanation

We read the types and scopes from the conventionalCommit.json file. Then we override types and scopes configuration by setting a value for type-enum and scope-enum respectively. We set the case of the scope to `camelCase`. About the subject, we indicate that it is never empty and has is in `lower-case,` and the max length is 72 characters.

The number 2 is the error level, 1 for warning level, and 0 to disable the rule.

For more details, check the documentation of commitlint.

Commit hook

Now we want to perform commit message format validation before executing the commit. Git provides some hook used to perform actions before, after, or during a Git action like (commit, push, pull, etc...). The one who interests us is the commit-msg hook. Check this link to know more about it.

There is a Node package that helps us to define an action for git hook easily called husky we are going to install:

yarn add -D husky

# Activate husky
yarn husky install

# Add the hook
yarn husky add .husky/commit-msg "yarn commitlint --edit $1"

Open the package.json and add the code below as the children of the root object (same level as dependencies, devDependencies, scripts, etc..):

"husky": {
    "hooks": {
       "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
}

Test

Let's test what happens if our commit message doesn't follow the conventional commit rule.

git add .
git commit -m "setup commitlint for commit message validation"

Whoops! We got an error:

Now try with a valid message:

git add .
git commit -m "chore(app): setup commitlint for commit message validation"

Hooray 🎉

Let's see what happen if we provide a valid types but an unexisting scopes

As you see, you will be noticed when you use an invalid scope.

Find the final code for this tutorial here.

We reached the end of this tutorial, and I hope you found it interesting. Feel free to join me on Twitter for any questions.