git commit: Writing Effective Commit Messages and Best Practices
Deep dive into git commit, writing effective commit messages, conventional commits, signed commits, and commit best practices for clean version control history.
Introduction
git commit is the command that makes your changes permanent. It takes whatever is in the staging area and records it as a new node in your repository’s history. But committing is not just about saving work — it is about communication. Every commit message is a message to your future self and your teammates, explaining what changed and why.
A well-crafted commit history is a superpower. It enables precise bisecting to find bugs, clean cherry-picks for hotfixes, meaningful code review, and accurate release notes. A messy commit history is technical debt that compounds over time, making every investigation slower and every release riskier. The difference between the two comes down to how you use git commit.
This guide covers the mechanics of git commit, the art of writing effective commit messages, the Conventional Commits specification, signed commits for security, and the patterns that separate professional Git users from beginners. For staging fundamentals, see Master git add.
When to Use / When Not to Use
Craft careful commits when:
- Working on shared codebases where others will read your history
- Preparing pull requests — clean commits make reviews dramatically easier
- Fixing bugs — atomic commits enable
git bisectto find the exact breaking change - Implementing features — each logical step should be its own commit
- Working in regulated industries requiring audit trails
Quick commits are acceptable when:
- Working on a personal scratch repository
- Making trivial changes like typo fixes
- Experimenting with throwaway code
- You plan to squash before merging
Core Concepts
A commit is an immutable object in Git’s database that contains:
- Tree: A snapshot of all file contents at that moment
- Parent(s): Reference to the previous commit(s) — one parent for normal commits, two for merge commits
- Author: Who wrote the changes (name, email, timestamp)
- Committer: Who committed the changes (may differ from author in rebasing or cherry-picking)
- Message: The human-readable description of what changed and why
graph LR
A[Staging Area<br/>Prepared changes] -->|git commit| B[New Commit Object]
B --> C[Tree: File snapshot]
B --> D[Parent: Previous commit]
B --> E[Author info]
B --> F[Commit message]
The Commit Chain
graph LR
A[Commit A<br/>Initial] -->|parent| B[Commit B<br/>Feature start]
B -->|parent| C[Commit C<br/>Feature complete]
C -->|parent| D[Commit D<br/>Bug fix]
A ~~~ B ~~~ C ~~~ D
Each commit points to its parent, forming a linked list that Git traverses to reconstruct history.
Architecture or Flow Diagram
The Commit Creation Process
sequenceDiagram
participant WD as Working Directory
participant SA as Staging Area
participant ODB as Object Database
participant HEAD as HEAD Reference
Note over WD,SA: You have staged changes
SA->>ODB: git commit creates blob objects
SA->>ODB: Creates tree object (directory snapshot)
ODB->>ODB: Creates commit object with metadata
HEAD->>HEAD: HEAD points to new commit
Note over HEAD: Branch reference updated
Note over ODB: Commit is now permanent
Note over ODB: SHA-1 hash uniquely identifies it
Conventional Commits Structure
graph LR
A[feat/fix/docs/style/refactor/test/chore] -->|type| B[(scope)]
B -->|description| C[Subject line]
C -->|blank line| D[Body - what and why]
D -->|blank line| E[Footer - breaking changes, refs]
Step-by-Step Guide / Deep Dive
Basic Commit
# Commit staged changes with a message
git commit -m "Add user authentication module"
# Commit with a multi-line message
git commit -m "Add user authentication module
Implement JWT-based authentication with refresh tokens.
Includes login, logout, and token validation endpoints."
Using the Commit Message Editor
When you omit -m, Git opens your configured editor for a multi-line message:
git commit
The editor shows:
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch main
# Changes to be committed:
# modified: src/auth.py
# new file: src/auth_test.py
Amending Commits
# Fix the last commit message
git commit --amend -m "Correct commit message"
# Add forgotten files to the last commit
git add forgotten-file.py
git commit --amend --no-edit
# Change both message and content
git add additional-change.py
git commit --amend -m "feat: add authentication with rate limiting"
Warning: Amending rewrites history. Never amend commits that have been pushed to a shared branch.
Signed Commits
# Sign a commit with GPG
git commit -S -m "feat: add user authentication"
# Sign with a specific key
git commit -S <key-id> -m "feat: add user authentication"
# Configure automatic signing
git config --global commit.gpgSign true
# Sign with SSH key (Git 2.34+)
git config --global gpg.format ssh
git config --global user.signingKey ~/.ssh/id_ed25519.pub
git config --global commit.gpgSign true
Committing with Hooks
# Pre-commit hook example (.git/hooks/pre-commit)
#!/bin/sh
# Run linter before allowing commit
if ! npm run lint -- --quiet; then
echo "Linting failed. Commit aborted."
exit 1
fi
# Check for secrets
if grep -r "API_KEY\|SECRET\|PASSWORD" --include="*.py" --include="*.js" .; then
echo "Potential secrets detected. Commit aborted."
exit 1
fi
Conventional Commits
The Conventional Commits specification standardizes commit messages:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
Types:
| Type | When to Use | Example |
|---|---|---|
feat | New feature | feat(auth): add OAuth2 login |
fix | Bug fix | fix(api): handle null response body |
docs | Documentation only | docs: update API reference |
style | Formatting, no code change | style: fix indentation in auth module |
refactor | Code change, no behavior change | refactor(auth): extract token validation |
perf | Performance improvement | perf: reduce database queries in user list |
test | Adding or fixing tests | test(auth): add token expiration tests |
chore | Maintenance, dependencies | chore: upgrade express to 4.18 |
ci | CI/CD configuration | ci: add linting to GitHub Actions |
build | Build system changes | build: update webpack configuration |
Examples:
# Simple conventional commit
git commit -m "feat(auth): add JWT token refresh endpoint"
# With body explaining why
git commit -m "fix(api): handle empty result set in search
The search endpoint returned 500 when no results matched
instead of returning an empty array. This caused frontend
errors and confused users expecting 'no results' messaging.
Fixes #1234"
# With breaking change footer
git commit -m "refactor(api): change user ID from int to UUID
All user identifiers are now UUIDs instead of sequential
integers for security and distributed system compatibility.
BREAKING CHANGE: User ID type changed from int to string (UUID)"
Commit Templates
Create a reusable commit message template:
# Create template file (~/.gitmessage)
cat > ~/.gitmessage << 'EOF'
# <type>(<scope>): <subject>
# |<---- Using a Maximum Of 72 Characters ---->|
# Explain what and why, not how
# Body is optional but recommended for non-trivial changes
# Footer is optional:
# BREAKING CHANGE: <description>
# Closes #<issue-number>
EOF
# Configure Git to use it
git config --global commit.template ~/.gitmessage
Partial Commits
# Commit only specific files from staging
git commit src/auth.py src/auth_test.py -m "feat: add authentication"
# Interactively select hunks to commit
git commit -p
# Commit with a specific author (for pair programming)
git commit --author="Jane <jane@example.com>" -m "feat: add search"
Production Failure Scenarios + Mitigations
| Scenario | Impact | Mitigation |
|---|---|---|
| Committing to wrong branch | Changes appear in wrong release, confusing history | Verify branch with git branch before committing; use branch protection rules |
| Empty or meaningless commit messages | Impossible to understand history, bisect fails | Enforce conventional commits with pre-commit hooks; use commit templates |
| Amending pushed commits | Force-push required, breaks teammates’ history | Never amend pushed commits; create new commit instead |
| Committing without running tests | Broken code in shared history | Use pre-commit hooks to run tests; configure CI to block bad commits |
| Large monolithic commits | Impossible to review, cherry-pick, or bisect | Stage selectively with git add -p; commit logical units separately |
| Committing with wrong author identity | Attribution errors, compliance issues | Configure user.name and user.email globally; verify with git log |
| Missing signed commits in regulated environments | Failed compliance audits | Enforce signed commits with commit.gpgSign and server-side hooks |
Trade-offs
| Approach | Advantages | Disadvantages | When to Use |
|---|---|---|---|
| Detailed commit messages | Clear history, easy bisect, good for review | Takes more time, requires discipline | Production code, team projects, open source |
| Brief commit messages | Fast, low friction | Unclear history, hard to debug | Personal projects, experimental code |
| Conventional Commits | Machine-parseable, enables auto-changelogs | Requires learning, strict format | Teams, CI/CD pipelines, semantic versioning |
| Signed commits | Cryptographic proof of authorship | Requires key management, slightly slower | Regulated industries, open source maintainers |
--amend | Clean history, fixes mistakes | Rewrites history, dangerous on shared branches | Local commits only, before pushing |
| Squash commits | Clean PR history, single logical change | Loses intermediate development context | Feature branches merged via PR |
Implementation Snippets
The Professional Commit Workflow
# 1. Verify what is staged
git diff --staged --stat
# 2. Run tests on staged changes
git stash --keep-index && npm test && git stash pop
# 3. Write a conventional commit message
git commit -m "feat(auth): add JWT token refresh endpoint
The refresh endpoint allows clients to obtain new access
tokens without requiring re-authentication. Tokens expire
after 15 minutes and refresh tokens after 7 days.
Implements RFC 6749 Section 6.
Closes #456"
# 4. Verify the commit
git log -1
git show --stat
Automated Commit Message Validation
#!/bin/bash
# .git/hooks/commit-msg
# Validates conventional commit format
commit_msg=$(cat "$1")
pattern="^(feat|fix|docs|style|refactor|perf|test|chore|ci|build)(\(.+\))?: .+"
if ! echo "$commit_msg" | grep -qE "$pattern"; then
echo "ERROR: Commit message does not follow Conventional Commits format."
echo "Expected: <type>(<scope>): <description>"
echo "Types: feat, fix, docs, style, refactor, perf, test, chore, ci, build"
exit 1
fi
Generating Changelogs from Commits
# Extract all feat commits for changelog
git log --oneline --grep="^feat" --since="2026-01-01"
# Extract all fixes
git log --oneline --grep="^fix" --since="2026-01-01"
# Generate changelog with git-cliff (third-party tool)
git cliff --config cliff.toml
# Using conventional-changelog
npx conventional-changelog -p angular -i CHANGELOG.md -s
Committing with Multiple Authors
# Add co-author trailer (GitHub recognizes this)
git commit -m "feat: add collaborative editing
Co-authored-by: Jane Smith <jane@example.com>
Co-authored-by: Bob Wilson <bob@example.com>"
Observability Checklist
- Logs: Use
git log --onelineto review recent commit messages for clarity - Metrics: Track average commit message length — messages under 20 characters are likely unhelpful
- Traces: Use
git log --statto trace which files each commit touched - Alerts: Pre-commit hooks should reject messages that do not follow the team’s convention
- Audit: Run
git log --format="%h %s %an %ad" --date=shortfor audit-ready commit history - Health: Periodically review
git log --onelineto ensure commit granularity is appropriate - Validation: Verify signed commits with
git log --show-signature
Security/Compliance Notes
- Signed commits provide non-repudiation: GPG or SSH-signed commits cryptographically prove who created the commit, which is essential for compliance in regulated industries
- Commit messages may contain sensitive information: Never include passwords, API keys, or internal URLs in commit messages — they are permanently visible in
git log - Author identity can be spoofed: Without signed commits, anyone can set
user.nameanduser.emailto impersonate another developer. Signed commits prevent this - Compliance requires audit trails: Regulated industries (finance, healthcare) often require signed commits with meaningful messages that explain the purpose of each change
- Commit hooks enforce policy: Server-side hooks can reject commits that do not meet organizational standards for signing, message format, or content
Common Pitfalls / Anti-Patterns
- “Fixed stuff” commit messages: These provide zero information. Every commit message should answer: what changed and why? The “how” is visible in the diff
- Committing without reviewing staged content: Skipping
git diff --stagedmeans you might commit debug code, wrong files, or incomplete changes - Amending pushed commits: This rewrites history and forces everyone who pulled to resolve conflicts. Only amend local, unpushed commits
- Huge monolithic commits: A single commit with 50 changed files is impossible to review meaningfully. Split into logical units
- Committing merge conflicts markers: Leaving
<<<<<<<,=======,>>>>>>>markers in committed files breaks builds. Always verify after resolving conflicts - Using
git commit -awithout understanding: It stages all tracked changes and commits them. Untracked files are not included, and you lose the opportunity to review what is being committed - Inconsistent commit message format: Mixing styles (some with scopes, some without, some with bodies, some without) makes history hard to parse programmatically
Quick Recap Checklist
-
git commit -m "message"commits staged changes with a message -
git commitwithout-mopens the editor for multi-line messages -
git commit --amendmodifies the last commit (local only) -
git commit -Screates a cryptographically signed commit - Conventional Commits format:
type(scope): description - Commit types: feat, fix, docs, style, refactor, perf, test, chore, ci, build
- Subject line should be under 72 characters
- Body explains what and why, not how (the diff shows how)
- Footer for breaking changes and issue references
- Pre-commit hooks enforce quality standards
- Never amend or rebase commits that have been pushed to shared branches
- Use
git log --statto verify commit contents after committing
Interview Q&A
git commit only commits changes that are already staged with git add. git commit -a (or --all) automatically stages all tracked modified files and commits them in one step. The critical difference is that -a does not include new untracked files — it only handles files Git already knows about. Using -a bypasses the review step, which can lead to accidental commits.
Conventional Commits is a standardized format for commit messages: type(scope): description. It matters because it makes commit history machine-parseable, enabling automated changelog generation, semantic versioning, and release automation. Tools like semantic-release and standard-version read conventional commits to determine version bumps and generate release notes automatically. It also enforces consistency across team members.
git commit --amend creates a new commit that replaces the previous HEAD commit. It combines the current staging area with the previous commit's content and replaces the old commit's message if a new one is provided. The old commit becomes unreachable (though recoverable via git reflog). This rewrites history, so it should only be used on local, unpushed commits. After amending, the new commit has a different SHA-1 hash.
Signed commits use GPG or SSH keys to cryptographically sign the commit object. When you run git commit -S, Git creates a signature of the commit content using your private key. Others can verify this signature with your public key, proving that you created the commit and that it has not been tampered with. This is important for security (preventing impersonation), compliance (audit trails), and open source trust (GitHub shows "Verified" badges on signed commits).
A good commit message has three parts. The subject line (under 72 chars) states what changed in imperative mood: "Add authentication" not "Added authentication." The body (separated by a blank line) explains why the change was needed — the context and motivation. The footer (optional) references related issues and documents breaking changes. The golden rule: the diff shows how, the message should explain what and why.
Production Failure: Empty Commit Messages Blocking CI
A developer pushes 15 commits with messages like “fix”, “wip”, and "" (empty). The CI pipeline is configured with commitlint to enforce Conventional Commits. Results:
- CI pipeline fails — every commit rejected by the message validation hook
- PR blocked — cannot merge until all commit messages are rewritten
- Team blocked — dependent branches cannot rebase on the broken history
- Time wasted — 30 minutes of interactive rebase to fix messages that should have taken 30 seconds each
Mitigation: Install commitlint and husky to catch bad messages before they leave your machine:
npm install --save-dev @commitlint/cli @commitlint/config-conventional
npx husky init
echo "npx commitlint --edit $1" > .husky/commit-msg
Trade-offs: Signed vs Unsigned Commits
| Aspect | Signed Commits (-S) | Unsigned Commits |
|---|---|---|
| Security | Cryptographic proof of authorship | Author identity can be spoofed |
| Verification | GitHub shows “Verified” badge | No verification indicator |
| Setup cost | Requires GPG/SSH key generation and config | Works out of the box |
| Speed | Slightly slower (signing overhead) | Fastest possible |
| Compliance | Meets audit requirements for regulated orgs | May fail compliance checks |
| Key management | Keys must be backed up, rotated, protected | No key management needed |
| Team adoption | Requires everyone to configure signing | No team coordination needed |
| When to use | Open source maintainers, regulated industries | Personal projects, internal team repos |
Observability Checklist: Commit Message Linting in CI
- commitlint: Enforce Conventional Commits format with
@commitlint/cliand@commitlint/config-conventional - husky pre-commit hook: Run
commitlint --editon every commit to catch format violations locally before they reach CI - CI pipeline gate: Add a commitlint step in GitHub Actions/GitLab CI that fails the build on non-compliant messages
- Message length check: Enforce 72-character subject line limit — longer messages indicate unclear thinking
- Type validation: Ensure only approved types are used (feat, fix, docs, style, refactor, perf, test, chore, ci, build)
- Scope consistency: Validate scopes against a known list (auth, api, ui, db, ci) to prevent typos and inconsistency
- Breaking change detection: Parse
BREAKING CHANGE:footers to auto-trigger major version bumps in semantic release - Duplicate detection: Flag identical commit messages that suggest copy-paste errors or lazy commits
- Secret scanning: Run
gitleaksortrufflehogas a CI step to catch secrets in commit messages - Sign-off enforcement: Require
Signed-off-by:trailer for DCO compliance withgit commit -s
Resources
- Conventional Commits Specification — Official specification
- Pro Git — Committing Changes — Official guide
- Git Commit Documentation — Complete reference
- How to Write a Git Commit Message — Classic guide by Chris Beams
- Git Signing Documentation — GitHub’s guide to signed commits
Category
Related Posts
Automated Changelog Generation: From Commit History to Release Notes
Build automated changelog pipelines from git commit history using conventional commits, conventional-changelog, and semantic-release. Learn parsing, templating, and production patterns.
Commit Message Conventions: Conventional Commits, Angular Style, and Semantic Commits
Master commit message conventions including Conventional Commits, Angular style, and semantic commits. Learn automated changelog generation, linting enforcement, and team-wide standards.
Centralized vs Distributed VCS: Architecture, Trade-offs, and When to Use Each
Compare centralized (SVN, CVS) vs distributed (Git, Mercurial) version control systems — their architectures, trade-offs, and when to use each approach.