In version control systems like Git, commit messages are the best way to document your development progress. Commit messages tell other developers what you have done for each change you make in your code base. It's not just about other developers though, commit messages are important to you as well. Let's say you want to know what you did yesterday, how, and why, the commit messages history will always be your friend. Considering how important commit messages are in the sphere of software development, we cannot ignore the need to keep them clear and meaningful. Commit messages are important but not if they don't make sense. In this article, we will discuss how to write commit messages that clearly communicate changes and don't leave you or other developers wondering what happened during that change.
A well-written commit message has three parts: the commit tag, the subject, and the body. The tag classifies the change to a particular category. The subject declares the change in a single statement. The commit message body provides more details about the change. Look at the following commit message for example:
refactor: Validate reference IDs after authenticating routes
- Move the Reference ID validators and
validation error handlers below the authentication
middleware in the routes where ID is required
In the above commit message,
The word
refactor
is the message tag that classifies the change as a refactor (We will talk more about tags shortly).The statement 'Validate reference ids after authenticating routes' is the subject of the message.
The longer sentence below the subject is the message body.
If you had no idea that a commit message has three parts, you can now see why that is so and appreciate their importance. The tag classifies the change. The subject tells you what the change is about without boring you with the details. Finally, the body outlines the details of the change.
In this article, we will talk about the semantic tag.
The Semantic tag.
The semantic tag helps to clarify what the change is about by classification. There are several semantic tags that can be used to classify changes in commit messages. Here are some of the tags that I have used and you can use too:
- feat
- refactor
- fix
- breaking
- test
- chore
- revert
- docs
- style
The question you are probably asking yourself now is "What do these tags mean and how will I know when and where to use which one?". Relax, We have got you covered. Before the end of this article, We will have talked all about the above tags, what they mean, and when and where to use them. We will also throw in a few examples for you to see how you can use them in practice. Happy now? Let's get started with the feat
tag.
1. The feat
tag
The feat
tag is used to classify a change that introduces a new feature or functionality in the system you are building. This is the tag you use on a commit message when the change you have just made has introduced a behavior that didn't exist in the system before.
For example, you are building an app that accepts user input and stores the data in a database. The system is working as expected, then you decide to validate user input. Now users can only input data that the system agrees with. The system will reject the data it considers invalid and accept only the data it considers valid. This is a behavior that did not exist earlier. The change has introduced a new behavior in the system. It is therefore classified as a feat
. In this case, you will write your commit message as follows:
feat: Validate user input
- Ensure users enter valid data before accepting their input
It's that simple, now you can spot a change that introduced something new without reading any further than the feat
tag.
2. The refactor
tag
Away from Git commit messages, refactoring code normally means changing the implementation of a program in a way that does not alter its behavior. For example, replacing double quotes with single quotes in a string initialization.
In relation to git commit messages, a change is classified as a refactor
if the implementation of a program has changed but the program still does exactly what and how it was doing before the change.
For example, say you have a function that takes an array of numbers and returns the sum of the numbers.
function sumArray(numbers) {
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
sum += numbers[i];
}
return sum;
}
Then you decide to do something more simplified like this
function sumArray(numbers) {
return numbers.reduce((sum, number) => sum + number, 0);
}
You see, you have reduced the implementation of the same function from 6 lines of code to 1 line and you still achieve the same goal. In this case, you would have a commit message that reads as follows:
refactor: Change the implementation of the sumArray function to a more
simplified one
- simplify the sumArray function using Array.reduce method
Changes that can be classified as refactor
are not just limited to simplifying implementations of functions. The following changes can also be classified as refactor
:
- Renaming variables, functions, methods, or classes
- Removing redundant utility functions
- Extracting code into reusable functions or methods
- Splitting a large function into smaller functions with single responsibilities
- Replacing a library with another library that achieves the same purpose
- Optimizing functionalities without changing their outputs
- Reorganizing code logical structure or execution order without changing the output
The list of changes that can be classified as a refactor
is endless. The most important thing to remember is that a refactor
is any change that alters the code without adding, eliminating, or altering a functionality in your system.
3. The fix
tag.
Just like it sounds, the fix
tag is used to classify changes that fix problems in the system. Problems can be bugs, issues, errors, crashes, etc.
Now assume you are building an app that authenticates users before allowing access. The authentication works, unregistered users cannot access the system. That's what you want but you also have another behavior you don't want. Registered users cannot access the system either. Your registered users enter their correct emails and passwords but the system keeps saying the password is wrong.
You look through your code and realize that you are encrypting the user's password twice before saving it but decrypting once when trying to log in. Twice how? Once in the login controller and again in the User schema just before saving the new user document to the database.
You are relieved after finding out the problems. You remove the encryption at the controller and remain with one encryption process, at the data schema. Now new users can register and log in smoothly with their correct credentials.
The change in the paragraph above fixes an issue in the authentication of the system. This is the kind of change you classify as a fix
and the commit message can read as follows:
fix: Resolve the issue preventing registered users from logging in with
correct credentials
- Remove double encryption of user password during registration
- Remove the encryption of passwords in the user registration
controller and use the encryption in the user schema only.
The above commit message communicates what it does very clearly. You can spot the change that fixed a problem in the system without reading the whole commit message. That is how useful the fix
tag can be.
An authentication issue fix is only one of the many kinds of changes that can be classified as a fix
. The following kinds of changes can also be considered as fix
:
- Handling uncaught exceptions and errors
- Handling rejected Promises
- Implementing global error handlers to prevent an application from crushing due to unexpected errors
- Correcting logical errors in calculation algorithms
- Taking care of compatibility issues across browsers in web apps
4. The breaking
tag
The breaking
tag is used to classify breaking changes. Breaking changes are a little like refactors in that they do not introduce new functionalities to your system or codebase but they are more impactful than refactors. A breaking change forces changes in other parts of the system that depend on the part that has been changed. They break backward compatibility in systems and codebases. If a breaking change occurs in one component in the system, every component that depends on the changed component has to be updated as well.
An example can explain it better. Assume you have a base controller class called BaseController
in the libraries folder of your system. The BaseController
class has a constructor that accepts one parameter. Your system has 6 apps namely users, events, venues, venue types, event categories, and authenticator. Each of the six apps in the system has a controller class that inherits the BaseController
class and implements a constructor.
You have decided to change the constructor of the BaseController
class by adding one more parameter called appName
. Now all the classes that extend the BaseController
must update their constructors to match the new implementation. All the apps in the system will be affected because of one change in the base class. This is what we call a breaking
change. The above change can be committed as follows.
breaking: Update the BaseController constructor to accept two
parameters
- Add a new parameter called appName to the constructor of
the base controller class
Make sense? Yes. Altering the constructor of a base class is not the only kind of change that can be classified as a breaking
change, here are a few more:
- Adding a method or a field to an interface that is implemented by other components in the system or codebase.
- Changing the return type of a method in a base class
- Changing the structure or properties in a database schema
- Renaming or removing a public method from a class
- Altering method signatures by modifying the type of parameters, reducing or increasing the number of parameters in a function or a method.
That's it about breaking
changes but not all. These are changes that demand attention from other parts of the system. In a development team, the impact of the change can go as far as affecting other developers' work too. When they pull the breaking changes, they will be forced to update their components that depend on the changed entity.
5. The test
tag
The test
tag is related to the practice of test-driven development. In test-driven development, we are always encouraged to write a failing test before we finish implementing a function, class, or whatever it is we are writing code for. Every time you write a test, you create a change you want to commit before you continue working on the implementation of the test subject. All changes related to tests are classified as test
.
Say you are building a backend system, you want to implement POST route handlers for the users application. You have written unit tests with your favorite testing libraries. You want to commit this change before making any more changes to your code base. Because we are talking about tags and meaningful commit messages, you will write your commit message as follows:
test: Write tests for users POST route
- Write test cases to test input validators, response status code
and response body of users POST route
That simple! Tests are the most obvious and straightforward type of changes that you won't ever need to scratch your head to classify correctly.
You might think that the test
tag is only usable for changes that involve writing unit tests. You may right, but what do you think about the following?
- Creating mocks for classes, methods, functions, API endpoints, and data
- Adding end-to-end tests that emulate real user scenarios
- Refactoring and organizing testing code to improve maintainability
- Creating a custom testing library by encapsulating the functionalities of a testing library
6. The chore
tag
Just like it sounds, a chore
is a change that keeps your project environment in order. These changes are not refactors - because they don't touch any lines of code, they are not fixes - because they don't resolve issues or bugs - and they do not introduce new features to the system. The chore
changes are necessary for maintaining and improving the project in ways that do not affect the experience of the end user.
Now what kind of change do you suppose we can classify as a chore? Let's say you have been building and improving your project to the point that you have some files that you no longer use for the functionalities in your system. This could be a class file that you have replaced with a third-party library or UI components that you don't use anymore. The best thing to do about these files is to delete them. The resulting change of deleting an unused file does not affect the functionality or the codebase of your system but it does keep things clean in your project. This is the kind of change that you classify as a chore
. If you were to write a commit message, it would look more or less like the following:
chore: Remove unused files
- Delete the files for the ABC class and xyz components that are
no longer used in the system
The chore
changes are not limited to removing unused files, they may also include things like:
- Containerizing your project by adding a Dockerfile
- Updating README.md
- Adding npm scripts to the package.json file
- Installing dependencies and development dependencies
- Configuring testing environment
- Configuring development tools
- Updating existing modules or packages.
That's not all about the chore
tag but it's enough to give you an idea of what it's all about. Let's see what next.
7. The revert
tag
The revert tag is used when you do a git revert
, that is when you undo a change that had been committed so that your code base can look like nothing happened at all. A revert is not a kind of change that should leave you wondering about what just happened because you won't see it anywhere. A revert is a change that erases evidence of what was done before. Therefore it is very important that you tag all reversed changes as revert
because unlike git reset
, a revert does not leave unstaged changes in the working area.
Now, under what circumstances would you want to revert a commit? Assume you removed a functionality that allowed users to delete their information from the system. For some reason, you didn't want them to delete their details, at least not directly. You had committed the change as a feat
below:
feat: Disable users from deleting their details from the system
That went well. After several other changes in the system, you now want users to be able to delete their details directly anyway. You don't want to rewrite the code that you had written in the past so you will simply do a git revert
. This is when you use the revert
tag on a commit message. In my experience revert messages tag and commit themselves but in case you are curious to see what it would look like, be my guest:
revert: Revert "feat: Disable users from deleting their details from
the system"
Just like any other kind of change, a revert can be done under several circumstances including:
The need to maintain integrity in the commit history. This is for teams that prefer to undo changes via a revert over a reset. They want to keep records of all changes including the ones you want to remove.
Undoing changes after they have been deployed. Assume you introduced a feature but it is misbehaving in production. The safest way to deal with it is to revert the change so you can work on a better implementation while the buggy feature is off the production code.
Undoing a mistaken commit, for example, you accidentally deleted important files from the project, committed the change and now you want them back.
That covers the revert tag, let's see about the docs
tag.
8. The docs
tag
A change is classified as docs
if it involves writing or removing plain texts on a markdown file or on codebase as comments or docstrings. All changes to code documentation are considered docs including updating the content of the README file.
Let's say you have been working on a project on your own for some time. You want to invite a few team members to join you in the development. Inviting new members will require you to write some instructions on how to set up the project locally for their sake. You will write the instructions in the README.md or any other markdown file. Once you are done, you will commit the change with a message that looks more or less like this:
docs: Create instructions for setting up the project locally
Other changes where the docs
tag is appropriate are :
- Creating or updating a client or developer documentation
- Adding or removing comments in a codebase
- Updating or creating user manuals
- Adding docstrings to a codebase
The list is surely longer than we can cover today. The bottom line is, that the docs
tag is used when the change you are committing means that you are communicating something in plain words. This includes all cases where you are altering information from existing documentation.
9. The style
tag
You might think that the style
tag is used when you add style to user interfaces but that's very far from the truth. The style tag is used when you make a change that does not affect the logic or semantics of the code but affects the arrangement of the lines of code. These are changes related to the indentation of code, the amount of whitespace between lines of code, the position of curly braces in control structures of functions, whether you used curly braces to enclose one line of code in an else block, etc. The petty things that affect how your code visually looks are committed as style
.
Assume you initially had a habit of writing very long lines of code without breaking into the next line. After reading the book Clean Code by Uncle Bob, you decide to make your code a little cleaner by breaking the lines to be shorter. This is not a refactor because you did not change any characters in the code. This is a style
which can be committed as follows
style: Reformat code
- Break long lines of code into shorter lines
The other changes that can be committed as style
include:
- Removing or adding whitespaces in the code file
- Renaming variables and functions for the sake of conventional consistency
- Reformatting comments and docstrings to improve readability
- Aligning and indenting code within blocks for visual clarity
- Rearranging methods in a class or function in a file
- Altering the order of imports
That's enough to give you an idea of what kind of changes can be tagged as style
.
The tags we have discussed above are not all that exists in the software engineering world but they are enough for a beginner to intermediate developer. The list grows longer as you get more experience as a developer. These tags are not hard rules in version control systems but a good practice in writing meaningful commit messages. They may not make a lot of sense now but the more you use them, the more you will appreciate them. Use these tags and more that you will learn along the way and you will always be proud of the commit messages you write.
Top comments (4)
I love this way to tag commit messages. But currently I changed to this format:
Why did I make such a change? Well, for many reasons the company I work for use cherry pick to choose the changes to send to production :/ and tagging with the feature name is very helpful for that specific case
TBH I miss tagging my commits in the conventional commits spec
Thanks for this. Had to write a note for myself on the topic ... With structured commits to more meaningfulness and evaluability
Loved it!
Detailed Article