#!/usr/bin/env bash # # commit-msg-lint.sh — enforce Go-style commit messages. # # Reference: https://go.dev/wiki/CommitMessage # # pkg/path: short summary in the imperative mood # # Optional body explaining what changed and why, wrapped at ~76 columns. # The blank line between the summary and the body is required. # # Fixes #123 # # Usage: # commit-msg-lint.sh --file # lint a single message file (git commit-msg hook) # commit-msg-lint.sh --rev # lint the message of one commit # commit-msg-lint.sh --range # lint every commit in a..b (exclusive of a) # ... | commit-msg-lint.sh # lint a message on stdin # # Flags: # --strict treat warnings as errors (exit non-zero on warnings too) # # Exit status: 0 = clean, 1 = at least one error (or warning under --strict). set -euo pipefail # Maximum length of the summary line. Go's guideline is "less than 76". MAX_SUMMARY_LEN=76 # Bare prefixes that look like Conventional Commits rather than a Go package # path. These are rejected because the summary should name the package/path # affected, e.g. `ogre: ...` instead of `fix: ...`. CONVENTIONAL_TYPES="feat fix chore refactor perf style ci revert" # Special prefixes that are not repo paths but are conventionally allowed, # following the Go project (e.g. `all:` for tree-wide changes). PATH_ALLOWLIST="all" # Space-separated list of real top-level entries in the repo (dirs and files), # used to validate that a prefix names an actual path. Populated per run from # the tree being linted; empty means "couldn't determine, skip path checks". TOPLEVEL="" # is_toplevel — true if is a real top-level entry or allowlisted. is_toplevel() { case " $TOPLEVEL " in *" $1 "*) return 0 ;; esac case " $PATH_ALLOWLIST " in *" $1 "*) return 0 ;; esac return 1 } # Non-imperative openers (past tense / gerund) that read as "described the # change" rather than "make the change". Warning only. NON_IMPERATIVE="added adds adding fixed fixes fixing updated updates updating \ removed removes removing changed changes changing implemented implements \ implementing created creates creating deleted refactored refactoring \ improved improves improving bumped bumps" STRICT=0 errors=0 warnings=0 err() { printf ' \033[31merror\033[0m %s\n' "$1" >&2; errors=$((errors + 1)); } warn() { printf ' \033[33mwarn\033[0m %s\n' "$1" >&2; warnings=$((warnings + 1)); } # lint_message