Code Published on the 13th November 2023

A convention for clearer commits

Talking about the elegance of a Git commits tree (git-tree) often means talking about the use of Git Flow, but an equally crucial aspect is often overlooked: the commit messages. Imagine a tree with a straight trunk and beautiful branches, but whose leaves are hard to see. It's not so elegant anymore, is it? Similarly, a well-structured git-tree can lose all its charm without a clear convention for commits.

A convention for clearer commits

In the often complex world of programming, the developer is frequently faced with projects versioned with git, where commit conventions seem to be more of a suggestion than a rule. Whether everyone applies their own methodology, or the standards established at the outset erode with time and the arrival of new collaborators, the result is a hard-to-follow commit history. Add to this generic commits such as "misc fixes" or the disreputable "wip", and you have a recipe for confusion. Fortunately, there's a solution to this dilemma, a way to bring clarity and consistency to your version management: commit conventions.

Conventional Commits to improve commit messages

What is a conventianl commit?

Conventional Commits is a standardized specification for adding clarity and consistency to commit messages. It is not tied to any particular programming language or versioning tool, but simply defines a set of rules for well-formatted commits. It is based on the Angular Commit Guidelines in its logic, but is much more permissive: type and scope are totally free and can vary from project to project. Please note that the various types proposed by Angular (build, ci, docs, feat, fix, perf, refactor, style, test) are recommended at first, as they are generally sufficient.

As defined in their documentation, here's the standard commit format:

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

To demonstrate this, here are a few examples:

  • feat(settings)!: allow users to disable notifications
  • fix: prevent opening forbidden resources
  • docs: update delivery instructions in README.md
  • test(ui): add unit tests on dates formatting

The convention is closely related to the SemVer specification, which provides a unified nomenclature for version numbers of published software and libraries. Given a version number formatted MAJOR.MINOR.PATCH (e.g. for the version named "7.22.3", MAJOR=7, MINOR=22, PATCH=3):

  • if there are any non-backward-compatible changes, i.e. commits with BREAKING CHANGE in the commit footer (and/or a ! after the type/scope), increment MAJOR for the new version.
  • otherwise, if backward-compatible features have been added, i.e. feat commits, MINOR should be incremented.
  • otherwise, if there are backward-compatible bug fixes, i.e. fix type commits, increment PATCH.

Implementing Conventional Commits is generally straightforward. There are several tools available to help you adopt this specification in your project, some of which are listed below. However, you can find all the information you need on its official website.

Alternative naming conventions for commits messages

Many other naming conventions exist. For a project, a platform or even a team, you can define your own naming convention. Here are a few examples:

  • gitmoji: Uses emojis to describe the type of the commit (✨ for a new feature, 🐛 for a bug fix, 📝 for documentation, etc.). It's more fun, but not without its drawbacks; the emoji isn't necessarily supported on all terminals, and the shortcode alternative (:sparkles:, :bug:, ...) lacks a single convention for uniform rendering across platforms.
  • Jira Smart Commits: Handy for linking work done on a commit with a Jira task. Very closely linked to the tool, it allows you to perform commands directly on the task (add time, change status, etc.). However, the commit itself is less readable and lacks context.
  • jQuery Commit Guidelines: A specific convention used for the jQuery project, it is very similar to Conventional Commits but with a different format. Although more restrictive, it allows issues and pull-requests to be referenced from GitHub.

The conclusion to be drawn from this exploration is that Conventional Commits is a simple, flexible specification that's easy to implement and adopt. It is therefore recommended as long as you don't have any specific requirements.

In fact, there's nothing to stop you from linking a GitHub issue or adding an emoji to a commit body, for example.

Ecosystem

Many tools have been developed around Conventional Commits to facilitate its adoption and use. Here are some of the tools we use:

#1 Generating commit messages

For those who need guidance, there are tools available to generate commit messages that conform to the chosen convention. One of the most widely used is commitizen, which can be used on the command line instead of git commit:

➜  splash.iosapp git:(feature/universal-links) ✗ git cz
cz-cli@4.3.0, cz-conventional-changelog@3.3.0

? Select the type of change that you're committing:
  chore:    Other changes that don't modify src or test files
  revert:   Reverts a previous commit
❯ feat:     A new feature
  fix:      A bug fix
  docs:     Documentation only changes
  style:    Changes that do not affect the meaning of the code (white-space,
formatting, missing semi-colons, etc)
[...]

The interface then asks for the scope, message, optional description and breaking changes. The result is a commit correctly formatted according to Conventional Commits. It is of course possible to configure commitizen to use a different naming convention.

Other generators are available, such as the VSCode Conventional Commits plugin for those who prefer to stay within the IDE.

#2 Linting to check commit messages

Having a convention is good. Respecting it is better. In the ecosystem, tools are available to check that commit messages respect the convention. Here we have chosen commitlint, a popular tool in the community.

By default, commitlint proposes a configuration corresponding to Conventional Commits. Once installed, simply run the commitlint command to check that commits respect the convention:

commitlint --from=HEAD~1

It can also be integrated into your CI/CD:

  • As an action launched at each pull-request, to check all new commits that are about to be merged:
    commitlint --from=origin/main --to=HEAD
  • As a git-hook, on each commit, to check it locally:
    npx husky add .husky/commit-msg 'npx --no -- commitlint --edit ${1}'

Here's an example of commitlint in action:

commitlint --from=origin/develop --to=HEAD --verbose
⧗   input: docs: add setup steps in Readme.md
✔   found 0 problems, 0 warnings
⧗   input: FIX(api): change api headers to authenticate
✖   type must be lower-case [type-case]
✖   type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test] [type-enum]

✖   found 2 problems, 0 warnings
ⓘ   Get help: <https://github.com/conventional-changelog/commitlint/#what-is-commitlint>

Note in this example: although Conventional Commits specifies that case is irrelevant, commitlint's default configuration is stricter and imposes lower case for the type. However, it is possible to modify this rule or change the convention used, to adapt the tool to your needs.

Commitlint isn't the only git linter out there: we could mention gitlint or Conventional Commits Linter, which work in a similar way.

Automatic changelog generation tools

A central point in the adoption of a naming convention is the automatic generation of changelogs. Several tools are available online for this purpose. These include:

  • Release Please, developed by Google, lets you do this via a GitHub action.
  • The fastlane plugin semantic_release, which can be integrated into other CI/CDs.

These tools parse the git-tree to sort commits and prepare a release note for each version calculated using SemVer-related syntax.

For our need for a simple, clear changelog, we wrote a Python script ourselves, which takes a tag or commit hash as a parameter and creates a release note with the commits following this starting point. It doesn't take into account the commits' body and footer, but that's something you can add as you see fit.

import re
import subprocess
import sys

# Run git command
git_command = f"git log --pretty=format:%s {sys.argv[1]}..HEAD"
result = subprocess.run(git_command, shell=True, stdout=subprocess.PIPE).stdout.decode('utf-8')

# Define regex for commits
regex = re.compile(r'(?P<type>\w+?)(\((?P<scope>.+?)\))?(?P<breaking>!)?:\s(?P<message>.+)')
types = ["feat", "fix", "build", "ci", "chore", "docs", "perf", "refactor", "revert", "style", "test"]

# Process commits
commits = []
for line in result.split('\n'):
    match = regex.match(line)
    if match and match.group('type') in types:
        commits.append({
            'type': match.group('type'),
            'scope': match.group('scope'),
            'isBreakingChange': match.group('breaking') is not None,
            'message': match.group('message')
        })

# Print changelog
hasBreakingChanges = any(commit['isBreakingChange'] for commit in commits)
print(f"Release note{' WITH BREAKING CHANGES:' if hasBreakingChanges else ':'}")

for type in types:
    filtered_commits = [commit for commit in commits if commit['type'] == type]
    if not filtered_commits:
        continue
    print(f"\n{type}")
    for commit in sorted(filtered_commits, key=lambda x: x['scope'] or ""):
        print(f"- {commit['scope']} {commit['message']}" if commit['scope'] else f"- {commit['message']}")

We run the command python3 changelog.py 2.3.1 and here is the output:

Release note WITH BREAKING CHANGES:

feat
- handle universal links
- (settings) allow users to disable notifications

fix
- change api headers to authenticate

docs
- add setup steps in Readme.md.

Conclusion

By adopting Conventional Commits, you create a structure in your commit messages that will make your work more readable and understandable to everyone who interacts with your code. This helps not only your current team, but also those who will join your project in the future. Many tools exist to help you adopt this convention, and it can be customized to suit your needs. Adding this step to your workflow may seem tedious at first, but the long-term benefits are obvious.

And if you don't have the time to add automatic checks, you can always start by adopting the convention and leaving the tools for later. In the worst-case scenario, if a commit lands without respecting the Conventional Commits specification, it's no big deal - it simply means that the commit will be ignored by specification-based tools.

Of course, it's always possible to contribute if you want to flesh out the ecosystem around Conventional Commits or improve the project itself.

Rémi

Software Engineer · Mobile

Read his/her presentation

Do not miss any of our news

Every month, receive your share of information about us, such as the release of new articles or products.