Git Rebase vs Git Merge: Differences, Examples and Which One to Use
git merge and git rebase both integrate changes from one branch into another, and both are correct. The difference is the history they leave behind: merge preserves what actually happened, including a merge commit; rebase rewrites your commits so they appear to have been made on top of the latest target branch, giving a straight line. Teams argue about this endlessly, so this tutorial shows both on the same repository, explains the trade-offs, and ends with a policy that works for pull-request-based workflows on GitHub, GitLab and Azure Repos.
Prerequisites: Git 2.40+ and the basics from Git branches. All commands work on Linux, macOS and Windows (Git Bash).
Set up a repository to experiment
mkdir rebase-vs-merge && cd rebase-vs-merge
git init -b main
echo "v1" > app.txt && git add . && git commit -m "C1: initial app"
echo "v2" >> app.txt && git commit -am "C2: add v2"
# Feature branch with two commits
git switch -c feature/login
echo "login page" > login.txt && git add . && git commit -m "F1: login page"
echo "login tests" > login_test.txt && git add . && git commit -m "F2: login tests"
# Meanwhile main moves on
git switch main
echo "hotfix" > hotfix.txt && git add . && git commit -m "C3: hotfix"
git log --oneline --graph --all
# * 3c3c3c3 (HEAD -> main) C3: hotfix
# | * f2f2f2f (feature/login) F2: login tests
# | * f1f1f1f F1: login page
# |/
# * 2c2c2c2 C2: add v2
# * 1c1c1c1 C1: initial appThe branches have diverged: main has C3, feature/login has F1 and F2, and they share C2 as the merge base. Let’s integrate the feature both ways.
Option A – git merge
git switch main
git merge feature/login
# Merge made by the 'ort' strategy.
git log --oneline --graph
# * 9a9a9a9 (HEAD -> main) Merge branch 'feature/login'
# |
# | * f2f2f2f (feature/login) F2: login tests
# | * f1f1f1f F1: login page
# * | 3c3c3c3 C3: hotfix
# |/
# * 2c2c2c2 C2: add v2
# * 1c1c1c1 C1: initial appA new merge commit with two parents ties the histories together. Nothing that existed before was changed: F1 and F2 keep their hashes, which means anyone who already had them is unaffected. The price is a non-linear graph that becomes hard to read with dozens of parallel branches. If main had not moved (no C3), Git would have done a fast-forward instead: simply moving the main pointer to F2 with no merge commit. Use --no-ff to force a merge commit anyway (useful to mark where a feature landed) or --ff-only to refuse a merge when a fast-forward is impossible.
Option B – git rebase
# Undo the merge to try the other way (safe here: nothing was pushed)
git reset --hard 3c3c3c3
git switch feature/login
git rebase main
# Successfully rebased and updated refs/heads/feature/login.
git log --oneline --graph --all
# * e2e2e2e (HEAD -> feature/login) F2: login tests
# * e1e1e1e F1: login page
# * 3c3c3c3 (main) C3: hotfix
# * 2c2c2c2 C2: add v2
# * 1c1c1c1 C1: initial app
# Now main can fast-forward: linear history, no merge commit
git switch main
git merge --ff-only feature/loginRebase took each feature commit, replayed it on top of C3 and created new commits (e1, e2) with new hashes; the old f1/f2 are now unreferenced. The history reads as if you had started the feature after the hotfix. Clean, linear, easy to git bisect, but it is a rewrite, and that has consequences when the branch is shared.
The golden rule of rebasing
Never rebase a branch that other people have based work on. Rebasing main, develop or any shared branch replaces commits your teammates already have; their next pull produces duplicate commits and conflicts. Rebase your own feature branches; merge into shared ones. If you must rewrite a branch you already pushed (your own PR branch), push with git push --force-with-lease, which refuses to overwrite commits you have not seen.
Handling conflicts
Conflicts happen in both cases when the same lines changed on both sides. The workflow differs slightly:
| merge | rebase | |
|---|---|---|
| When conflicts appear | Once, for the whole merge | Commit by commit, as each is replayed |
| Fix, then | git add <file> → git commit | git add <file> → git rebase --continue |
| Skip a commit | n/a | git rebase --skip |
| Give up | git merge --abort | git rebase --abort |
| “Ours” and “theirs” | ours = current branch | reversed: ours = the branch you rebase onto |
git rebase main
# CONFLICT (content): Merge conflict in app.txt
git status # shows which commit is being replayed
# ... edit app.txt, remove <<<<<<< ======= >>>>>>> markers ...
git add app.txt
git rebase --continue
# Remember resolutions so repeated conflicts resolve automatically
git config --global rerere.enabled trueInteractive rebase: clean up before you open a PR
The killer feature of rebase is -i: rewrite the last N commits of your own branch to squash “fix typo” commits, reorder, reword or drop them, so reviewers see a coherent story.
git rebase -i main # or: git rebase -i HEAD~4
# Editor opens with one line per commit (oldest first):
# pick e1e1e1e F1: login page
# pick e2e2e2e F2: login tests
# pick a3a3a3a fix typo
# pick b4b4b4b wip
#
# Change to:
# pick e1e1e1e F1: login page
# squash a3a3a3a fix typo # melt into F1, edit the message
# pick e2e2e2e F2: login tests
# fixup b4b4b4b wip # melt into F2, discard its message
# Other verbs: reword (edit message), edit (stop to amend), drop, exec (run a command, e.g. tests)Shortcut: commit fixes with git commit --fixup=<hash>, then git rebase -i --autosquash main arranges them automatically. Set git config --global rebase.autosquash true to make it the default.
git pull: merge or rebase?
By default git pull merges the remote branch into your local one, producing “Merge branch ‘main’ of …” commits that clutter history. Most teams prefer rebasing local commits on top of the remote:
git pull --rebase # one-off
git config --global pull.rebase true # always
git config --global rebase.autoStash true # stash/unstash dirty work automaticallyMerge strategies on the pull request
Hosting platforms offer three buttons that map to what you learned above:
| Button | Result on main | Good for |
|---|---|---|
Merge commit (--no-ff) | All PR commits + a merge commit | Keeping full history; easy revert of a whole feature (git revert -m 1) |
| Squash and merge | One commit per PR | Small PRs, noisy commit habits, very readable main |
| Rebase and merge | PR commits replayed linearly, no merge commit | Curated commits (after interactive rebase), bisect-friendly history |
A policy that works for most teams
- Work on short-lived feature branches; keep them current with
git rebase main(orgit pull --rebase origin main) instead of merging main into the feature. - Before opening the PR,
git rebase -ito squash fixups into meaningful commits; push with--force-with-lease. - Integrate into
mainthrough the PR with Squash or Rebase and merge; protectmainso nobody force-pushes it. - Never rebase
main, release branches, or any branch someone else has checked out.
Recovering from a bad rebase
git reflog # every position HEAD had, including before the rebase
# e2e2e2e HEAD@{0}: rebase (finish): returning to refs/heads/feature/login
# f2f2f2f HEAD@{3}: commit: F2: login tests <- the state before rebase
git reset --hard HEAD@{3} # branch is exactly as before
# or, non-destructively:
git branch backup-before-rebase f2f2f2fRewritten commits are not deleted immediately; they stay reachable through the reflog for at least 30 days, so a mistaken rebase is almost always recoverable.
Key takeaways
- Merge preserves history and adds a merge commit; rebase rewrites commits to produce a linear history.
- Rebase your own branches, merge into shared ones; never rebase public branches.
- Interactive rebase (
-i,--autosquash) is how you produce reviewable commits. pull.rebase=trueand--force-with-leasemake the rebase workflow safe day to day.- The reflog is your undo button.
Next tutorial
Next: git stash and git cherry-pick. Official docs: Pro Git – Rebasing, git-rebase reference.
Retour parcours Git — hub de la série et leçons sœurs.


