DEV Community

Enda
Enda

Posted on

Enforcing Conventional Commits using Git hooks

I'm a huge fan of Conventional Commits. This relatively young specification provides a set of rules for creating an explicit, standardised commit history.

I've tried my best to follow the convention in all my projects. When I'm running low on caffeine I sometimes forget to follow the convention or let a typo slip past my eyes. To resolve this, I decided to create a dynamic commit-msg Git hook which automatically validates all of my commit messages, and prevents them from being committed if any of the conditions in the config file fail.

Creating the hook

Note: for the hook to read configuration values from a JSON file, you will need to install jq.

In your Git project, add a commit-msg hook in .git/hooks by renaming the sample script that is there already.

$ mv .git/hooks/commit-msg.sample .git/hooks/commit-msg

To start, let's read configuration values from a configuration file, which will later be added to the project's root.

config=commit-msg.config.json

# set variables
enabled=$(jq -r .enabled $config)
revert=$(jq -r .revert $config)
types=($(jq -r '.types[]' $config))
min_length=$(jq -r .length.min $config)
max_length=$(jq -r .length.max $config)

If the config file does not exist, or if the enabled property is false, we can exit the hook and skip validation.

if [[ ! -f $config || ! $enabled ]]; then
    exit 0
fi

Building a dynamic regex string

Let's start the regex with a hat symbol (^), which indicates the beginning of the string.

regexp="^("

If $revert is set to true, the commit message can optionally be prefixed with "revert:".

if $revert; then
    regexp="${regexp}revert: )?(\w+)("
fi

Let's get all of the allowed types from commit-msg.config.json.

for type in "${types[@]}"
do
    regexp="${regexp}$type|"
done

This next piece does two things. If an opening bracket comes immediately after the type, It must be closed and contain a scope. The second thing is that the type and/or scope must be followed with a colon symbol (:).

regexp="${regexp})(\(.+\))?: "

Here were are setting limits on the maximum and minimum length of the message immediately after the colon.

regexp="${regexp}.{$min_length,$max_length}$"

Validating the commit message

Now that the regex has been built, let's see some examples of how the commit message could look.

revert: docs: updated table of contents
feat(user): added constructor to model
fix: incorrect URL in link

Get the first line of the commit message:

msg=$(head -1 $1)

In the following statement, we are checking if the commit message passes the regex validation. If it fails, print out a custom error message and exit the script.

if [[ ! $msg =~ $regexp ]]; then
  echo -e "\n\e[1m\e[31m[INVALID COMMIT MESSAGE]"
  echo -e "------------------------\033[0m\e[0m"
  echo -e "\e[1mValid types:\e[0m \e[34m${types[@]}\033[0m"
  echo -e "\e[1mMax length (first line):\e[0m \e[34m$max_length\033[0m"
  echo -e "\e[1mMin length (first line):\e[0m \e[34m$min_length\033[0m\n"

  # exit with an error
  exit 1
fi

Configuration

The great thing about the Conventional Commit specification is its flexibility. You choose the rules that best suit your requirements. This flexibility is why I decided to make my hook configurable. Below is a sample configuration file.

Add it to the root of your project and make sure it matches the file name in the script; which in this case is commit-msg.config.json.

{
    "enabled": true,
    "revert": true,
    "length": {
        "min": 1,
        "max": 52
    },
    "types": [
        "build",
        "ci",
        "docs",
        "feat",
        "fix",
        "perf",
        "refactor",
        "style",
        "test",
        "chore"
    ]
}

Making your Git hook global

It is possible to create global Git hooks which are added to all of your projects on git init.

Enable Git templates:

git config --global init.templatedir '~/.git-templates'

Create a directory to save your global hooks:

mkdir -p ~/.git-templates/hooks

Create a commit-msg hook in ~/.git-templates/hooks/commit-msg, and make it executable:

touch ~/.git-templates/hooks/commit-msg
chmod u+x ~/.git-templates/hooks/commit-msg

You can repeat the steps from Making the hook and add them to the global commit-msg hook.

Sailr

If you'd rather not do any of the above, and want a ready-to-go solution, I've made this into its own project called Sailr.

Here's how you can install Sailr:

git clone https://github.com/craicoverflow/sailr
cd sailr
make install

Top comments (1)

Collapse
 
honarkhah profile image
Honarkhah

Thanks for creating this repo, but there were some issue because the syntax does not match with sh, and you should change you shebang to bash