Sign in
← Git: basics
12 min · medium · Lesson 4 of 6

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:

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:

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:

When it's dangerous:

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

What you'll need for the tasks

In the next lesson we'll cover what to do when merge or rebase runs into a conflict.

Done reading?

Mark the lesson as read to move your track progress forward.

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.

Get access for $49

Tell the agent: use MCP izi, give me lesson git-basics-04-merge-vs-rebase