DEV Community

Yashika Vijayvargiya
Yashika Vijayvargiya

Posted on Originally published at Medium on

Git Merge vs Rebase: What Actually Happens to Your Commit History?

If you’ve worked with Git for a while, you’ve probably seen commands like:

git merge main
Enter fullscreen mode Exit fullscreen mode

and:

git rebase main
Enter fullscreen mode Exit fullscreen mode

Both commands can be used to bring changes from one branch into another.

So a common question is:

If both merge and rebase bring my branch up to date, what’s actually the difference?

The short answer is:

Merge combines histories. Rebase rewrites your branch’s history by replaying your commits on top of another branch.

But that sentence alone isn’t enough to really understand the difference.

Let’s start from the beginning.

First, understand what a Git branch really is

Before comparing merge and rebase, let’s understand one important thing.

A Git branch is basically a movable pointer to a commit.

Suppose we have:

A → B → C
        ↑
       main
Enter fullscreen mode Exit fullscreen mode

main currently points to commit C.

Now we create a feature branch:

A → B → C
        ↑
       main
        ↑
      feature
Enter fullscreen mode Exit fullscreen mode

Both branches initially point to the same commit.

Now you make a change on the feature branch:

A → B → C → D
        ↑    ↑
       main feature
Enter fullscreen mode Exit fullscreen mode

feature has moved forward, while main has stayed at C.

This is the basic idea behind Git branching.

Now let’s create a real situation

Imagine you’re working on a Rails application.

You create a branch:

git switch -c feature/login
Enter fullscreen mode Exit fullscreen mode

At that moment:

A → B → C
        ↑
       main
        ↑
   feature/login
Enter fullscreen mode Exit fullscreen mode

You make two commits:

A → B → C → D → E
        ↑   ↑
       main feature/login
Enter fullscreen mode Exit fullscreen mode

Maybe:

D = Add login form
E = Add authentication logic
Enter fullscreen mode Exit fullscreen mode

Meanwhile, another developer adds a change to main:

A → B → C → F
        ↑ ↑
       main
Enter fullscreen mode Exit fullscreen mode

Now your repository looks like this:

               D → E
             ↗ ↑
A → B → C
             ↘
              F
              ↑
             main
Enter fullscreen mode Exit fullscreen mode

More clearly:

              D → E feature/login
             /
A → B → C
             \
              F main
Enter fullscreen mode Exit fullscreen mode

This is called diverged history.

Your feature branch and main have moved in different directions.

Now you need to bring main's changes into your feature branch.

That’s where merge and rebase come in.

Option 1: Git Merge

You are on your feature branch:

git switch feature/login
Enter fullscreen mode Exit fullscreen mode

Then:

git merge main
Enter fullscreen mode Exit fullscreen mode

Git takes the histories from both branches and creates a new merge commit when a true merge is required.

The history becomes:

              D → E
             /      \
A → B → C           M
             \     /
                F
Enter fullscreen mode Exit fullscreen mode

M is the merge commit.

The important thing is:

Git keeps both histories as they actually happened.

Your feature work happened on one branch.

The main work happened on another branch.

The merge commit records that those two lines were brought together.

Git’s documentation describes merge as joining development histories and, for a true merge, creating a commit with both branch tips as parents.

What does the merge commit mean?

This is important for beginners.

Suppose you see:

A → B → C → F
        \
         D → E
              \
               M
Enter fullscreen mode Exit fullscreen mode

You might wonder:

“What code is inside M?”

The merge commit represents the combined result of both branches.

Conceptually:

main changes
     +
feature changes
     ↓
combined result
Enter fullscreen mode Exit fullscreen mode

The merge commit has two parents:

        M
       / \
      E   F
Enter fullscreen mode Exit fullscreen mode

That is one of the important differences between merge and rebase.

Fast-forward merge

There is another situation beginners should know about.

Suppose main hasn't changed since you created your feature branch:

A → B → C
        ↑
       main
        \
         D → E
              ↑
           feature
Enter fullscreen mode Exit fullscreen mode

If you run:

git switch main
git merge feature
Enter fullscreen mode Exit fullscreen mode

Git doesn’t need to create a merge commit.

It can simply move main forward:

A → B → C → D → E
                ↑
             main
             feature
Enter fullscreen mode Exit fullscreen mode

This is called a fast-forward merge.

There is no divergent history to combine.

Git simply moves the branch pointer forward. Git’s merge documentation distinguishes this from a true merge commit.

Option 2: Git Rebase

Now let’s go back to our original situation.

We have:

              D → E
             /
A → B → C
             \
              F
              ↑
             main
Enter fullscreen mode Exit fullscreen mode

You are on:

feature/login
Enter fullscreen mode Exit fullscreen mode

Instead of merging main, you can run:

git rebase main
Enter fullscreen mode Exit fullscreen mode

This sounds complicated, but the basic idea is surprisingly simple.

Rebase takes your feature commits and replays them on top of the latest ** main.**

Git’s official documentation describes rebase as taking the changes introduced by commits on one branch and replaying them on another base.

So originally:

              D → E
             /
A → B → C
             \
              F
Enter fullscreen mode Exit fullscreen mode

After:

git rebase main
Enter fullscreen mode Exit fullscreen mode

Git effectively does this:

A → B → C → F → D' → E'
Enter fullscreen mode Exit fullscreen mode

Now:

A → B → C → F → D' → E'
                  ↑ ↑
              new D new E
Enter fullscreen mode Exit fullscreen mode

Your feature commits have been replayed on top of the latest main.

Wait… why are D and E now D’ and E’?

This is one of the most important concepts in understanding rebase.

You originally had:

D
E
Enter fullscreen mode Exit fullscreen mode

After rebase, Git creates new commits based on the same changes:

D'
E'
Enter fullscreen mode Exit fullscreen mode

They may contain the same code changes, but they are different Git commits.

Why?

Because a Git commit is not identified only by its code changes.

Its identity also depends on its parent and other metadata.

Before rebase:

C → D → E
Enter fullscreen mode Exit fullscreen mode

After rebase:

C → F → D' → E'
Enter fullscreen mode Exit fullscreen mode

Since D' has a different parent than D, its commit identity changes.

This is why rebase rewrites history.

The easiest way to remember it

Think of merge as:

“Let’s combine these two roads.”

And rebase as:

“Let’s move my road so it starts from the latest point on the other road.”

Merge

        D → E
      /      \
A → B → C     M
     \       /
       F ──
Enter fullscreen mode Exit fullscreen mode

Rebase

Before:

        D → E
      /
A → B → C → F
Enter fullscreen mode Exit fullscreen mode

After:

A → B → C → F → D' → E'
Enter fullscreen mode Exit fullscreen mode

A real-world analogy

Imagine you’re writing a book.

You started writing Chapter 5 based on version 1 of the book.

Meanwhile, another writer updated Chapters 1–4.

You now have two choices.

Merge

You say:

“Let’s keep both versions of history and combine the work.”

The final history shows that two people worked in parallel and their work was combined later.

Rebase

You say:

“Let’s take my Chapter 5 changes and apply them to the newest version of Chapters 1–4.”

Now the history looks as though your Chapter 5 work happened after the latest version.

That’s essentially what rebase does to commits.

Merge vs Rebase at a glance

MergeRebaseCombines branchesYesYesCreates merge commitUsually, when histories divergeNo, not for the rebased commitsKeeps original branch historyYesNoRewrites commitsNoYesProduces linear historyNot necessarilyUsuallySafer for shared/published historyGenerallyRequires cautionUseful for cleaning local historyLess soYes

The biggest difference is history.

Does rebase change the final code?

This is another common beginner question.

Suppose both operations are performed correctly and conflicts are resolved consistently.

The final project files can be the same.

For example:

Merge result:

A → B → C → F → M


Rebase result:

A → B → C → F → D' → E'
Enter fullscreen mode Exit fullscreen mode

The commit history is different, but the final snapshot can represent the same combined code.

Git’s documentation explicitly notes that the final snapshot can be the same while the history differs.

So the major difference isn’t:

“Which one gives me the correct code?”

It is:

“How do I want the project’s history to look?”

Why do developers like rebase?

One major reason is cleaner history.

Imagine your project has:

main
 ├── feature A
 ├── feature B
 ├── feature C
 └── feature D
Enter fullscreen mode Exit fullscreen mode

If everyone frequently merges main into their feature branches, history can become filled with commits like:

Merge branch 'main' into feature/login
Merge branch 'main' into feature/login
Merge branch 'main' into feature/login
Enter fullscreen mode Exit fullscreen mode

The history becomes harder to read.

With rebase, a developer can update their local feature branch:

git fetch origin
git rebase origin/main
Enter fullscreen mode Exit fullscreen mode

and keep their own commits on top of the latest main.

The result can look much more linear:

A → B → C → D → E → F → G
Enter fullscreen mode Exit fullscreen mode

rather than:

A → B → C ─────────────── M
         \              /
          D → E → F → G
Enter fullscreen mode Exit fullscreen mode

Git’s documentation specifically notes that rebasing can produce a cleaner, linear history.

But if rebase is cleaner, why not always use it?

Because rebase rewrites history.

This is where beginners can get into trouble.

Imagine you’ve already pushed:

A → B → C → D
Enter fullscreen mode Exit fullscreen mode

to GitHub.

Another developer has based their work on commit D.

Now you rebase your branch.

Git may create:

A → B → C → D'
Enter fullscreen mode Exit fullscreen mode

D' is a new commit.

From Git’s perspective:

D ≠ D'
Enter fullscreen mode Exit fullscreen mode

You have rewritten the branch’s history.

If you force-push that rewritten history, the other developer may have to deal with the changed commit ancestry.

That’s why rebasing shared/published branches requires caution. Git’s documentation recommends avoiding rebasing commits that have already been published unless you understand the consequences.

A simple rule for beginners

Remember this:

Rebase your private/local work. Be careful rebasing shared work.

For example:

feature/login
Enter fullscreen mode Exit fullscreen mode

is your personal branch.

You can generally rebase it while you’re developing.

But:

main
Enter fullscreen mode Exit fullscreen mode

is shared by the whole team.

Rewriting main history is usually a very different situation.

What happens when there is a conflict?

Both merge and rebase can have conflicts.

Suppose you changed:

def welcome_message
  "Welcome!"
end
Enter fullscreen mode Exit fullscreen mode

and another branch changed the same lines:

def welcome_message
  "Welcome back!"
end
Enter fullscreen mode Exit fullscreen mode

Git may not know which version you want.

You’ll see a conflict.

With merge:

git merge main
Enter fullscreen mode Exit fullscreen mode

you resolve the conflict and then:

git add .
git commit
Enter fullscreen mode Exit fullscreen mode

or use:

git merge --continue
Enter fullscreen mode Exit fullscreen mode

depending on the merge state/workflow.

With rebase:

git rebase main
Enter fullscreen mode Exit fullscreen mode

you resolve the conflict and then:

git add .
git rebase --continue
Enter fullscreen mode Exit fullscreen mode

If you decide that you don’t want to continue the rebase:

git rebase --abort
Enter fullscreen mode Exit fullscreen mode

Git provides --abort specifically to stop a rebase and attempt to restore the state from before the rebase.

Why can rebase feel harder during conflicts?

Because rebase processes your commits one at a time.

Imagine your feature has:

D → E → F
Enter fullscreen mode Exit fullscreen mode

During rebase, Git essentially replays:

D'
E'
F'
Enter fullscreen mode Exit fullscreen mode

If D conflicts, Git stops.

You resolve it.

Then:

git rebase --continue
Enter fullscreen mode Exit fullscreen mode

Git moves to the next commit.

If E also conflicts, you resolve that one too.

So you may encounter multiple conflict resolutions during one rebase.

This happens because rebase replays the commits individually.

What happens during a merge conflict?

Merge generally considers the two branch tips and their common ancestor to combine the changes.

For example:

              D → E
            /
A → B → C
            \
             F → G
Enter fullscreen mode Exit fullscreen mode

Git compares the relevant histories and attempts to produce one combined result.

If Git can’t automatically determine the correct result, it stops and asks you to resolve the conflict.

Git’s normal merge strategy is a three-way merge using the two branch tips and their merge base.

Rebase is not “better merge”

This is an important point.

You will sometimes hear:

“Always rebase. Merge is bad.”

That’s an oversimplification.

Merge and rebase solve related problems but have different effects on history.

Git’s own documentation doesn’t say that one is universally better. The appropriate choice depends on the project and team workflow.

A team may deliberately prefer merge commits because they want to preserve the history of how branches were integrated.

Another team may prefer a linear history because it makes the project easier to navigate.

Neither approach is universally correct.

Example: working on a feature in a team

Suppose you’re working on:

feature/payment
Enter fullscreen mode Exit fullscreen mode

Meanwhile, main has received new changes.

You want the latest main changes before opening/updating your PR.

You can do:

git fetch origin
git rebase origin/main
Enter fullscreen mode Exit fullscreen mode

Your history becomes:

main:
A → B → C → D

feature:
            E → F
Enter fullscreen mode Exit fullscreen mode

After:

git rebase origin/main
Enter fullscreen mode Exit fullscreen mode

you get:

A → B → C → D → E' → F'
Enter fullscreen mode Exit fullscreen mode

Your feature commits now sit on top of the latest main.

What if you use merge instead?

You could instead do:

git fetch origin
git merge origin/main
Enter fullscreen mode Exit fullscreen mode

The history could become:

          E → F
         / \
A → B → C → D → M
Enter fullscreen mode Exit fullscreen mode

The branch contains the same combined work, but the history records the merge explicitly.

What does git pull --rebase mean?

You may also have seen:

git pull --rebase
Enter fullscreen mode Exit fullscreen mode

This is another source of confusion.

Normally:

git pull
Enter fullscreen mode Exit fullscreen mode

essentially does:

git fetch
+
integration
Enter fullscreen mode Exit fullscreen mode

Depending on your configuration/options, that integration can use merge or rebase. Git documents git pull --rebase as fetching and then rebasing the local work onto the fetched branch.

So:

git pull --rebase
Enter fullscreen mode Exit fullscreen mode

roughly means:

Get latest remote changes
        ↓
Put my local commits on top of them
Enter fullscreen mode Exit fullscreen mode

For example:

Before:

remote: A → B → C
local:           \
                 D → E
Enter fullscreen mode Exit fullscreen mode

After:

A → B → C → D' → E'
Enter fullscreen mode Exit fullscreen mode

git pull vs git pull --rebase

This is a useful distinction:

Regular pull

Depending on configuration:

git pull
Enter fullscreen mode Exit fullscreen mode

can integrate remote changes using merge.

You might get:

A → B → C ───── M
         \     /
          D → E
Enter fullscreen mode Exit fullscreen mode

Pull with rebase

git pull --rebase
Enter fullscreen mode Exit fullscreen mode

gives you:

A → B → C → D' → E'
Enter fullscreen mode Exit fullscreen mode

Again, the goal is usually to avoid unnecessary merge commits in your local branch history.

What should a fresher use?

For someone new to Git, I recommend first becoming comfortable with merge.

Understand:

git switch
git fetch
git merge
git pull
git push
Enter fullscreen mode Exit fullscreen mode

Once you understand branches and commits, learn rebase.

Why?

Because merge is conceptually closer to:

“These two branches have changes. Combine them.”

Rebase requires understanding:

“Take these existing commits and recreate them on a different base.”

Once that mental model clicks, rebase becomes much easier.

A simple decision guide

Use merge when:

  • You want to preserve the actual branching history.
  • The branch is shared by multiple developers.
  • You don’t want to rewrite existing history.
  • Your team prefers merge commits.
  • You are integrating a completed branch into another branch.

Example:

git switch main
git merge feature/login
Enter fullscreen mode Exit fullscreen mode

Use rebase when:

  • You’re updating your own feature branch.
  • You want a cleaner linear history.
  • Your commits haven’t been shared yet.
  • Your team follows a rebase-based workflow.
  • You want to replay your work on top of the latest main.

Example:

git switch feature/login
git fetch origin
git rebase origin/main
Enter fullscreen mode Exit fullscreen mode

One important warning about force push

After rebasing a branch that has already been pushed, you may need:

git push --force-with-lease
Enter fullscreen mode Exit fullscreen mode

Notice that I used:

--force-with-lease
Enter fullscreen mode Exit fullscreen mode

rather than:

--force
Enter fullscreen mode Exit fullscreen mode

--force-with-lease provides an additional safety check so you don't blindly overwrite a remote branch that has moved unexpectedly.

But even with --force-with-lease, you should understand that you're rewriting the remote branch's history.

Don’t casually rebase and force-push a branch that other developers are actively using.

The easiest mental model

If you remember only one thing from this article, remember this:

Merge

Keep both histories and connect them.

      D → E
     /     \
A → B       M
     \     /
      F → G
Enter fullscreen mode Exit fullscreen mode

Rebase

Take my commits and replay them on top of the latest base.

Before:

      D → E
     /
A → B → C → F

After:

A → B → C → F → D' → E'
Enter fullscreen mode Exit fullscreen mode

Final comparison

Imagine you and another developer start from the same point:

A → B
Enter fullscreen mode Exit fullscreen mode

You create:

C → D
Enter fullscreen mode Exit fullscreen mode

They create:

E → F
Enter fullscreen mode Exit fullscreen mode

With merge

Git says:

“You both worked from B. I’ll combine the two histories.”

        C → D
      /      \
A → B         M
      \       /
       E →  F
Enter fullscreen mode Exit fullscreen mode

With rebase

Git says:

“I’ll take your C and D changes and replay them after E and F.”

A → B → E → F → C' → D'
Enter fullscreen mode Exit fullscreen mode

That’s the core difference.

Conclusion

git merge and git rebase aren't simply two commands that do the same thing.

They represent two different approaches to history.

Merge preserves the branching story.

Rebase rewrites your branch so your commits appear on top of another branch.

If you’re working on your own feature branch, rebasing onto the latest main can give you a clean, linear history.

If you’re working with shared history where preserving existing commits and branch relationships matters, merging can be the safer choice.

The most important thing isn’t memorizing:

git merge
Enter fullscreen mode Exit fullscreen mode

versus:

git rebase
Enter fullscreen mode Exit fullscreen mode

It’s understanding what Git is doing to your commit graph.

Once you understand the graph, the commands become much easier to remember.

Quick cheat sheet

# Merge
git switch feature/login
git merge main

# Rebase
git switch feature/login
git rebase main

# Update remote information first
git fetch origin

# Rebase your feature onto remote main
git rebase origin/main

# Continue after resolving a rebase conflict
git add .
git rebase --continue

# Cancel an ongoing rebase
git rebase --abort

# Push rewritten history safely
git push --force-with-lease
Enter fullscreen mode Exit fullscreen mode

Remember:

MERGE → combine histories

REBASE → replay commits on a new base
Enter fullscreen mode Exit fullscreen mode

Top comments (0)