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.

  • Git
  • Version Control
  • Workflow