Building a Conventional Commits Generator: Stop Writing Bad Git Messages
Building a Conventional Commits Generator: Stop Writing Bad Git Messages#
We’ve all been there. You run git log on a project and see messages like “fix”, “update”, or the infamous “asdf”. Six months later, you have no idea what any commit actually did.
Conventional Commits solves this with a standardized format that’s both machine-readable and human-friendly. Let’s build a commit message generator from scratch, and dig into the details that actually matter.
The Format#
The specification is refreshingly simple:
<type>(<scope>): <description>
<body>
<footer>
Here’s what it looks like in practice:
feat(auth): add OAuth2 login support
Implement Google and GitHub OAuth2 authentication flow.
Includes token refresh and session management.
Closes #123, #456
The spec defines 11 types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, and revert. Each one has a clear meaning, so a changelog generator or release script can process them automatically.
The 72-Character Rule#
Why 72 characters for the description line? It’s not arbitrary. Git’s default display width is 80 columns, and the type(scope): prefix typically eats up 8-12 characters. That leaves around 70 for the actual description. Beyond 72, git log --oneline wraps and becomes unreadable.
This convention comes from the Linux kernel community — decades of Git history baked into a simple rule. Here’s the validation:
const descriptionError = useMemo(() => {
if (description && description.length > 72) {
return 'Description should be under 72 characters'
}
return ''
}, [description])
The Core: Building the Message#
The heart of a commit generator is string assembly. The logic is straightforward — around 25 lines:
const commitMessage = useMemo(() => {
let msg = ''
// Type is required
msg += commitType
// Scope is optional
if (scope.trim()) {
msg += `(${scope.trim()})`
}
// Breaking change marker
if (breakingChange) {
msg += '!'
}
// Description is required
if (description.trim()) {
msg += `: ${description.trim()}`
} else {
msg += ': '
}
// Body and footer are separated by blank lines
if (body.trim()) {
msg += `\n\n${body.trim()}`
}
const footerParts: string[] = []
if (breakingChange) {
footerParts.push('BREAKING CHANGE: ')
}
if (footer.trim()) {
footerParts.push(footer.trim())
}
if (footerParts.length > 0) {
msg += `\n\n${footerParts.join('\n')}`
}
return msg
}, [commitType, scope, description, body, footer, breakingChange])
Three details worth noting:
!placement: The breaking change marker goes between the scope parenthesis and the colon —feat(api)!: remove deprecated v1 endpoints. This is specified by the convention.BREAKING CHANGEin footer: The keyword must be uppercase, followed by a colon and space. It goes on its own line before any other footer content.- Blank line separators: The header, body, and footer sections are separated by two newlines — this is how Git parses commit messages internally.
Why useMemo Matters for Live Preview#
The generator shows a live preview that updates as the user types. Without optimization, every keystroke triggers a string re-assembly and a React re-render. useMemo solves this elegantly:
const commitMessage = useMemo(() => { ... }, [dependencies])
It only recalculates when a dependency actually changes. If the user edits the body, the scope and description calculations are preserved. This pattern is cleaner than useEffect + setState because it eliminates an extra render cycle.
Choosing a Scope Convention#
Scope describes which module the change affects. Common choices include directory names (auth, api, ui) or microservice names. There’s no right answer — consistency matters more than the choice itself.
One pattern I’ve seen work well: use npm package names. In a monorepo, commits like feat(@scope/core): add retry logic and feat(@scope/hooks): expose useQuery map directly to changelog sections.
Footer Semantics You Should Know#
Footers are primarily used for issue tracking and attribution:
Closes #123
Refs: #456
Co-authored-by: Name <email>
Reviewed-by: Name <email>
GitHub and GitLab recognize these keywords and can auto-close issues when a PR merges. This is why the generator’s footer input suggests Closes #123, Refs: #456 as placeholder text.
Wrapping Up#
The real value of Conventional Commits isn’t aesthetics — it’s automation. Structured commit messages feed directly into changelog generators, semantic version calculators, and release pipelines. Angular, ESLint, and most major open-source projects use this standard. It’s well worth adopting.
Try the online tool: Git Commit Generator
Related tools: Changelog Generator | README Generator
Published: 2026-05-02 06:09 GMT+8