Privacy-Period-Tracker/.githooks/commit-msg

111 lines
4.2 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Every commit says what kind of change it is, before it says anything else.
#
# ## Why
#
# Ninety-nine of this repository's commits already carry a conventional type —
# `feat:`, `fix:`, `docs:`, `chore:` — and ninety-six do not, including a run of
# recent ones written as bare sentences. That split is the problem: `git log
# --grep '^fix'` answers "what have we fixed" for half the history and quietly
# omits the other half, which is worse than having no convention at all, because
# the answer looks complete.
#
# So the type is required, and the vocabulary is closed. A closed list is the
# point — `feat`, `feature` and `feat!` as three spellings of one idea is how a
# convention stops being searchable.
#
# ## The vocabulary
#
# feat a new capability somebody can use
# fix a defect. The thing behaved wrongly and now does not
# ui appearance, layout, copy, or interaction, with no change in what
# the software knows or decides
# docs documentation only
# test tests only, with no change to what they test
# refactor same behaviour, different shape. If behaviour changed it is not this
# security hardening, boundaries, secret handling. Kept separate from `fix`
# on purpose: "what have we hardened" is a question worth being able
# to ask on its own, and it is the one an auditor asks first
# perf faster or lighter, same answers
# chore tooling, dependencies, releases. The bucket for work that is not
# about the product
#
# `harden`, `style` and `content` each appear once or twice in the history and
# are deliberately not here — they are `security`, `ui` and `docs` under other
# names, and a synonym is a hole in a closed list.
#
# ## Scope is optional, and lowercase
#
# fix(admin): ... the fifteen existing `admin` scopes, and `release`,
# `integrations`, `db` and the rest, all keep working.
#
# ## What it deliberately does not enforce
#
# Subject length. Thirty-five existing subjects run past 72 characters, several
# of them deliberately, and rejecting a commit for a well-written 80-character
# sentence would teach people to use `--no-verify` — which switches off the
# checks that actually matter. The type is the part a tool reads; the length is
# a matter of taste and stays that way.
#
# ## Escape hatch
#
# SKIP_GUARDS=1 git commit ... skips this too, and says so
#
# The same variable the pre-commit hook uses, because two switches for "I know
# what I am doing" is one more than anybody will remember.
set -uo pipefail
say() { printf '\033[1mcommit-msg:\033[0m %s\n' "$*" >&2; }
message_file="$1"
subject=$(head -1 "$message_file")
if [ -n "${SKIP_GUARDS:-}" ]; then
say "SKIP_GUARDS set — the commit type was NOT checked."
exit 0
fi
# Git writes these itself, or writes them on a human's behalf during a rebase.
# Rejecting them would break `git merge`, `git revert` and autosquash for a
# convention none of them ever agreed to.
case "$subject" in
"Merge "*|"Revert "*|"fixup!"*|"squash!"*|"amend!"*)
exit 0
;;
esac
# A comment-only file is an aborted commit; git handles that itself.
if [ -z "${subject// /}" ]; then
exit 0
fi
TYPES="feat|fix|ui|docs|test|refactor|security|perf|chore"
if printf '%s' "$subject" | grep -qE "^(${TYPES})(\([a-z0-9._-]+\))?!?: .+"; then
exit 0
fi
say "the subject line needs a type."
say ""
say " got: ${subject}"
say ""
say " expected: <type>: <subject> e.g. fix: stop counting milestones as records"
say " <type>(<scope>): <subject> ui(admin): mark cloud models on the picker"
say ""
say " types: feat a new capability"
say " fix a defect, now not"
say " ui appearance, layout, copy — no change to what it decides"
say " docs documentation only"
say " test tests only"
say " refactor same behaviour, different shape"
say " security hardening, boundaries, secrets"
say " perf faster or lighter, same answers"
say " chore tooling, dependencies, releases"
say ""
say " Your message is kept. Run 'git commit' again to edit it, or"
say " SKIP_GUARDS=1 git commit ... to bypass this loudly."
exit 1