Merge and rebase
What merge does, what rebase does, when to pick which, and why you don't rebase public history.
Why
You have a feature branch off main. While you were working on it, other people's commits landed in
main. Now your branch is behind:
main: A --- B --- C --- D
\
feature: E --- F
You need to combine your E, F with the current main. There are two ways: merge and rebase. The
resulting code is the same either way, but the history differs. The choice affects how git log reads
six months from now, and whether you can safely push the branch to a shared repository.
Merge
git switch main
git merge feature
Git creates a new merge commit M. A regular commit has one parent, a merge commit has two: the last
commit of main and the last commit of feature.
main: A --- B --- C --- D --- M
\ /
feature: E --- F ------
You can see both parents right in the commit object:
git cat-file -p HEAD
tree 85a98967b78db46b4094db08c0a3e05a1d15337c
parent a6b211f9c30f57a8ff496dfd71d02e2a6b76e86c
parent 4cdf75e3449a71f3ac9f328e2b116d95a074df86
...
What merge gives you:
- the history stays as is, not a single existing commit gets rewritten;
Mrecords the fact of the merge itself: who merged the branch and when;- the log shows the parallel work.
Fast-forward merge
A special case: main didn't move while you were working. Then merge just moves the main pointer
forward to the tip of feature, and no M appears:
before:
main: A --- B
\
feature: C --- D
after:
main, feature: A --- B --- C --- D
Git says so directly:
Updating a6b211f..31e4e09
Fast-forward
The history stays linear. Some teams ask for fast-forward to be disabled with the git merge --no-ff
flag. Then a merge commit always appears, and it's easy to find in the log where a feature started and
ended.
The opposite flag, git merge --ff-only, allows only a fast-forward. If a merge commit is unavoidable,
Git refuses to merge, and you find out right away:
fatal: Not possible to fast-forward, aborting.
Rebase
git switch feature
git rebase main
Git takes your commits E, F, cuts them off from the old base, and reapplies them on top of fresh
main:
before:
main: A --- B --- C --- D
\
feature: E --- F
after:
main: A --- B --- C --- D
\
feature: E' --- F'
E' and F' are new commits with new hashes. The changes in them are the same, but the commit has a
different parent, and the hash is computed from the parent too, so for Git these are different objects:
git rev-parse --short feature
4cdf75e
git rebase main
Successfully rebased and updated refs/heads/feature.
git rev-parse --short feature
8fdca61
After that, feature merges into main as a fast-forward, and main's history ends up linear, with no
merge commits.
When to choose what
Two rules most teams go by:
- rebase for your own feature branch before merging into
main.main's history stays linear and reads like a list of features, not a web of parallel commits; - merge for integrating someone else's work. When you merge someone's branch, the merge commit honestly marks that another person's contribution landed here.
Pulling fresh main into your own long-lived branch works either way. Rebase keeps the line clean but
rewrites your commits' hashes. Merge rewrites nothing, but adds merge commits to the branch's history.
Often a team doesn't decide this case by case, but sets a merge method for the repository instead. On GitHub, a Pull Request has three buttons: "Create a merge commit", "Squash and merge" and "Rebase and merge". On GitLab, the project settings have a merge method: "Merge commit", "Merge commit with semi-linear history" or "Fast-forward merge", with squash as a separate checkbox. Squash glues all the branch's commits into one. Then there's one rule for everyone, and nobody has to think about it in every MR.
Golden rule: don't rebase public history
If commits are already pushed and might sit in someone's local copy, don't rebase them.
The reason is the same new hashes. Your old E, F are gone, replaced by E', F'. Your teammate's clone
still has the old E, F. If you overwrite the branch on the server with the new ones, the next
git pull leaves your teammate with two histories that have diverged:
fatal: Need to specify how to reconcile divergent branches.
If they merge them, the history ends up with both E, F and E', F': the same changes twice plus an
extra merge commit. And if your teammate managed to push their own commit before your force-push, their
commit disappears from the server entirely. On a shared branch, this breaks the whole team's work.
When rebase is safe:
- rebasing your own local branch that nobody else has seen yet;
- rebasing your own feature branch, even an already-pushed one, if you're sure you're the only one working on it.
When it's dangerous:
- rebasing
main,develop, or any shared, long-lived branch; - rebasing a branch someone else is working on.
How to push a branch after rebase
You can rebase your own pushed feature branch. But a plain git push after that refuses to work:
! [rejected] feature -> feature (non-fast-forward)
error: failed to push some refs to '../origin.git'
The logic makes sense: the server has E, F, you now have E', F', and one isn't a continuation of
the other. The branch can only be updated by overwriting it.
The first thing the internet suggests is git push --force. It works, but blindly: the command wipes
the branch on the server with your local version and doesn't check what's there right now. If your
teammate managed to add a commit to the same branch in the meantime, their work disappears without a
single question asked.
The right way:
git push --force-with-lease
The flag checks first: the branch on the server has to point to exactly the commit you saw at your
last fetch (that's what your origin/feature points to). If it matches, Git overwrites. If it
doesn't match, because someone pushed after you, the push is rejected:
! [rejected] feature -> feature (stale info)
Then you first look at what showed up there and bring it into your branch.
There's a trap. The check runs against origin/feature, and any git fetch updates that, including a
background one some IDEs run. After such a fetch, someone else's commit is already "visible", and
--force-with-lease happily wipes it out, even though you never actually looked at it. Since Git 2.30
there's an extra flag, --force-if-includes: it rejects the push if the fresh origin/feature isn't
included in your local branch.
! [rejected] feature -> feature (remote ref updated since checkout)
The rule is simple: --force-with-lease instead of --force always, and force only on your own
branch. Nobody forces into main even with a check, which is why it's usually protected in the
repository settings so a force-push there is technically impossible.
Common mistakes
git rebase mainwhile you're on the wrong branch. Rebase moves the current branch. If you're onmainand rebase it ontofeature,main's commits get rewritten. Checkgit branchbefore rebasing.git merge featureintomainwhen you wanted a linear history. Ifmainmoved ahead, you get a merge commit. Rebase the branch first, then merge, or go straight forgit merge --ff-onlyso Git refuses instead of quietly making a merge commit.git pullon a branch you just rebased. Git merges the oldE, Fwith the newE', F', and the history ends up with everything twice. After rebasing your own branch, you need a push with--force-with-lease, not a pull.git fetchright before--force-with-lease, without looking at what came in. The check protects nothing after that: look atgit log origin/featurebefore you push.git push --forceon a shared branch. Other people's commits vanish from the server.
What you'll need for the tasks
git switch <branch>,git branchgit rebase <base>(moves the current branch)git merge <branch>,git merge --ff-only <branch>git log --oneline --graph --all,git log <branch>git fetch,git push --force-with-leasegit cherry-pick <commit>: copies a single commit onto the current branch- the
-C <folder>flag: run a Git command against a repository in another folder
In the next lesson we'll cover what to do when merge or rebase runs into a conflict.
Interview-style questions
Interview questions on this topic: 3
Included with practice access, $49 one-time. Get access
Answer out loud or in writing, then reveal the answer and honestly mark whether you knew it.
Done reading?
Mark the lesson as read to move your track progress forward.
Lesson marked as done.
Go through it with an AI agent
The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.
Tell the agent: use MCP izi, give me lesson git-basics-04-merge-vs-rebase
Progress saved in this browser
Clear your history or open this on your phone, and the marks will be gone. Leave your email and they'll move to your account. No password needed.
Done. moved to your account. You can see your progress on any device. My progress