git logo

Conventional Commits

In my programming adventure, like many programmers, I have had many opportunities to go through git history and check a codebase lifecycle. With different results. Sometimes the research went successful and fast, another time it was painful and slow. The second situation pushed me to always create commit messages as valuable and informative as possible to reach a high level of readability. It is important not only for me but also for my coworkers so I learnt to use best practices like a good explanation of a change, imperative mood etc. But can we do more? Can we write commit messages in a better way? I revisited this topic and the answer is yes! Conventional Commits Spec is a general specification for improving commit messages and favours the automation of the processes related to a repository.

Conventional Commits Specification

The Conventional Commits Spec describes the structure of the commit message to create a highly descriptive and transparent commit history. Spec provides clear instructions on how to write messages easy to read by humans and computers. By the first one, we are able to track the commit history without checking what is inside each change. The second one allows the creation of automated processes on top of our repository.

In my experience, git messages are very often treated by programmes like a necessary evil. We try to put there some alphanumeric strings because it is required to publish our change. Very often, it is only one word or task number from JIRA. But we can improve it and make our programming life better and easier 🙂

Let’s take a look at the general structure of each message which comes from the Conventional Commits spec.

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

We can divide the above template into mandatory parts as well as optional ones. As you can notice, only 2 items are mandatory and provide the biggest feature of the Conventional Commits idea. <type> and <description> should be kept as informative as possible because mainly they are presented in the history listening and they present a context of the change. The rest is also important but complements the previous 2 items.

Commit type

The <type> field is responsible for providing the intent of the commit that was made. It communicates e.g. if the change provides a fix or a new feature. It should be treated like a category of a change by which we are able to quickly catch the context of the change. Generally, this value should come from an enumerated set. Below, I presented types based on the Angular commits convention and my experience but without any obstacles, each development team can have its own clear list of types.

  • feat – The commit provides of implementation of a new feature.
  • fix – The commit fixes a bug.
  • build – The commit affects the building app process or includes external dependencies.
  • ci – The commit changes of Continuous Integration or Continuous Delivery configuration/script.
  • docs – The commit updates documentation.
  • perf – The commit improves the performance boost of an app/algorithm but does not change business logic.
  • refactor – The commit refactors the existing codebase but does not change business logic.
  • style – The commit improves the style of the codebase but does not change implementation.
  • test – The commit adds, updates or corrects tests.
  • security – The commit created or changes the security or updates client secrets.
  • revert – The commit reverts the existing change included in the main branch.
  • chore – The commit provides a maintenance change for a product or a repository.

By the <type> field we are able to scan through our repository history to filter commits related to only a specific context. It could be helpful to generate the release notes.

Commit scope

The <scope> is optional and should be provided in the bracket just after the <type> (e.g feat(api): desc). This is a noun which describes a section of the codebase where a change appears. As the <type>, the value of <scope> should come from an enumerated set. Commonly, a name of a package or app module is a perfect choice for this purpose.

By the <scope> field, we are able to recognise which part of an application was changed. This information allows us to narrow down our search results.

Commit description

The <description> field is a very short summary of a made change. We can consider the <description> as a title or an introduction to the <body> where we can include more details. Obviously, if we can fit our whole intent of the change in only one line, that is perfect.

It is also nice to use the imperative mood in the <description>, start a sentence with a capital letter and cut out all articles like a, an or the. In that way, we keep the value of the <description> as short as possible. E.g. instead of “Created a provider of the logged customer data” we should write “Create logged user data provider”. In many cases, a performed action during development matches really great for this place.

Commit body

When we need more space for an explanation of our commit purpose and the <description> is not enough, we can put some contextual sentences in the <body> of the commit message. The form of the <body> is free. We should just remember to separate each paragraph by a new line.

A good <body> answers the question of why the change was made rather than how. If it is required, an interested developer always can read a codebase or compare diffs between versions of the source code to understand the implementation details. But an answer to why it was implemented could not be that easy and here we have a space to provide it in a clear way.

In the <body>, in contrast to the <description>, we should keep a high level of our grammar. We should start sentences with capital letters and use proper grammar and punctuation, just like in the natural writing language.

Commit footers

To provide metadata for our commits like linked issues/tickets or references to other commits, we can provide them in the footers. This section is optional and each footer should keep the format below:

<token>: <value>

The word <token> must not contain whitespaces but if we need to use more than one word for token creation, we can just use - sing for each whitespace. By this, we gain an easier way for automated processing of metadata by scripts which are on top of our repository.

The <value> does not have as many restrictions as the <token>. The <value> may contain white spaces or even new lines. The end of the <value> field is when the next <token>: string appears but it is common behaviour to put each new footer in the new line.

Breaking changes

We sometimes break the contract of our application. We introduce changes which affect e.g. the API of our service and completely change its behaviour. Those changes are called “breaking changes”. With the Conventional Commits Specification, we are able to communicate this fact and sign the specific commit as the commit which provides breaking changes. We can highlight it in two ways.

We can use the reserved footer with the ‘BREAKING CHANGE’ token and description after the colon. This is the only exception to the token construction which allows whitespace and needs to be written in uppercase, e.g. BREAKING CHANGE: send confirmation mail after account registration.

The second way is to use ! after the <type> and the commit description shall be used to describe the breaking change, e.g feat!: Send confirmation mail after account registration. Sometimes it is hard to formulate a descriptive message in the imperative mood so in that case, we can skip it.

Conventional Commits Examples

Commit with type and description

feat: Add type to User entity

Commit with type, scope and description

feat(user): Add type to User entity

Commit with type, description, scope, multiple paragraph body and footers

feat(user): Add type to User entity

Introduced a type field to the User entity class. The field type is enumarated (UserType) and depends on a user role in the application.

By default, the field is initialized with UserType.RESEARCHER value.

Jira-ticket: PUN-100
Miro-link: https://miro.com/user-type-flow 

Commit with the breaking change footer

feat: Add required api header Client-id 

BREAKING CHANGE: Api requires Client-id header in each request.

Commit with ! to communicate breaking change

feat!: Api requires Client-id header in each request

Commit with ! and the breaking change footer

feat!: Add required api header Client-id 

BREAKING CHANGE: Api requires Client-id header in each request.

Compatibility with Semantic Versioning

Have you ever heard about Semantic Versioning? If not, no worries, I am going to make a short introduction to this topic but for more info, you can reach this site 🙂

It is the convention of versioning an application where we divide the version number into 3 parts – MAJOR.MINOR.PATCH. Each piece has its own meaning and can roughly indicate the difference/compatibility in relation to the previous build of an application and how big an impact it has on us.

  • MAJOR – we should increment this number when we introduce break changes.
  • MINOR – we should increment this number when introducing new functionalities and keep backward compatibility.
  • PATCH – we should increment this number when we do bug fixing.

Let’s consider some examples to bring this topic more real:

  • If the previous build’s number was 1.2.3, after providing breaking changes the following number will be 2.0.0.
  • If the previous application version was 1.2.3 then after introducing a new feature, the following number will be 1.3.0.
  • If the previous build’s version was 1.2.3, then after fixing a bug, the next version will be 1.2.4.

I mentioned it because Conventional Commits Spec can easily support managing the Semantic Versioning. When we try to establish what version of our app we need to put into release notes, we can find the good one by scanning our git history and taking the steps below:

  • Every BREAKING CHANGE footer or ! sign in the commit should increase the MAJOR version number and reset other numbers. 
  • Every feat commit type should increase the MINOR version number and reset the PATCH number.
  • Every fix commit type should increase the PATCH version number.

Compatibility with Changelog

The Conventional Commits Spec also offers good support for managing the changelog and organising release notes. Good explanation of what is the changelog and how to create one, you can find here.  But shortly, it is the way of grouping changes for each release. By this, we can communicate to stakeholders e.g. what we added or fixed in the comparison to the previous release.

We can feed each group of changes by grouping commits science last release by their type.

  • Added -> feat
  • Changed -> change
  • Deprecated -> deprecate
  • Removed -> remove
  • Fixed -> fix
  • Security -> security

Check out my other posts. Do you know git reflog? If not, I recommend reading this article to be prepared for unexpected circumstances.

Other useful links:

Automation is a never-ending story. It is always a better way to generate the result instead of writing it manually for a couple of hours. Conventional Commits Spec will be something that you will like or hate, depending on your approach to time management and your regularity. In fact, when you spend a small amount of time preparing a good commit message, you can gain a huge boost in your workflow later.

The same programmers as us work everywhere.
Oskar K. Bogacz

Similar Posts