Skip to content

Instantly share code, notes, and snippets.

@alemidev
Last active July 16, 2023 18:17
Show Gist options
  • Save alemidev/2166c562d44c5ae785f10916ff8770ba to your computer and use it in GitHub Desktop.
Save alemidev/2166c562d44c5ae785f10916ff8770ba to your computer and use it in GitHub Desktop.
A git hook that forces you to respect Commit Convention
#!/bin/bash
#
# Commit Convention Hook | alemi <me@alemi.dev> May 2022
#
# A simple git hook to enforce (sort of) Commit Convention.
# www.conventionalcommits.org
#
# This script uses bash builtin regex matching, so it's not sh compatible.
#
# Commit message will be stripped of comments (lines starting with #) and matched
# against a regular expression enforcing Commit Convention.
# If no match is found, this script will try to point out commit message issues.
#
# A brief explaination of the Commit Convention can be requested with COMMIT_CONVENTION_HELP=1
# A custom regex can be provided with COMMIT_CONVENTION_REGEX
# By default, accepted commit types are
# build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test
# but these can be changed with COMMIT_CONVENTION_TYPES="your|types"
# The default separator is a colon ':', but can be customized with COMMIT_CONVENTION_SEPARATOR="|"
# Disable issues hints with COMMIT_CONVENTION_HINTS_DISABLED=1
#
# This hook (and commit checking) can be bypassed by adding flag --no-verify to git command
#
# To install this hook, simply put it into the .git/hooks folder of your project with name 'commit-msg',
# and make sure that it's executable. Bash needs to be installed.
#
# Check environment for options and set defaults
if [[ -z $COMMIT_CONVENTION_TYPES ]]; then
COMMIT_CONVENTION_TYPES="build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test"
fi
if [[ -z $COMMIT_CONVENTION_SEPARATOR ]]; then
COMMIT_CONVENTION_SEPARATOR=":"
fi
if [[ -z $COMMIT_CONVENTION_REGEX ]]; then
COMMIT_CONVENTION_REGEX="^($COMMIT_CONVENTION_TYPES)([[(]"$'(\w+)'"[])]|)(!|)$COMMIT_CONVENTION_SEPARATOR (.{1,45})"$'(\n\n.*$|\n$|$)'
fi
# strip commit message of comments
INPUT_COMMIT_MESSAGE=$(sed -e "s/^#.*$//g" "$1")
# test against commit convention regex
if [[ $INPUT_COMMIT_MESSAGE =~ $COMMIT_CONVENTION_REGEX ]]; then
# we also check length and empty line thanks to regex above. length is not perfect: we have
# to assume that commit type is short (5 chars), but it may be longer.
# this allows for some leeway in commit title length, but precise error reporting
# if commit doesn't comply with convention.
# using very long scopes can bypass length check, but if we're using scopes that long,
# I think we can go with an exception to the rule
exit 0 # Everything good
fi
echo "Failed creating commit: message does not comply with Commit Convention"
# Print help message giving some info about Commit Convention if requested
if [[ $COMMIT_CONVENTION_HELP ]]; then
# sed works line by line, so cannot be used. tr replaces 1 char with 1 char, so cannot add indent
TABBED_COMMIT_MESSAGE=$(echo "$INPUT_COMMIT_MESSAGE" | awk 1 ORS='\n ')
cat <<-ENDSTRING
A correctly formatted commit message should look like
<type>[(<optional scope>)][!]: <description>
[<optional body>]
[<optional footer>]
Valid types are:
$COMMIT_CONVENTION_TYPES
Scope can be anything, could reference a branch or tag
Add an optional ! bang before colon to signal breaking changes
Type, scope and description should all be in one line and less than 80 characters long
Your body has no length or format restrictions, just leave an empty line
It's good practice to reference contributors in a footer
Examples:
fix: memory leak
feat!: added ability to define buffer sizes at launch time
refactor(config): reworked config storage
Input commit message:
$TABBED_COMMIT_MESSAGE
Issues:
ENDSTRING
fi
# Attempt to parse commit message more leniently and suggest issues if not disabled
if [[ -z $COMMIT_CONVENTION_HINTS_DISABLED ]]; then
IFS=$'\n' readarray INPUT_COMMIT_LINES <<< "$INPUT_COMMIT_MESSAGE" # split commit message at newlines
if [[ ${#INPUT_COMMIT_LINES[0]} -ge 50 ]]; then
echo "* commit title too long (${#INPUT_COMMIT_LINES[0]})"
fi
if [[ ${#INPUT_COMMIT_LINES[*]} -gt 1 ]]; then
if [[ ${#INPUT_COMMIT_LINES[1]} -gt 1 ]]; then # newline is included
echo "* no blank line after commit title"
fi
fi
LENIENT_COMMIT_REGEX="(\w*)((\!|)[[(](.*)[])]|)(!|)($COMMIT_CONVENTION_SEPARATOR|)( |)(.*?)"$'(\n.*|$)'
if [[ "$INPUT_COMMIT_MESSAGE" =~ $LENIENT_COMMIT_REGEX ]]; then
if [ -n "${BASH_REMATCH[3]}" ]; then
echo "* breaking mark before scope (should be after)"
fi
if [[ -z "${BASH_REMATCH[6]}" ]]; then
echo "* missing separator ($COMMIT_CONVENTION_SEPARATOR) after type and scope"
fi
if [[ -z "${BASH_REMATCH[7]}" ]]; then
echo "* missing whitespace after separator"
fi
if [[ -z "${BASH_REMATCH[8]}" ]]; then
echo "* missing description"
fi
# This must happen last (even if I'd want it first) because it overrides regex matches
TYPE="${BASH_REMATCH[1]}"
TEST="^($COMMIT_CONVENTION_TYPES)$"
if [[ $TYPE =~ $TEST ]]; then :; else # TODO how to invert regex?
echo "* unknown type '$TYPE', expected ($COMMIT_CONVENTION_TYPES)"
fi
else
echo "* empty or unparsable commit"
fi
fi
# Fail, making git abort this commit
exit 1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment