git merge and git rebase do the same job: bring changes from one branch into another. The history they leave behind differs, and that history decides which command fits your case.
What each command does
Merge writes a commit with two parents. That commit records the moment two paths met, and every feature commit survives as written.
git checkout feature
git merge main
* merge branch 'main' into feature
|\
| * add user validation
| * create user model
|/
* main commit
Rebase replays your commits on top of the destination. It discards the originals and writes copies with the same content and new parents. The hashes change, so every old reference to those commits (a branch, a tag, a link in the PR) points at an object nobody has anymore.
git checkout feature
git rebase main
* add user validation
* create user model
* main commit
When merge is the better fit
- The branch has several authors, and the record of who contributed what has to stay in history.
- The branch lives for weeks, as a release branch does, and you want the real order of events preserved.
- The team follows Git Flow, and the merge commit becomes the meeting point between release and main.
In a pull request the merge commit carries metadata: who approved, the PR number, the date. The GitHub page Configuring pull request merges lists the three accepted formats and what each one writes into history.
When rebase is the better fit
- Your branch holds one to three commits and main has moved. You want to catch up without stacking merge commits on the line.
- You are shaping the final message ahead of a squash merge.
- The team reads history with
git log --onelineand hunts regressions withgit bisect. A straight line shortens both.
git fetch origin
git rebase origin/main
# conflict: resolve in the files and continue
git add .
git rebase --continue
Public history stays put
Pro Git opens a section titled "Do not rebase public history" with one rule that takes no exceptions: once another developer has pulled the commits you rewrote, the rebase forces a force push and splits history in two. Each side works on a different version of the same commit, and the next git pull mixes them together.
Rewrite commits only you have pulled. Branches other people read get a merge. When a rebase goes wrong, git reflog keeps the discarded commits for days and git rebase --abort returns you to where you started.
Squash merge
A squash merge compresses every feature commit into one before main receives it. Main turns into a line with one commit per feature, and the PR discussion keeps the detail the commit message drops.
Teams that squash give up the sequence of attempts inside the branch. In exchange git log on main reads like a changelog. The arrangement most teams land on: small commits in the branch, squash at merge time.
git checkout main
git merge --squash feature-branch
git commit -m "feat(auth): add JWT refresh token rotation"
The full flow
# 1. new branch from up-to-date main
git checkout main && git pull
git checkout -b feature/user-validation
# 2. local commits as you work
git add . && git commit -m "feat: add user model"
git commit -m "feat: add validation logic"
# 3. before the PR, catch up with main
git fetch origin
git rebase origin/main
# 4. conflict: resolve and continue
git rebase --continue
# 5. push and open the PR, with squash merge enabled on the repo
Rebase keeps the branch current until the PR opens. Squash merge keeps main readable after it lands. The two commands occupy different moments of the flow.
References
- Pro Git, chapter 3.6: Rebasing (Scott Chacon and Ben Straub, Apress)
- git-rebase and git-merge (official Git documentation)
- Configuring pull request merges (GitHub Docs)
Enjoyed this content?
I build web products and AI solutions the right way — solid architecture, maintainable code, and real delivery.
Let's talk