#!/usr/bin/env bash # Enforce commit message layout and line length (pure bash; no helper scripts). # Enable with: git config core.hooksPath .githooks # # Required shape (merge commits skip all checks while .git/MERGE_HEAD exists): # 1) subject (non-empty, <=80 chars) # 2) blank line # 3) one or more summary lines (no "- " bullet prefix), <=80 chars each # 4) blank line # 5) one or more body lines, each a "- ..." bullet, <=80 chars each # # Merge templates from Git can exceed 80 columns; skip checks then. set -euo pipefail msg_file=${1:?commit-msg hook requires path to message file} if [[ -f .git/MERGE_HEAD ]]; then exit 0 fi lines=() lineno=0 while IFS= read -r line || [[ -n "${line}" ]]; do lineno=$((lineno + 1)) len=${#line} if ((len > 80)); then echo "❌ COMMIT MESSAGE: line ${lineno} is ${len} chars (max 80)" echo "" echo "Required: subject, blank, summary, blank, then \"- ...\" bullets." echo "See .github/prompts/commit.prompt.md" echo "" echo "Offending line (truncated for display):" printf '%s\n' "${line}" | cut -c1-120 exit 1 fi lines+=("$line") done <"${msg_file}" # Drop trailing empty lines so optional final newline does not confuse counts. while ((${#lines[@]} > 0)); do last=$(( ${#lines[@]} - 1 )) if [[ -z "${lines[last]}" ]]; then unset "lines[last]" else break fi done n=${#lines[@]} if ((n < 5)); then echo "❌ COMMIT MESSAGE: need subject, blank, summary, blank, bullets" echo "(five or more lines). See .github/prompts/commit.prompt.md" exit 1 fi if [[ -z "${lines[0]}" ]]; then echo "❌ COMMIT MESSAGE: first line (subject) must not be empty" exit 1 fi if [[ -n "${lines[1]}" ]]; then echo "❌ COMMIT MESSAGE: line 2 must be blank (separator after subject)" exit 1 fi bullet_idx=-1 for ((i = 2; i < n; i++)); do line=${lines[i]} if [[ "${line}" =~ ^[[:space:]]*-[[:space:]] ]]; then bullet_idx=$i break fi done if ((bullet_idx < 0)); then echo "❌ COMMIT MESSAGE: no bullet list found (lines starting with \"- \")" echo "After the summary paragraph and its trailing blank line, add details" echo "as \"- ...\" bullets. See .github/prompts/commit.prompt.md" exit 1 fi if ((bullet_idx < 3)); then echo "❌ COMMIT MESSAGE: add a summary line after the subject blank line" echo "before the blank that precedes bullets." exit 1 fi if [[ -n "${lines[bullet_idx - 1]}" ]]; then echo "❌ COMMIT MESSAGE: blank line required between summary and bullet list" echo "(line $((bullet_idx)) should be empty)." exit 1 fi has_summary=0 for ((i = 2; i < bullet_idx - 1; i++)); do line=${lines[i]} if [[ -n "${line}" ]] && [[ "${line}" =~ ^[[:space:]]*-[[:space:]] ]]; then echo "❌ COMMIT MESSAGE: summary line $((i + 1)) starts like a bullet" echo "Use prose first, one blank line, then hyphen bullets." exit 1 fi if [[ -n "${line}" ]]; then has_summary=1 fi done if ((has_summary == 0)); then echo "❌ COMMIT MESSAGE: summary paragraph is empty before the bullet list" echo "Add one to three sentences, then a blank line, then bullets." exit 1 fi for ((i = bullet_idx; i < n; i++)); do line=${lines[i]} if [[ -z "${line}" ]]; then echo "❌ COMMIT MESSAGE: empty line $((i + 1)) inside bullet block" echo "Remove blanks between bullets or end after the last bullet." exit 1 fi if ! [[ "${line}" =~ ^[[:space:]]*-[[:space:]] ]]; then echo "❌ COMMIT MESSAGE: line $((i + 1)) is not a hyphen bullet" echo "Each bullet line must start with dash then space after summary." exit 1 fi done exit 0