Git Rebase vs Merge
4 min readLet’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#
- Extra Work: dev2 has to either:
- Rebase his/her work on top of dev1 new commits (complicated)
- Create a merge commit (creates messy history)
- Redo his/her work from scratch (time-consuming)
- Lost Context: The original commit sequence and timing is lost.
- 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:
- dev2’s history: D→E→F
- 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 common ancestor (E)
- dev2’s latest commit (F)
- dev1’s latest commit (G)
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:
- Git can find a common ancestor (E)
- Original commits remain intact with the same hashes
- Git can often auto-merge without conflicts
- dev2 doesn’t need to redo any work
- The branch history shows the true sequence of development
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