#!/usr/bin/env bash # # Check a commit message against Conventional Commits, and its scope against # the epics this repo's work belongs to. # # Two differences from headquarters' hook of the same name, both deliberate. # # It enforces the *form* as well as the scope. headquarters lets an # unconventional subject through, because that repo has years of them; this # one has had the convention from its second commit and there is no reason to # let it slip. # # And the epic ids are written down here rather than read from plan/, because # helm has no plan/ - its epics live in headquarters, which is not guaranteed # to be checked out beside this repo and is certainly not guaranteed to be at # any particular revision. A hardcoded list that is occasionally stale is # better than a hook that passes silently whenever a sibling directory is # missing. # # Run by prek at the commit-msg stage, with the message file as $1. set -euo pipefail # Conventional Commits types. TYPES="feat fix docs style refactor perf test build ci chore revert" # Epic ids from headquarters' plan/. Add one here when an epic that helm does # work for is added there. EPIC_SCOPES="unit-library unit-search unit-rules forces" # Scopes that are deliberately not epics. Keep this short: every entry is a # commit that cannot be found from an epic. NON_EPIC_SCOPES="deps scripts bridge ci repo" msg_file="${1:?usage: check-commit-scope.sh }" # First line that is not a comment or blank; git strips the comments later. subject="$(grep -v '^#' "$msg_file" | grep -m1 . || true)" # An empty message aborts the commit on git's side, so it is not this hook's # to complain about. [ -n "$subject" ] || exit 0 # A revert or a merge git wrote itself is not the author's prose to police. case "$subject" in Revert\ * | Merge\ *) exit 0 ;; esac # type(scope)!: subject, where the scope is optional. The pattern is a # variable because bash's [[ ]] parser trips on an unquoted ")". full_re='^([a-z]+)(\(([^)]*)\))?!?: .+' if [[ ! "$subject" =~ $full_re ]]; then { echo "commit subject is not Conventional Commits:" echo " $subject" echo echo "Write: type(scope): what changed" echo " type: what changed" echo # shellcheck disable=SC2086 echo "Types: $(printf '%s, ' $TYPES | sed 's/, $//')" echo "Scopes: $EPIC_SCOPES $NON_EPIC_SCOPES" } >&2 exit 1 fi type="${BASH_REMATCH[1]}" scopes="${BASH_REMATCH[3]}" case " $TYPES " in *" $type "*) ;; *) { echo "unknown commit type: $type" echo # shellcheck disable=SC2086 printf ' %s\n' $TYPES | sort } >&2 exit 1 ;; esac # No scope is allowed: not every commit belongs to an epic. [ -n "$scopes" ] || exit 0 known="$EPIC_SCOPES $NON_EPIC_SCOPES" bad="" IFS=',' read -ra parts <<<"$scopes" for scope in "${parts[@]}"; do scope="$(printf '%s' "$scope" | tr -d '[:space:]')" [ -n "$scope" ] || continue case " $known " in *" $scope "*) ;; *) bad="$bad $scope" ;; esac done [ -n "$bad" ] || exit 0 { echo "commit scope is not an epic or a known non-epic:$bad" echo echo "A scope names the epic in headquarters' plan/ that the work belongs" echo "to, so a commit is findable from it. Use one of:" echo # shellcheck disable=SC2086 printf ' %s\n' $known | sort -u echo echo "or drop the scope. If a new epic exists in headquarters, add its id" echo "to EPIC_SCOPES in this script." } >&2 exit 1