Git Merge, Rebase & Squash — Part 4: Interactive Rebase and Advanced Rebase

On this page
Abstract
Preparing a clean commit history before integrating feature branches is essential for maintaining a comprehensible project repository. However, raw development histories are often messy, requiring developers to rewrite and reorganize the commit graph. Interactive rebase provides granular control over this process through commands like pick, reword, edit, squash, fixup, and drop, allowing developers to meticulously tidy individual commits. Workflows can be streamlined using autosquash for automatic fixups. Developers can isolate specific commit sequences using rebase --onto to detach stacked branches, or preserve the original merge topology by applying the --rebase-merges flag. Furthermore, combining a rebase with a fast-forward merge achieves a linear history without sacrificing individual commits, whereas executing an interactive rebase prior to a merge facilitates a controlled manual squash. Because these operations fundamentally rewrite history, they require careful execution on private branches alongside --force-with-lease when updating remote repositories. This article outlines these advanced rebase techniques to equip developers with precise commands for curating repository history.

Part 4 builds on the basic rebase from Part 3 and moves into advanced territory. We start with interactive rebase, which lets you tidy commits one at a time (reword, combine, delete, reorder), then cover autosquash, rebase --onto for moving only part of a branch onto a new base, preserving merge topology with --rebase-merges, and finish with two patterns you will use constantly: rebase + fast-forward merge and rebase + squash.
By the end you will be able to clean up a feature branch's history before opening a PR, and understand how each advanced rebase command rewrites the commit graph. Everything in this part rewrites history, so do it on a private branch and use git push --force-with-lease when you push over what was there before.
Interactive rebase: tidy commits before opening a PR
Interactive rebase is used to organise commits before you open or merge a PR.
git rebase -i HEAD~4
Git opens an editable todo list showing your last four commits:
pick a111111 Add login form
pick b222222 Fix typo
pick c333333 Fix validation
pick d444444 Change button text
You change the command in front of each commit to tell Git what to do with it. There are six core commands to know.
pick
Keep the commit exactly as it is; change nothing.
pick a111111 Add login form
reword
Keep the changes, but edit only the commit message.
reword a111111 Add login form
edit
Stop at that commit so you can change its contents (edit the actual files).
edit a111111 Add login form
When the rebase pauses at that commit, edit the files and then run:
git add .
git commit --amend
git rebase --continue
squash
Merge this commit into the one before it, and open an editor to combine the two commit messages.
pick a111111 Add login form
squash b222222 Fix typo
squash c333333 Fix validation
The result is a single commit.
fixup
Like squash, but discard the commit message of the commit being folded in.
pick a111111 Add login form
fixup b222222 Fix typo
fixup c333333 Fix validation
The result keeps only the message of a111111.
drop
Remove the commit from history entirely — both its changes and its message.
drop b222222 Debug logging
The six interactive-rebase commands at a glance
| Command | Keeps the changes? | Keeps the message? | Typical use |
|---|---|---|---|
pick | Yes | Yes | Keep the commit as-is |
reword | Yes | Edit it | Fix only the commit message |
edit | Yes (pauses to amend) | Yes | Change the file contents of that commit |
squash | Yes (folded into previous) | Combines both | Merge into the previous commit, keeping both messages |
fixup | Yes (folded into previous) | Discards its own | Merge into the previous commit, ignoring its message |
drop | No | No | Remove the commit completely |
Reordering commits
Besides changing commands, you can also reorder the lines in the todo list.
Original:
pick a111111 Add UI
pick b222222 Add tests
pick c333333 Add API
Changed to:
pick c333333 Add API
pick a111111 Add UI
pick b222222 Add tests
Git will replay the commits in the new order you arranged. Conflicts can arise if the commits depend on one another (for example, a later commit edits a file an earlier commit created).
Autosquash: automatic fixups
If you already know at commit time that a new commit should be folded into a specific older commit, create a fixup commit tied to that target commit up front.
git commit --fixup=<commit-hash>
Example:
git commit --fixup=a111111
Then run the rebase with --autosquash:
git rebase -i --autosquash HEAD~5
Git moves the fixup commit to sit right after its target commit and automatically sets that line's command to fixup, so you never have to move lines in the todo list yourself.
A real-world workflow:
git add .
git commit --fixup=a111111
git rebase -i --autosquash main
Rebase --onto: move only part of a branch
Use this when you want to move only part of a branch onto a new base, not the whole line.
Suppose you have this structure, where feature-b was branched off feature-a in turn:
Now you want to detach feature-b from feature-a and place it directly after main.
git rebase --onto main feature-a feature-b
As a result, commits G and H are re-created as G' and H' after main, while feature-a stays where it was:
The general form of the command:
git rebase --onto <new-base> <old-base> <branch>
Which means:
Take the commits that lie after
old-baseonbranchand place them ontonew-base.
In the example above, new-base is main, old-base is feature-a (the point where feature-b split off), and branch is feature-b. So the commits after feature-a (namely G and H) are lifted onto main.
Rebasing with merge commits: --rebase-merges
A normal rebase can flatten an existing merge structure into a straight line, because Git only replays ordinary commits and drops the merge commits.
If you want to try to preserve the original merge topology:
git rebase --rebase-merges main
Or the short form:
git rebase -r main
This suits a branch that already has sub-branches or internal merge commits, and where you want the merge structure to keep its shape after the rebase.
Rebase + fast-forward merge
This is the pattern that combines rebase with a fast-forward merge to get a linear history while keeping every one of the feature's individual commits.
Start with a feature branch split off at C while main has moved ahead to G:
D---E
/
A---B---C---F---G main
Do it in two steps — rebase the feature up to date with main first, then fast-forward:
git switch feature
git rebase main
git switch main
git merge --ff-only feature
The result is linear, with D and E re-created as D' and E' after G:
This gives you three things at once:
- All of the feature's individual commits are preserved.
- History is linear and easy to read.
- No extra merge commit is added.
Rebase + squash
This pattern tidies the history inside the branch with an interactive rebase first, then merges it in. Unlike a squash merge, you control how the commits are combined on the branch itself.
Tidy the branch first with an interactive rebase onto main:
git switch feature/login
git rebase -i main
Fold the small commits into the main one with fixup:
pick Add login feature
fixup Fix typo
fixup Fix CSS
fixup Fix tests
Then fast-forward merge into main:
git switch main
git merge --ff-only feature/login
On main you are left with a single commit, S:
The outcome resembles a squash merge, but the difference is that the branch's history was cleaned up first and then the pointer was fast-forwarded in — rather than Git collapsing the whole branch into one commit at merge time.
Key takeaways
- Interactive rebase (
git rebase -i) lets you organise commits one at a time through a todo list using six commands —pick,reword,edit,squash,fixup,drop— and you can reorder commits by moving the lines. squashcombines both messages, whilefixupdiscards the folded commit's message; pair--fixup=<hash>with--autosquashto have Git arrange the fixup lines for you.git rebase --onto <new-base> <old-base> <branch>moves only the commits after old-base onto new-base — ideal for detaching stacked branches from one another.--rebase-merges(-r) tries to preserve the original merge topology instead of flattening it into a line.- Rebase + ff merge gives a linear history while keeping individual commits; rebase + squash reduces the branch to one commit before merging. Both rewrite history, so use them on private branches with
--force-with-lease.