Writing a Conventional Commits Helper

Oct 23, 2025 (3 days ago)

Contents

Why write another one?

I am aware that there are existing tools that help you author your commit messages in the style of conventional commits. I have myself been using cz-git for a long while when I was first getting into formatting my commit messages better and using conventional commit style. It is a perfectly fine tool and honestly I have looked into how it's working to take inputs in my own shell script which implements the same.

I myself think that while it is good that the CLI I mentioned above has a plugin system that enables different cases to be handled on a per use-case basis, I am using the same thing over and over and can use something more better than JavaScript.

I remember I tried searching for some Go packages to help with this, (and maybe Rust?) but I didn't find anything useful. Hence I set forth to write my own small helper script.

Few utilities

It is nice to have some utilities and functions to make the repetitive code structure easier to reuse or to just use good tools for input from user.

Some of them are:

  • gum - A tool from Charm which provides highly configurable, ready-to-use utilities for writing shell scripts. They have their own little example for writing a conventional commit helper (meta!)
  • A little function check_exists that checks whether a command is installed and exits otherwise
Code for check_exists
1check_exists() {
2 type "$1" &>/dev/null
3 if [[ $? -ne 0 ]]; then
4 echo "$1 not found, exiting."
5 exit
6 fi
7}
8
9check_exists "git"
10check_exists "gum"
  • A function check_added_files to check whether we have added any files to Git at all before going to commit the changes.
Code for check_added_files
1check_added_files() {
2 git status | grep "Changes to be committed" >/dev/null
3
4 if [ $? -ne 0 ]; then
5 echo "There are no changes to be committed."
6 echo "Did you forget to add?"
7 echo "Are you in a valid git repo?"
8 exit 1
9 fi
10}
11
12check_added_files

Conventional Commits

You can find the full specification at conventionalcommits.org. However for a quick refresher at the different parts is below.

A typical commit structured with conventional commit looks as follows:

1<type>[optional scope]: <description>
2
3[optional body]
4
5[optional footer(s)]

Let us look at the implementation that takes in all the different parts from the user.

Accessing user input and building the message

Type

The type can be one of some predefined ones. There can be more depending on the project and other adapters. We store them in an array to access them later.

1available_commit_types=(
2 "feat"
3 "fix"
4 "docs"
5 "style"
6 "refactor"
7 "perf"
8 "test"
9 "build"
10 "ci"
11 "chore"
12)

If you are familiar with fzf, you can use gum choose or gum filter to get a picker for the different types above.

1final_commit_msg=""
2
3selected_commit_type=$(gum filter --header="Type of commit" ${available_commit_types[@]})
4if [ $? -ne 0 -o -z $selected_commit_type ]; then
5 exit
6fi
7final_commit_msg=$selected_commit_type

Scope

The scope of the commit is optional and must be in brackets after the type. We handle the optional case by looking at the return value from the gum input command and add it to the commit if provided.

1# Scope of the commit (optional)
2selected_scope=$(gum input --header="Scope of the commit (optional)")
3if [ $? -ne 0 ]; then
4 exit
5elif [ -z $selected_scope ]; then
6 final_commit_msg="${final_commit_msg}"
7else
8 final_commit_msg="${final_commit_msg}(${selected_scope})"
9fi

Breaking change?

A breaking change MUST have either a note in the footer or a ! after the type and scope. We ask whether it is one or not using gum choose and looking at the return value from the command which reflects the user's choice.

1# Breaking change
2gum confirm "Is this a breaking change?"
3is_breaking=$?
4if [ $is_breaking -ne 0 -a $is_breaking -ne 1 ]; then
5 exit
6fi
7
8if [ $is_breaking -eq 0 ]; then
9 final_commit_msg="${final_commit_msg}!"
10fi

Commit message

Next, we ask the user for the actual commit message. We have a comfortable character limit for easy viewing capped at 50 characters using --char-limit flag. This is one of the last time we get to abort the commit by supplying an empty commit message.

1# Commit Message
2commit_message=$(gum input --header="Commit message" --char-limit=50)
3if [ $? -ne 0 ]; then
4 exit
5fi
6
7if [[ -z "${commit_message}" ]]; then
8 echo "Empty commit message, aborting"
9 exit 1
10fi
11
12final_commit_msg="${final_commit_msg}: ${commit_message}"

Description

There might be an extended description of the commit which can then be accessed via other git commands. We include them in an input box from the user.

1# Description of the message
2description=$(gum write --placeholder "Details of this change")

Confirmation

We confirm the user for the final commit and depending on whether we have a description or not, we add a newline in between for the final commit message.

1if [ ! -z "${description}" ]; then
2 gum confirm "Commit changes?" && git commit -m "${final_commit_msg}
3
4 ${description}"
5else
6 gum confirm "Commit changes?" && git commit -m "${final_commit_msg}"
7fi

Demo

Here's a gif for the demo:

Demo of my script for conventional commits making an initial git commitDemo of my script for conventional commits making an initial git commit
Details of the commit message for the initial commit which is now formatted as per conventional commit.Details of the commit message for the initial commit which is now formatted as per conventional commit.

References