#!/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: : e.g. fix: stop counting milestones as records" say " (): 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