Git rebase vs Git merge

Let's consider three developers - dev1, dev2, and dev3 - working on a common repository.

Initial State

Every dev with the same repository state.

A---B---C (main)

Step 1: dev1 creates a Feature Branch

dev1 creates a feature branch and makes two commits.

A---B---C (main)
      \
      D---E (feat-dev1)

Step 2: dev1 pushes to remote

dev1 pushes his/her branch(feat-dev1) to the remote shared repository so others can review or contribute.

# dev1's command
      git push origin feat-dev1

Step 3: dev2 pulls and works on top of dev1 work

dev2 pulls dev1 branch and adds his/her own commit.

# dev2 commands
      git fetch
      git checkout feat-dev1
      git add .
      git commit -m "Add validation logic"

Now dev2 local repository looks like.

A---B---C (main)
      \
      D---E---F (feat-dev2)

Step 4: Meanwhile, dev1 Rebases

dev1 decides to rebase the branch to incorporate new/recent changes from main.

# dev1 commands
      git checkout feat-dev1
      git rebase main

This creates a new history for that branch with new commit hashes(NOTE: changes in the file remains same, but the commit hash change).

A---B---C (main)
      \
      D'---E' (feat-dev1)

Note that D' and E' are new commits with different hashes than D and E(original commit by dev1), even though they contain the same changes.

Step 5: dev1 force pushes

dev1 needs to force push because the branch rewroted the history(D => D', E => E').

# dev1 command
      git push --force origin feat-dev1

Step 6: The Problems Begin

Now when dev2 tries to push his/her work.

# dev2 command
      git push origin feat-dev1

He/she gets an error like:

! [rejected] feat-dev1 -> feat-dev1 (non-fast-forward)
      error: failed to push some refs to 'origin/repo.git'
      hint: Updates were rejected because the tip of your current branch is behind
      hint: its remote counterpart. Integrate the remote changes (e.g.
      hint: 'git pull ...') before pushing again.

Step 7: dev2 tries to pull

When dev2 tries to pull.

# dev2 command
      git pull

He/she gets merge conflicts because his/her commit F is based on the old commits D and E, which no longer exist in the remote repository(because it is replaced by D' and E').

Step 8: Confusion for dev3

dev3, who hasn't even started working yet but pulls the latest code.

# dev3 commands
      git fetch
      git checkout feat-dev1

dev3 gets dev2 rebased version (D', E') and has no idea that dev2 has already built work on the original commits. When dev2 eventually gets his/her work sorted and pushes, it will likely cause more conflicts.

The Aftermath

  1. Extra Work: dev2 has to either:
  2. Lost Context: The original commit sequence and timing is lost.
  3. Broken References: If anyone referenced the original commits in issues or pull requests, those references are now pointing to non-existent commits.

Better Approach: Merging Instead of Rebasing

Instead of rebasing, dev1 should have merged the main branch.

# dev1 better approach
      git checkout feat-dev1
      git merge main

This would result in

A---B---C (main)
      \ \
      D---E---G (feat-dev1)

Where G is a merge commit. This preserves the existing commits D and E that dev2 based his/her work on, while still incorporating the latest changes from main.

What Happens When dev2 Pulls After a Merge?

Even with the merge approach, dev2 still needs to pull the latest code, but there's a crucial difference in how Git handles this situation compared to the rebase scenario.

Merge Scenario: After dev1 Merges Main

dev1 has merged main into feat-dev1, creating this structure:

A---B---C (main)
      \ \
      D---E---G (feat-dev1) // G is the merge commit

What Happens When dev2 Pulls

dev2 local repository currently looks like:

A---B---C (main)
      \
      D---E---F (feat-dev1) // F is dev2's commit

When dev2 runs git pull, Git sees:

  1. dev2's history: D→E→F
  2. Remote history: D→E→G

Since the commits D and E are the same in both histories (same commit hashes), Git can perform a clean three-way merge between:

The result would be:

A---B---C (main)
      \ \
      D---E---G---H (feat-dev1)
      \ /
      F---/

Where H is an automatic merge commit that combines dev2's changes (F) with dev1's merge from main (G).

Key Difference from Rebase

With the merge approach:

This is why merging is safer for shared branches - it preserves the commit history that others might be building upon, whereas rebasing rewrites it completely.

The key principle is: in public repositories, add to history through merges rather than rewriting it through rebases.

Rebase is really a problem in public(OSS) repositories, since many people work on same branches, and rebasing frequently wil change the commit hash and cause conflicts frequently