This image shows a library with busts in front of aisles holding old books. I chose this image because I felt that the busts could represent the developers that contributed to a software application and the books document their contributions, much like the log of a Git repository.
Photo by Giammarco on Unsplash

Conventional Commits: A Better Way

Michael Collins
Neudesic Innovation
8 min readJan 23, 2022

--

Let’s be honest: a lot of developers do not use the commit log. How many times do you think that developers have started on an existing project by typing git log in the terminal and reading through the messages? And if a developer did sit down and typed in git log, how many of the messages would have provided actual value in describing the change? How many messages were simple messages like fixed bug or deleted file? How many messages helped the reader understand if a new feature was being implemented?

Conventional Commits is a specification that seeks to improve commit messages in general. Conventional Commits defines a standard format commit messages. Using Conventional Commits, the reader can glean valuable context about a commit such as whether the commit implements a new feature, refactors existing code, fixes a defect, adds a unit test, or deletes code. Conventional Commits also provides commit messages that can easily be processed by automated tools to produce documentation such as release notes. In this post, we will take a detailed look at Conventional Commits.

Standardizing Commit Messages

Conventional Commits is a specification for the format and content of a commit message. The concept behind Conventional Commits is to provide a rich commit history that can be read and understood by both humans and automated tools. Conventional Commits have the following format:

<type>[(optional <scope>)]: <description>

[optional <body>]

[optional <footer(s)>]

Commit Types

The <type> field provides the context for the commit. The <type> field communicates the intent of the change that was made. Did the commit introduce a new feature? Did the commit improve unit testing and code coverage? Did the commit improve the documentation in the project?

The <type> field is an enumerated type. Your project can define what values that you want to use. On my projects, I define the following <type> values:

  • build: The commit alters the build system or external dependencies of the product (adding, removing, or upgrading dependencies).
  • change: The commit changes the implementation of an existing feature.
  • chore: The commit includes a technical or preventative maintenance task that is necessary for managing the product or the repository, but it is not tied to any specific feature or user story. For example, releasing the product can be considered a chore. Regenerating generated code that must be included in the repository could be a chore.
  • ci: The commit makes changes to continuous integration or continuous delivery scripts or configuration files.
  • deprecate: The commit deprecates existing functionality, but does not remove it from the product. For example, sometimes older public APIs may get deprecated because newer, more efficient APIs are available. Removing the APIs could break existing integrations so the APIs may be marked as deprecated in order to encourage the integration developers to migrate to the newer APIs while also giving them time before removing older functionality.
  • docs: The commit adds, updates, or revises documentation that is stored in the repository.
  • feat: The commit implements a new feature for the application.
  • fix: The commit fixes a defect in the application.
  • perf: The commit improves the performance of algorithms or general execution time of the product, but does not fundamentally change an existing feature.
  • refactor: The commit refactors existing code in the product, but does not alter or change existing behavior in the product.
  • remove: The commit removes a feature from the product. Typically features are deprecated first for a period of time before being removed. Removing a feature from the product may be considered a breaking change that will require a major version number increment.
  • revert: The commit reverts one or more commits that were previously included in the product, but were accidentally merged or serious issues were discovered that required their removal from the main branch.
  • security: The commit improves the security of the product or resolves a security issue that has been reported.
  • style: The commit updates or reformats the style of the source code, but does not otherwise change the product implementation.
  • test: The commit enhances, adds to, revised, or otherwise changes the suite of automated tests for the product.

The <type> field can be used with automated tools to automatically generate release notes or update a change log for the product release. If you keep a change log following the Keep a Changelog style, you could map the <type> fields to change log headings like this:

  • featAdded
  • changeChanged
  • deprecateDeprecated
  • removeRemoved
  • fixFixed
  • securitySecurity

Scopes

The <scope> field is optional and is used to tag a package or module of the product that the commit affects. The <scope> field is based on an enumeration like the <type> field. On new projects, I typically start without a scope and as my app or project becomes more complex, I will start defining scopes.

Description

The <description> field is a very short summary of the intent or content included in the comment. The <description> field can be used as a title to introduce the change and then use the <body> field to provide more details of the change.

When writing the <description> field, I prefer to use future tense. Instead of fixed some bug, I will write fix some bug. Or instead of created class MyClass, I will write create MyClass. I write the title to describe the action that I sought to perform when making the change that the commit represents. I also start my commit messages in lower case. This is a personal preference, but I think that it appears better with the <type> and optional <scope> fields preceding it.

⚠️ IMPORTANT: The first line of Git commit messages are recommended by some standards to be 52 characters or less to be displayed as titles when body text is present. I try to keep the total length of the <type>, <scope>, and <description> fields to this length if possible.

Body

If necessary, additional details in the commit can be included in the body of the commit message. The body can include multiple paragraphs to describe the change that was made or feature that was added. Providing documentation in the <body> field is recommended for all but simple changes. By providing detailed documentation in the body, other developers can be informed of the changes that occurred in the product by reading the Git commit log. Body messages should focus more on explaining why a change was made rather than how. If interested, the reader can read the source code in the commit or look at the diff view between commits to understand how the change was made.

The body should be written in natural language with proper casing, grammar, and punctuation. Se sentence case and start all sentences with a capital letter. All sentences should conclude with a period.

⚠️ IMPORTANT: The maximum length of body lines should not exceed 72 characters. 72 characters is the recommended maximum length to make reading the commit history using the git log command in the terminal. In the terminal, Git will indent commit messages by 8 spaces. Enforcing the 72 character line limit will ensure that the body is readable in the terminal.

Footers

Footers are optional, but can provide additional metadata for a commit; be used to alert readers and tools to significant changes such as breaking changes; or can link commits to issues or pull requests. Footers are written using the format:

<token>: <value>

The Conventional Commits specification introduces the BREAKING CHANGE footer token that is used to indicate that the commit introduces a non-backwards compatible change. A breaking change will result in the major version number of the product version being incremented following the Semantic Versioning specification. The value for a BREAKING CHANGE footer is a description of the breaking change to be included in the product’s change log and release notes.

GitHub defines keywords that can be used as footer tokens to link commits to issues or pull requests. These keywords will automate activity suck as closing an issue when a pull request merges the commit into the main branch. The GitHub keywords are:

  • Close
  • Closes
  • Closed
  • Fix
  • Fixes
  • Fixed
  • Resolve
  • Resolves
  • Resolved

The footer value should be a reference to a GitHub issue. If the commit contains a bug fix that has been identified in the project’s GitHub Issues database with identifier 10, then the footer would be written as:

Resolves: #10

If the commit fixes an issue that has been identified in a different repository, you can link the commit to the other repository’s issue by prepending the repository name to the issue:

Resolves: neudesic/projectcenter#22

You can specify multiple footers to link the commit to multiple issues.

Automating Version Numbering

Conventional Commits and Semantic Versioning go together well. Using automated tools that scan the commits between product builds, you can automate the changing of the version number for each build:

  • The presence of a fix commit can increase the patch version number. If the previous release version was 1.2.3, making a fix command will increment the version to 1.2.4.
  • The presence of a feat commit can increment the minor version number and reset the patch version. If the previous build’s version number was 1.2.4, a feat commit will increment the version number to 1.3.0.
  • The presence of a BREAKING CHANGE footer can increment the major version number. If the previous version number was 1.3.0, the presence of a BREAKING CHANGE header can increase the product version number to 2.0.0.

Ensuring Conventional Commit Adoption

Adopting Conventional Commits is one thing, but enforcing and validating that everyone is really following that standard is something else. Fortunately, there are tools that you can use to ensure that commit messages follow the standard and they can either be run as part of a pull request build on GitHub, or installed as a pre-commit hook in your team’s local Git repositories.

Commitlint is a tool written in Node.js that will validate commit messages to ensure that they use the Conventional Commit specification. Commitlint can be installed to run as a pre-commit hook using Husky. When configured in your developer’s local repositories, Husky and Commitlint will ensure that your commit does not succeed unless it meets your team’s rules for proper commit messages.

My rules for Commitlint are shown below:

The rules define the list of commit types that I introduced earlier. I also configure that the maximum line length of the header is 52 characters and the body is 72 characters. The line length rules are set as warnings because there will be odd cases where I may need to include a URL in a commit message that will be longer than 72 characters.

Conclusion

Git commit messages and your repository’s history are important artifacts that need to be treated responsibly. The commit history can be valuable if it contains meaningful information that will allow someone to understand the current state of an application by exploring its history. Adopting Conventional Commits will help to keep your Git history meaningful, and will give you the added benefit of being able to automate the generation of release notes or keeping your change log up to date.

--

--

Michael Collins
Neudesic Innovation

Senior Director of Application Innovation at Neudesic; Software developer; confused father