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 a MINOR version bump in SemVer.
  • fix: A bug fix. Correlates to a PATCH version 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-cli can 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-msg hook 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*
  

; then
  echo "Error: There must be a blank line between the subject and the body."
  exit 1
fi

# Optional: Validate body line length
# while IFS= read -r line; do
#   if [ $(echo "$line" | wc -c) -gt 75 ]; then # 72 chars + newline
#     echo "Error: Body line exceeds 72 characters: $line"
#     exit 1
#   fi
# done <<< "$BODY"

echo "Commit message format OK."

Common Pitfalls

  • Overly Generic Messages: Avoid messages like "Update files" or "Minor changes." These provide no context.
  • Too Much Detail in Subject: Keep the subject concise. Move detailed explanations to the body.
  • Missing Blank Line: Forgetting the blank line between the subject and body breaks formatting in many Git tools.
  • Inconsistent Standards: If not all team members follow the convention, the benefits of automated tooling diminish significantly. Use commit message linters or Git hooks to enforce standards.
  • Confusing What with Why: Remember the diff shows *what* changed. The message should explain *why* it changed.
  • Not Committing Often Enough: Large, sprawling commits are difficult to review and understand. Break down changes into smaller, logical units, each with its own clear commit message.

Back to the knowledge base · Ask the AI assistant