Skip to main content

Cherry-pick & Interactive Rebase

Two Tools for Precise History Surgery

Merging and rebasing move whole branches. Sometimes you need finer control:

  • git cherry-pick — copy one specific commit onto another branch
  • git rebase -i — rewrite a series of commits: squash them together, reword messages, reorder, or drop them entirely

I use cherry-pick a few times a month (usually for hotfixes) and interactive rebase almost daily (to clean up my own branch before opening a PR).

Cherry-pick: Copy a Commit Across Branches

Cherry-pick takes the changes introduced by a commit and replays them as a new commit on your current branch. The original stays where it was; the copy gets a new hash.

The classic scenario: a hotfix

Rizwan is deep into a feature branch when Hafsa reports a production bug. He fixes it with one commit on his feature branch — but the fix needs to ship from main now, not whenever the feature merges.

# Find the hash of the fix commit
git log --oneline
# e4f5a6b Fix crash when invoice list is empty <- this one
# c3d4e5f Add invoice PDF export
# b2c3d4e Start invoice redesign

# Copy just the fix onto main
git switch main
git cherry-pick e4f5a6b
git push origin main

The fix now exists twice: once on feature and once (with a new hash) on main. When the feature branch eventually merges, Git is usually smart enough to notice the duplicate change and not conflict — but if it does conflict, the resolution is trivial because both sides are identical.

Useful cherry-pick variations

Pick several commits at once
git cherry-pick a1b2c3d e4f5a6b
Pick a range - everything after A up to and including B
git cherry-pick a1b2c3d..e4f5a6b
Record where the commit came from
git cherry-pick -x e4f5a6b

The -x flag appends (cherry picked from commit e4f5a6b) to the message — I always use it when back-porting fixes to release branches, so the trail is auditable later.

Apply the changes but do not commit yet
git cherry-pick --no-commit e4f5a6b

When cherry-pick conflicts

Like merge and rebase, cherry-pick pauses on conflicts. Fix the files, then:

git add .
git cherry-pick --continue # finish
# or
git cherry-pick --abort # give up, back to the starting state
Cherry-pick duplicates history — use it sparingly

Every cherry-pick creates a second copy of a change with a different hash. That's fine for hotfixes and back-ports, but if you find yourself cherry-picking constantly between long-lived branches, the branching strategy is the real problem.

Interactive Rebase: Rewrite Your Branch's Story

My branches while I'm working look like this:

9f8e7d6 fix lint
8e7d6c5 actually fix the test
7d6c5b4 fix test
6c5b4a3 wip
5b4a3f2 Add password reset endpoint

Nobody reviewing my PR needs to see that archaeology. Interactive rebase lets me rewrite those five commits into one or two meaningful ones before pushing.

Rewrite the last 5 commits
git rebase -i HEAD~5

Git opens your editor with a todo list — one line per commit, oldest first:

pick 5b4a3f2 Add password reset endpoint
pick 6c5b4a3 wip
pick 7d6c5b4 fix test
pick 8e7d6c5 actually fix the test
pick 9f8e7d6 fix lint

You edit the list, save, and close — Git then replays the commits according to your instructions.

The commands you'll actually use

CommandShortWhat it does
pickpKeep the commit as-is
rewordrKeep the commit, edit its message
squashsMelt into the previous commit, combine messages
fixupfMelt into the previous commit, discard this message
editePause here so you can amend the commit
dropdDelete the commit entirely

Example: squash the mess into one commit

pick 5b4a3f2 Add password reset endpoint
fixup 6c5b4a3 wip
fixup 7d6c5b4 fix test
fixup 8e7d6c5 actually fix the test
fixup 9f8e7d6 fix lint

Save and close. Result:

git log --oneline
# 3a2b1c0 Add password reset endpoint

Five commits became one, keeping only the first message. Had I used squash instead of fixup, Git would have opened the editor to let me combine all five messages into a new one.

Example: reorder commits

The todo list is replayed top to bottom, so reordering lines reorders history. Moving a commit line up or down in the file moves it in the branch. Just be aware that reordering commits that touch the same files can cause conflicts — Git will pause and let you resolve them, same as any rebase.

Fixup commits and --autosquash

The workflow I use most: a reviewer asks for a change to a specific commit in my PR. Instead of a address review comments commit, I make a fixup commit targeted at the original:

git add .
git commit --fixup 5b4a3f2

This creates a commit titled fixup! Add password reset endpoint. Later, one command folds every fixup into its target automatically:

git rebase -i --autosquash main

Git pre-arranges the todo list — fixups already moved under their targets and marked fixup. I just save and close.

Make autosquash the default
git config --global rebase.autosquash true

Then plain git rebase -i main handles fixup commits automatically.

Pushing After a Rewrite

Both cherry-pick cleanup and interactive rebase rewrite commits, so if the branch was already pushed, a normal git push gets rejected. Update the remote with:

git push --force-with-lease

Never a bare --force. The --force-with-lease version refuses to push if someone else added commits to the branch since you last fetched — it protects teammates' work from being silently wiped out.

The golden rule still applies

Only rewrite commits on your own feature branches. Never interactively rebase main, develop, or any branch other people build on. Rewriting shared history forces everyone else into painful recovery work.

Quick Reference

# Cherry-pick
git cherry-pick <hash> # copy a commit here
git cherry-pick -x <hash> # copy and record the source
git cherry-pick A..B # copy a range
git cherry-pick --continue # after resolving conflicts
git cherry-pick --abort # bail out

# Interactive rebase
git rebase -i HEAD~5 # rewrite the last 5 commits
git rebase -i main # rewrite everything since main
git commit --fixup <hash> # targeted fix for an earlier commit
git rebase -i --autosquash main # fold fixups automatically
git rebase --abort # bail out mid-rebase

# Afterwards, if the branch was already pushed
git push --force-with-lease