Coding tips

Git Merge vs Rebase vs Squash: Differences, History and When to Use Each (Tested Examples)

Merge keeps every commit and adds a merge commit, rebase replays your commits on top for a straight line, and squash turns the whole branch into one commit. Here is what each does to a real repo, with the graphs, conflicts and undo steps I tested.

By 12 min read 8 views
Cover image for Git Merge vs Rebase vs Squash: Differences, History and When to Use Each (Tested Examples)

All three bring a branch's changes into another branch. The difference is what they leave in the history. git merge keeps every commit and adds a merge commit that joins the two lines. git rebase replays your commits on top of the target branch, giving you a straight line with new commit hashes. A squash merge (git merge --squash) collapses the whole branch into one new commit. Use merge for shared or long-lived branches, rebase to tidy your own branch before you share it, and squash when one feature should be one commit on main.

I ran every command below in real throwaway repositories on Git 2.47.3. To keep the examples comparable, each demo starts from the same four commits with fixed dates, so you'll see the same hashes repeat from section to section.

In this guide:

  • Quick comparison: merge vs rebase vs squash
  • The starting point for every example
  • Git merge: keep the full history
  • Fast-forward merges and --ff-only
  • Git rebase: replay your commits on top
  • Squash merge: one tidy commit
  • Squash with interactive rebase instead
  • How conflicts work in each
  • The golden rule: don't rebase commits others already have
  • git pull: merge or rebase?
  • How to undo a merge, rebase or squash
  • Which one should you use?
  • Wrapping up
  • Frequently asked questions

Quick comparison: merge vs rebase vs squash

MergeRebase (then fast-forward)Squash merge
Commandgit merge featuregit rebase main on the branchgit merge --squash feature
History shapeBranches and joinsStraight lineStraight line
Feature commits kept on mainAll, unchangedAll, with new hashesNone, replaced by one
Commits on main in my test6 (incl. 1 merge commit)53
Rewrites existing commits?NoYesNo (but the branch looks unmerged)
ConflictsResolved oncePossibly once per commitResolved once
Safe on a branch others use?YesNo, needs a force pushYes, on the target branch

GitHub's pull request button offers the same three choices ("Create a merge commit", "Squash and merge" and "Rebase and merge"), described in its docs on pull request merges.

The starting point for every example

Each demo repo has one commit on main, then a feature branch with three commits, and then one more commit on main. So the two branches have diverged:

$ git log --oneline --graph --all
* fa8d6c6 Update README
| * 16636e7 Add login tests
| * 465634d Fix typo in login form
| * 30ccf83 Add login form
|/
* a328534 Initial commit

This is the normal situation on a team: you worked on a feature while someone else pushed to main.

Git merge: keep the full history

From main, a merge creates a new commit with two parents, one from each line:

$ git merge --no-edit feature
Merge made by the 'ort' strategy.
login.html | 1 +
login.test | 1 +
2 files changed, 2 insertions(+)
$ git log --oneline --graph
*   610a658 Merge branch 'feature'
|\
| * 16636e7 Add login tests
| * 465634d Fix typo in login form
| * 30ccf83 Add login form
* | fa8d6c6 Update README
|/
* a328534 Initial commit
commits on main: 6 | merge commits: 1

Notice that the feature commits keep their original hashes (16636e7 and so on). Nothing was rewritten, so merge is always safe on branches other people have pulled. The cost is a busier graph, including small commits like "Fix typo in login form". The git merge documentation covers every option, including --no-ff, which forces a merge commit even when one isn't needed.

Fast-forward merges and --ff-only

If main hadn't moved, Git could just slide the main pointer forward to the branch tip. That's a fast-forward, and it creates no merge commit. Because our branches diverged, a fast-forward is impossible, and --ff-only refuses:

$ git merge --ff-only feature
hint: Diverging branches can't be fast-forwarded, you need to either:
hint: 	git merge --no-ff
hint: 	git rebase
fatal: Not possible to fast-forward, aborting.

That hint sums up the choice nicely: make a merge commit, or rebase first so a fast-forward becomes possible.

Git rebase: replay your commits on top

Rebase takes the commits that are only on your branch and replays them, one by one, on top of the latest main. Then main can fast-forward:

feature tip before rebase: 16636e7
$ git rebase main
Successfully rebased and updated refs/heads/feature.
feature tip after rebase:  7f1e471
$ git merge feature
Updating fa8d6c6..7f1e471
Fast-forward
$ git log --oneline --graph
* 7f1e471 Add login tests
* 24f7862 Fix typo in login form
* a6e3a76 Add login form
* fa8d6c6 Update README
* a328534 Initial commit
commits on main: 5 | merge commits: 0

The history is a straight line, and every commit is still there. But look at the hashes: 16636e7 became 7f1e471. Rebase doesn't move commits; it creates new ones with the same changes and a different parent. That's why rebasing a branch someone else already has causes trouble (more on that below). The git rebase documentation describes all the modes.

Squash merge: one tidy commit

A squash merge applies the combined changes of the whole branch to your working tree and stages them, but doesn't commit:

$ git merge --squash feature
Automatic merge went well; stopped before committing as requested
Squash commit -- not updating HEAD
$ git status --short
A  login.html
A  login.test
$ git commit -m "Add login form (#12)"
[main 4443149] Add login form (#12)
$ git log --oneline --graph
* 4443149 Add login form (#12)
* fa8d6c6 Update README
* a328534 Initial commit
commits on main: 3 | merge commits: 0

Three feature commits became one. Reading main is now easy: one commit per feature, often with the pull request number in the message.

There's a catch. The new commit has no link to the branch, so Git doesn't consider feature merged:

$ git branch -d feature
error: the branch 'feature' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D feature'
$ git branch -D feature
Deleted branch feature (was 16636e7).

That's expected after a squash, so -D is fine here. My guide on deleting a Git branch locally and remotely covers the remote side too. The other catch is that the small commits are gone from main, so git bisect and git blame can only point to the whole feature.

Squash with interactive rebase instead

You can also squash on your own branch before merging. git rebase -i main opens a todo list. Change pick to fixup (or squash, which keeps the messages) for the commits you want folded into the one above:

$ git rebase -i main
pick 30ccf83 Add login form
pick 465634d Fix typo in login form
pick 16636e7 Add login tests
--- todo after editing ---
pick 30ccf83 Add login form
fixup 465634d Fix typo in login form
fixup 16636e7 Add login tests
Successfully rebased and updated refs/heads/feature.
$ git log --oneline --graph
* bccc2e1 Add login form
* fa8d6c6 Update README
* a328534 Initial commit

The result looks like a squash merge, but you control which commits get combined and you can keep two or three meaningful ones. In my test I scripted the edit with GIT_SEQUENCE_EDITOR; normally Git opens your editor. Pro Git's chapter on rewriting history walks through reword, edit and split as well.

How conflicts work in each

Merge and squash show conflicts once, for the combined result. Rebase replays commit by commit, so a long branch can stop several times. Here's a single conflict during a rebase, where both branches changed the same line:

$ git rebase main
CONFLICT (content): Merge conflict in config.txt
error: could not apply aa849c4... Raise price to 12
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
$ git status --short
UU config.txt
$ cat config.txt
<<<<<<< HEAD
price = 11
=======
price = 12
>>>>>>> aa849c4 (Raise price to 12)

One thing that surprises people: during a rebase, HEAD is the branch you're rebasing onto (here main, with price = 11), and the other side is your own commit. It's the opposite of what you see in a merge. After editing the file, mark it resolved and carry on:

$ git rebase --continue
[detached HEAD 3f13a0b] Raise price to 12
Successfully rebased and updated refs/heads/feature.

If it gets messy, git rebase --abort puts the branch back exactly as it was. In my test, git log --oneline -1 showed the original aa849c4 afterwards. Rebase also refuses to start with uncommitted changes:

$ git rebase main
error: cannot rebase: You have unstaged changes.
error: Please commit or stash them.
$ git rebase --autostash main
Applied autostash.
Successfully rebased and updated refs/heads/feature.

Commit or stash your work first (see git stash pop vs apply), or let --autostash stash and reapply it for you.

The golden rule: don't rebase commits others already have

After rebasing a branch you had already pushed, a normal push is rejected because the remote still holds the old commits:

$ git push origin feature
! [rejected]        feature -> feature (non-fast-forward)
error: failed to push some refs to '/tmp/gitdemo-nov03/remote.git'
$ git push --force-with-lease origin feature
+ 16636e7...7f1e471 feature -> feature (forced update)

On your own feature branch, --force-with-lease is the safe way to push. Unlike --force, it refuses if someone else pushed to the branch since you last fetched. On main or any branch teammates build on, don't rebase at all. Their copies still point at the old commits, and they'll end up with duplicates. Pro Git calls this the perils of rebasing: don't rebase commits that exist outside your repository and that people may have based work on.

git pull: merge or rebase?

git pull is a fetch followed by a merge or a rebase. When your local branch and the remote have both moved, recent Git versions make you choose:

$ git pull
hint: You have divergent branches and need to specify how to reconcile them.
hint:   git config pull.rebase false  # merge
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
fatal: Need to specify how to reconcile divergent branches.
$ git pull --rebase
Successfully rebased and updated refs/heads/main.
$ git log --oneline --graph
* cb10056 My change
* 8641bd9 Teammate change
* cc67c24 First

git pull --rebase puts your unpushed commits on top of your teammate's, with no "Merge branch 'main' of ..." noise. That's safe because those commits only existed on your machine. To make it the default, run git config --global pull.rebase true. The git pull documentation lists the related options.

How to undo a merge, rebase or squash

All three are reversible as long as you act before pushing. Right after a merge, ORIG_HEAD points to where you were:

$ git reset --hard ORIG_HEAD
HEAD is now at fa8d6c6 Update README

For a rebase, the reflog remembers the branch tip before the rewrite:

$ git reflog -4 feature
7f1e471 feature@{0}: rebase (finish): refs/heads/feature onto fa8d6c665bc0bab081997151d397ed17d6ed790c
16636e7 feature@{1}: commit: Add login tests
465634d feature@{2}: commit: Fix typo in login form
$ git reset --hard feature@{1}
HEAD is now at 16636e7 Add login tests

A squash merge is just a normal commit, so undo it like any other commit. git reset --hard throws away uncommitted work, so check git status first. My post on undoing the last commit while keeping your changes covers the softer options.

Which one should you use?

SituationBest choiceWhy
Merging a finished feature into a shared mainMerge, or squash mergeNo rewritten history for others
Updating your private branch with the latest mainRebaseStraight history, no extra merge commits
Branch full of "wip" and "fix typo" commitsSquash merge, or interactive rebaseOne clean commit per feature
Long-lived release or shared team branchMergeKeeps context and is safe for everyone
Pulling before you pushgit pull --rebaseAvoids pointless merge commits
You need every commit for git bisectMerge or rebaseSquash hides the individual steps

Many teams combine them: rebase your own branch to stay current, then squash merge the pull request. Whatever you pick, agree on it as a team. Mixing styles at random is what makes history hard to read. And if a merge ever refuses with "refusing to merge unrelated histories", that's a different problem, explained in this guide.

Wrapping up

Merge records what really happened, rebase rewrites it into a straight line, and squash compresses it into one step. None of them is "correct". They trade accurate history against readable history. For most solo and small-team projects, rebasing your own branches and squash merging into main gives a clean log without much risk.

For more small, useful utilities, see my free tools.

Frequently asked questions

What is the main difference between git merge and git rebase?

Merge joins two branches with a new merge commit and keeps every original commit as it was. Rebase replays your commits on top of the other branch, creating new commits with new hashes and a straight history.

Is squash merge the same as rebase?

No. A squash merge combines the whole branch into a single new commit on the target branch. A rebase keeps each commit separately but moves them. You can squash during an interactive rebase with fixup or squash, which gets a similar result.

Is git rebase dangerous?

It's safe on commits that only you have. It becomes risky on commits you've already pushed and others have pulled, because rebase creates new commits and their copies still point at the old ones. Use --force-with-lease when you must push a rebased branch.

Why does git branch -d say not fully merged after a squash merge?

The squash commit has no parent link to the feature branch, so Git can't see that the work is merged. If you've checked that the squash commit landed, delete the branch with git branch -D.

Should git pull merge or rebase?

For most people, rebase. git pull --rebase puts your local, unpushed commits on top of the remote ones and avoids extra merge commits. Set it once with git config --global pull.rebase true.

Does rebase cause more conflicts than merge?

Not more in total, but it can stop more often. Merge shows all conflicts once, while rebase replays commits one at a time and can stop at each commit that conflicts. git rebase --abort always takes you back to where you started.

Can I undo a git rebase?

Yes. Run git reflog for the branch, find the entry from before the rebase (for example feature@{1}), and reset to it with git reset --hard feature@{1}. Check git status first because a hard reset discards uncommitted changes.

Which merge strategy is best for pull requests on GitHub?

There's no single best one. Squash and merge keeps main to one commit per pull request, a merge commit keeps the full branch history, and rebase and merge keeps every commit in a straight line. Pick one per repository and stick with it.

Last checked: 3 November 2026. Figures, settings and guidance can change, so confirm anything important with the sources linked above.

Share X LinkedIn WhatsApp Facebook
Was this helpful?

0%