Something went wrong. Try again.
Web frontend and supporting services for lance.blue
Something went wrong. Try again.
3.4 kB · 93 lines
Shell
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394#!/usr/bin/env bash## Check a commit message's Conventional Commits scope against plan/.## An epic's id is its commit scope, so `feat(camo): ...` belongs to# plan/camo.md. This catches the two ways that goes wrong: a typo, and a scope# naming an epic nobody has written down.## Valid ids are the filenames in plan/ and plan/complete/, where finished# epics are archived. It used to read plan/README.md's tables as well, for an# epic that was a row before it was a file; those tables are generated from# the files now (scripts/gen-plan-readme.py), so a row that no file backs# cannot exist and reading them would only find the same ids twice.## It is not a rule that every commit belongs to an epic. A commit with no# scope passes, and so does anything in NON_EPIC_SCOPES — build work,# dependency bumps and repo chores are not epics and should not have to# pretend otherwise. What fails is a scope that is neither.## Run by prek at the commit-msg stage, with the message file as $1.
set -euo pipefail
# Scopes that are deliberately not epics. Keep this short: every entry is a# commit that cannot be found from plan/.NON_EPIC_SCOPES="plan deps scripts deploy ci"
# The component scopes this repo used before epics. They still pass so that# adopting the hook does not reject ordinary work, but prefer the epic: `web`# says which directory changed, `camo` says what the change was for. This list# should shrink to nothing.LEGACY_SCOPES="web api matches manifest todo"
msg_file="${1:?usage: check-commit-scope.sh <commit-msg-file>}"repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"plan_dir="$repo_root/plan"
# 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
# type(scope)!: subject. No parentheses means no scope, which is allowed. The# pattern is a variable because bash's [[ ]] parser trips on an unquoted ")".subject_re='^[a-z]+\(([^)]*)\)!?:'if [[ ! "$subject" =~ $subject_re ]]; then exit 0fiscopes="${BASH_REMATCH[1]}"
known="$NON_EPIC_SCOPES $LEGACY_SCOPES"
# Epics with a file, including the completed ones under plan/complete/. An# epic that is finished keeps its id: a follow-up fix to it is still that# epic's commit, and refusing the scope would only push the change under a# vaguer one.for f in "$plan_dir"/*.md "$plan_dir"/complete/*.md; do [ -e "$f" ] || continue id="$(basename "$f" .md)" [ "$id" = "README" ] && continue known="$known $id"done
# Collapse whitespace so the membership test below has single separators.known="$(printf '%s' "$known" | tr -s '[:space:]' ' ')"
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" ;; esacdone
[ -n "$bad" ] || exit 0
{ echo "commit scope not in plan/:$bad" echo echo "A scope is an epic id, so that a commit is findable from the epic it" echo "belongs to. Use one of:" echo # shellcheck disable=SC2086 printf ' %s\n' $known | sort -u echo echo "or write plan/<id>.md for the epic first, or drop the scope."} >&2exit 1