#!/usr/bin/env bash # # This prepare-commit-msg hook prepares commit messages to ensure # that they meet our conventional commit standard. # # Whenever `git` detects an incoming commit, it will trigger this hook. If the message does not # our conventional commit format, it will set it to the following format. (commit and merge only) # # "type(scope): message" # If an issue number is detected at the front of the message and it is not a merge, also adds "___. Fixes #1234" # # Reference: https://github.com/keymanapp/keyman/wiki/Pull-Request-and-Commit-workflow-notes # COMMIT_MSG_FILE="$1" COMMIT_SOURCE="$2" SHA1="$3" # Get the directory where this script lives, i.e. resources/git-hooks HOOK_DIRECTORY="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" if [ -f $(dirname "${BASH_SOURCE[0]}")/commit-msg-defs ]; then # This script was run from a symlink and the repo in question has its own commit-msg-defs. . $(dirname "${BASH_SOURCE[0]}")/commit-msg-defs else # We'll grab the settings from the keymanapp/keyman repository. . "$HOOK_DIRECTORY/../build/jq.inc.sh" # Configurable options for message length. min_length=4 max_length=128 # Read from our globally shared JSON-formatted scopes and types scopes=( $("$JQ" -r '[.scopes | paths | reduce .[] as $item (""; if . == "" then $item else . + "/" + $item end)] | join(" ")' < "$HOOK_DIRECTORY/../scopes/scopes.json") ) types=( $("$JQ" -r '.commitTypes | join(" ")' < "$HOOK_DIRECTORY/../scopes/commit-types.json") ) fi # build the regex pattern based on the config file function build_regex() { retypes= for type in "${types[@]}" do retypes="${retypes}$type|" done retypes="(${retypes%?})" rescope= for scope in "${scopes[@]}" do rescope="${rescope}$scope|" done # %? removes last | rescope="(${rescope%?})?" recherry="(cherry-pick\/)?" # Optionally captures an issue number at the front of the branch's name. rebranchname="(([[:digit:]]+)\-)?(.+)" regexp="^${retypes}\/${rescope}\/?${recherry}${rebranchname}\s*$" } function prepend_scope() { # COMMIT_MSG_FILE and $COMMIT_SOURCE are already available. # Step 1: if the branch follows our preferred branch conventions, we can use that! branch="$(git rev-parse --abbrev-ref HEAD)" build_regex # Perform the actual regex check. if [[ $branch =~ $regexp ]]; then TYPE="${BASH_REMATCH[1]}" SCOPE="${BASH_REMATCH[2]}" CHERRYPICK="${BASH_REMATCH[3]}" # 4 - just ISSUE, but with an appended '-'. Ashame BASH doesn't support non-capture groups. ISSUE="${BASH_REMATCH[5]}" NAME="${BASH_REMATCH[6]}" EXTRA_WHITESPACE="\n" # For known merge commits, consider the merge operation to be a 'chore'. if [ "${COMMIT_SOURCE}" = "merge" ]; then TYPE="chore" ISSUE= # Invalidates adding the 'postfix' for these commit types. EXTRA_WHITESPACE= fi # Build commit message title if [ -z "$SCOPE" ]; then prefix="$TYPE: $EXTRA_WHITESPACE" else prefix="$TYPE($SCOPE): $EXTRA_WHITESPACE" fi if [ ! -z "$CHERRYPICK" ]; then prefix ="${prefix}🍒" fi # Add commit message trailers if [ "${COMMIT_SOURCE}" != "merge" ]; then if [ -n "${ISSUE// }" ] || [ ! -z "$CHERRYPICK" ]; then prefix="${prefix}\n" fi if [ -n "${ISSUE// }" ]; then # Refer to https://unix.stackexchange.com/a/146945 for explanation. prefix="${prefix}Fixes: #$ISSUE\n" fi if [ ! -z "$CHERRYPICK" ]; then prefix="${prefix}Cherry-pick-of: #_ID_" fi fi postfix=$( cat < # - Use imperative, present tense ('attach' not 'attaches', 'attached' etc) # - Don't include a period at the end of the title # - Always include a blank line before trailers # - More: https://github.com/keymanapp/keyman/wiki/Pull-Request-and-Commit-workflow-notes EOF ) # Reuse any existing message text, wrapping it in our conventional commit formatting before # presenting it to the user for any final edits. echo -e "$prefix$(cat $COMMIT_MSG_FILE)$postfix" > $COMMIT_MSG_FILE fi # If it doesn't follow our branch-name format, we can't provide any help. } # # Configurable options for types, scopes and message length. # # Step 1 - is the prepared message already in our conventional commits format? $HOOK_DIRECTORY/commit-msg -q $COMMIT_MSG_FILE isConventional=$? # Grabs the exit code. if [ $isConventional -eq 0 ]; then # If so, great! Don't do a thing! exit 0 fi # Debugging aides #echo "COMMIT_MSG_FILE=$COMMIT_MSG_FILE" #echo "COMMIT_SOURCE=$COMMIT_SOURCE" # The default message is NOT in conventional format. Time to help out with that. case $COMMIT_SOURCE in squash) # We don't help with squashes - this tends to combine two separate messages, # and that can be messy. exit 0 ;; merge|commit|'') # '' = plain `git commit` # We can help here! prepend_scope ;; *) # Default handler - we don't handle these types yet. Might be worth considering. # Known possibilities: message (-m), template (-t) exit 0 ;; esac