#!/usr/bin/env bash
#
# Validate one commit message against the conventional-commit format.
#
# This is not a style checker. `release-plz` derives the next version number
# and the entire changelog from these prefixes, so a message it cannot parse
# contributes nothing to the version bump and never appears in the changelog —
# silently, with no error anywhere. That is the failure this exists to prevent.
#
# Usage:
#   commit-msg-lint.sh <file>      # a file holding the message (git hook)
#   commit-msg-lint.sh -m "<msg>"  # a literal message (CI)
#
# Exits 0 if the message is acceptable, 1 with an explanation if not.
set -euo pipefail

# Must stay in sync with the commit_parsers in release-plz.toml. Types absent
# from that list are accepted here (they are valid conventional commits) but
# produce no changelog entry.
readonly BUMPS_MINOR='feat'
readonly BUMPS_PATCH='fix|perf|refactor|docs'
readonly BUMPS_NOTHING='test|chore|ci|build|style|revert'
readonly TYPES="$BUMPS_MINOR|$BUMPS_PATCH|$BUMPS_NOTHING"

# Long subjects are truncated in `git log --oneline`, GitHub's commit list and
# the generated changelog alike.
readonly MAX_HEADER=72

usage() {
    echo "usage: $0 <message-file> | $0 -m <message>" >&2
    exit 2
}

[ $# -ge 1 ] || usage

if [ "$1" = "-m" ]; then
    [ $# -ge 2 ] || usage
    message=$2
else
    [ -f "$1" ] || { echo "$0: no such file: $1" >&2; exit 2; }
    message=$(cat "$1")
fi

# A commit-msg hook receives the whole editor buffer, comments included.
header=$(printf '%s\n' "$message" | grep -v '^#' | sed '/./,$!d' | head -n1)

reject() {
    cat >&2 <<EOF

  ✗ $1

    got: ${header:-(empty)}

    Commit messages must be conventional commits — release-plz reads them to
    decide the next version and to write the changelog. A message it cannot
    parse is silently dropped from both.

      <type>[(scope)][!]: <subject>

      feat: 2x2 gauge grid for narrow terminals        -> minor bump
      fix(ports): keep the selection visible           -> patch bump
      feat!: drop the per-core heatmap                 -> MAJOR bump
      chore: bump ratatui                              -> no bump, hidden

    type must be one of:
      $BUMPS_MINOR                       (minor bump)
      $(echo "$BUMPS_PATCH" | tr '|' ' ')      (patch bump)
      $(echo "$BUMPS_NOTHING" | tr '|' ' ')   (no bump, hidden from the changelog)

    A trailing '!' before the colon, or a 'BREAKING CHANGE:' footer, forces a
    major bump.

EOF
    exit 1
}

# Generated by git or GitHub and not reworadable; release-plz skips them too.
case "$header" in
    "Merge "*|"Revert "*|fixup\!*|squash\!*) exit 0 ;;
esac

[ -n "$header" ] || reject "commit message is empty"

if ! printf '%s' "$header" | grep -qE "^($TYPES)(\([a-z0-9._/-]+\))?!?: .+"; then
    # Point at the most likely mistake rather than just restating the rule.
    if printf '%s' "$header" | grep -qiE "^($TYPES)"; then
        if printf '%s' "$header" | grep -qE "^[A-Z]"; then
            reject "type must be lowercase"
        fi
        reject "missing the ': ' after the type (and its optional scope)"
    fi
    reject "no recognised type prefix"
fi

if [ "${#header}" -gt "$MAX_HEADER" ]; then
    reject "subject line is ${#header} characters; keep it under $MAX_HEADER"
fi
