Rebase & Cherry-pick — Rewriting History
Introduction: The Art of History
Every commit you make is a permanent record — but "permanent" in Git means permanently stored in the object database, not permanently fixed in the branch graph. Git gives you powerful tools to reshape how commits are arranged, combined, and described.
This sounds alarming when you first hear it. "Rewriting history" feels dangerous. But used correctly — specifically, on private local branches before sharing them — rebase and interactive rebase are essential tools for producing a commit history that is clear, readable, and genuinely useful as documentation.
This lesson covers two powerful operations:
- Rebase: replaying a series of commits onto a different base
- Cherry-pick: applying a specific commit from one branch onto another
Both operations create new commits — the originals still exist in the object database, but the branch pointer now points to the new versions.
git rebase — Replaying Commits on a New Base
The Core Concept
Imagine you have this history:
feature was created from B, and you have made two commits: D and E. Meanwhile, main has advanced to C.
A merge would create a merge commit M that has two parents (C and E):
A rebase replays your feature branch commits (D and E) on top of the new base (C), producing new commits D' and E':
Now the feature branch has a linear history starting from C. When you merge this into main, it will be a fast-forward — no merge commit needed.
The result is a clean, linear history with no merge commits.
Performing a Rebase
Git will:
- Find the common ancestor of
feature/add-priorityandmain(the commit where they diverged) - Temporarily set aside the commits on
feature/add-prioritysince that ancestor - Fast-forward the branch to
main's tip - Replay each set-aside commit one at a time on top of the new base
If no conflicts occur, the rebase completes silently. The feature branch now sits cleanly on top of main.
Handling Rebase Conflicts
Just like merging, rebasing can encounter conflicts if both branches changed the same lines. When a conflict occurs during rebase:
The workflow:
Note: unlike a merge, where you resolve all conflicts at once (since a merge is a single commit), a rebase may present conflicts commit-by-commit — each commit being replayed can introduce its own conflict.
Interactive Rebase: Rewriting Your Commits
Interactive rebase (git rebase -i) is one of Git's most powerful features. It lets you:
- Reorder commits
- Edit commit messages (reword)
- Squash multiple commits into one
- Split one commit into multiple
- Drop (delete) commits entirely
- Execute shell commands between commits (exec)
The typical use case: you have been working on a feature and made a messy series of commits:
Before merging to main, you want to clean this up. Interactive rebase lets you.
Starting Interactive Rebase
Git opens your editor with a list of commits in chronological order (oldest first — opposite of git log):
Each line starts with an action keyword and a commit hash and message. You change the action keywords to control what happens.
Interactive Rebase Action Keywords
pick (or p) — Use this commit as-is (default). No changes.
reword (or r) — Use this commit, but open the editor to edit the commit message.
edit (or e) — Pause at this commit to allow amending (add files, change content, etc.). Use git rebase --continue to proceed.
squash (or s) — Merge this commit into the previous commit. Opens editor to combine the commit messages.
fixup (or f) — Like squash, but discard this commit's message (keep only the previous commit's message).
drop (or d) — Delete this commit entirely. The changes disappear.
exec (or x) — Run a shell command after this commit.
break — Pause the rebase here (like edit but without amending).
Reorder — Simply move lines up or down to reorder commits.
Example: Squashing Commits
Let's clean up the messy feature history:
Save and close the editor. For the squash commits, Git opens the editor to let you write a combined message:
Edit this to a clean final message:
After saving, the result is a single clean commit representing all that work.
Example: Rewording a Commit Message
When you save, Git pauses at that commit and opens your editor with the current message. Edit it and save.
Example: Dropping a Commit
The changes from those commits disappear from the branch. Be careful: if subsequent commits depend on the dropped commit, you may get conflicts or broken code.
Example: Splitting a Commit
If a commit does too much and you want to split it:
When Git pauses at that commit:
The Golden Rule of Rebasing
Never rebase commits that have been pushed to a shared remote branch.
This is not a style preference — it is a hard rule with technical consequences.
When you rebase, you create new commit objects (with different hashes) to replace the original ones. If you have already pushed those originals to origin/main and someone else has pulled them, their local main contains the original commits. When you force-push the rebased commits, their history diverges from yours. The next time they pull, Git reports a conflict that looks deeply confusing and requires manual repair.
Safe contexts for rebase:
- Local feature branches not yet pushed
- Feature branches on a remote that only you are working on (acceptable to force-push)
- Running
git pull --rebaseto rebase your local changes onto fetched remote changes (this is always on commits no one else has)
Unsafe contexts:
main,master,develop, or any branch shared with teammates- Any branch that others have pulled
git cherry-pick — Applying Specific Commits
git cherry-pick applies the changes from one or more specific commits to your current branch, creating new commits with the same changes but different hashes.
Use cases:
- A bug fix on a feature branch needs to also be applied to
mainbefore the feature is ready to merge - You want one specific commit from a colleague's branch without merging the whole branch
- You accidentally committed to the wrong branch and need to move that commit
Output:
A new commit f2e3d4c appears on main with the same changes as 7a8b9c0 (but a different hash, since the parent is different).
Cherry-picking Multiple Commits
Cherry-pick Conflicts
Cherry-pick can conflict just like merge and rebase. Resolve conflicts the same way:
Cherry-pick Caveats
Cherry-pick duplicates commits — the same change exists in two places in the history with different hashes. This can complicate future merges: if you later merge the original branch, Git may see the cherry-picked commit as a duplicate change and handle it correctly, or it may create subtle conflicts. It depends on how closely the histories have evolved since the cherry-pick.
Use cherry-pick deliberately:
- For applying critical fixes across branches (hotfix → main and release branches)
- For recovering accidentally committed-to-wrong-branch situations
- Avoid it as a long-term substitute for proper branching strategy
Rebase vs Merge: When to Use Which
This is a genuine design choice and teams have strong opinions. Here is a principled framework:
Use Merge When:
- The branch history itself is meaningful (e.g., "team A worked on this feature over 3 weeks")
- Multiple developers contributed to the branch and you want to preserve authorship context
- You are merging a long-lived release branch or develop branch
- You prefer the principle of "never rewrite shared history" as an absolute rule
Use Rebase When:
- You want a clean, linear history on
mainthat is easy togit bisectandgit log - You are updating your local feature branch with the latest
main(before merging or PR review) - You want to clean up a messy sequence of "WIP" and "fix typo" commits before sharing
- Your team has agreed on a rebase-based workflow
Use Interactive Rebase When:
- You have made a series of exploratory commits and want to present them as a clean sequence
- You want to squash fixup commits into their parent commits before merging
- You need to reorder commits to tell a more logical story
- You want to split an overly large commit into focused pieces
A Practical Policy
Many teams adopt this policy:
- Rebase feature branches onto
mainbefore opening a Pull Request (to keep the branch current and produce a clean base for review) - Squash the entire feature branch to a single commit (or a small handful) when merging to
main(GitHub/GitLab have "squash merge" options in their PR UIs) - Merge release branches and hotfixes with
--no-ffto preserve their existence in history
Practical Exercises
Exercise 1: Rebase a Feature Branch
Exercise 2: Interactive Rebase — Squash and Reword
Exercise 3: Cherry-pick a Hotfix
Challenge: Full Cleanup Workflow
- Create a feature branch with at least 8 commits: some are "WIP", some fix typos in earlier commits, some are real features
- Use
git rebase -ito squash the WIP and typo commits into their related feature commits - Reword the remaining commit messages to follow good conventions
- Rebase the cleaned branch onto main
- Merge to main with
--no-ff - Examine the final git log and confirm it reads as clear, professional history
Summary
git rebase <base>replays your branch's commits on top of the specified base, producing a linear history without merge commits. The original commits are replaced with new ones (different hashes, same changes).- Interactive rebase (
git rebase -i) lets you squash, fixup, reword, reorder, drop, or edit commits. It is the tool for cleaning up messy work before sharing. - When rebasing encounters conflicts, resolve them file by file, stage the resolved files, and run
git rebase --continue. Usegit rebase --abortto cancel. - The golden rule: never rebase commits that have been pushed to a shared remote branch. Rebasing rewrites history; pushing rewritten history onto a shared branch causes painful conflicts for everyone.
git cherry-pick <hash>applies the changes from a specific commit to your current branch, creating a new commit with the same diff but a different hash and parent.- Use rebase to update feature branches and produce clean history; use merge to integrate long-lived branches; use cherry-pick for targeted application of specific commits.
The next lesson covers tagging and releases — how to mark specific commits as releases, use semantic versioning, and build a release workflow around Git tags.