Git merge vs rebase, with the real graphs side by side
Git merge vs rebase: merge preserves history and adds a commit, rebase rewrites it and adds none. Here is one branch integrated both ways.

git merge and git rebase both bring one branch's commits into another. They differ in what happens to history.
- Merge keeps both branches exactly as they were and adds one new commit with two parents. Nothing existing changes.
- Rebase replays your commits on top of the other branch as new commits with new hashes. The originals are discarded.
git merge main # safe, honest, adds a merge commit
git rebase main # clean, linear, rewrites your commits
Everything else in this argument follows from that one difference. The graphs below came out of two identical repositories built by a script, integrated one each way, on git 2.50.0.
The starting point
Both repositories have the same history. main has three commits, a feature branch has two of its own, and main gained a commit after the branch was created:
* d95b445 Add stylesheet
| * 9370e03 Add logout function
| * ad39abd Add login function
|/
* d8bca5f Add app entry point
* 18c9ae7 Add README
The |/ is the fork. Both lines share the two commits below it, then diverge. This is the everyday situation: you branched, you worked, and someone else pushed to main while you did.
What merge produces
git checkout main
git merge feature
* 320caef Merge branch 'feature'
|\
| * 9370e03 Add logout function
| * ad39abd Add login function
* | d95b445 Add stylesheet
|/
* d8bca5f Add app entry point
* 18c9ae7 Add README
The fork is still visible, and a new commit 320caef sits on top joining the two lines.
Two measurements from that repository:
commits on main: 6
parents of HEAD: 2
Six commits: the five that existed plus the merge commit. Two parents, which is what makes it a merge commit and what draws the diamond.
Notice the feature commit hashes: ad39abd and 9370e03. They are unchanged. They are the same objects that were on the branch before, in the same order, with the same contents.
What rebase produces
Same starting repository, different command:
git checkout feature
git rebase main
git checkout main
git merge feature # fast-forward, no merge commit
* 5ade4a3 Add logout function
* 1c4b543 Add login function
* 54d8bc8 Add stylesheet
* 4796191 Add app entry point
* 06d0510 Add README
One straight line. No fork, no diamond, no merge commit.
commits on main: 5
parents of HEAD: 1
Five commits, not six. Rebase added no commit of its own, it just moved yours.
And here is the part that matters more than the picture:
feature hashes BEFORE rebase: ad11f49, 6a7afab
feature hashes AFTER rebase: 1c4b543, 5ade4a3
Different hashes. Same messages, same changes, different commits. Git did not move ad11f49; it built a new commit with the same content on a new base and threw the old one away. A commit's hash covers its parent, so changing the parent necessarily changes the hash.
That is the whole reason rebase is dangerous on shared branches, and it is worth stating plainly rather than as folklore.
Side by side
| Merge | Rebase | |
|---|---|---|
| Commits added | 1 (the merge commit) | 0 |
| Existing commits changed | None | All of the ones you moved |
| Hashes preserved | Yes | No |
| Resulting graph | Forked, then joined | One straight line |
| Shows when work happened | Yes | No, it looks sequential |
| Safe on a pushed branch | Yes | No |
| Conflicts appear | Once | Once per replayed commit |
The one rule
Never rebase commits other people already have.
That is the whole safety rule, and it is the one thing worth memorising from this post. It appears in git's own documentation and in Atlassian's guide as "the golden rule of rebasing".
The reason follows directly from the hash change. If a colleague has ad11f49 and you replace it with 1c4b543, their git and yours now disagree about history. Their next pull tries to reconcile two versions of the same work, and they end up with duplicated commits, or a merge that resurrects what you rewrote, or a conflict in code neither of you touched.
The practical version:
- Your own local branch, not pushed? Rebase freely. Nobody else has those commits.
- Pushed, but nobody else works on it? Rebasing means a force push.
--force-with-leaserather than--force, because it refuses if the remote has commits you have not seen. - A shared branch,
main, or anything a pull request is open against? Merge. Not negotiable.
The conflict inversion nobody warns you about
Conflicts happen either way, but during a rebase the markers mean something different, and this catches everyone.
Real conflict from a rebase, where main set a port to 5000 and the feature branch set it to 8080:
<<<<<<< HEAD
const port = 5000
=======
const port = 8080
>>>>>>> e01da3d (Use port 8080 for feature work)
Read that carefully. HEAD is 5000, which came from main. Your own change, 8080, is the incoming one at the bottom.
During a merge, HEAD is your branch and the other branch is incoming. During a rebase it is reversed, because git checked out main and is replaying your commits on top of it one at a time. From git's position, your work is the thing arriving.
If you have ever resolved a rebase conflict, picked "keep mine" out of habit, and discovered you deleted your own change, that is why. The commit hash and message in the bottom marker are the reliable tell: e01da3d (Use port 8080 for feature work) is your commit, so that side is yours.
The rest of the flow is the same as any conflict:
# edit the file, remove the markers
git add config.js
git rebase --continue
git status shows UU config.js while it is unresolved. git rebase --abort puts everything back exactly as it was, at any point, with nothing lost. That escape hatch is worth knowing before you need it.
One more difference: a merge conflicts once, a rebase can conflict once per commit. Replaying eight commits over a much-changed main can mean eight rounds of the same conflict. When that starts happening, git rebase --abort and merge instead. That is not giving up, it is the correct call.
Which should you use?
For most people most of the time: merge, and use rebase for one specific job.
Use merge when:
- The branch is shared, or a pull request is open on it.
- You are pulling
maininto a long-running feature branch. - The history genuinely matters, such as a release branch where you may need to know what was integrated when.
- You are unsure. Merge is the recoverable option.
Use rebase when:
- Updating your own unpushed branch onto the latest
mainbefore opening a pull request. This is the high-value case: your work sits on top of currentmain, reviewers see only your commits, and the diff is honest. - Cleaning up your own local commits before sharing them, with
git rebase -ito squash "fix typo" and "actually fix typo" into one real commit.
The workflow most teams land on combines them: rebase your own branch onto main while you work, then merge it in through a pull request. You get a linear, readable branch and a merge commit that records the integration.
There is no correct answer that applies everywhere, and teams reasonably disagree. What is not a matter of taste is the golden rule.
Squash merge, the third option nobody mentions
The merge-or-rebase framing leaves out the option many teams actually use, and it is the default button on GitHub and GitLab pull requests.
git merge --squash takes every change from the branch and stages it as one set of edits, which you then commit yourself:
git checkout main
git merge --squash feature
git commit -m "Add login feature"
Run against a branch with three commits, including the familiar "Fix typo" and "Actually fix typo" pair:
* cd43e13 Add login feature
* 4da6697 Add stylesheet
* 315a7ea Add app entry point
* b109bc3 Add README
commits on main: 4
parents of HEAD: 1
Three messy commits became one clean one. main reads as a list of features rather than a list of half-finished thoughts, and nobody has to see that you fixed the same typo twice.
Two things about it that surprise people.
It does not commit for you. Git stops after staging and prints "Squash commit -- not updating HEAD". You write the message, which is the point: it is your chance to describe the feature rather than accept a generated summary.
Git does not consider the branch merged. After squashing, git branch --merged main listed only main, not feature. The content is on main but no commit records the connection, so tools that rely on that link cannot see it. In practice this means --merged will not tell you the branch is safe to delete, and merging it again would try to reapply everything.
| Merge | Rebase | Squash merge | |
|---|---|---|---|
Commits added to main |
1 | 0 | 1 |
| Branch's individual commits kept | Yes | Yes, rewritten | No, collapsed into one |
| Graph | Forked | Linear | Linear |
| Branch shows as merged | Yes | Yes | No |
Squash merge suits the common case well: a short-lived branch whose intermediate commits were never meant to be history. It suits a long-running branch with genuinely distinct commits badly, because it throws away information you may later want.
git pull is doing one of these already
git pull is git fetch followed by an integration step, and by default that step is a merge. That is where the "Merge branch 'main' of github.com..." commits in your history came from. You did not choose them, pull made them.
git pull --rebase # replay your local commits on top instead
git config --global pull.rebase true # make it the default
Turning this on is the cheapest history cleanup available, and it is safe for the usual case, because your unpushed local commits are exactly the ones nobody else has.
Cheat sheet
# Update my branch with the latest main (my branch is unpushed)
git checkout feature
git rebase main
# Integrate a finished branch into main
git checkout main
git merge feature
# Something went wrong mid-rebase
git rebase --abort
# Finish a conflicted rebase
git add <file>
git rebase --continue
# Tidy my own last 3 commits before sharing
git rebase -i HEAD~3
# Pull without generating merge commits
git pull --rebase
Try it on a throwaway repo
The graphs in this post took about thirty seconds to produce, and doing it yourself is worth more than reading it. Make a directory, git init, commit twice, branch, commit twice more, commit once on main, then integrate it both ways in two copies. git log --oneline --graph --all after each.
Seeing the same five commits become six-with-a-fork and five-in-a-line settles the concept in a way no diagram does. Nothing is at risk, because none of it is pushed anywhere.
Git Basics runs branching, merging and rebasing in a real terminal in your browser, so the conflict markers above are something you resolve rather than something you read.
Related reading: How to Undo Your Last Git Commit for reset, revert and the difference between them, which is the same "rewrite or record" trade in a smaller frame.
More from the blog

git cherry-pick: move one commit anywhere
Cherry-pick copies a commit onto your current branch. It creates a new commit with a new hash, so the original stays where it was.
Read more
CORS error: what the browser is actually blocking
A CORS error is the browser refusing a response the server already returned with a 200. Here is that proved with two real origins, and the fixes.
Read moreReady to write some code?
Put this into practice - start your first free lesson. No setup, no credit card.