DEV Community

Cover image for A Guide to Understanding the Nuances of Git Merge and Rebase
Kev
Kev

Posted on

A Guide to Understanding the Nuances of Git Merge and Rebase

Introduction

I've seen many developers avoid using rebase due to its complexity. I've also noticed that some teams even forbid rebase in their projects altogether.

In this guide, we will walk through the basics of managing code changes in Git using both merge and rebase strategies. This tutorial aims to clarify these often misunderstood commands with a hands-on demonstration.

I may be too verbose in walking you through these steps one by one, but I suggest you start from the Recap section, or jump straight to the Key Takeaways section.

Table of Contents

Scenario Setup

Initialise a Git Repository and Create a Starting File

First, create a new directory and initialise a Git repository:

mkdir git-nuance-demo-merge && \
cd git-nuance-demo-merge && \
git init && \
git branch -M main
Enter fullscreen mode Exit fullscreen mode

Next, create a text file named example.txt:

touch example.txt
Enter fullscreen mode Exit fullscreen mode

Make Initial Commits A and B

Now, add a first line to example.txt and commit it as Commit A:

+Initial content
Enter fullscreen mode Exit fullscreen mode

Proceed by adding a second line and commit it as Commit B:

Initial content
+Added by leader
Enter fullscreen mode Exit fullscreen mode

Bseline Commit Graph

Now we can visualize the project's baseline with this commit graph:

A---B [main]
Enter fullscreen mode Exit fullscreen mode

Create a Feature Branch

Assuming developer Kev needs to add a new feature, he branches off from main:

git checkout main && \
git checkout -b feature
Enter fullscreen mode Exit fullscreen mode

Commit C

While Kev works on the feature branch, another developer, Dash, updates the main branch. Dash modifies the second line in Commit C:

git checkout main
Enter fullscreen mode Exit fullscreen mode
Initial content
-Added by leader
+Modified by Dash
Enter fullscreen mode Exit fullscreen mode

Commit D (Developing on Feature Branch)

Simultaneously, Kev modifies the same line on his branch in Commit D:

git checkout feature
Enter fullscreen mode Exit fullscreen mode
Initial content
-Added by leader
+Modified by Kev
Enter fullscreen mode Exit fullscreen mode

Initial Git Commit Graph

The commit graph is now:

A---B---C [main]
     \
      D [feature]
Enter fullscreen mode Exit fullscreen mode

And check their Git history details by running git log main and git log feature:

The commit IDs (hashes) will be different if you try following my steps from the beginning on your device.

  • main
/workspaces/git-nuance-demo-merge (main) $ git log main

commit ac466bea2d13762608660e27798ba08b840529db (HEAD -> main)
Author: Dash <dash@test.com>

    C

commit fd6ecc11625c41c06e150015254b16a620c1cd44
Author: Leader <leader@test.com>

    B

commit 9d00689a138f8d1fd6b7d370bae29bcfb27fec19
Author: Leader <leader@test.com>

    A

Enter fullscreen mode Exit fullscreen mode
  • feature
/workspaces/git-nuance-demo-merge (main) $ git log feature

commit 58beb1e4f1d47f9bf1364be906a2e22b5280be1d (HEAD -> feature)
Author: Kev <kev@test.com>

    D

commit fd6ecc11625c41c06e150015254b16a620c1cd44
Author: Leader <leader@test.com>

    B

commit 9d00689a138f8d1fd6b7d370bae29bcfb27fec19
Author: Leader <leader@test.com>

    A

Enter fullscreen mode Exit fullscreen mode

Decision Time: Merging vs. Rebasing

Imagine Kev needs to release the features in branch feature, he faces a decision: to merge or rebase his feature branch onto the updated main branch.:

  • Merge: merge feature into main and release main to production environment.
  • Rebase: Rebase feature onto main, and release feature.

We all know that Kev and Dash both edited the second line, meaning a conflict is definitely going to happen in both options.

Next, we're going to apply both options and examine the different consequences of each.

Before continuing, copy the initial status into two separate folders so that we can apply each option in its own folder:

cd .. && \
cp -r git-nuance-demo-merge git-nuance-demo-rebase

Option 1: Merge feature into main

Merge

Merging results in a new commit on main that combines the changes from both branches:

cd git-nuance-demo-merge
Enter fullscreen mode Exit fullscreen mode
git checkout main
Enter fullscreen mode Exit fullscreen mode
git merge feature
Enter fullscreen mode Exit fullscreen mode

merge feature into main

Merge Conflict

This approach creates a merge conflict that Kev resolves by favouring his changes:

Initial content
<<<<<<< HEAD
Modified by Dash
=======
Modified by Kev
>>>>>>> feature
Enter fullscreen mode Exit fullscreen mode

Pay attention to the terminal console:

/workspaces/git-nuance-demo-merge (main) $ git merge feature
Auto-merging example.txt
CONFLICT (content): Merge conflict in example.txt
Automatic merge failed; fix conflicts and then commit the result.
Enter fullscreen mode Exit fullscreen mode

Check this out: "fix conflicts and then commit the result"

It instructs us to both fix and commit so that the conflicts can be resolved. This means a new commit is needed when resolving merge conflicts.

Resolve it by selecting "Accept Incoming Change", which in this case means accepting the changes from the feature branch.

fix git merge conflict and commit

If you check the log by running git log main, you'll notice the new merge commit in the logs saying "Merge branch...":

/workspaces/git-nuance-demo-merge (main) $ git log main

commit 4d6ac5eb811be1a6130c6bbcde215946c618dd95 (HEAD -> main)
Merge: ac466be 58beb1e
Author: Kev <kev@test.com>

    Merge branch 'feature'

commit 58beb1e4f1d47f9bf1364be906a2e22b5280be1d (feature)
Author: Kev <kev@test.com>

    D

commit ac466bea2d13762608660e27798ba08b840529db
Author: Dash <dash@test.com>

    C

commit fd6ecc11625c41c06e150015254b16a620c1cd44
Author: Leader <leader@test.com>

    B

...
Enter fullscreen mode Exit fullscreen mode

Wondering what the change is in this new commit?

Run git diff 4d6ac5e~ 4d6ac5e (4d6ac5e is the commit ID (hash))

/workspaces/git-nuance-demo-merge (main) $ git diff 4d6ac5e~ 4d6ac5e
diff --git a/example.txt b/example.txt
index 22ad65d..d49a266 100644
--- a/example.txt
+++ b/example.txt
@@ -1,2 +1,2 @@
 Initial content
-Modified by Dash
+Modified by Kev
Enter fullscreen mode Exit fullscreen mode

If you take a deeper look at the details of this new commit, pay special attention to the line:

Merge: ac466be 58beb1e

What does it mean?

This merge commit has two parents, pointing to commit C and commit D:

merge commit parents

Graph After Merge

After resolving the conflict, the commit graph looks like:

A---B---C---M [main]
     \       /
      D [feature]
Enter fullscreen mode Exit fullscreen mode

Option 2: Rebase feature onto main

Before continuing with the rebase process, take a look at the feature branch's logs at this point:

/workspaces/git-nuance-demo-merge (main) $ git log feature

commit 58beb1e4f1d47f9bf1364be906a2e22b5280be1d (Head -> feature)
Author: Kev <kev@test.com>

    D
...
Enter fullscreen mode Exit fullscreen mode

The commit D's ID (hash) is 58beb1e....

The change in this commit is:

/workspaces/git-nuance-demo-merge (main) $ git diff 58beb1e~ 58beb1e
diff --git a/example.txt b/example.txt
index a53a0e2..d49a266 100644
--- a/example.txt
+++ b/example.txt
@@ -1,2 +1,2 @@
 Initial content
-Added by leader
+Modified by Kev
Enter fullscreen mode Exit fullscreen mode

The commit ID and changes will be altered after the rebase; we'll see why.

Rebase

Let's now start the rebase process.

Alternatively to option 1, rebasing involves integrating the changes from main directly into the feature branch history.

cd git-nuance-demo-rebase
Enter fullscreen mode Exit fullscreen mode
git checkout feature
Enter fullscreen mode Exit fullscreen mode
git rebase main
Enter fullscreen mode Exit fullscreen mode

rebase feature onto main

Rebase Conflict

It creates a merge conflict that is almost identical to the one in option 1:

Initial content
<<<<<<< HEAD
Modified by Dash
=======
Modified by Kev
>>>>>>> 58beb1e (D)
Enter fullscreen mode Exit fullscreen mode

But what's in the terminal console is different:

/workspaces/git-nuance-demo-rebase (feature) $ git rebase main
Auto-merging example.txt
CONFLICT (content): Merge conflict in example.txt
error: could not apply 58beb1e... D
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".
Could not apply 58beb1e... D
Enter fullscreen mode Exit fullscreen mode

It instructs us to "Resolve all conflicts manually", and continue with git rebase --continue.

Alright, let's follow the hint to resolve the conflict (also by selecting "Accept Incoming Change").

git add example.txt && \
git rebase --continue
Enter fullscreen mode Exit fullscreen mode

fix git rebase conflict and commit

Take a look at the content after running git rebase --continue:

D

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# interactive rebase in progress; onto 05cfdd2
# Last command done (1 command done):
#    pick 58beb1e D
# No commands remaining.
# You are currently rebasing branch 'feature' on '05cfdd2'.
#
# Changes to be committed:
# modified:   example.txt
#
Enter fullscreen mode Exit fullscreen mode

Git offers us an editor to edit the commit message of D.

You can rewrite commit D by editing the message (lines starting with "#" will be ignored).

I made no changes and just closed the commit editor, so the rebase process proceeded with the rewriting of commit D.

Now that the rebase process has finished, take a look at the commit history of the feature branch:

/workspaces/git-nuance-demo-rebase (feature) $ git log feature

commit 047c3480da20d9dd6c530d3698b4e047f1f1d9d9 (HEAD -> feature)
Author: Kev <kev@test.com>

    D

commit ac466bea2d13762608660e27798ba08b840529db (main)
Author: Dash <dash@test.com>

    C

...
Enter fullscreen mode Exit fullscreen mode

Git Logs

Pay special attention to the commit ID of the new D.

It's now 047c348.... Remember what it was before the rebase? It was 58beb1e....

And the change in this commit is:

/workspaces/git-nuance-demo-rebase (feature) $ git diff 047c348~ 047c348
diff --git a/example.txt b/example.txt
index 22ad65d..d49a266 100644
--- a/example.txt
+++ b/example.txt
@@ -1,2 +1,2 @@
 Initial content
-Modified by Dash
+Modified by Kev
Enter fullscreen mode Exit fullscreen mode

See? The change is also different from the previous commit D because the commit D was rewritten in the rebase process.

Graph After Rebase

Rebase modifies the original commit D on the feature branch:

A---B---C [main]
         \
          D' [feature]
Enter fullscreen mode Exit fullscreen mode

In this case, we performed a history rewrite, causing the original commit D to disappear and the new commit D' to emerge.

Recap

Commit History

Now let's recap the primary steps and Git histories in both options we covered.




Option 1: Merge Option 2: Rebase
Commit Diff Author
Merge ```diff -Modified by Dash +Modified by Kev ``` Kev
D ```diff -Add by leader +Modified by Kev ``` Kev
C ```diff -Add by leader +Modified by Dash ``` Dash
B ```diff +Add by leader ``` Leader
A ... Leader
Commit Diff Author
D' ```diff -Modified by Dash +Modified by Kev ``` Kev
D (actually disappeared) ```diff -Add by leader +Modified by Kev ``` Kev
C ```diff -Add by leader +Modified by Dash ``` Dash
B ```diff +Add by leader ``` Leader
A ... Leader

You can also view the histories in my demo repositories:

Commit Graph of Taking Each Option

  • Merge
A---B---C---M [main]
     \       /
      D [feature]
Enter fullscreen mode Exit fullscreen mode
  • Rebase
A---B---C [main]
         \
          D' [feature]
Enter fullscreen mode Exit fullscreen mode

The obvious difference is that there are five commits (A-B-C-D-M) after a merge, and four (A-B-C-D') after a rebase (D disappears because it is rewritten).

Git Rebase can be Dangerous

Merge preserves the exact historical sequence of changes and is generally easier for beginners to understand and handle, especially in a collaborative environment.

In contrast, Rebase rewrites the commit history to make it appear as if your changes were created on top of the latest remote commits.

This can result in a cleaner, more linear commit history but can be confusing because it alters the commit history. This might be trickier in a collaborative project unless all contributors are comfortable with Git.

This is typical evidence of why Git rebase can be dangerous:

In more complex, real-world scenarios, there is a risk that you could rewrite, overwrite, or delete another developer's commit from the history, preventing your teammates from finding their previously made commits.

There are some basic principles in using rebase like "Don’t rebase a branch that’s been published remotely" and "Create a backup branch from the tip of the branch you’re about to rebase" etc.

There are some basic principles for using rebase, such as "Don’t rebase a branch that’s been published remotely" and "Create a backup branch from the tip of the branch you’re about to rebase".

Choosing between merge and rebase based on your project's needs is a broad topic that is not comprehensively covered within the scope of this post. For more detailed information, I recommend reading "Differences Between Git Merge and Rebase — and Why You Should Care".

Key Takeaways: Merge or Rebase?

Both strategies have their merits:

  • Merge: You create a new commit that shows how conflicts were resolved, allowing your teammates to clearly see the resolution process.

  • Rebase: You modify existing commits. When resolving conflicts during a rebase, you may alter code or commit messages that others have previously made. This can disrupt collaborative work if not used carefully.

Top comments (0)