Skip to main content
DevOps Insightsguides & answers

Git · commonly confused

Git merge vs rebase

Short answer

Both bring your branch up to date with another one. Merge adds a commit with two parents and leaves every existing commit as it was. Rebase replays your commits on top of the other branch as new commits with new IDs. That is the whole trade: a straight history in exchange for rewriting commits — harmless on work only you have, and trouble on anything others have already pulled.

Reproduced on Git 2.50, . Where the text says “reproduced”, the behaviour was run on that version, not taken from documentation.

First published

On this page · 10 sections

In simple terms

You branched off main, main moved on, and your branch needs main's new work. There are two ways to combine them. They leave you with the same files and a different history.

Merge ties the two lines together with a new commit that has both of them as parents. Nothing that already existed changes. The history shows your branch, and the point where it rejoined.

Rebase picks up your commits and re-applies them one at a time on top of the latest main, as if you had started from there. The result is a straight line, but those are new commits. The originals are left behind, and anyone who had them now holds commits your branch no longer contains.

Git's own documentation follows from that. Rebase your own unpushed work as much as you like to tidy it; once others may have built on it, merge instead. The Pro Git book's version: rebase local changes before you push, and never rebase anything you have pushed somewhere.

Words used on this page

commit ID
A hash computed from the commit's contents, which include its parent. The same change on a different parent gets a different ID.
merge base
The most recent commit two branches share. git merge-base main feature prints it.
merge commit
A commit with two parents, one from each branch it joins. It changes no existing commit.
rebase
Re-creating your commits, one at a time, on top of a different starting commit. The copies have new IDs; the originals are left behind.
fast-forward
Moving a branch forward to a later commit that already contains it, without a merge commit. A push is accepted without force only when it is a fast-forward.
remote-tracking branch
Your local record of where a branch on the remote pointed at your last fetch, such as origin/feature. It does not update by itself.
divergent branches
Two versions of a branch where each has commits the other lacks. After a rebase, your branch and the remote's copy are divergent.
force push
A push that replaces the remote branch even when that removes commits from it. --force-with-lease and --force-if-includes add safety checks.
ORIG_HEAD
Where the branch pointed before the last merge, rebase or reset. git reset --hard ORIG_HEAD undoes a rebase straight after it.
reflog
Your local log of where HEAD and each branch have pointed. It is not pushed, and it is where an older branch position can still be found.

What actually happens

A merge commit records two parents: the tip of your branch and the tip of the one you merged. Reproduced on Git 2.50: after git merge main the new commit had two parents, and every original feature commit was still an ancestor of the branch. If your branch has no commits of its own, Git fast-forwards by default instead — it moves the branch to the other tip and creates no merge commit at all.

A rebase finds the merge base, takes each of your commits after it, and re-creates them on the new base. A commit's ID is a hash over the commit, and the commit names its parent, so the same change on a new parent is a new commit. Reproduced: feat2's ID changed, the original was no longer in the branch, and the history became one line with no merge commits. Git leaves the old tip in ORIG_HEAD, which is how a rebase you regret is undone — git reset --hard ORIG_HEAD restored it exactly — and git reflog keeps older positions.

Conflicts arrive differently. A merge reconciles the final state of each branch, once. A rebase applies your commits in turn, so a line that main changed and several of your commits touched can conflict at each of them. Reproduced with two feature commits editing a line main had also changed: the merge stopped once, the rebase twice.

Rewriting shows up at push time. The remote still has the commits you replaced, so pushing the rebased branch is not a fast-forward and is rejected: ! [rejected] feature -> feature (non-fast-forward). Overwriting needs a force push, and --force-with-lease is the safer form — reproduced, it refused with "stale info" when a teammate had pushed since our last fetch. Whoever pulled the old commits now has a diverged branch. On Git 2.50 with no pull setting, their git pull stops with "fatal: Need to specify how to reconcile divergent branches."

The same update, two histories

# Your branch is behind main. Option 1: merge main in.
git switch feature
git merge main          # one new commit with two parents; nothing rewritten

# Option 2: replay your commits on top of main.
git switch feature
git rebase main         # new commits with new IDs, in a straight line

# See which shape you have.
git log --graph --oneline main feature

# Undo a rebase you regret. The old tip is kept.
git reset --hard ORIG_HEAD

# Publish a rebased branch that was already pushed — only if it is yours alone.
git push --force-with-lease --force-if-includes origin feature

# Decide what git pull does, rather than being asked.
git config pull.rebase false   # merge
git config pull.rebase true    # rebase
git config pull.ff only        # refuse anything but a fast-forward

ORIG_HEAD is replaced by the next command that moves HEAD a long way — another rebase, merge or reset — so use it straight away. After that, git reflog still lists where the branch was.

Try it yourself

Before you start

  • Git installed (git --version). Everything on this page was reproduced on Git 2.50.1.
  • A clean working tree. git status should say nothing to commit, working tree clean; with unstaged edits, git rebase refuses with error: cannot rebase: You have unstaged changes. Commit or git stash first.
  • A safe place to practise: a scratch repository whose remote is a local bare repository made with git init --bare. A force push there cannot reach anyone else's work.
  • Know who else has the branch. Run git fetch, then git branch -r --contains <commit>. If any remote branch is listed, the commit has been pushed and others may have pulled it: rebase only if you know nobody else uses that branch, and merge otherwise.
  • Before any force push, git fetch and then git log <branch>..origin/<branch>. Every commit it lists is on the remote but not in your branch, and a force push would remove it.

Bringing the same branch up to date with merge, then with rebase

Reproduced on Git 2.50.1 (Apple Git-155) in a scratch repository whose remote was a local bare repository, with global and system config switched off and the time zone set to UTC. main and feature had both been pushed and had diverged: feature has two commits of its own and main has one. The merge is done on a copy, try-merge, so the rebase can start from the same place. The author is a placeholder and the repository path is replaced with <path>. git rebase redraws a Rebasing (1/2) counter in place; the output shows the line left on screen.

Command

git log --graph --oneline --decorate --all
git switch -c try-merge
git merge --no-edit main
git log --graph --oneline --decorate
git switch feature
git rebase main
git log --graph --oneline --decorate
git push origin feature

Output

$ git log --graph --oneline --decorate --all
* 19f730e (HEAD -> feature, origin/feature) Add retries
* 3a7df21 Add timeout
| * e6756a6 (origin/main, main) Add README
|/  
* 27c5f80 Add app config

$ git switch -c try-merge
Switched to a new branch 'try-merge'

$ git merge --no-edit main
Merge made by the 'ort' strategy.
 README | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 README

$ git log --graph --oneline --decorate
*   cb3e62b (HEAD -> try-merge) Merge branch 'main' into try-merge
|\  
| * e6756a6 (origin/main, main) Add README
* | 19f730e (origin/feature, feature) Add retries
* | 3a7df21 Add timeout
|/  
* 27c5f80 Add app config

$ git switch feature
Switched to branch 'feature'
Your branch is up to date with 'origin/feature'.

$ git rebase main
Successfully rebased and updated refs/heads/feature.

$ git log --graph --oneline --decorate
* 4ba0746 (HEAD -> feature) Add retries
* 974cc4f Add timeout
* e6756a6 (origin/main, main) Add README
* 27c5f80 Add app config

$ git push origin feature
To <path>/origin.git
 ! [rejected]        feature -> feature (non-fast-forward)
error: failed to push some refs to '<path>/origin.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

How to read it

  • Before: feature has Add timeout and Add retries, which main lacks, and main has Add README. origin/feature sits on the same commit as feature, because it was pushed.
  • Merge: Merge made by the 'ort' strategy. added one commit, cb3e62b, and the graph shows two lines joining at it, one per parent. 19f730e and 3a7df21 kept their IDs, so anyone who already pulled them has nothing to reconcile.
  • Rebase: the same two commits came back as 974cc4f and 4ba0746, on top of Add README, in one straight line. The messages match, but they are new commits. origin/feature has dropped out of the graph because the remote still points at the originals.
  • The push is [rejected] as (non-fast-forward). The hint says to use git pull, but here that cannot help: on Git 2.50.1 it stopped with fatal: Need to specify how to reconcile divergent branches. To back out, git reset --hard ORIG_HEAD. To publish, the branch must be yours alone and the push forced.
  • In a copy of this setup, git push --force-with-lease origin feature printed + edc732e...c8a7b53 feature -> feature (forced update). A teammate who had the old branch then got the same fatal from git pull, and git pull --no-rebase left Add timeout and Add retries in their history twice.

The mistake this causes

Rebasing a branch other people have already pulled — a shared feature branch, or main itself — and force-pushing the result.

Why people do it Rebase does not move commits, it replaces them. Everyone who had the old ones now holds commits the remote no longer has, and their next pull has to reconcile two histories that make the same changes but share nothing after the merge base.

What you see Your plain push is rejected as non-fast-forward. After a force push, teammates' pulls stop asking how to reconcile divergent branches — and if they choose merge, every rebased commit appears twice. Reproduced: feat1 and feat2 each showed up twice in the teammate's history, the originals and their copies.

If it goes wrong

error: cannot rebase: You have unstaged changes. followed by error: Please commit or stash them.
Commit the work or run git stash, rebase, then git stash pop. git rebase --autostash does the stash for you, but Git's manual warns that applying it afterwards can cause conflicts.
CONFLICT (content): Merge conflict in app.conf followed by error: could not apply <id>... <message> during a rebase
Edit the file to the result you want, git add it, then git rebase --continue. A rebase can stop again at the next commit that touches the same lines; reproduced, two commits editing a line main had changed stopped the rebase twice and the merge once. git rebase --abort returns to where you started.
! [rejected] feature -> feature (non-fast-forward) straight after a rebase
The remote still has the commits you replaced. Do not pull. If others may have the branch, run git reset --hard ORIG_HEAD and merge instead. If the branch is yours alone, run git push --force-with-lease --force-if-includes origin feature.
fatal: Need to specify how to reconcile divergent branches. on git pull, after someone force-pushed a rebased branch
This is Git 2.50's message when no pull mode is set. Do not choose merge here: it keeps the old commits next to their copies, and brings back any commit the rebase dropped. Reproduced, git pull --rebase replaced the old commits with the rebased ones cleanly, which is what the Pro Git book recommends after an upstream rebase.
! [rejected] feature -> feature (stale info), or with --force-if-includes, (remote ref updated since checkout)
Someone pushed to the branch after you last looked. Run git fetch, read their commits with git log feature..origin/feature, and rebase onto origin/feature so they are included before you push again.

How it shows up in production

Two people share a feature branch. One rebases it onto the latest main to tidy it before review, and the push is rejected as non-fast-forward.

Taking the rejection for an obstacle rather than a warning, they force-push. The branch on the server now holds rebased copies of every commit.

The other person's next pull stops and asks how to reconcile divergent branches. They pick merge. The branch now contains each commit twice. Where a rebased copy differs from its original, for example because conflicts were resolved during the rebase, the merge also stops with conflicts.

Nothing was lost; the history now says the work happened twice. The fix is an agreement rather than a command: merge main into shared branches, or decide in advance who may rewrite one and when.

How to tell which one you are hitting

Has anyone else pulled these commits?
If not, rebase is safe — you are only rewriting your own copy. If they have, merge, so nobody is left holding commits the branch no longer contains.
Does the repository require a linear history?
GitHub's branch protection has a rule for it: it prevents pushing merge commits to the branch, and pull requests must use a squash or rebase merge. Then the choice is the team's, not yours.
Is the branch long-lived and touching files main keeps changing?
A rebase can stop at a conflict once per commit; a merge reconciles the final states once. Many small commits through contested files make a rebase slow going.
Was your push just rejected as non-fast-forward?
If you rebased, the remote still has the commits you replaced. Force-push with --force-with-lease only if the branch is yours alone; otherwise you are about to do the mistake above to someone else.

Security considerations

  • A fetch can disarm --force-with-lease. Reproduced: a teammate pushed Add log level, and the lease refused with (stale info). After a plain git fetch, the same command printed (forced update) and the teammate's commit was gone from the remote. With --force-if-includes added, the push refused with (remote ref updated since checkout) until the commit was integrated. Git's push manual warns that editors fetching in the background defeat the lease the same way.
  • A rebase that dropped a commit can be undone by a teammate's merge. Reproduced: a commit adding debug.env with a fake token was dropped by a rebase and force-pushed, and a teammate's git pull --no-rebase brought the commit and the file back. The old commit also stays in every clone that fetched it. Rewriting history does not remove a leaked secret; rotate it.
  • Rebasing drops commit signatures. Reproduced with an SSH signing key: a signed commit had a gpgsig header before git rebase main and none after. git rebase -S re-signed every rewritten commit with your key, including any that someone else wrote, so your signature then vouches for their changes.
  • Force pushes rewrite shared history without telling anyone. GitHub blocks force pushes on protected branches by default; keep it that way for main and long-lived shared branches, so a rebase there cannot happen by accident.
  • A rebase that resolves conflicts changes code after review. GitHub's branch protection can dismiss stale pull request approvals when the diff changes; without it, an approval given before the rebase stays on code nobody reviewed in that form.

In an interview

How this gets asked in an interview

Usually phrased as What is the difference between git merge and git rebase, and when would you use each?

What a strong answer contains Merge joins two histories with a commit that has two parents and changes nothing that already exists. Rebase re-creates your commits on a new base, giving a linear history with new commit IDs. Rebase your own local work to keep history readable; do not rebase what others have pulled, because they keep the originals and end up with duplicates. The experienced answer names the costs too: a rebase can stop at a conflict once per commit where a merge stops once, and publishing a rebased branch needs a force push — --force-with-lease, not --force.

The follow-up "You rebased and force-pushed a shared branch. What do your teammates see?" Their next pull finds divergent branches; merging brings in both the old commits and their rebased copies. Next time, merge main into the shared branch instead.