Last active
July 16, 2023 18:17
-
-
Save alemidev/2166c562d44c5ae785f10916ff8770ba to your computer and use it in GitHub Desktop.
A git hook that forces you to respect Commit Convention
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# | |
# Commit Convention Hook | alemi <[email protected]> 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