Introduction
Every developer working with Git reaches a moment when they need to pull changes from one branch into another—and suddenly face a choice that feels deceptively simple. Should you merge, or should you rebase? The answer shapes your project's history, affects how your team reviews code, and can even prevent merge conflicts down the line. In this guide, we'll explore Git rebase vs merge: a Git & GitHub guide for developers, examining when each approach makes sense, how to execute them safely, and the discipline required to use them well in a team environment.
What is merge? Keeping true history with a merge commit
A merge combines two branches by creating a new commit—called a merge commit—that ties together the histories of both branches. When you merge, Git creates a commit object that explicitly records the point in time when two lines of work came together. This approach preserves the complete, chronological record of how your project evolved.
Let's walk through a real scenario. You're working on a payment feature in a feature branch while your teammate pushes fixes to main. You finish your work and want to integrate your changes:
git switch main
git pull origin main
git merge feature/payment
Git examines the common ancestor of both branches, then applies all commits from feature/payment that aren't in main. If there are no conflicting changes, Git creates a merge commit automatically. You'll see something like:
* commit 7a9f2c1 (HEAD -> main)
|\ Merge: 3d4e5f6 8b2c1a9
| | Author: Your Team <team@company.dev>
| | Date: Wed Jan 10 14:23:45 2024 +0000
| |
| | Merge branch 'feature/payment' into main
| |
| * commit 8b2c1a9 (feature/payment)
| | Author: Developer A <dev@company.dev>
| | Date: Wed Jan 10 14:15:22 2024 +0000
| |
| | Add Stripe integration
| |
* | commit 3d4e5f6
|/ Author: Developer B <dev@company.dev>
| Date: Wed Jan 10 13:45:12 2024 +0000
|
| Update documentation
Why use merge? The merge commit acts as a clear marker: "At this moment, these two pieces of work came together." This is invaluable for understanding the project's structure. When you run git log --graph, you see the exact topology—branches diverging and converging. For code review workflows on GitHub, merging preserves pull request context and makes it clear which commits belonged to which feature.
The trade-off is that your history can become complex. With many features developed in parallel, your Git graph starts to look like a subway map. But that complexity is true—it reflects reality.
What is rebase? Replaying commits for a linear history
Rebase is a different beast entirely. Instead of creating a merge commit, rebase replays your commits on top of the target branch, as if you had started from that branch from the beginning. The result is a perfectly linear history with no branching.
Let's take the same scenario, but use rebase instead:
git switch feature/login
git rebase main
Git finds the common ancestor between feature/login and main, then "replays" each of your feature commits on top of main's latest commit. If you had three commits on your feature branch, Git essentially:
- Temporarily stashes your commits
- Fast-forwards your branch to main's tip
- Applies each of your commits one by one
Your history now looks linear:
* commit a1b2c3d (HEAD -> feature/login)
| Author: Developer A <dev@company.dev>
| Date: Wed Jan 10 14:30:00 2024 +0000
|
| Add password validation
|
* commit 9f8e7d6
| Author: Developer A <dev@company.dev>
| Date: Wed Jan 10 14:28:00 2024 +0000
|
| Add login form component
|
* commit 8c7b6a5 (main, origin/main)
| Author: Developer B <dev@company.dev>
| Date: Wed Jan 10 13:45:12 2024 +0000
|
| Update documentation
Why use rebase? A linear history is easier to read and understand. When you're hunting for the commit that introduced a bug using git bisect, a clean line of commits helps. Your pull request diffs are simpler to review because you're not wading through merge commit noise. For teams that care deeply about commit hygiene, rebase enables that discipline.
The cost is that you're rewriting history. Each replayed commit gets a new hash. If someone else pulled your feature branch before you rebased it, they're now out of sync—and attempting to push will cause chaos.
The golden rule: Never rebase commits others have already pulled
This rule is non-negotiable. Once you push a commit that another developer has pulled, that commit is published. Rewriting it is a violation of the implicit contract between team members.
Imagine this scenario: You push feature/auth to the repository. Your teammate pulls it and starts building on top of it. Then you realize you made a typo in your commit message and decide to rebase and force-push a "corrected" version. Your teammate now has commits that no longer exist in the repository. When they try to push their work, Git gets confused. They might accidentally undo your changes, or their push might fail entirely. At best, you've wasted their time troubleshooting. At worst, you've corrupted the shared history.
The golden rule protects team velocity: rebase your commits before you share them; never rebase commits that are already shared.
This means:
- Rebase on your local feature branch before opening a pull request—go ahead.
- Rebase a branch that only you have pulled—safe.
- Rebase a branch that's been pushed and others have pulled—absolutely not.
If you've accidentally pushed and someone has pulled, communicate with the team, agree to do a hard reset together, or just live with the slightly messier history and merge it instead.
Team choice: Rebase locally, merge or squash on the server
The most pragmatic workflow combines the benefits of both approaches. Your team establishes a convention:
- Locally: Developers rebase their feature branches on main before pushing. This ensures your feature branch has a clean, linear sequence of commits on top of the latest main.
- On the server (GitHub): When merging a pull request, use either a regular merge commit or a squash merge, depending on your preference.
The local rebase approach:
# You're on your feature branch
git rebase main
# If conflicts arise, resolve them
# Then continue the rebase
git rebase --continue
# Force-push only to your own branch
git push origin feature/payment --force-with-lease
The --force-with-lease flag is safer than a naked --force. It fails if someone else has pushed to the branch since you last fetched, preventing accidental data loss.
On GitHub, you have three merge options:
- Create a merge commit: Preserves the full history. Good for important features where you want to see the exact moment integration happened.
- Squash and merge: Combines all commits from the PR into a single commit. Clean, linear main branch. Good for small features or bugfixes where individual commits within the PR don't matter.
- Rebase and merge: Replays your PR commits directly onto main without a merge commit. Linear history plus per-commit granularity. Good for teams that value individual commit messages.
Choose one strategy and stick with it. Many teams use squash-and-merge for main, which keeps the primary branch extremely clean while still preserving detailed histories in pull requests for code review purposes.
Practical workflow example
Here's how a team might orchestrate this in practice:
# Developer starts a feature
git switch -c feature/user-dashboard
# Commits several times
git commit -m "Add dashboard layout"
git commit -m "Add chart component"
git commit -m "Connect to API"
# Before pushing, rebase on latest main
git fetch origin
git rebase origin/main
# Resolve any conflicts if they exist
# git rebase --continue
# Push the clean feature branch
git push origin feature/user-dashboard
# On GitHub, open a PR, get review, then merge using
# "Squash and merge" to keep main clean
The result: your pull request shows all three commits for review clarity, but main receives just one well-crafted commit summarizing the entire feature. Future developers reading main history see a curated, high-level narrative of the project's evolution.
Common mistakes
Mistake 1: Rebasing shared branches. You rebase a branch that others are working on, causing their local work to diverge from the remote. Solution: only rebase branches you own.
Mistake 2: Rebasing, then merging, then rebasing again. Mixing rebase and merge on the same branch creates confusing history. Solution: pick one integration strategy per branch and stick with it.
Mistake 3: Ignoring conflicts during rebase. Rebase conflicts can be subtle. You might resolve them incorrectly and not realize it until tests fail. Solution: run your test suite after resolving a rebase, and take time to review each conflict carefully.
Mistake 4: Using --force instead of --force-with-lease. A naked force-push will overwrite any commits pushed by teammates. Solution: always use --force-with-lease to add a safety check.
Key takeaways
- Merge creates a merge commit and preserves the true, branching history. Use it when you want an explicit record of integration points.
- Rebase replays commits linearly on top of the target branch, creating a cleaner but rewritten history. Use it locally before sharing.
- The golden rule: Never rebase commits that others have already pulled. Rebase before you push; merge or squash after you push.
- Best practice for teams: Developers rebase locally, then use squash-and-merge or merge-commit on the server depending on team preference.
- Always use
--force-with-leasewhen force-pushing, never a naked--force. - Choose one integration strategy per repository and document it in your team's Git workflow guide.






Comments
Loading comments…