Lint and auto-normalize Conventional Commit subjects. Plugs into pre-commit, prek, husky, and lefthook as a commit-msg hook.
whittle parses a commit message, applies a configurable set of transforms (lowercase, strip noise chars, replace and with &, normalize scope separators, …), drops body + trailers, and finally validates the result against your rules. If the message can't be brought into compliance, the hook fails.
Add to your .pre-commit-config.yaml:
repos:
- repo: https://github.com/smarlhens/whittle
rev: v0.1.0
hooks:
- id: whittle-fixThen install the commit-msg stage hook:
prek install --hook-type commit-msg
# or
pre-commit install --hook-type commit-msgInstall the npm wrapper (downloads the matching binary on install):
npm install --save-dev @smarlhens/whittle
npx husky add .husky/commit-msg 'npx whittle fix "$1"'npm install --save-dev @smarlhens/whittlecommit-msg:
commands:
whittle:
run: npx whittle fix {1}# Rust toolchain
cargo install whittle
# Or download a prebuilt binary
# https://github.com/smarlhens/whittle/releasesOnce whittle is on $PATH, wire it up manually:
# .husky/commit-msg or .git/hooks/commit-msg
#!/bin/sh
whittle fix "$1"Out of the box, whittle-fix applies these transforms to the commit subject:
| Component | Transform |
|---|---|
| type | lowercase |
| scope | lowercase, / → -, \ → - |
| description | lowercase, and → &, normalize smart quotes (“ ” ‘ ’ → " '), em/en dashes (— –) → -, collapse runs of ! or ?, strip / \ [ ] { }, strip standalone dots (keep version dots like 1.2.3), strip trailing dot, collapse whitespace |
| body | dropped |
| footers | dropped (denies Co-Authored-By and Co-authored-by by default) |
Validation rules:
- Must parse as Conventional Commits.
- Type must be one of:
feat, fix, refactor, perf, docs, test, chore, build, ci, style, revert. - Subject ≤ 72 characters.
Point whittle at a whittle.toml:
hooks:
- id: whittle-fix
args: [--config, whittle.toml]Example whittle.toml that mirrors the defaults:
[scope]
lowercase = true
replace = [
{ from = "/", to = "-" },
{ from = "\\", to = "-" },
]
[description]
lowercase = true
collapse_whitespace = true
trailing_dot = "strip" # keep | strip
strip_chars = ["/", "\\", "[", "]", "{", "}"]
internal_dots = "keep_in_numbers" # all | none | keep_in_numbers
replace = [
{ from = '\band\b', to = "&", regex = true },
{ from = "“", to = "\"" }, # “ → "
{ from = "”", to = "\"" }, # ” → "
{ from = "‘", to = "'" }, # ‘ → '
{ from = "’", to = "'" }, # ’ → '
{ from = "—", to = "-" }, # — → -
{ from = "–", to = "-" }, # – → -
{ from = "!{2,}", to = "!", regex = true },
{ from = '\?{2,}', to = "?", regex = true },
]
[body]
keep = false
[footers]
keep = false
deny = ["Co-Authored-By", "Co-authored-by"]
[rules]
max_subject_length = 72
require_conventional = true
allowed_types = [
"feat", "fix", "refactor", "perf", "docs", "test",
"chore", "build", "ci", "style", "revert",
]whittle check <file> # validate only, exit 1 on violation
whittle fix <file> # apply transforms in place, then validate
whittle --config whittle.toml fix .git/COMMIT_EDITMSGcommitlintrequires Node + complex config.compilerla/conventional-pre-commitvalidates but cannot rewrite. You must hand-fix every message.
whittle does both, ships as a single static binary, and runs under either pre-commit or prek.