Skip to main content
TWYTech World by Yashrajsinh

Git Rebase vs Merge Complete Guide

Y
Yashrajsinh
··12 min read·Intermediate

Git Rebase vs Merge Complete Guide

The rebase versus merge debate is one of the most discussed topics in Git. Both commands integrate changes from one branch into another, but they do it in fundamentally different ways that affect your project history, your team's workflow, and your ability to debug problems later. Understanding the tradeoffs lets you choose the right tool for each situation rather than dogmatically picking one over the other.

This guide explains how merge and rebase work internally, when each approach is appropriate, how to handle conflicts in both workflows, and how to use interactive rebase to craft a clean commit history before sharing your work.

What You Will Learn

This guide explains the mechanics and trade-offs of Git merge versus Git rebase. You will learn when each strategy produces a cleaner history, how to handle conflicts in both workflows, and how to configure team conventions that prevent history rewrites from disrupting collaborators.

Prerequisites

You should understand Git commits, branches, and basic merging. Familiarity with reading commit graphs using git log or a GUI tool will help you visualize the differences between merge and rebase outcomes. Experience resolving merge conflicts is beneficial but not strictly required.

Concept Overview

Merge creates a new commit that joins two branch histories, preserving the exact sequence of events. Rebase replays commits onto a new base, producing a linear history that is easier to read but rewrites commit hashes. Choosing between them depends on whether your team values traceability of parallel work or a clean linear narrative.

Step-by-Step Explanation

The sections below demonstrate each approach with concrete examples, showing the resulting commit graph and explaining the implications for collaboration. You will see how interactive rebase gives fine-grained control and how squash merges offer a middle ground between the two strategies.

How Git Merge Works

A merge combines two branches by creating a new commit that has two parents. This merge commit ties the histories together without modifying any existing commits. The original branch history is preserved exactly as it happened.

When you merge a feature branch into main, Git finds the common ancestor of both branches, computes the differences from that ancestor to each branch tip, and combines those differences into a single result. If the same lines were modified in both branches, Git reports a conflict that you must resolve manually.

# Switch to the target branch
git switch main
git pull --ff-only
 
# Merge the feature branch with a merge commit
git merge --no-ff feature/payment-gateway
 
# The --no-ff flag forces a merge commit even if fast-forward is possible
# This preserves the fact that work happened on a branch
 
# View the resulting history
git log --oneline --graph --all

The resulting history looks like a railroad track with branches diverging and converging. Each merge commit marks where a branch was integrated. This topology preserves the full context of how development happened: which commits were part of which feature, when integration occurred, and what the state of each branch was at any point in time.

Merge commits are safe because they never rewrite history. Every commit retains its original hash, timestamp, and parent references. This means other developers who have pulled your commits will never have their history invalidated. For shared branches and production integrations, merge is the conservative and correct choice.

The downside of merge commits is that they can make the history harder to read. A repository with many short-lived branches produces a dense graph of merge commits that obscures the linear progression of changes. Some teams find this noise acceptable because it preserves context. Others prefer a cleaner linear history.

How Git Rebase Works

A rebase moves a sequence of commits to a new base. Instead of creating a merge commit, it replays your commits one by one on top of the target branch. The result is a linear history that looks as if you wrote your changes after the latest commit on the target branch.

Internally, Git takes each commit on your branch that is not on the target, computes its diff, and applies that diff as a new commit on top of the target. The new commits have different hashes because their parent pointers changed, even though the diffs are identical. The original commits still exist in the reflog but are no longer reachable from any branch.

# Switch to your feature branch
git switch feature/search-optimization
 
# Rebase onto the latest main
git fetch origin
git rebase origin/main
 
# If there are conflicts, resolve them file by file
# After resolving each conflict:
git add src/search/index.ts
git rebase --continue
 
# If you want to abort the rebase entirely:
git rebase --abort
 
# After successful rebase, force push your branch
git push --force-with-lease origin feature/search-optimization

The resulting history is perfectly linear. Your feature commits appear after the latest main commits as if you started your work from the current tip of main. There are no merge commits, no branch topology, and no visual noise in the log.

Rebase produces a cleaner history at the cost of rewriting commits. The new commits have different hashes, which means anyone who based work on the original commits will have conflicts when they pull. This is why the golden rule of rebase exists: never rebase commits that have been pushed to a shared branch that others are working from.

When to Use Merge

Use merge when integrating approved work into a shared branch like main or develop. Merge commits are safe, reversible, and preserve the full context of how changes were developed. They are the right choice in these situations:

Merging pull requests into main. The merge commit marks the integration point and can be reverted as a single unit if the feature causes problems. Most Git hosting platforms create merge commits by default when you click the merge button.

Integrating long-running branches. If a branch has been shared with other developers or has been the base for other branches, merging preserves everyone's history without forcing rebases across the team.

Working in regulated environments. Audit trails require that commit history is immutable. Merge commits provide a clear record of when changes were integrated and by whom.

When you want to preserve branch context. The merge commit's two-parent structure lets tools like git log --first-parent show only the merge commits on main, giving you a high-level view of features without the individual commits.

# Merge with a descriptive message
git switch main
git merge --no-ff feature/user-dashboard -m "Merge feature/user-dashboard: Add analytics widgets"
 
# View only merge commits on main for a high-level history
git log --first-parent --oneline main

When to Use Rebase

Use rebase to clean up your local work before sharing it. Rebase is a tool for crafting a clear, logical commit history that tells a story. It is appropriate in these situations:

Updating your feature branch with the latest main. Instead of creating a merge commit that adds noise to your branch history, rebase replays your work on top of the latest main. This keeps your branch clean and makes the eventual merge into main simpler.

Cleaning up commits before opening a pull request. Interactive rebase lets you squash fixup commits, reorder changes for logical flow, and rewrite messages for clarity. The reviewer sees a polished history rather than your stream-of-consciousness development process.

Working alone on a branch. If nobody else has pulled your branch, rebasing is safe because you are the only one affected by the rewritten history.

Maintaining a linear project history. Some teams prefer a strictly linear main branch where every commit represents a complete, tested change. Rebase plus squash-merge achieves this.

# Update your feature branch with the latest main
git switch feature/api-caching
git fetch origin
git rebase origin/main
 
# Your commits now sit on top of the latest main
# The pull request diff will be clean and up to date

Interactive Rebase

Interactive rebase is one of Git's most powerful features for crafting commit history. It lets you reorder, edit, squash, split, and drop commits before sharing them. Think of it as editing a draft before publishing.

# Interactively rebase the last 5 commits
git rebase -i HEAD~5
 
# Git opens your editor with a list of commits:
# pick abc1234 Add caching layer
# pick def5678 Fix typo in cache key
# pick ghi9012 Add cache invalidation
# pick jkl3456 WIP debugging
# pick mno7890 Remove debug logs
 
# Change the commands to craft your history:
# pick abc1234 Add caching layer
# fixup def5678 Fix typo in cache key
# pick ghi9012 Add cache invalidation
# drop jkl3456 WIP debugging
# drop mno7890 Remove debug logs
 
# Save and close the editor
# Git replays the commits with your modifications

The available commands in interactive rebase are:

  • pick keeps the commit as is
  • reword keeps the commit but lets you edit the message
  • edit pauses the rebase so you can amend the commit
  • squash combines the commit with the previous one, keeping both messages
  • fixup combines the commit with the previous one, discarding its message
  • drop removes the commit entirely

Interactive rebase is safe to use on commits that have not been pushed. Once you push, other developers may have based work on those commits, and rewriting them causes problems. Use interactive rebase as the final polish before opening a pull request, not after others have reviewed it.

Handling Conflicts

Both merge and rebase can produce conflicts when the same lines are modified in both branches. The resolution process differs slightly between the two.

During a merge, all conflicts appear at once. You resolve them, stage the files, and complete the merge with a single commit. If the conflicts are too complex, you can abort the entire merge and try a different approach.

# During a merge conflict
git status  # Shows conflicted files
# Edit each conflicted file, removing conflict markers
git add src/api/handler.ts
git add src/api/middleware.ts
git merge --continue  # Or: git commit

During a rebase, conflicts appear one commit at a time as Git replays each commit. You resolve the conflict for the current commit, continue the rebase, and potentially face another conflict on the next commit. This can be more tedious for branches with many commits, but it also means each resolution is smaller and more focused.

# During a rebase conflict
git status  # Shows conflicted files for the current commit
# Edit the conflicted file
git add src/api/handler.ts
git rebase --continue  # Moves to the next commit
 
# If the next commit also conflicts, repeat the process
# If you get lost or want to start over:
git rebase --abort

A useful technique for complex rebases is to do a trial merge first. Merge main into your branch to resolve all conflicts at once, verify everything works, then reset and do the rebase knowing what the final result should look like.

Squash Merge Strategy

Many teams use a hybrid approach called squash merge. When merging a pull request, all commits on the feature branch are squashed into a single commit on main. This gives you the benefits of detailed commits during development and a clean linear history on main.

# Squash merge via command line
git switch main
git merge --squash feature/notification-system
git commit -m "Add real-time notification system
 
Implements WebSocket-based notifications with:
- Connection management and reconnection logic
- Message queue for offline delivery
- UI components for notification display
- Integration tests for the full flow
 
Closes #234"
 
# The feature branch commits are collapsed into one
# Main has a clean linear history

GitHub, GitLab, and Bitbucket all offer squash merge as a pull request merge option. The squashed commit message typically includes the pull request title and number, making it easy to trace back to the full discussion and individual commits if needed.

Squash merge works well when feature branches contain many small commits that are not individually meaningful, such as "fix lint", "address review comments", or "WIP". The squashed commit on main represents the complete feature as a single logical unit.

The tradeoff is that you lose the granular history on main. If you need to bisect within a feature later, the individual commits are gone. Some teams mitigate this by keeping feature branches around for a retention period before deleting them.

Rebase Workflow for Teams

Teams that prefer linear history often adopt a rebase workflow where developers rebase their branches before merging. This requires discipline and clear conventions to avoid the pitfalls of rewriting shared history.

# Developer workflow with rebase
# 1. Start a feature branch
git switch main
git pull --ff-only
git switch -c feature/rate-limiting
 
# 2. Work on the feature with multiple commits
git commit -m "Add token bucket algorithm"
git commit -m "Integrate rate limiter with API middleware"
git commit -m "Add rate limit headers to responses"
 
# 3. Before opening PR, rebase onto latest main
git fetch origin
git rebase origin/main
 
# 4. Clean up commits with interactive rebase if needed
git rebase -i origin/main
 
# 5. Force push the rebased branch
git push --force-with-lease origin feature/rate-limiting
 
# 6. Open pull request - reviewer sees clean linear commits
# 7. After approval, fast-forward merge into main
git switch main
git merge --ff-only feature/rate-limiting
git push origin main

The --force-with-lease flag is critical in this workflow. Unlike --force, it refuses to overwrite commits that someone else pushed to your branch. This prevents accidentally destroying a colleague's work if they pushed a commit to your branch during review.

Teams using this workflow should agree on when rebasing is acceptable. A common convention is: rebase freely before opening a pull request, but never rebase after review has started. This ensures reviewers' comments remain attached to the correct commits.

Recovering from Rebase Mistakes

Rebase rewrites history, but Git's reflog keeps a record of every state your branch has been in. If a rebase goes wrong, you can always recover.

# View the reflog to find the state before the rebase
git reflog
# Output shows entries like:
# abc1234 HEAD@{0}: rebase (finish): returning to refs/heads/feature/search
# def5678 HEAD@{1}: rebase (pick): Add search filters
# ghi9012 HEAD@{2}: rebase (start): checkout origin/main
# jkl3456 HEAD@{3}: commit: Add search filters  <-- this is pre-rebase
 
# Reset to the state before the rebase
git reset --hard HEAD@{3}
 
# Your branch is back to its pre-rebase state
# All commits are restored with their original hashes

The reflog retains entries for 90 days by default. As long as you notice the mistake within that window, recovery is straightforward. This safety net makes rebase less scary: you can always undo it.

Another recovery technique is to create a backup branch before rebasing. If anything goes wrong, the backup branch still points to the original commits.

# Create a backup before rebasing
git branch backup/feature-search feature/search-optimization
 
# Perform the rebase
git rebase origin/main
 
# If something goes wrong, restore from backup
git reset --hard backup/feature-search
 
# Clean up the backup when you no longer need it
git branch -d backup/feature-search

Team Conventions and Configuration

Teams should document their merge strategy and configure Git to enforce it. Here are settings that support common workflows:

# Configure pull to rebase by default instead of merge
git config --global pull.rebase true
 
# Configure rebase to automatically squash fixup commits
git config --global rebase.autoSquash true
 
# Create fixup commits that auto-squash during rebase
git commit --fixup=abc1234
# Later, interactive rebase will automatically mark these as fixup
 
# Configure merge to always create merge commits (no fast-forward)
git config --global merge.ff false
 
# Or configure merge to only fast-forward (reject if not possible)
git config --global merge.ff only

Document your team's conventions in a CONTRIBUTING.md file. Specify whether pull requests should be merged, squash-merged, or rebased. Specify whether developers should rebase their branches before requesting review. Specify the commit message format and any required prefixes or ticket references.

Comparing History Outcomes

To make the difference concrete, consider a feature branch with three commits being integrated into main that has advanced by two commits since the branch was created.

After a merge, the history shows the branch diverging and converging with a merge commit at the junction. The five original commits plus the merge commit are all visible. Running git log --oneline --graph shows the railroad track pattern.

After a rebase and fast-forward merge, the history is perfectly linear. The three feature commits appear after the two main commits as if they were written sequentially. There is no merge commit and no branch topology visible.

After a squash merge, the history shows a single commit on main that contains all the changes from the three feature commits. The linear history is even cleaner, but the granular development steps are lost from main.

Each outcome has its place. Use the one that matches your team's values around history readability, traceability, and debugging convenience.

Real-World Decision Framework

Here is a practical framework for deciding between merge and rebase in common scenarios:

Integrating a reviewed pull request into main: use merge or squash merge. This preserves the integration point and is safe for shared branches.

Updating your feature branch with the latest main: use rebase. This keeps your branch clean and avoids unnecessary merge commits in your pull request.

Cleaning up commits before review: use interactive rebase. Polish your history so reviewers see logical, well-described commits.

Integrating a long-running branch that others depend on: use merge. Rebasing would rewrite commits that others have based work on.

Combining multiple small fixup commits: use interactive rebase with fixup or squash. This reduces noise without losing the final result.

Working on a branch with a colleague: use merge to integrate their changes. Rebasing a shared branch forces your colleague to reset their local copy.

The key principle is: rebase for local cleanup, merge for shared integration. This gives you clean history without the risks of rewriting public commits.

Integration with CI/CD

Your merge strategy affects how CI/CD pipelines behave. Merge commits trigger builds on the merged result, which may differ from what was tested on the feature branch if main advanced between the last CI run and the merge. Merge queues solve this by re-running CI on the merged result before completing the merge.

Rebase workflows ensure the feature branch is always up to date with main before merging, which means CI results on the branch accurately reflect what main will look like after the merge. This reduces the chance of broken builds on main.

Teams deploying with Docker containers benefit from linear history because each commit on main corresponds to exactly one deployable artifact. Bisecting production issues is straightforward when the history is linear and each commit is a complete, tested change.

Real-World Use Cases

Teams maintaining long-running release branches often prefer merge to preserve the audit trail of when features were integrated. Library maintainers who curate a public changelog frequently rebase feature branches before merging to keep the history bisectable. Monorepo teams use squash merges to keep the trunk readable while preserving detailed history in feature branches.

Best Practices

Never rebase commits that have been pushed to a shared branch unless your team has explicitly agreed on a force-push workflow. Use interactive rebase locally to clean up work-in-progress commits before opening a pull request. Configure your CI to run on both the feature branch and the merge result to catch integration issues early.

Common Mistakes

The most dangerous mistake is rebasing a branch that other developers have based work on, which forces everyone to reconcile divergent histories. Another common error is resolving the same conflict repeatedly during a long rebase chain instead of using git rerere to automate resolution. Teams also forget to update their local tracking branches after a force-push, leading to confusing duplicate commits.

Summary

Merge and rebase are complementary tools, not competitors. Merge preserves history and is safe for shared branches. Rebase creates linear history and is ideal for local cleanup. Squash merge offers a middle ground that collapses feature branches into single commits on main.

The best teams use both: rebase to keep feature branches up to date and polished, merge to integrate approved work into main safely. Document your conventions, configure Git to support them, and use --force-with-lease whenever you push rebased branches. Combined with solid Git workflow practices and branch protection rules, a clear merge strategy keeps your repository history useful and your team productive.

Intermediate12 min read

Git Bisect and Blame for Debugging

Use git bisect to binary-search for the commit that introduced a bug and git blame to trace line-by-line authorship in any codebase.

Intermediate11 min read

Git Branching Strategies

Compare Git Flow, GitHub Flow, trunk-based development, and release branching to choose the right strategy for your team size and release cadence.

Beginner10 min read

Git Learning Roadmap

Master Git from basic commands to advanced workflows covering branching, merging, rebasing, collaboration, and CI/CD integration for teams.