Apply conventional commit style on your project with commitlint
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 (editing files, adding a new file, and deleting files), we must commit to adding our change in Git history. A commit is usually a message that summarizes what we did on the codebase.
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.
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
Rules for footer commit
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:
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.