Let's consider three developers - dev1, dev2, and dev3 - working on a common repository.
Every dev with the same repository state.
A---B---C (main)
dev1 creates a feature branch and makes two commits.
A---B---C (main)
\
D---E (feat-dev1)
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
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)
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.
dev1 needs to force push because the branch rewroted the history(D => D', E => E').
# dev1 command
git push --force origin feat-dev1
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.
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').
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.
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.
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.
dev1 has merged main into feat-dev1, creating this structure:
A---B---C (main)
\ \
D---E---G (feat-dev1) // G is the merge commit
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:
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).
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