Squash, Bisect, and Cherry-Pick
Git gives us specialized tools for three situations that a normal branch merge does not solve by itself:
- Squash: turn several small draft commits into one focused change.
- Bisect: search commit history for the change that introduced a bug.
- Cherry-pick: replay one selected commit on another branch.
These operations all work with commits, but they solve different problems. Squash changes how work is recorded, bisect investigates history, and cherry-pick copies the effect of a selected change.
Part 1: Squash — many draft commits become one
Section titled “Part 1: Squash — many draft commits become one”While implementing a Blue Theme feature, the branch may accumulate temporary checkpoints:
F1: wip ↓F2: fix colors ↓F3: address reviewBefore sharing or merging the work, we may want one meaningful commit:
S1: feat: add blue themeThis is called squashing. The final project change can remain the same, but the way it is represented in history becomes simpler.
Squashing with interactive rebase
Section titled “Squashing with interactive rebase”To clean up commits that exist only on our local feature branch:
git switch feature-blue-themegit statusgit rebase -i mainGit opens a plan containing commits after the branch diverged from main:
pick a12bc34 wip: start blue themepick b23cd45 fix button colorspick c34de56 address review commentsKeep the first commit with pick and mark later commits with squash:
pick a12bc34 wip: start blue themesquash b23cd45 fix button colorssquash c34de56 address review commentsGit combines their changes and asks for the final commit message:
feat: add blue themesquash includes the selected commit messages while preparing the final
message. fixup combines the changes but normally discards the later draft
messages:
pick a12bc34 feat: add blue themefixup b23cd45 fix button colorsfixup c34de56 address review commentsWhy hashes change
Section titled “Why hashes change”A commit ID includes information such as its content tree, parent, metadata, and message. Combining commits creates a new commit object with a new parent relationship and therefore a new hash.
The old commits are replaced in this branch’s visible history:
Before: A──F1──F2──F3 feature-blue-themeAfter: A──S1 feature-blue-themeThis is a history rewrite, not merely a visual grouping.
Squash merge is different
Section titled “Squash merge is different”A receiving branch can take the final combined effect without preserving the feature’s individual commits:
git switch maingit merge --squash feature-blue-themegit statusgit commit -m "feat: add blue theme"git merge --squash prepares the combined change in the staging area. The
following git commit creates one normal commit on main.
Important consequences:
- The new commit normally has one parent, so it is not a merge commit.
- Git does not record the feature branch as a merged parent.
- The source branch’s original commits are not rewritten by this command.
- GitHub’s Squash and merge option follows the same general history shape: one resulting commit on the pull request’s target branch.
Use squash when temporary commits do not deserve a permanent place in the target history. Do not squash independent, well-designed commits merely to make the commit count smaller.
Part 2: git bisect — find the first bad commit
Section titled “Part 2: git bisect — find the first bad commit”Suppose production behavior is correct at C1 but broken at C5:
C1────C2────C3────C4────C5good badThe bug was introduced somewhere in this range. Testing every commit in order would be slow when the range contains hundreds of commits.
git bisect uses binary search. Git checks out a commit near the middle;
our test tells it whether that commit is good or bad. Each answer eliminates
roughly half the remaining range.
Start the search
Section titled “Start the search”Use known endpoints:
git bisect startgit bisect bad HEADgit bisect good C1The labels mean:
- Good: the bug is absent.
- Bad: the bug is present.
They do not mean that the entire commit is high or low quality. They describe only the behavior being investigated.
Git checks out a candidate commit. Test that exact state:
dotnet testThen report the result:
git bisect goodor:
git bisect badGit repeatedly chooses another candidate until it reports something similar to:
c34de56 is the first bad commitInspect the result rather than assuming it proves the exact faulty line:
git show c34de56Finally, leave bisect mode and return to the branch or commit where the search started:
git bisect resetSkip an untestable commit
Section titled “Skip an untestable commit”If a candidate cannot build for an unrelated reason, do not label it good or bad incorrectly:
git bisect skipGit tries another candidate. If too many nearby commits are skipped, it may report a small set of possible first-bad commits instead of one certain answer.
Automate bisect with a test
Section titled “Automate bisect with a test”If one command reliably detects the bug, Git can run the search automatically:
git bisect start HEAD C1git bisect run dotnet testgit bisect resetFor git bisect run, the test command’s exit status is the signal:
0means good.1through127, except125, mean bad.125means this commit cannot be tested and should be skipped.
Confirm that the chosen test fails only for the behavior being investigated. An unrelated flaky test can mislead the entire search.
Why focused commits help debugging
Section titled “Why focused commits help debugging”A bisect result such as fix: calculate invoice tax correctly gives us a
small, understandable change to inspect. A commit named updates that changes
400 unrelated files is a terrible debugging boundary even if bisect identifies
it correctly.
Small, focused commits improve review, rollback, and investigation—not only history appearance.
Part 3: git cherry-pick — copy one selected change
Section titled “Part 3: git cherry-pick — copy one selected change”Suppose hotfix contains an urgent correction H1, while main has already
advanced independently:
H1 hotfix /C1────C2 mainWe need the effect of H1, not the entire hotfix branch history. Stand on the
receiving branch and cherry-pick the selected commit:
git switch maingit statusgit cherry-pick H1The result is conceptually:
H1 hotfix /C1────C2────H1' mainH1' introduces the same change when possible, but it is a new commit
object:
- It has a different parent (
C2). - Its metadata can differ.
- It therefore has a different commit hash.
Cherry-pick does not move the original commit and does not merge the complete source branch.
When cherry-pick is useful
Section titled “When cherry-pick is useful”Good examples include:
- Copying one security fix to an older release branch.
- Applying the same production hotfix to several supported versions.
- Recovering one useful commit that was created on the wrong branch.
- Taking an urgent correction from a feature that is not ready to merge.
To record the origin in the new commit message, which is useful for public or shared backports:
git switch release/1.0git cherry-pick -x a12bc34-x adds a line identifying the original commit. It does not preserve the
original hash as the new commit’s identity.
Cherry-pick conflicts
Section titled “Cherry-pick conflicts”The destination may have changed since the selected commit was created. If Git cannot replay the patch safely, it pauses with a conflict.
Inspect and resolve each file, then stage the resolution:
git statusgit add PaymentService.csgit cherry-pick --continueTo cancel the complete operation and restore the pre-cherry-pick state:
git cherry-pick --abortDo not run a plain commit merely to escape the conflict. Finish with
--continue so Git completes the operation it started.
Choosing the right tool
Section titled “Choosing the right tool”| Goal | Tool | Main effect |
|---|---|---|
| Clean up local draft checkpoints | git rebase -i with squash or fixup |
Rewrites several commits as fewer new commits |
| Land one combined feature result | git merge --squash |
Stages one combined change for a normal target-side commit |
| Find when a regression began | git bisect |
Searches the commit range using good/bad tests |
| Copy one specific fix | git cherry-pick <commit> |
Replays the change as a new commit on the current branch |
| Integrate a complete branch history | git merge <branch> |
Connects or advances histories according to the commit graph |
Common mistakes
Section titled “Common mistakes”- Squashing shared commits and then force-pushing without coordinating with the team.
- Assuming interactive rebase squash and squash merge record the same history.
- Starting bisect without one reliable known-good and one known-bad point.
- Marking an untestable bisect candidate as good or bad instead of skipping it.
- Forgetting
git bisect resetand wondering whyHEADis detached. - Assuming cherry-pick preserves the original commit hash.
- Standing on the source branch instead of the receiving branch before cherry-picking.
- Cherry-picking an entire feature commit by commit when a merge better expresses the relationship.
- Resolving a conflict without running the relevant build and tests.
Summary
Section titled “Summary”Squash → clean up how draft work is recordedBisect → search history for the change that introduced a bugCherry-pick → replay one selected change on the current branchSquashing with interactive rebase rewrites commit history, while squash merge creates one target-side commit without a true merge relationship. Bisect needs a trustworthy test and known good/bad boundaries. Cherry-pick copies a change but creates a new commit with a different hash.
Knowledge check
Section titled “Knowledge check”- Why does an interactive rebase squash create a new commit hash?
- How does
git merge --squashdiffer from a normal merge commit? - What two known points does
git bisectneed before it can search? - What should you run when a bisect candidate cannot be tested?
- Why is a cherry-picked commit not the same commit object as its source?
- When should you prefer a complete merge over cherry-pick?