#!/bin/sh # commit-msg hook to enforce Conventional Commits (https://www.conventionalcommits.org/) # This script checks the commit message subject (first line) for a conventional commit format. # If the message does not conform, the hook exits non-zero to block the commit. # Read the commit message (first line) if [ -z "$1" ]; then echo "commit-msg hook: no message file provided" >&2 exit 0 fi MSG_FILE="$1" read -r FIRST_LINE < "$MSG_FILE" || FIRST_LINE="" # Trim leading/trailing whitespace FIRST_LINE="$(echo "$FIRST_LINE" | sed -e 's/^[ \t]*//' -e 's/[ \t]*$//')" # Allow empty message (let git handle it), or allow merges/reverts case "$FIRST_LINE" in Merge:*|merge:*|Revert:*|revert:*) exit 0 ;; esac # Conventional Commit regex (POSIX ERE) # [type](scope)!?: subject # types: feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|wip # scope: any chars except ) regex='^\[(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|wip)\](\([^\)]+\))?(!)?: .+' printf "%s" "$FIRST_LINE" | grep -E "$regex" >/dev/null 2>&1 if [ $? -eq 0 ]; then exit 0 fi cat <<'EOF' >&2 ERROR: Commit message does not follow Conventional Commits. Expected format: [type](scope)?: subject Examples: [feat]: add new feature [fix(parser)]: handle edge case [docs]!: update API docs (breaking change) Allowed types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert, wip You can bypass this hook locally by running: git commit --no-verify EOF exit 1