Skip to content

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 review

Before sharing or merging the work, we may want one meaningful commit:

S1: feat: add blue theme

This is called squashing. The final project change can remain the same, but the way it is represented in history becomes simpler.

To clean up commits that exist only on our local feature branch:

Terminal window
git switch feature-blue-theme
git status
git rebase -i main

Git opens a plan containing commits after the branch diverged from main:

pick a12bc34 wip: start blue theme
pick b23cd45 fix button colors
pick c34de56 address review comments

Keep the first commit with pick and mark later commits with squash:

pick a12bc34 wip: start blue theme
squash b23cd45 fix button colors
squash c34de56 address review comments

Git combines their changes and asks for the final commit message:

feat: add blue theme

squash 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 theme
fixup b23cd45 fix button colors
fixup c34de56 address review comments

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-theme
After: A──S1 feature-blue-theme

This is a history rewrite, not merely a visual grouping.

A receiving branch can take the final combined effect without preserving the feature’s individual commits:

Terminal window
git switch main
git merge --squash feature-blue-theme
git status
git 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────C5
good bad

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

Use known endpoints:

Terminal window
git bisect start
git bisect bad HEAD
git bisect good C1

The 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:

Terminal window
dotnet test

Then report the result:

Terminal window
git bisect good

or:

Terminal window
git bisect bad

Git repeatedly chooses another candidate until it reports something similar to:

c34de56 is the first bad commit

Inspect the result rather than assuming it proves the exact faulty line:

Terminal window
git show c34de56

Finally, leave bisect mode and return to the branch or commit where the search started:

Terminal window
git bisect reset

If a candidate cannot build for an unrelated reason, do not label it good or bad incorrectly:

Terminal window
git bisect skip

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

If one command reliably detects the bug, Git can run the search automatically:

Terminal window
git bisect start HEAD C1
git bisect run dotnet test
git bisect reset

For git bisect run, the test command’s exit status is the signal:

  • 0 means good.
  • 1 through 127, except 125, mean bad.
  • 125 means 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.

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 main

We need the effect of H1, not the entire hotfix branch history. Stand on the receiving branch and cherry-pick the selected commit:

Terminal window
git switch main
git status
git cherry-pick H1

The result is conceptually:

H1 hotfix
/
C1────C2────H1' main

H1' 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.

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:

Terminal window
git switch release/1.0
git 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.

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:

Terminal window
git status
git add PaymentService.cs
git cherry-pick --continue

To cancel the complete operation and restore the pre-cherry-pick state:

Terminal window
git cherry-pick --abort

Do not run a plain commit merely to escape the conflict. Finish with --continue so Git completes the operation it started.

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
  • 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 reset and wondering why HEAD is 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.
Squash → clean up how draft work is recorded
Bisect → search history for the change that introduced a bug
Cherry-pick → replay one selected change on the current branch

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

  1. Why does an interactive rebase squash create a new commit hash?
  2. How does git merge --squash differ from a normal merge commit?
  3. What two known points does git bisect need before it can search?
  4. What should you run when a bisect candidate cannot be tested?
  5. Why is a cherry-picked commit not the same commit object as its source?
  6. When should you prefer a complete merge over cherry-pick?