Keeping merge commits with --rebase-merges Jump to heading
By default, git rebase linearises history: merge commits in the range are dropped, and the commits from both sides are replayed one after another. For a simple feature branch that is what you want. For an integration branch that deliberately merges several topic branches โ a release candidate assembled from features, or a long-lived branch that periodically merges sub-branches โ flattening destroys the structure that made it understandable. --rebase-merges keeps the shape: it replays commits along each side, then recreates each merge on top. It also exposes that structure in the todo list, so you can edit it. This page explains the todo listโs new commands and the conflict behaviour that surprises people, within interactive rebase workflows.
When to use this approach Jump to heading
- The branch you need to rebase contains merge commits that you want to keep.
- You maintain an integration branch built by merging topic branches, and the base needs to move.
- You want to reorder or drop whole topic branches inside an integration branch.
- For a simple feature branch with no merges, a plain rebase is simpler; this page is not needed.
Step 1 โ See what a plain rebase would do Jump to heading
A plain rebase of a branch containing merges replays every non-merge commit linearly and discards the merges.
git log --oneline --graph main..integration
# * e71b0c2 Merge branch 'topic/search' into integration
# |\
# | * 4c2d9a1 Add search ranking
# | * 9a0e3f7 Add search index
# * | b80f115 Merge branch 'topic/export' into integration
# ... Step 2 โ Rebase with --rebase-merges and read the todo list Jump to heading
With -i, the todo list uses three extra commands: label names the current position, reset moves to a named position, and merge recreates a merge commit.
git rebase -i --rebase-merges main label onto
# Branch topic-export
reset onto
pick 1a7c3e0 Add CSV export
pick 6f20b9d Add export scheduling
label topic-export
# Branch topic-search
reset onto
pick 9a0e3f7 Add search index
pick 4c2d9a1 Add search ranking
label topic-search
reset onto
merge -C b80f115 topic-export # Merge branch 'topic/export' into integration
merge -C e71b0c2 topic-search # Merge branch 'topic/search' into integration Read it as a script: build each topic from onto, label its tip, then go back to onto and merge the topics in order. merge -C <sha> reuses the original mergeโs commit message.
Step 3 โ Expect merge conflicts to be resolved again Jump to heading
This is the part that surprises people. A recreated merge is a new merge: Git merges the rebased topic into the rebased integration branch, which may conflict where the original merge did, and the original resolution is not copied over automatically.
# If a recreated merge conflicts, resolve as for any merge, then continue
git status --short | grep '^UU'
git add -A && git rebase --continue Enable rerere before rebasing an integration branch. If you resolved the same conflict when making the original merge with rerere on, Git replays your resolution automatically; see automating repeated conflict resolution with rerere.
git config rerere.enabled true
git config rerere.autoUpdate true # stage reused resolutions automatically (review before continuing) Step 4 โ Edit structure through the todo list Jump to heading
Because the structure is explicit, editing it is editing text. Drop a topic from the integration branch by deleting its merge line (and optionally its block). Reorder merges by reordering the merge lines. Add a topic by inserting a merge of an existing branch.
reset onto
merge -C e71b0c2 topic-search # merge search first now
# merge -C b80f115 topic-export <- removed: export is not in this release
merge topic/billing-fixes # add a branch that was not merged before โ ๏ธ SAFETY WARNING: Rebasing an integration branch rewrites every commit on it, including merges. Anyone who based work on the old integration branch must move it, and the branch must be pushed with
git push --force-with-lease. If the rebuilt branch is wrong,git reset --hard ORIG_HEADrestores the previous version immediately after the rebase.
Step 5 โ Verify that the rebuilt branch matches intent Jump to heading
Compare the old and new versions of the branch. git range-diff shows each commitโs change before and after, including merges, which is the clearest way to confirm the rebase did not alter content unintentionally.
git range-diff ORIG_HEAD...HEAD
git diff ORIG_HEAD HEAD --stat # differences here should be only the new base's changes Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Can I make --rebase-merges the default? Jump to heading
Set rebase.rebaseMerges = true. Many teams prefer to leave it off and use it explicitly, because flattening is the right default for ordinary feature branches.
What does rebase-cousins mean? Jump to heading
By default, topics that branched from a commit outside the rebased range keep their original base. --rebase-merges=rebase-cousins moves those onto the new base too, which is usually what you want for an integration branch built from topics of mixed age.
Why not just merge main into the integration branch instead? Jump to heading
Often that is the better choice: it records the update without rewriting anything. Rebasing with merges is for when you need a clean rebuilt branch โ typically when preparing a release candidate or when the integration branch is private to one person.
Related Jump to heading
- Interactive Rebase Workflows โ the parent topic.
- Updating Stacked Branches with --update-refs โ moving several related branches in one rebase.
- Merge vs Rebase for Long-Running Branches โ whether to rebase at all.
- Reviewing a Force-Push with git range-diff โ the comparison used in step 5.