Rebase Branches Cleanly

Rebase branches to keep history clean in this Git Tutorial lesson. Practical steps, comparisons, troubleshooting, and what to learn next.

Focus: rebase branches to keep history clean

Sponsored

Your commit history is a story — and too often it reads like a messy first draft. Merging long-lived feature branches into main dumps Merge branch 'feature/login' into main commits into your log, interleaving your work with your teammates' in a way that makes git log feel like a puzzle. You want a clean, linear history that's easy to review, easy to bisect, and easy to understand months later. The answer is to rebase branches to keep history clean — a technique that rewrites your branch's commits on top of the latest main, giving you a straight line of logical, focused changes. This lesson will show you exactly how to do it safely, when to choose it over merging, and how to recover when something goes sideways.

The problem this lesson solves

Every Git user hits the same wall: you've been working on a feature branch for a few days, and when you're ready to integrate it, your branch has drifted far behind main. Other developers have pushed new commits — maybe a refactor, a new API, a bug fix. Your feature branch is based on an outdated version of the codebase.

If you merge at this point, two things happen:

  • Git creates a merge commit that has two parents — one from your branch, one from main.
  • Your branch's commits are interleaved with the new commits from main, but the log shows a non-linear history with branch points and merge bubbles.

Let's see this in action. Imagine this history:

        A---B---C feature/login
       /
D---E---F---G main

If you run git merge feature/login while on main, you get:

        A---B---C feature/login
       /         \
D---E---F---G---M main (merge commit)

The problem? git log --oneline now shows commits like M, C, B, A, G, F, E, D in an order that doesn't represent a logical sequence of changes. The history is cluttered and harder to review. A reviewer can't easily see "these three commits are the feature" because they're mixed in with unrelated commits from main.

This is the core pain: merging preserves all the noise of parallel work in your history. Whether you're on a team of two or two hundred, that noise makes git blame, git bisect, and code review slower and more confusing.

Core concept / mental model

Think of your commit history as a story told in chronological order. When you rebase, you're saying: "I want to tell my story as if I had started my work after the latest commit on main."

Technically, git rebase does this:

  • It finds the common ancestor of your branch and main (often called the merge base).
  • It collects all the commits that exist on your branch but not on main.
  • It replays those commits one by one on top of the latest commit of main.

The result is a linear history: your commits are stacked directly on top of the current main, with no merge commit and no branching in the log.

D---E---F---G---A'---B'---C'  main (after rebase and fast-forward)

Note the apostrophes: A', B', C' are new commits — even if the message and diff are identical, each has a new SHA-1 hash because their parent changed. That's the key mental model to internalize: rebase rewrites history. It's not a merge-then-squash; it's a full replay of your changes.

This is why rebasing a branch that's already been shared with others (pushed to a remote and pulled by teammates) is dangerous — you'd be rewriting commits that others depend on.

Here's a simple definition to keep handy:

Rebase (verb): to reapply commits from one branch onto the tip of another branch, creating a linear history. Merge (verb): to combine two branches by creating a new commit that has two parents — preserving the branching structure.

How it works step by step

The rebase process is straightforward once you break it down:

  1. Switch to your feature branchgit switch feature/login. You must be on the branch you want to rebase.
  2. Rebase onto the target branchgit rebase main. Git will: - Identify commits that are in feature/login but not in main. - Reset your branch to point to main's tip. - Apply each of your commits, one at a time, as new commits on top.
  3. Resolve any conflicts — If a commit's changes conflict with the new base, Git pauses and asks you to fix. You edit the files, git add, and run git rebase --continue.
  4. Fast-forward main (optional) — Once your rebase is done, switch to main and run git merge feature/login. Since your branch is now directly ahead of main, Git will fast-forward — no merge commit, just a clean pointer move.

The key difference from merge: rebase replays your commits, merge combines them. Both integrate changes, but they produce different history shapes.

Hands-on walkthrough

Let's walk through a concrete example. Assume you're on a feature branch called feature/login, and main has advanced.

Setup

# Starting state: your feature branch has two commits
git log --oneline feature/login
# 2a03f4c Add login form validation
# 8b41c8a Create login form component

git log --oneline main
# 9f6e2a4 Update API client
# c1b55d3 Fix typo in docs
# a1b2c3d Initial commit

Rebase onto main

git switch feature/login
git rebase main
# First, rewinding head to replay your work on top of it...
# Applying: Create login form component
# Applying: Add login form validation

If there are no conflicts, you're done. Check the history:

git log --oneline feature/login
# 3e8f0a1 Add login form validation
# 2faab73 Create login form component
# 9f6e2a4 Update API client
# c1b55d3 Fix typo in docs
# a1b2c3d Initial commit

Notice the new SHA hashes (3e8f0a1, 2faab73) and the linear ordering. Your commits now sit on top of main's latest work.

Fast-forward main

Now integrate into main cleanly:

git switch main
git merge feature/login
# Updating 9f6e2a4..3e8f0a1
# Fast-forward

No merge commit — perfect linear history.

Handling conflicts

Conflicts force you to slow down. If main modified a file you also changed, rebase stops:

git rebase main
# Auto-merging src/login.py
# CONFLICT (content): Merge conflict in src/login.py
# error: could not apply 8b41c8a... Create login form component
# Resolve all conflicts manually, mark them as resolved with "git add", then run "git rebase --continue".

Open the file, find the conflict markers (<<<<<<<, =======, >>>>>>>), edit to keep the right code, then:

git add src/login.py
git rebase --continue

Git opens your editor to confirm the commit message — save and close, and the rebase continues.

Compare options / when to choose what

You now have two main integration strategies: rebase and merge. Here's a practical comparison:

Feature Rebase Merge
History shape Linear, clean Non-linear, complex
Commit IDs on feature branch Rewritten Preserved
Merge commit No Yes
Ability to bisect bugs Easier Harder
Safe for shared branches? No — rewrites public history Yes
When to use Private feature branches before PR Long-lived or shared branches
Learning curve Steeper (need conflict resolution) Gentler

When to choose rebase:

  • Your branch is local or only pushed to your own fork.
  • You want a clean, linear history for code review (e.g., open-source contributions).
  • You're integrating a short-lived feature (a few commits) that you can safely rewrite.

When to choose merge:

  • The branch has been pushed and others are using it.
  • You need to preserve the exact commit hashes (e.g., for audit or compliance).
  • You're merging a release branch that has been tested widely.

Troubleshooting & edge cases

Even the best rebase can hit snags. Here are the most common ones and how to fix them.

"Cannot rebase: You have unstaged changes"

Git refuses to rebase if your working tree isn't clean. Fix by stashing or committing:

git stash
git rebase main
git stash pop

Conflicts that won't resolve

If you're in a conflict and want to exit the entire rebase:

git rebase --abort

This restores your branch to its pre-rebase state. If you've resolved some conflicts but realize it's not worth it, abort and start over — or use git rebase --quit to stop without restoring.

Rebase onto the wrong branch

You rebased onto main but meant to rebase onto develop. No panic — you can re-rebase:

git rebase --abort  # if still in progress
git rebase develop

Or, if you already finished the rebase, use git reflog to find your old branch tip and reset:

git reflog
# 3e8f0a1 HEAD@{0}: rebase finished
# 8b41c8a HEAD@{1}: rebase: checkout feature/login
# ... [old commit]
git reset --hard <old-commit>

Rebased a shared branch by accident

If you already pushed and force-pushed, your teammates will have conflicts. Communicate immediately. If it's a small team, the standard fix is for everyone to run:

git fetch
git rebase origin/feature/login  # or use original branch then re-apply

Pro tip: Never rebase a branch that others have based their own work on. If you must, coordinate with your team and use git push --force (or --force-with-lease if your remote supports it) with warning.

What you learned & what's next

You've mastered the core idea of how to rebase branches to keep history clean: you can explain why rebasing beats merging for linear history, you can apply it safely in a practical exercise, and you've seen the edge cases that trip up most developers. You now know that rebase replays commits, that it rewrites history, and that it's the right tool for private feature branches.

You've also learned to compare it with merge and choose the right approach based on whether the branch is shared — a critical skill for any team.

Next in this Git Tutorial track, you'll move on to more advanced collaboration techniques. Up next: Interactive rebase — the tool that lets you squash, reorder, and edit commits during a rebase. With a solid foundation in plain rebase, you'll be ready to craft even more intentional, readable history.

Practice recap

In your local test repo, create a feature branch, add a commit, switch to main, add another commit, then rebase the feature branch onto main. Verify the linear history with git log --oneline --graph. Then introduce a conflict on purpose and practice resolving it with git add and git rebase --continue.

Common mistakes

  • Rebasing a branch that's already been pushed and used by teammates — this rewrites public commits and causes painful conflicts for others.
  • Forgetting to stash or commit local changes before running git rebase — Git aborts and you lose your bearings.
  • Running git rebase --continue without adding resolved files — the rebase gets stuck and can be confusing to recover from.
  • Merging main into your feature branch before rebase — this creates a merge commit and defeats the purpose of a linear history.
  • Force-pushing a rebased branch to a shared remote without coordination — you can permanently lose work if others based branches on the old version.

Variations

  1. Interactive rebase (git rebase -i) — lets you squash, reorder, and edit commits for an even more curated history.
  2. Rebase with --onto — move a subset of commits onto a different base, useful for fixing branching mistakes.
  3. Using git pull --rebase instead of merge while syncing with a remote — keeps your local history linear when fetching new changes.

Real-world use cases

  • On a team of 10+, rebase feature branches onto the latest main before opening a PR, so reviewers see only your changes in a clean, linear sequence.
  • In open-source contributions, rebase your fork's feature branch atop the upstream main to resolve conflicts early and keep the maintainer's log tidy.
  • During a release branch hotfix, rebase your small fix onto the release branch to apply it without a merge commit, preserving a straight release history.

Key takeaways

  • Rebase replays your branch's commits on top of another branch's tip, creating a linear, commit-by-commit history.
  • Rebase rewrites commit hashes — never rebase a branch that others have already pulled.
  • After rebasing, integrating into main becomes a clean fast-forward with no merge commit.
  • Conflicts during rebase are resolved one commit at a time — use git rebase --continue after staging fixes, or --abort to bail out.
  • Choose merge over rebase when a branch is shared or when preserving exact commit IDs matters.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.