As developers, it’s common for us to jump into existing projects — sometimes with large code bases — to fix a bug or work on a new feature. We often start by navigating the source code to understand how it was built and how components within the application interact with each other.
But even when we find the exact place where a bug occurs, it may not be clear what the right solution to the problem is or how it got there in the first place.
Thankfully, there’s a tool called Git that can investigate who and why a particular code was changed or added when used correctly. But, if your experience is anything like mine, it’s possible that when accessing the Git history of your project, you find something like this:
Not very helpful, right? It is impossible to identify which commit is relevant to us, as all descriptions are the same or not, well, descriptive.
Can this be fixed? Let’s discuss how commitlint comes to save the day.
Commitlint is the ESLint for your commit messages. It performs validations on any text against a predefined commit format. Users can configure these formats to their needs or adopt pre-built-in conventions, such as conventional commits.
Because the tool can be piped to the output of other processes, it easily integrates with your development workflow by validating the messages right before committing changes, pushing, or using any other Git hook.
Before learning how to set it up, let’s see it in action:
Commitlint is easy to set up for either npm or Yarn projects. Let’s start by installing the tool as a dev dependency.
Because we will be using the default configuration, we need to install two different commitlint modules, the CLI tool, and the actual configuration. From your terminal, run:
npm install --save-dev @commitlint/{cli,config-conventional}
Or, using Yarn:
yarn add @commitlint/{cli,config-conventional} --dev
Lastly, you need to create a commitlint.config.js
file with your configuration options. To do that, you can execute the following in your terminal:
echo "module.exports = { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js
For commit message validations to run automatically on every Git commit command, we will use Husky, a tool that enables us to set up Git hooks quickly.
It’s pretty straightforward, so let’s jump into the commands:
Install Husky
npm install husky --save-dev
Activate hooks
npx husky install
Add commit-msg hook
npx husky add .husky/commit-msg 'npx --no-install commitlint --edit $1'
Install Husky
yarn add husky --dev
Activate hooks
yarn husky install
Add commit-msg hook
yarn husky add .husky/commit-msg 'yarn commitlint --edit $1'
With everything now set up, we can try to commit with an invalid text format and see what happens:
~ git commit -m "commit" ⧗ input: commit ✖ subject may not be empty [subject-empty] ✖ type may not be empty [type-empty] ✖ found 2 problems, 0 warnings ⓘ Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint
It is clear that commitlint rejected the message “commit.” We also know the reasons why, so let’s fix our message and see the results:
~ git commit -m "chore: add commitlint on commit-msg" [master (root-commit) e0f064f] chore: add commitlint on commit-msg 5 files changed, 3412 insertions(+) create mode 100644 .gitignore create mode 100755 .husky/commit-msg create mode 100644 commitlint.config.js create mode 100644 package-lock.json create mode 100644 package.json
When the message satisfies the criteria, the commit command continues its workflow and stores the changes.
It’s all done. Commitlint is now validating all of your commit messages and helping you enhance your commit history. Now let’s discuss the default convention and how we can write quality commit messages.
Let’s leave the technical realm for a moment to focus on writing — more precisely, how to write good commit messages that are self-explanatory and pass the commitlint default validations.
A good typical commit message will have the following structure:
<type>(<scope?>): <subject!> <BLANK LINE> <body?> <BLANK LINE> <footer?>
Let me explain each part.
The type
is mandatory and determines the intent of the change. Here are possible values:
build
: changes affecting build systems or external dependenciesci
: updating configuration files for continuous integration and deployment serviceschore
: updating grunt tasks etc.; no production code changedocs
: documentation-only changesfeat
: a new featurefix
: a bug fixperf
: a code change that improves performancerefactor
: a code change that neither fixes a bug nor adds a featurestyle
: changes that do not affect the meaning of the code (white-space, formatting, missing semicolons, etc.)test
: adding missing tests or correcting existing testsA scope is an optional value that provides additional contextual information about the change. For example, when the module’s name, npm package, or particular routine was affected.
The scope, when present, must be contained within parenthesis.
The subject is the headline of the commit. It should summarize in one sentence the nature of change.
For the subject, consider the following rules:
The body is an optional space to provide additional information about the change, its motivation, and what was done. As it is the case of the subject, the body is written in the present tense.
Lastly, the footer is an optional placeholder for referential information, e.g., alert for breaking changes or refer US numbers or references.
Breaking changes should start with the word “BREAKING CHANGE:” with space or two newlines.
First, let’s look at some examples I’ve created:
Example 1:
feat(payment): add a new endpoint to calculate taxes This allows the payment module to calculate taxes for an order based on the order information. Currently only US sales tax and European VAT are supported Refs #45621
Example 2:
build(docs-infra): regenerate docs after deployment pipeline completes Automates the process of building the documentation portal after changes are merged into develop, release and master branches.overloads. PR Close #43614
Here are some other excellent examples from GitHub:
Example 1:
fix(bazel): construct a manifest file even when warnings are emitted Previously if _any_ diagnostics were emitted, regardless of their category, the manifest would not be generated. This means that if a target emits only warnings and no errors, it would still fail to build because it does not generate all the required output files (specifically the `.es5.MF` file). Now the manifest file is generated as long as there are no error diagnostics in the result. This makes `ng_module()` support compiler warnings as a user would expect. Added a test that uses extended template diagnostics to trigger the invalid banana in box diagnostic. This generates a warning and uses Skylib's `build_test()` to verify that it builds successfully. Unfortunately, there is no easy way to verify that the warning diagnostic is emitted at all. `expected_diagnostics` should be able to do that, but it doesn't seem to have any effect on `ng_module()` and may not be integrated. Instead, testing that a target with warnings builds correctly is the best we can easily do here without a deeper investigation. PR Close #43582
Example 2:
docs: reviewed tag added (#43472) PR Close #43472
Example 3:
test(router): refactor tests to not use deprecated loadChildren (#43578) Many of the tests in the router code use the deprecated loadChildren as a string. This has been deprecated for years and can easily be changed to just a function that returns the module. PR Close #43578
Developers hate spending time on trivial tasks such as formatting text. That’s why they built amazing automation tools, such as ESLint, Prettier, and now commitlint — to make their lives easier. More importantly, they built these tools because they know the value of having nicely formatted and standardized code and messages.
Would you invest time in this automation and process for the value it brings to you, your project, and your organization? I certainly do!
ESLint and Prettier are already part of our lives. Let’s welcome commitlint to the developer’s productivity tools family.
Thanks for reading!
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.