Generating a commit message template per branch Jump to heading
A single commit.template is better than nothing and worse than it could be. The prompts a feature branch needs β what changed, why now, which ticket β are not the prompts a hotfix needs, which are closer to what broke, what the blast radius was, and which release the fix must reach. Because prepare-commit-msg runs with the branch already checked out, choosing the right template is a few lines of shell. This recipe wires that up, within commit message hooks and templates.
When to use this approach Jump to heading
- Your branches already follow a naming convention such as
feat/,fix/orrelease/. - Different kinds of change genuinely need different information recorded.
- Messages are read later by release notes, incident reviews or an audit.
- Authors currently stare at an empty editor and write one line to escape it.
- If every change on your repository is the same shape, one static template is simpler and enough.
Step 1 β Write one template per branch kind Jump to heading
Keep them in the repository so they are reviewed like code and travel with a clone.
mkdir -p .git-templates
cat > .git-templates/feature.txt <<'TPL'
# <type>(<scope>): <subject> keep under 72 characters, imperative mood
#
# Why is this change needed?
#
# What does a reviewer need to know that the diff does not show?
#
# Refs:
TPL
cat > .git-templates/hotfix.txt <<'TPL'
# fix(<scope>): <subject>
#
# Symptom observed in production:
#
# Root cause:
#
# Blast radius / who was affected:
#
# Which release branches must this reach?
#
# Refs:
TPL # Verification: the files exist and are committed, not sitting untracked
git add .git-templates && git status --short .git-templates Step 2 β Select the template from the branch name Jump to heading
#!/usr/bin/env sh
# .husky/prepare-commit-msg β pick a template, then fill in what we already know.
set -eu
msg_file="$1"; source="${2:-}"
# Leave generated and -m messages alone entirely.
case "$source" in merge|squash|commit) exit 0 ;; esac
# Only act on an empty message β never overwrite what someone has written.
grep -qv '^#' "$msg_file" 2>/dev/null && [ -s "$msg_file" ] && \
grep -q '[^[:space:]#]' "$msg_file" && exit 0
branch=$(git symbolic-ref --short HEAD 2>/dev/null || echo '')
case "$branch" in
fix/*|hotfix/*) tpl=.git-templates/hotfix.txt ;;
release/*) tpl=.git-templates/release.txt ;;
*) tpl=.git-templates/feature.txt ;;
esac
[ -f "$tpl" ] || exit 0
cat "$tpl" "$msg_file" > "$msg_file.new" && mv "$msg_file.new" "$msg_file" # Verification: check which template each branch kind resolves to
for b in feat/PAY-1 fix/PAY-2 release/2.4.0; do
printf '%s -> ' "$b"
case "$b" in fix/*|hotfix/*) echo hotfix ;; release/*) echo release ;; *) echo feature ;; esac
done The guard on an already-populated message is the important part. A git commit -m, an amend, or a rebase continuation must not have a template pasted into it.
Step 3 β Pre-fill the derivable fields Jump to heading
Everything the branch name already states should arrive filled in. The authorβs attention is the scarce resource; spend it on the parts only they can supply.
# Appended inside the same hook, after the template is in place
key=$(printf '%s' "$branch" | grep -oE '[A-Z]{2,10}-[0-9]+' | head -n1 || true)
[ -n "$key" ] && printf 'Refs: %s\n' "$key" >> "$msg_file"
# On a release branch, the version is in the name
case "$branch" in
release/*) printf 'Release: %s\n' "${branch#release/}" >> "$msg_file" ;;
esac # Verification on a scratch branch, with commit.verbose on for good measure
git config commit.verbose true
git checkout -b fix/PAY-931-timeout && git commit --allow-empty Step 4 β Keep the comment markers from leaking into history Jump to heading
Template lines begin with # so Git strips them β but only under the default cleanup mode. A hook that appends diagnostics after the message, or a team that switches cleanup modes, can leave the prompts in the commit.
# Confirm cleanup is stripping comments as expected
git config --get commit.cleanup || echo "default (strips # lines)"
# After a test commit, the body must not contain the prompts
git commit --allow-empty -m "$(cat .git-templates/feature.txt)" 2>/dev/null
git log -1 --format='%b' | grep -c '^#' || echo "clean: no comment lines in the body" If you need to keep # lines in a message deliberately, switch to commit.cleanup=scissors, which strips only below a scissors line and leaves everything above untouched.
Step 5 β Roll it out without a manual setup step Jump to heading
A template is worthless if half the team never applies it. Install the hook path through the repositoryβs hook manager, so a fresh clone picks it up during dependency installation rather than during an onboarding conversation.
# With Husky, the hooks directory is set by the install step
git config --get core.hooksPath # expect .husky or similar
# A one-line drift check that belongs in CI
test "$(git config --get core.hooksPath)" = ".husky" \
|| echo "hooks are not installed in this clone" The wider version of this problem β making every clone agree on configuration β is covered in bootstrapping a developer machine for Git.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Why not use a global template in the home directory? Jump to heading
Because it is invisible to everyone else and cannot be reviewed. A template committed to the repository changes through a pull request like any other rule, and a new contributor gets it on their first clone instead of on their first correction.
Does this interfere with commitizen or a prompt-based tool? Jump to heading
They overlap, so pick one. A guided prompt replaces the editor entirely and is the better fit when the convention is strict; templates are lighter and keep the editor workflow. Running both means the templateβs prompts appear inside an answer the tool already composed.
What happens on a detached HEAD? Jump to heading
git symbolic-ref fails, the branch string is empty, and the default template applies. That is the right behaviour during a rebase or a bisect, where imposing a hotfix template on a replayed commit would be noise.
Related Jump to heading
- Commit Message Hooks & Templates β the parent topic and the hook ordering.
- Enforcing Issue Keys in Commit Messages β the rule that this derivation keeps from firing.
- Wiring Commitizen Into an Existing Repo β the prompt-driven alternative to templates.