Write Commit Messages That Future-You Will Thank You For
Effective commit messages are paramount for maintaining a healthy and understandable codebase. They serve as a concise historical record, facilitating…
Effective commit messages are paramount for maintaining a healthy and understandable codebase. They serve as a concise historical record, facilitating code reviews, debugging, and onboarding new team members. This guide outlines best practices for crafting commit messages that provide maximum clarity and utility, focusing on structure, content, and the adoption of Conventional Commits.
The Anatomy of a Good Commit Message
A well-structured commit message typically consists of a subject line and an optional, more detailed body. Adhering to certain conventions for each part significantly enhances readability and usefulness.
Subject Line: Imperative Mood, Concise
The subject line is the most critical part of a commit message, often displayed in truncated views by Git tools. It should be a succinct summary of the change.
- Imperative Mood: Start the subject line with a verb in the imperative mood, as if giving a command. Examples: "Add feature X," "Fix bug Y," "Refactor Z." Avoid past tense ("Added," "Fixed") or present participle ("Adding," "Fixing").
- Conciseness: Keep the subject line short, ideally under 50 characters, and strictly enforce a maximum of 72 characters. This ensures it displays fully in various Git interfaces (e.g.,
git log --oneline, GitHub pull request lists). - Capitalization: Capitalize the first letter of the subject line.
- No Trailing Punctuation: Do not end the subject line with a period.
Examples of good subject lines:
feat: Implement user authentication via OAuth2
fix: Prevent infinite loop in data parsing
docs: Update README with installation instructions
chore: Upgrade Node.js dependency to 16.14.0
Body: Explain Why, Not What
The body of the commit message, separated from the subject by a blank line, provides crucial context and explanation. Its primary purpose is to articulate *why* a change was made, not merely *what* was changed.
- Separation: Always include a blank line between the subject and the body. Git and many tools rely on this separation.
- Wrap at 72 Characters: For readability, wrap the body text at 72 characters. This is a historical convention from email and terminal displays that still holds value for consistent formatting across different tools.
- Focus on Rationale: Describe the problem being solved, the motivation behind the change, and any alternative approaches considered. Explain trade-offs, design decisions, and potential impacts.
- Avoid Redundancy: Do not reiterate information evident from the code diff. The diff shows *what* changed; the commit message explains *why*.
- Detail Implementation Notes (If Necessary): If specific implementation details are complex or non-obvious, they can be included, but prioritize the rationale.
Example of a good commit body:
Refactor: Decouple authentication from user service
The previous design coupled the user authentication logic directly to
the user management service. This made it difficult to swap out
authentication providers (e.g., from local DB to OAuth) without
modifying the core user service.
This commit introduces an `AuthGateway` interface and an `OAuth2AuthGateway`
implementation. The `UserService` now depends on the `AuthGateway` interface
via dependency injection, allowing for flexible authentication provider
swapping. This also improves testability of both components independently.
Future work includes implementing a `LocalDBAuthGateway` for development
environments.
Adopting Conventional Commits
Conventional Commits is a lightweight convention on top of commit messages, providing an explicit specification for commit history. It structures the commit message with a type and an optional scope, enabling automated tooling for tasks like generating changelogs, detecting breaking changes, and semantic versioning (SemVer) bumps.
Structure of a Conventional Commit Message
A conventional commit message generally follows this format:
<type>(<scope>): <subject>
<body>
<footer>
Types
The <type> is mandatory and describes the nature of the change. Common types include:
feat: A new feature. Correlates to aMINORversion bump in SemVer.fix: A bug fix. Correlates to aPATCHversion bump in SemVer.docs: Documentation-only changes.style: Changes that do not affect the meaning of the code (whitespace, formatting, semicolons, etc.).refactor: A code change that neither fixes a bug nor adds a feature.perf: A code change that improves performance.test: Adding missing tests or correcting existing tests.chore: Other changes that don't modify source code or tests (e.g., build process, auxiliary tools, dependency updates).build: Changes that affect the build system or external dependencies (e.g., npm, yarn).ci: Changes to our CI configuration files and scripts.
Scope (Optional)
The <scope> provides additional contextual information about where the change was made. It's enclosed in parentheses. For example, feat(api), fix(auth), docs(readme). The scope is optional and can be omitted if the change is broad or affects multiple areas.
Breaking Changes
A breaking change is indicated by adding ! after the type/scope or by explicitly stating BREAKING CHANGE: in the footer. This signifies a non-backward compatible change and typically results in a MAJOR version bump in SemVer.
feat(api)!: Remove deprecated v1 user endpoint
This commit removes the /api/v1/users endpoint which has been
deprecated since version 2.0.0.
Users of this endpoint must now migrate to /api/v2/users.
BREAKING CHANGE: The /api/v1/users endpoint is no longer available.
Footers
Footers are used for referencing issues (e.g., Closes #123, Refs #456) or providing BREAKING CHANGE information. Each footer line should also wrap at 72 characters.
Tools and Automation
Adopting Conventional Commits unlocks powerful automation possibilities.
- Automated Changelog Generation: Tools like
conventional-changelog-clican parse your commit history and generate a markdown changelog file automatically, grouping changes by type (e.g., Features, Bug Fixes). - Semantic Release: Libraries like Semantic Release automate the entire release workflow. Based on your commit messages, they determine the next version number (patch, minor, major), generate release notes, and publish packages to npm or similar registries.
- Git Hooks: Implement client-side Git hooks (e.g.,
commit-msghook using Husky) to validate commit message format before allowing a commit. This ensures team-wide adherence to standards.
Example commit-msg hook using a simple script (.git/hooks/commit-msg or via Husky):
#!/bin/sh
# .git/hooks/commit-msg or .husky/commit-msg
COMMIT_MSG_FILE=$1
SUBJECT=$(head -n1 "$COMMIT_MSG_FILE")
BODY=$(sed '1d;/^$/d' "$COMMIT_MSG_FILE") # Get body, skipping subject and blank lines
# Validate subject line length (max 72 chars, recommended 50 for visibility)
if [ $(echo "$SUBJECT" | wc -c) -gt 72 ]; then
echo "Error: Subject line is too long (max 72 characters)."
echo "Subject: $SUBJECT"
exit 1
fi
# Validate subject line starts with conventional commit type
if ! echo "$SUBJECT" | grep -qE '^(feat|fix|docs|style|refactor|perf|test|chore|build|ci)(\(.+\))?: .+'; then
echo "Error: Subject line must follow Conventional Commits spec (e.g., 'feat(scope): My subject')."
echo "Subject: $SUBJECT"
exit 1
fi
# Ensure blank line between subject and body if body exists
if [ -n "$BODY" ] && ! head -n2 "$COMMIT_MSG_FILE" | tail -n1 | grep -qE '^\s*