Atomic Commits
Status: Complete
Category: Delivery
Default enforcement: Advisory
Author: PushBackLog team
Tags
- Topic: delivery, version-control
- Skillset: engineering
- Technology: git
- Stage: execution, review
Summary
An atomic commit contains exactly one logical change — no more, no less. Each commit should be independently sensible: the codebase should be in a consistent, working state before and after it. Bundling multiple unrelated changes into one commit makes git history opaque, makes bisection to find regressions harder, and makes reverting a specific change without affecting others impossible.
Rationale
Commits are the unit of rollback
When a deployment causes a production incident, the first tool is rollback. But which change caused the problem? If ten unrelated changes were bundled into one commit, rolling back means reverting all ten — including changes that are fine and may be urgently needed. Atomic commits make individual changes independently revertable: git revert <commit-sha> affects exactly the change it describes, nothing more.
Git history as documentation
The git log is a narrative of how the system evolved. “Refactor user service + fix payment bug + update dependencies” describes nothing useful. “Fix null pointer exception when user has no billing address” explains exactly what changed and why. A well-maintained history of atomic, descriptively-named commits is one of the most valuable forms of documentation in a codebase — searchable, permanent, and linked to code.
Bisection requires atomicity
git bisect binary-searches the commit log to find the commit that introduced a bug. This only works if each commit represents one coherent change and the test suite can identify the fault. If commits are large, multi-concern bundles, bisect finds the bundle but not the specific change — reducing its utility.
Guidance
What “one logical change” means
A logical change is a single coherent unit of intent. Examples:
| Atomic | Not atomic |
|---|---|
fix: handle null billing address in checkout | fix payment bug, add user avatar, update README |
feat: add POST /api/users endpoint | add users endpoint and refactor auth middleware |
chore: upgrade lodash to 4.17.21 | upgrade all dependencies |
refactor: extract parseAddress() into utils | refactor + add tests + fix bug found during refactor |
When refactoring uncovers a bug, commit the bug fix separately from the refactoring. When a feature requires a dependency upgrade, commit the upgrade separately.
Staging partial changes with git
Git’s staging area allows committing part of a file:
# Interactively stage hunks (parts of files)
git add -p
# Or interactively stage lines in a specific file
git add -p src/checkout.ts
# Review staged changes before committing
git diff --staged
This allows a developer to write multiple changes in a single working session and then separate them into atomic commits before pushing.
Squashing and reorganising before push
If working on a branch with messy WIP commits, clean up before the branch is reviewed:
# Interactively rebase the last 5 commits — squash, reorder, rename
git rebase -i HEAD~5
# Commands available in interactive rebase:
# pick — keep as is
# reword — keep but edit the message
# squash — combine with previous commit
# fixup — combine with previous, discard message
# drop — remove the commit
Important: only rewrite history for commits that have not been pushed to a shared branch. Never force-push to main or a branch others have checked out.
Relationship to Conventional Commits
Atomic commits work hand-in-hand with Conventional Commits. The commit type (fix, feat, chore, etc.) only makes sense for one logical change. A commit subject of fix: handle null billing address is atomic and descriptive. fix: address multiple bugs is a sign that the commit contains multiple changes that should be separated.
Work-in-progress commits
During active development, WIP commits are acceptable — they mark progress and serve as checkpoints. The important discipline is to clean up before creating a PR:
# WIP commits during development — these are fine
git commit -m "wip: start user validation"
git commit -m "wip: add tests"
git commit -m "wip: fix edge case"
# Before pushing for review — squash into atomic commits
git rebase -i origin/main
# → reorganise into: feat: add user email validation
# → or: feat: validate email + fix: handle existing-email error (two commits)
Review checklist
- Each commit contains exactly one logical change
- The commit message describes the what and the why of the change
- The codebase is in a compilable, testable state at every commit
- Unrelated changes (formatting, refactoring, bug fixes discovered en route) are committed separately
- WIP commits are squashed before submitting for review