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.

published: reading time: 16 min read updated: March 31, 2026

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 bisect to 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:

TypeWhen to UseExample
featNew featurefeat(auth): add OAuth2 login
fixBug fixfix(api): handle null response body
docsDocumentation onlydocs: update API reference
styleFormatting, no code changestyle: fix indentation in auth module
refactorCode change, no behavior changerefactor(auth): extract token validation
perfPerformance improvementperf: reduce database queries in user list
testAdding or fixing teststest(auth): add token expiration tests
choreMaintenance, dependencieschore: upgrade express to 4.18
ciCI/CD configurationci: add linting to GitHub Actions
buildBuild system changesbuild: 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

ScenarioImpactMitigation
Committing to wrong branchChanges appear in wrong release, confusing historyVerify branch with git branch before committing; use branch protection rules
Empty or meaningless commit messagesImpossible to understand history, bisect failsEnforce conventional commits with pre-commit hooks; use commit templates
Amending pushed commitsForce-push required, breaks teammates’ historyNever amend pushed commits; create new commit instead
Committing without running testsBroken code in shared historyUse pre-commit hooks to run tests; configure CI to block bad commits
Large monolithic commitsImpossible to review, cherry-pick, or bisectStage selectively with git add -p; commit logical units separately
Committing with wrong author identityAttribution errors, compliance issuesConfigure user.name and user.email globally; verify with git log
Missing signed commits in regulated environmentsFailed compliance auditsEnforce signed commits with commit.gpgSign and server-side hooks

Trade-offs

ApproachAdvantagesDisadvantagesWhen to Use
Detailed commit messagesClear history, easy bisect, good for reviewTakes more time, requires disciplineProduction code, team projects, open source
Brief commit messagesFast, low frictionUnclear history, hard to debugPersonal projects, experimental code
Conventional CommitsMachine-parseable, enables auto-changelogsRequires learning, strict formatTeams, CI/CD pipelines, semantic versioning
Signed commitsCryptographic proof of authorshipRequires key management, slightly slowerRegulated industries, open source maintainers
--amendClean history, fixes mistakesRewrites history, dangerous on shared branchesLocal commits only, before pushing
Squash commitsClean PR history, single logical changeLoses intermediate development contextFeature 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 --oneline to review recent commit messages for clarity
  • Metrics: Track average commit message length — messages under 20 characters are likely unhelpful
  • Traces: Use git log --stat to 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=short for audit-ready commit history
  • Health: Periodically review git log --oneline to 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.name and user.email to 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 --staged means 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 -a without 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 commit without -m opens the editor for multi-line messages
  • git commit --amend modifies the last commit (local only)
  • git commit -S creates 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 --stat to verify commit contents after committing

Interview Q&A

What is the difference between `git commit` and `git commit -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.

What is the Conventional Commits specification and why does it matter?

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.

What happens when you run `git commit --amend`?

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.

How do signed commits work and why are they important?

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).

What should a good commit message contain?

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

AspectSigned Commits (-S)Unsigned Commits
SecurityCryptographic proof of authorshipAuthor identity can be spoofed
VerificationGitHub shows “Verified” badgeNo verification indicator
Setup costRequires GPG/SSH key generation and configWorks out of the box
SpeedSlightly slower (signing overhead)Fastest possible
ComplianceMeets audit requirements for regulated orgsMay fail compliance checks
Key managementKeys must be backed up, rotated, protectedNo key management needed
Team adoptionRequires everyone to configure signingNo team coordination needed
When to useOpen source maintainers, regulated industriesPersonal projects, internal team repos

Observability Checklist: Commit Message Linting in CI

  • commitlint: Enforce Conventional Commits format with @commitlint/cli and @commitlint/config-conventional
  • husky pre-commit hook: Run commitlint --edit on 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 gitleaks or trufflehog as a CI step to catch secrets in commit messages
  • Sign-off enforcement: Require Signed-off-by: trailer for DCO compliance with git commit -s

Resources

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.

#git #version-control #changelog

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.

#git #version-control #conventional-commits

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.

#git #version-control #svn