git add -p, git rebase -i and the art of a clean commit history
Monster commits that mix refactoring, bug fixes and new features slow down code review, make git bisect useless and turn reverting a single change into a coin toss. With git add -p, git commit -p and git rebase -i, every change can be split into clearly scoped, atomic commits that stay traceable, testable and safely revertible on their own.
Table of Contents
- 1. What makes a commit atomic
- 2. The problem with monster commits
- 3. Recognizing mixed changes in the working tree
- 4. git add -p / git add -i in detail
- 5. Splitting hunks further with s and editing manually with e
- 6. git commit -p as a shortcut
- 7. Splitting after the fact: git reset -p and git rebase -i
- 8. The payoff: git bisect, git revert and git cherry-pick
- 9. Team workflow, conventions and comparison
- 10. Summary
- 11. FAQ
1. What makes a commit atomic
An atomic commit contains exactly one logical change and nothing else. Not one line, not one file, but one self-contained idea: a bug fix, a new method, a rename. The test is simple: can the commit be described in one sentence without the word "and"? If the answer is "fixes the discount calculator and also cleans up the imports," the commit is not atomic, it is a compromise between two unrelated intentions.
Atomic also means the commit can be built and tested in isolation. After a git checkout <commit>, the project should compile, the test suite should run, and the application should be in a consistent state, even if later commits are still missing. That is the key difference from arbitrarily small commits: atomic commits are as small as possible, but never smaller than the project's consistency allows.
2. The problem with monster commits
A monster commit mixes refactoring, new features and bug fixes into a single change spanning hundreds of lines across dozens of files. For the reviewer, such a commit is practically unreadable: which line change is intentional, and which is a side effect of an automated refactor? In practice this means reviewers skim the diff and approve it wholesale instead of truly understanding every change. That is exactly where bugs slip in that nobody can trace back afterward.
git bisect becomes useless with monster commits: the binary search inevitably lands on the 800-line commit that contains three unrelated changes, and it gives no answer as to which of the three caused the regression. git revert and git cherry-pick fail on mixed changes too: a revert unintentionally tears out things that were supposed to stay, and a cherry-pick onto a hotfix branch drags along refactoring nobody wants there.
3. Recognizing mixed changes in the working tree
Monster commits rarely happen on purpose. Work typically starts on a bug fix, and while reading the code an ugly variable name, a missing type hint, or a stale comment catches the eye, and gets fixed too under the motto "while I'm in here anyway." Two hours later, git status suddenly shows twelve changed files, even though the original bug fix only touches two.
The moment to catch this is before the commit, not after. git diff --stat gives a quick overview of how many files and lines changed. A closer look with git diff shows whether a single file contains several independent hunks. The rule of thumb: as soon as the draft commit message contains an "and" or a list, more than one logical change is sitting in the working tree, and it pays off to split before committing rather than fix it afterward.
4. git add -p / git add -i in detail: staging hunks interactively
git add -p (short for --patch) walks through every hunk, meaning every contiguous block of changes, one at a time and asks whether it should go into the staging area. The most important keys: y stages the hunk, n skips it, s tries to split the hunk into smaller pieces, e opens the editor for manual adjustment, q ends the session immediately. This lets you stage exactly the part of a working tree with mixed changes that belongs to the current commit, while the rest stays unstaged for the next commit.
git add -i instead opens an interactive menu with options like status, update, patch and diff. The patch menu item is equivalent to git add -p, but additionally lets you pick specific files up front to run patch mode against. That is handy when only two out of ten changed files actually belong to the current commit.
$ git add -p
diff --git a/src/Discount/PercentageCalculator.php b/src/Discount/PercentageCalculator.php
@@ -12,7 +12,7 @@ class PercentageCalculator
- return $price * $percentage;
+ return round($price * $percentage / 100, 2);
Stage this hunk [y,n,q,a,d,s,e,?]? y
diff --git a/src/Discount/PercentageCalculator.php b/src/Discount/PercentageCalculator.php
@@ -30,6 +30,10 @@ class PercentageCalculator
+ // TODO: remove after Q3 migration
+ private function legacyFallback(): float
+ {
+ return 0.0;
+ }
Stage this hunk [y,n,q,a,d,s,e,?]? n
5. Splitting hunks further with s and editing manually with e
Sometimes a single hunk still contains two unrelated changes, for example because both touch the same or an adjacent line. The s key then tries to break the hunk into smaller, independently stageable pieces. That does not always succeed: Git can only split a hunk if there are unchanged context lines between the changes. With two changes on directly consecutive lines, s aborts with a message saying the hunk cannot be split further.
For exactly that case there is e. The editor opens the hunk as patch text, where lines are marked with + or -. To exclude a line from the patch being staged, a + line is simply deleted, while a - line is turned into a context line without a prefix. Git then checks whether the edited patch can still be applied consistently. On syntax errors in the edited patch, the operation aborts without changing anything, so nothing can break.
# Manual edit mode (e): only keep the null-check, drop the log line
# Original hunk shown by git add -p -e:
@@ -45,6 +45,8 @@ class OrderValidator
if ($order === null) {
throw new InvalidArgumentException('Order must not be null');
}
+ $this->logger->debug('Validating order ' . $order->getId());
+ // Remove the debug line below by deleting the '+' line entirely
+ if (!$order->getItems()) {
# After editing: the debug-log '+' line is deleted,
# only the null-check hunk remains staged
6. git commit -p as a shortcut: building a clean commit sequence
git commit -p combines git add -p and git commit in one step: Git asks hunk by hunk, then opens the editor directly for the commit message of the change just assembled, and commits only that part. The rest stays unstaged and unchanged in the working tree for the next round. For splitting a mixed working tree into several atomic commits, this is often faster than add -p followed by a separate commit.
In practice: repeat git commit -p until git status shows a clean working tree. Each round gets its own precise commit message. Afterward, check with git log --oneline and git show <commit> for each individual commit. If every commit can be described in one sentence and builds on its own, the split was successful.
# Working tree has three unrelated changes mixed together
$ git status --short
M src/Discount/PercentageCalculator.php
M src/Order/OrderValidator.php
M composer.json
# Build three atomic commits directly, one git commit -p per concern
$ git commit -p
# ... select only the rounding-fix hunk, then write the message:
# "fix: round percentage discount to 2 decimals"
$ git commit -p
# ... select only the null-check hunk:
# "fix: reject null order in OrderValidator"
$ git add composer.json && git commit -m "chore: bump phpunit to 10.5"
$ git log --oneline -3
a1b2c3d chore: bump phpunit to 10.5
e4f5g6h fix: reject null order in OrderValidator
i7j8k9l fix: round percentage discount to 2 decimals
7. Splitting after the fact: git reset -p and git rebase -i
If the monster commit already exists, it is not lost, as long as it has not been pushed to a shared branch. git rebase -i HEAD~3 opens the last three commits for editing; the affected commit is switched from pick to edit. Once the rebase stops there, git reset HEAD^ undoes the commit without discarding the file changes. Everything ends up unstaged in the working tree, ready for git add -p and several new, atomic commits.
git reset -p works similarly but more directly: it unstages changes hunk by hunk from the staging area without resetting the entire commit. That is useful when too much was staged at once before committing. Important for both approaches: rewriting history with rebase is only safe as long as the affected commits have not been pushed or checked out by other team members. After pushing, only use --force-with-lease and coordinate explicitly with the team.
$ git rebase -i HEAD~3
# editor shows:
pick i7j8k9l fix: round percentage discount to 2 decimals
edit e4f5g6h fix: reject null order + bump phpunit in one commit
pick a1b2c3d docs: update changelog
# Rebase stops at the "edit" commit
$ git reset HEAD^
# changes are now unstaged, commit is undone, files intact
$ git add -p
# stage only the null-check hunk, then:
$ git commit -m "fix: reject null order in OrderValidator"
$ git add composer.json
$ git commit -m "chore: bump phpunit to 10.5"
$ git rebase --continue
8. The payoff: git bisect, git revert and git cherry-pick
With an atomic history, git bisect delivers an exact result. The binary search halves the commit range at every step, and if every commit contains exactly one logical change, git bisect ends up pointing to the exact one line that caused the regression, not an 800-line commit in which three more unrelated changes would still need to be tracked down. With git bisect run this process can even be fully automated, as long as a test script reliably detects the broken state.
git revert and git cherry-pick benefit directly too: reverting an atomic commit cleanly undoes exactly one change, without unintentionally dragging along refactoring or unrelated fixes. A cherry-pick onto a hotfix or release branch can be scoped to that one fix, without merge conflicts caused by unwanted, dragged-along refactoring. That is the practical value of atomic commits: each of these three operations turns from guesswork into a reliable, mechanical operation.
$ git bisect start
$ git bisect bad HEAD
$ git bisect good v2.4.1
# Automate the search with a script that exits non-zero on failure
$ git bisect run vendor/bin/phpunit --filter=DiscountCalculatorTest
# ... bisect narrows down through the atomic commit history ...
i7j8k9l is the first bad commit
commit i7j8k9l
fix: round percentage discount to 2 decimals
# Revert exactly this one concern, nothing else
$ git revert i7j8k9l
# Or cherry-pick just this fix onto the release branch
$ git cherry-pick i7j8k9l
9. Team workflow, conventions and comparison
Atomic commits reach their full value only when a team applies them consistently. A shared commit message convention, for example following Conventional Commits with prefixes like fix:, feat: or chore:, makes each commit's intent immediately obvious, even without opening the diff. In pull requests it is also worth deciding whether to merge with a merge commit, rebase, or squash: squash merges hide the atomic structure in the target branch but preserve it inside the pull request for the review phase, which is a good compromise for many teams.
The table below contrasts typical monster-commit patterns with their atomic counterparts.
| Situation | Monster commit pattern | Atomic pattern | Benefit |
|---|---|---|---|
| Refactoring during a bug fix | One commit with a rename and a fix | git add -p splits it into two commits | Review and revert stay independent |
| Commit message | "various fixes and cleanup" | One commit per logical change | History is readable without the diff |
| Debugging | git bisect lands on an 800-line commit | git bisect lands on the exact line | No manual follow-up search needed |
| Reverting a change | git revert drags along unwanted changes | git revert undoes exactly one change | No collateral damage from reverts |
| Hotfix on a release branch | cherry-pick drags along refactoring | cherry-pick carries only the one fix | Few to no merge conflicts |
In practice, it is worth adding a short CI check that validates commit messages against the team's format, for example with commitlint, so the convention does not just exist on paper but is enforced automatically on every push.
Mironsoft
Git workflows, code review processes and CI/CD for Magento teams
Establish a clean commit history across your team?
We help Magento and PHP teams professionalize their Git workflows: from commit conventions through code review guidelines to a CI/CD pipeline that checks automatically on every pull request.
Git Workflow Audit
Analysis of your commit history, branching strategy and review processes
Team Coaching
Hands-on training on git add -p, rebase -i and atomic commits
CI/CD Integration
Automated checks for commit conventions in your pipeline
10. Summary
Atomic commits instead of monster commits solve a recurring problem: code reviews nobody really reads anymore, git bisect that gives no usable answer, and reverts that break more than they fix. git add -p and git add -i split the working tree hunk by hunk into clearly scoped commits, git commit -p shortens the path there. Where the monster commit already exists, git reset -p and git rebase -i take back control after the fact, as long as the history has not been pushed yet.
The effort of writing clean, atomic commits does not pay off while writing them, it pays off every later time someone looks at the history: during review, while debugging with bisect, when reverting a single fix, and when cherry-picking onto a release branch. Teams that consistently enforce commit conventions and review guidelines turn the Git history from a chronological byproduct into a searchable, reliable tool.
Atomic commits instead of monster commits, the essentials at a glance
One commit, one purpose
git add -p and git commit -p stage and commit only the hunks that belong together.
Fixable after the fact
git reset -p and git rebase -i split existing commits apart, as long as they are still local.
Bisect, revert, cherry-pick
Atomic commits turn all three operations into mechanical, reliable steps instead of guesswork.
Team convention
Clear commit message formats and CI checks enforce atomic commits across the whole team.