Merge vs Rebase
When two lines of work meet, Git has to combine them. There are two main tools for this, merge and rebase, and each leaves a different history behind. This page explains how both work, how to clean up your commits before a pull request, one rule you must not break, what squash merging does, and how to resolve conflicts calmly.
Merge vs rebase
merge (fast-forward vs merge commit) compared with rebase.
Both commands do the same job: bring the changes of one branch into another. The difference is what history they leave behind.
Merge
git merge joins two branches. What it creates depends on the situation.
1. Fast-forward. If the target branch has no new commits since you branched off, Git has nothing to combine. It just moves the branch pointer forward. No new commit is created.
1Before:2main: A --- B3 \4feature: C --- D5 6After: git switch main, then git merge feature7main: A --- B --- C --- D (main simply moved forward to D)2. Merge commit. If both branches have new commits, their histories have diverged. Git creates a merge commit with two parents, one from each side.
1Before:2main: A --- B --- E3 \4feature: C --- D5 6After: git switch main, then git merge feature7main: A --- B --- E ------- M M has two parents: E and D8 \ /9feature: C --- D ----A merge does not change any existing commit. The history stays exactly as it happened, including the branch shape.
You can control which one happens:
git merge feature # fast-forward if possible, otherwise a merge commitgit merge --no-ff feature # always create a merge commit (keeps a record that the branch existed)git merge --ff-only feature # only fast-forward, and refuse if the branches diverged
Rebase
git rebase moves your branch so it starts from a new base. It does this by replaying your commits one by one on top of the new base.
Steps Git follows:
- Find the common ancestor of the two branches.
- Save your commits as patches.
- Reset your branch to the new base.
- Replay each saved commit on top, one at a time.
1Before:2main: A --- B --- E3 \4feature: C --- D5 6After: git switch feature, then git rebase main7main: A --- B --- E8 \9feature: C' --- D' (new commits, new hashes)C' and D' have the same changes as C and D, but they are new commits with new hashes, because their parent changed. The old C and D are not deleted right away. They are just no longer on any branch.
Now feature is a straight line on top of main. A final merge into main is a fast-forward, with no merge commit.
1After: git switch main, then git merge feature2main: A --- B --- E --- C' --- D' (one straight line)git switch featuregit rebase main # replay my commits on top of maingit switch maingit merge --ff-only feature # a clean fast-forward, no merge commitgit pull --rebase # fetch, then rebase my local commits on top of the remote
Comparing Them
| Merge | Rebase | |
|---|---|---|
| History shape | Branches and merge commits (a graph) | One straight line |
| Existing commits | Unchanged | Rewritten (new hashes) |
| Conflicts | Resolved once | May be resolved once per replayed commit |
| Safe on shared branches? | Yes | No (see the golden rule) |
| Keeps the real timeline? | Yes | No, it looks as if the work happened in order |
| Easy to undo? | git revert -m 1 <merge> | Harder, use git reflog |
| Good for | Combining shared work | Cleaning up your own work |
Which one should I use?
- To update your own feature branch with the latest
main, rebase gives a clean line. Merge also works and is safer if you are unsure. - To bring a finished feature into
main, use a merge (or a squash merge, covered below). - If the branch is shared with others, use merge.
Rule of thumb: rebase to tidy your own work, merge to combine shared work.
Interactive rebase
git rebase -i to squash, reorder, and reword commits before opening a PR.
While you work, your commits are often messy: wip, fix typo, oops. Interactive rebase lets you edit your commit history before anyone else sees it, so the pull request reads like a clean story.
Start It
You choose how far back to go. Everything after the starting point is open for editing.
git rebase -i HEAD~4 # edit the last 4 commitsgit rebase -i origin/main # edit every commit on my branch that is not on main yet
Git opens a todo list in your editor. Each line is one commit, and the oldest commit is at the top. This is the opposite of git log.
1pick 1a2b3c4 Add login form2pick 2b3c4d5 wip3pick 3c4d5e6 Fix typo in login form4pick 4d5e6f7 add validatonYou change the word at the start of each line, save, and close the editor. Git then replays the commits following your instructions.
The Commands
| Command | Short | What it does |
|---|---|---|
pick | p | Keep the commit as it is |
reword | r | Keep the changes, edit the message |
edit | e | Pause at this commit so you can change it, or split it |
squash | s | Merge into the commit above, and combine both messages |
fixup | f | Merge into the commit above, and throw away this message |
drop | d | Delete the commit completely |
To reorder commits, move the lines up or down. Each command also works on its own, for example deleting a line is the same as drop.
Example: Clean Up Before a PR
Start with this todo list:
1pick 1a2b3c4 Add login form2pick 2b3c4d5 wip3pick 3c4d5e6 Fix typo in login form4pick 4d5e6f7 add validatonChange it to:
1pick 1a2b3c4 Add login form2fixup 2b3c4d5 wip3fixup 3c4d5e6 Fix typo in login form4reword 4d5e6f7 add validatonThe result is two clean commits:
1Before (4 commits) After (2 commits)2 3Add login form Add login form (wip and the typo fix folded in)4wip Add validation (message fixed with reword)5Fix typo in login form6add validatonFixup Commits Made Easy
You often know right away that a new commit belongs to an older one. Mark it while you commit, and let Git arrange the todo list for you.
git commit --fixup 1a2b3c4 # make a commit that belongs to 1a2b3c4git rebase -i --autosquash origin/main # Git moves it under 1a2b3c4 and marks it fixup for yougit config --global rebase.autoSquash true # make --autosquash the default
When Something Goes Wrong
git rebase --continue # after fixing a conflict or finishing an edit, go ongit rebase --abort # stop and return to exactly how things were beforegit reflog # find the old state if you need it latergit reset --hard ORIG_HEAD # go back to where the branch was before the rebase
--abort is your safety button while the rebase is running. After it has finished, ORIG_HEAD or git reflog can still take you back.
After the Rebase
The commits are rewritten, so their hashes are new. If you already pushed the branch, a normal push is rejected. Push with:
git push --force-with-lease # only overwrites if nobody else added commits to this branch
This is safe only on a branch that you alone use. That brings us to the most important rule.
The golden rule
Never rebase shared or public branches.
Rebase rewrites history. It creates new commits and abandons the old ones. That is harmless when only you have the old commits. It is a problem when someone else already built on them.
What Goes Wrong
Say you and a teammate both have commits B, C, D from a shared branch. You rebase them and force push. Your teammate adds a commit E on top of the old D.
1Shared branch before: A --- B --- C --- D2 3You rebase onto a new base X and force push:4 A --- X --- B' --- C' --- D'5 6Your teammate still has: A --- B --- C --- D --- ENow there are two versions of the same work: B C D and B' C' D'. When your teammate pulls, Git sees two different histories. The result is:
- Duplicate commits in the history.
- Strange conflicts on code that nobody changed.
- A lot of confusion, and a real risk of losing work during the cleanup.
Where Is It Safe?
| Branch | Rebase it? |
|---|---|
| Your local commits that you have not pushed | Yes, freely |
| Your own feature branch that only you use | Yes, then --force-with-lease |
| A feature branch shared with teammates | No, unless everyone agrees and knows |
main, develop, release/*, or any public branch | Never |
Never rebase commits that exist outside your own repository and that other people may have based work on.
Protect Your Branches
Do not rely on memory. Branch protection rules on your Git host can block force pushes to main and other shared branches, so the mistake cannot happen.
If You Already Did It
Do not panic. Git remembers.
git reflog # list where HEAD has been, with the old hashesgit reset --hard ORIG_HEAD # right after a rebase: go back to the state before itgit reset --hard 4d5e6f7 # or go back to any hash found in the reflog
If it was already pushed, tell your team and agree on a plan. Usually everyone must move their work onto the new history with git rebase --onto, or create fresh branches from the new history.
Squash merge on PRs
What squash merging does to history.
A squash merge takes all commits of a pull request and turns them into one new commit on the target branch. The branch's own commits never appear in main.
1Before:2main: A --- B3 \4feature: C --- D --- E (wip, fix typo, address review)5 6After a squash merge:7main: A --- B --- S S has the changes of C + D + E, one parent: BThe new commit S has one parent (B), so there is no link to the feature branch. C, D, and E stay on the old branch, and are gone after you delete it.
# What the "Squash and merge" button does, done by hand:git switch maingit merge --squash feature # stages all the changes, but does NOT commitgit commit -m "Add login page (#42)"git branch -D feature # -D is needed, see the gotcha below
Why Teams Like It
- One commit per PR.
git logonmainreads like a list of features. - No messy commits such as
wiporfix typoin the main history. - Easy to revert. To undo a feature, revert one commit.
- Easy to read and bisect. Every commit on
mainis a complete, reviewed change. - Contributors do not need tidy commits. The team does not have to teach rebase to everyone.
The Downsides
- Detail is lost. Individual commits, and their separate messages, are gone from
main. A large PR becomes one huge commit, which is hard to understand later. git blameis less exact. A line shows the big squashed commit, not the small step where it was written.- The branch looks unmerged to Git. Git does not know
C D Ewere merged, sogit branch -drefuses. You must use-D. - Do not keep working on the same branch. After the squash, your branch has commits that Git thinks are new. If you continue and open another PR, you get false conflicts. Delete the branch and start a fresh one from
main. - Stacked branches need care. If branch
Bwas created from branchA, andAis squash merged,Bstill contains the oldAcommits. Fix it with:
git rebase --onto main A B # move only B's own commits onto main
Comparing the Three PR Merge Options
1Start: main: A --- B feature: C --- D --- E (branched from B)2 3Merge commit: A --- B ------------ M history keeps C D E and the branch shape4 \ /5 C --- D --- E6 7Squash and merge: A --- B --- S one new commit, C D E are not in main8 9Rebase and merge: A --- B --- C' --- D' --- E' each commit replayed, no merge commit| Option | History on main | Individual commits kept? | Good when |
|---|---|---|---|
| Merge commit | Graph with merge commits | Yes | You want the full real history |
| Squash and merge | One commit per PR | No | PR commits are messy, small PRs |
| Rebase and merge | Straight line | Yes | Commits are already clean |
Tip: Squash merging works best with small, focused pull requests. Then "one PR = one commit" makes sense. If a PR is huge, ask for it to be split first.
Resolving merge conflicts
Resolve conflicts confidently, and use git rerere for repeated conflicts.
A conflict happens when Git cannot decide how to combine two changes. This is normal, and it is not a sign that you did something wrong.
It usually happens when:
- Both branches changed the same lines of the same file.
- One branch edited a file that the other branch deleted or renamed.
If the changes touch different parts of a file, Git combines them by itself. It stops only when it needs your decision.
Step 1: See What Is Conflicting
git merge feature# Auto-merging app.js# CONFLICT (content): Merge conflict in app.js# Automatic merge failed; fix conflicts and then commit the result.git status # lists "both modified" files under "Unmerged paths"git diff --name-only --diff-filter=U # just the names of the conflicting files
Step 2: Read the Conflict Markers
Git edits the file and marks the conflict:
1function greet(name) {2<<<<<<< HEAD3 return "Hello, " + name + "!";4=======5 return "Welcome, " + name;6>>>>>>> feature/login7}| Part | Meaning |
|---|---|
<<<<<<< HEAD | Start. Below it is your side (the branch you are on) |
======= | The divider between the two versions |
>>>>>>> feature/login | End. Above it is the other side (the incoming branch) |
Step 3: Decide and Edit
Edit the file until it is what the code should really be, and delete all three marker lines. You can:
- Keep your side.
- Keep their side.
- Combine both, or write something new.
1function greet(name) {2 return "Welcome, " + name + "!";3}Do not pick a side without thinking. Ask: what did each change try to do, and what should the final code do? If you do not know, ask the author of the other change.
Step 4: Mark It Resolved and Finish
git add app.js # staging the file tells Git: this conflict is resolvedgit status # check that no unmerged paths are leftgit commit # finish a merge (or: git merge --continue)# or, during a rebase:git rebase --continue # go on to the next commit
Before you finish, run the tests and the build. A file with no markers can still be broken, because both sides together may not make sense.
If You Want to Stop
git merge --abort # cancel the merge, back to before it startedgit rebase --abort # cancel the rebase, back to before it started
Useful Helpers
git checkout --ours -- app.js # take the whole file from my sidegit checkout --theirs -- app.js # take the whole file from the other sidegit add app.js # then mark it resolvedgit mergetool # open a visual three-way merge toolgit config --global merge.conflictStyle zdiff3 # also show the original (Git 2.35+, else use diff3)
With zdiff3 or diff3, the conflict shows a third part: the original code before both changes. This makes it much easier to see what each side changed.
1<<<<<<< HEAD2 return "Hello, " + name + "!";3||||||| base4 return "Hello, " + name;5=======6 return "Welcome, " + name;7>>>>>>> feature/loginThe Ours and Theirs Trap
In a merge, "ours" is your branch and "theirs" is the branch you are merging in. That matches what you expect.
In a rebase, it is swapped. Git replays your commits on top of the other branch, so:
| Command | "ours" means | "theirs" means |
|---|---|---|
git merge | My branch | The branch being merged in |
git rebase | The branch I am rebasing onto (upstream) | My own commits being replayed |
So during a rebase, git checkout --theirs file gives you your own work. This catches many people by surprise.
Avoid Conflicts in the First Place
- Keep branches short-lived and PRs small.
- Pull or rebase often, so each update is small.
- Talk to teammates who work in the same files.
- Do not mix big formatting changes with real changes. Auto-format in its own commit.
- Squash your commits first before a long rebase. Rebase replays commit by commit, so you may fix the same conflict many times. With one commit, you fix it once.
git rerere: Resolve It Once
rerere stands for reuse recorded resolution. When it is on, Git remembers how you resolved a conflict. The next time the same conflict appears, Git applies your old fix automatically.
git config --global rerere.enabled true # turn it on for all repositories
How it works:
- A conflict happens. Git records the conflict (the "before").
- You resolve it and commit. Git records your resolution (the "after").
- Later, the same conflict shows up. Git fixes it for you and prints
Resolved 'app.js' using previous resolution.
1First time: conflict --> you fix it --> Git remembers the fix2Next time: same conflict --> Git applies the fix --> you only check itWhen it helps:
- A long-running branch that you rebase again and again onto a moving
main. - You merge, find a problem, undo the merge, and try again. The same conflicts return.
- You test a merge first, throw it away, and do the real merge later.
- You rebase a stack of commits and meet the same conflict at many steps.
Important details:
- Git applies the fix but does not stage it. You still need to
git addthe file and continue. Always check the result. - The memory is stored inside this repository, in
.git/rr-cache, and is not shared with teammates. - It matches the exact same conflict. A different conflict is not resolved for you.
git rerere status # files whose conflicts Git is tracking right nowgit rerere diff # show the resolution Git recorded or appliedgit rerere forget app.js # delete a saved resolution that was wrong
Summary
- Merge keeps history as it happened, using a fast-forward (no new commit) or a merge commit (two parents). Rebase replays your commits on a new base, giving a straight line with new commits.
- Use
git rebase -ito squash, fixup, reword, reorder, and drop commits before opening a PR. The oldest commit is at the top, and--abortis your safety button. - The golden rule: never rebase shared or public branches. Rebase rewrites history, so others' work no longer matches. On your own branch, use
--force-with-lease. - Squash merge turns a whole PR into one commit with one parent. It gives a clean history, but loses the individual commits. Delete the branch afterward.
- Conflicts are normal. Find them with
git status, read the markers, edit the file,git add, then continue the merge or rebase. Test before you finish. - During a rebase, "ours" and "theirs" are swapped.
git rerereremembers your resolutions and reuses them when the same conflict comes back.
Interview Answer: Merge vs Rebase
Both bring changes from one branch into another, but they leave different history. A merge keeps history as it happened. It is either a fast-forward, which only moves the pointer, or a merge commit with two parents. A rebase replays my commits on top of a new base, which gives a linear history but creates new commits with new hashes.
I use rebase to clean up my own local work. For example, I use git rebase -i to squash and fix up noisy commits and reword messages before opening a pull request. I use merge to combine shared work, because it never rewrites existing commits.
The golden rule is to never rebase shared or public branches. Rebase rewrites history, and teammates who based work on the old commits would get duplicate commits and confusing conflicts. On my own feature branch I can rebase and push with --force-with-lease.
For pull requests, many teams use squash merge. It turns the whole PR into one commit on main, which keeps history clean and easy to revert. The trade-off is that the individual commits are lost.
When I hit a merge conflict, I check git status, read the conflict markers, decide what the final code should be, remove the markers, git add the file, and continue the merge or rebase. Then I run the tests. I remember that "ours" and "theirs" are swapped during a rebase. For conflicts that keep coming back, I enable git rerere, so Git reuses my earlier resolution.