#!/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
