Programming
Facundo Spira • 12 SEP 2023
From chaos to clarity: the power of Conventional Commits
Effective communication is key to any team's success. One of the crucial yet often underestimated elements in this communication in development teams is the art of crafting commit messages.
Conventional Commits aims for effective communication among development teams. It involves a set of rules and guidelines that provide a structured format for commit messages, making them more readable, informative, and standardized across development teams. By categorizing the nature of changes made in a commit, Conventional Commits promotes good coding practices and facilitates effective communication within agile development teams.
Take a look at these two commits below: A (bad example) and B (good example). Can you tell what change is behind each one? Keep reading and discover how to improve the quality of your commits.
Example A:
git log --oneline
a896f446e Fixed lint issues
0d451df6a Fixed build issue
d48e6d7e5 Update vcp mobile
0d9c922a3 Fixed lint issue
5085b3ac9 Removed logs
0aa1b7acf Tests for user
23101b76f resolve PR comments
dc7b9fcec Added mandatory update banner
89a0e734b Added translation
2b4ae9127 Added espanol translation
And let’s compare it with the commit history of a repository that uses conventional commits
Example B:
git log --oneline
b347d10 chore(package.json): update dependencies
a8f2e0e fix(auth): handle null pointer exception in user login
6e9c441 feat(auth): add user authentication feature
fd6a7b3 docs(readme): update installation instructions
2d9fbcf refactor(api): simplify code structure
9eaf20d fix(database): resolve database connection issue
e562db7 feat(search): implement search functionality
0326f3a test(user): add unit tests for user registration
40f51a9 feat!(payment): add payment gateway integration
bae2c98 fix(auth): refactor user login process
As you may have noticed, the first block is more difficult to read and understand, with no clear structure or categorization of the changes made in each commit. In contrast, the second block is much easier to read and interpret. The commit messages are structured and consistent, clearly indicating the type of change made in each commit. This makes it straightforward to understand the purpose and context of each commit, enabling collaboration and future maintenance of the codebase.
Introducing “Conventional Commits”
As described above, using Conventional Commits leads to a more structured and consistent commit history, making it easier to understand and maintain the codebase. Developers can easily generate release notes, automate CHANGELOG generation, and enable powerful integrations with other development tools.
Let’s take a look at the specification and see how it works in detail.
The convention
Formally, conventional commits specification is a convention on top of commit messages which states that a commit message should be structured as follows:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
Below, we’ll dive into each of these different sections.
Type: what did the commit do?
The <type>
field provides the context for the commit, communicating the intent of the change that was made. It tries to answer the question “what did the commit do?”
The <type>
field is an enumerated type that can be defined differently on each specific project. However there are some conventions, for example the @commitlint/config-conventional
based on the Angular Convention, which defines the following types:
build
: The commit introduces a change that affect the build system or external dependencies.chore
: The commit includes necessary technical tasks to take care of the product or repository, but it's not related to any specific feature or user story. These tasks are like routine maintenance, such as releasing the product or updating code for the repository.ci
: The commit involves changes to the continuous integration (CI) configuration or scripts used to automate build, testing, and deployment processes.docs
: The commit updates or adds documentation, such as README files, comments, or user guides, without affecting the code's functionality.feat
: The commit introduces a new feature or enhancement to the product or codebase.fix
: The commit addresses and resolves a bug, error, or issue in the codebase.perf
: The commit makes code changes aimed at improving performance or optimizing existing functionality.refactor
: The commit involves code refactoring, which means restructuring or reorganizing the code without changing its external behavior.revert
: The commit undoes a previous commit, reverting the codebase to a previous state.style
: The commit deals with code style changes, such as formatting, indentation, or code comment adjustments, without affecting the code's functionality.test
: The commit includes changes related to testing, such as adding or modifying test cases, test suites, or testing infrastructure.
Scope: contextual identifier
The scope in a conventional commit message represents a contextual identifier for the changes made in that particular commit. It provides a way to categorize and organize commits, making it easier to track and manage changes in a project. It can be thought of as a descriptor for the location, component, or module that is being modified or affected by the commit.
For example, if a developer is working on a web application with multiple components such as the login system, user profile, and settings page, they can use scopes like login, profile or settings to indicate which part of the application the commit is related to. This helps other team members and future contributors understand the purpose of the commit without having to inspect the actual changes right away.
Description: a quick summary of what the commit intends to achieve
The description provides a concise summary of what the commit accomplishes, making it easier for team members and contributors to understand the purpose of the commit at a glance.
It should be written in the present tense and be clear and informative. It should convey what the code changes do, rather than how they were implemented. It is not necessary to provide every detail in the description; instead, focus on the high-level overview of the modifications.
Body: detailed description of what the commit does
Unlike the description, which provides a concise summary of the changes made in the commit, the body allows for a more detailed explanation of the commit. The body section is used to provide additional context, reasoning, or implementation details that might be helpful for other developers, reviewers, or future contributors to understand the commit thoroughly. It is particularly useful when the changes introduced in the commit are complex or when there are specific design decisions that need to be explained.
It is worth mentioning that having a body section is optional, and not every commit requires it. Simple and straightforward changes might not need a detailed explanation beyond the description.
Footer: additional context or references
Lastly, the conventional commit message can optionally contain one or more footers. Footers are used to include additional information related to the commit, such as references, links, or metadata. It can include various types of content, but the most common use is to reference issues, pull requests, or other commits that are related to the current commit. This referencing is done in a structured way, using the following syntax.
<token>: <value>
One special footer that the conventional commit convention adds is the BREAKING_CHANGE
footer. It is used to indicate that the commit introduces a change that is not backwards-compatible. The value of the footer is a description of the specific breaking change.
GitHub also defines some keywords that can be used as footer tokens to link commits to issues or pull requests which will automate certain activities. Some examples are:
Fixes: <issue number>
orCloses: <issue number>
Links the commit to the specified issue and automatically closes it when the commit is merged into the default branch.Refs: <pull request number>
Links the commit to the specified pull request.Co-authored-by: <name> [email]
Credits other contributors who collaborated on the commit.
Examples: Commit in action
As we mentioned, all commit messages must include a type and a description, while the other components are optional. Here are some examples of how a commit can look like.
1) Simple commit message:
feat: add login functionality
2) Commit with a scope and a multiline body:
feat(profile): add user profile page
This commit introduces a new user profile page that displays user information, profile picture, and recent activities. The profile information is fetched from the backend API.
3) Commit with a multiline body and a footer referencing an issue:
fix: resolve login form validation The login form validation was not properly handling special characters in the password field, causing login failures for some users. This commit addresses the issue by updating the validation regex pattern to allow special characters. Closes #123
4) Commit with a BREAKING CHANGE footer (without body):
feat!: upgrade authentication mechanism
BREAKING CHANGE: Please update your code to use the new authentication flow.
5) Commit using the full specification:
refactor(user): improve code modularity and readability in users controller
Extracted repeated code into separate functions for better maintainability and readability. The code is now organized into smaller, reusable modules.
Refs: #12
Co-authored-by: JohnDoe
How to enforce conventional commits in a repository
To help follow this convention in our projects, we use a tool called commitlint
along with Husky. This tool will check if the commit messages follow the conventional commits convention. If not, it will fail the commit and will not allow you to push the changes to the repository.
To do so, we need to first install Husky. You can do that following their guide here. After that is done, we need to follow these steps:
npm install -D @commitlint/{config-conventional,cli}
Then we need to create a commitlint.config.js file in the root of the project with the following content:
module.exports = { extends: ['@commitlint/config-conventional'] }
This uses the config-conventional preset, which is based on the Angular convention. However one can decide to further configure the rules to fit the needs of the project, for example by modifying the type-enum rule to only allow certain types of commits.
Conclusion
Conventional Commits might seem like a small piece in the development puzzle, but their impact is surprisingly significant. By embracing this practice, teams can reap the benefits of clearer communication, a deeper understanding of changes made, and a solid groundwork for future iterations. As projects grow in size and complexity, a well-defined commit history becomes an invaluable asset.
We invite you to dive into our blog for more insightful content about Node.js and other technologies.