Both git merge and git rebase integrate changes from one branch into another. They produce the same final code but a different history. Choosing well keeps history readable and avoids painful mistakes.
Merge: join histories
git switch feature
git merge main # bring main's changes into feature
# or, when finishing:
git switch main
git merge feature
If both branches have new commits, Git creates a merge commit with two parents:
* e5f6 Merge branch 'feature'
|\
| * c3d4 Add payment form (feature)
| * b2c3 Add cart model (feature)
* | a1b2 Fix header (main)
|/
* 9f8e Initial
Pros: history is preserved exactly as it happened; non-destructive; safe for shared branches. Cons: frequent “merge main into feature” commits clutter the log.
Fast-forward
If main has no new commits since feature branched off, Git simply moves main forward — no merge commit. Force one anyway with --no-ff to keep a visible record of the feature branch.
Rebase: replay commits
git switch feature
git rebase main
Git takes your feature commits and re-applies them on top of the latest main, creating new commits with new hashes:
* c3d4' Add payment form (feature)
* b2c3' Add cart model (feature)
* a1b2 Fix header (main)
* 9f8e Initial
Pros: linear, easy-to-read history; git log and git bisect are simpler.
Cons: rewrites commits — dangerous on branches others have based work on; conflicts may need resolving commit by commit.
The golden rule
Don’t rebase commits that others have already pulled. Rebasing creates new commits; anyone holding the old ones ends up with duplicate or diverged history. Rebase your local or personal branches freely; merge (or use the platform’s merge button) for shared branches.
After rebasing a branch you’d already pushed (your own feature branch), push with:
git push --force-with-lease
Interactive rebase: clean up before sharing
git rebase -i HEAD~4
pick b2c3 Add cart model
squash c3d4 WIP
reword d4e5 Add payment form
drop e5f6 Debug logging
Squash “WIP” and “fix typo” commits, reword messages, drop accidental commits — then open a pull request with a tidy series of meaningful commits.
Handling conflicts
Merge: resolve once, then git add + git merge --continue (or git commit).
Rebase: Git stops at each conflicting commit. Resolve, git add, git rebase --continue. To give up: git rebase --abort returns to where you started.
Enable conflict reuse so repeated conflicts resolve themselves:
git config --global rerere.enabled true
Pull: merge or rebase?
git pull = fetch + merge by default, which creates small merge commits whenever you and a colleague both committed. Many teams prefer:
git config --global pull.rebase true
Your local unpushed commits are replayed on top of the remote ones — a linear history without “Merge branch ‘main’ of …” noise. It’s safe because only your unpushed commits are rewritten.
Squash merges on GitHub/GitLab/Gitea
Platforms offer three buttons: merge commit, squash and merge (all feature commits become one commit on main) and rebase and merge. Squash merging is popular: main gets one clean commit per pull request, while the detailed history stays in the PR.
Which should you use?
| Situation | Use |
|---|---|
| Updating your private feature branch with main | rebase |
| Cleaning up commits before a PR | rebase -i |
| Integrating a finished feature into main | merge (or squash/rebase via the platform) |
| Shared long-lived branches | merge |
| Pulling with local unpushed commits | pull --rebase |
There’s no universally right answer — consistency within a team matters most. Agree on a policy (e.g. “rebase locally, squash-merge PRs”) and protect main.
Related: Git commands cheat sheet, branching strategies and cherry-pick.