Skip to content

What Is Rebasing?


What Will We Learn?

By the end of this chapter, you’ll know:

  • What rebasing actually does to your commit history
  • Why people bother with it instead of just using merge
  • When rebasing is a great idea, and when it can blow up in your face
  • A handful of best practices to keep you (and your team) safe
  • The nuances and trade-offs you should be aware of before you dive in

We’re keeping this chapter conceptual — the actual step-by-step rebasing process (interactive rebase, conflict resolution, --onto, etc.) gets its own chapter later.

What is Rebasing?

Rebasing is Git’s way of taking a series of commits and replaying them on top of a different base commit. Instead of tying two histories together with a merge commit, Git picks up your changes one by one and reapplies them as if you’d written them starting from the new base.

The easiest way to see the difference is to compare it with a merge. Say you branched feature off main, and main moved forward while you were working.

With a merge, both histories stay intact, and Git ties them together with a merge commit:

    %%{init: { 'gitGraph': {'showBranches': true, 'showCommitLabel': true}} }%%
gitGraph
    commit id: "A"
    commit id: "B"
    branch feature
    checkout feature
    commit id: "C"
    commit id: "D"
    checkout main
    commit id: "E"
    checkout feature
    merge main id: "Merge commit"
  

With a rebase, your commits are replayed on top of the latest main, so the history looks like it was linear all along:

    flowchart RL
    main["main"] -.-> E
    subgraph after["After Rebase — feature onto main"]
        B((B)) --> A((A))
        E((E)) --> B
        C((C')) --> E
        D((D')) --> C
        feature["feature"] -.-> D
    end
    style main fill:#888,stroke:#888,color:#fff
    style feature fill:#c2255c,stroke:#c2255c,color:#fff
  

Why Rebase?

The short answer: a cleaner, more readable history.

  • Linear loggit log reads top to bottom like a story, instead of a tangle of merge commits jumping in and out of branches.
  • Easier bisectinggit bisect works best on a straight line of commits. A history full of merges makes it harder to pin down which commit introduced a bug.
  • No merge-commit noise — every merge commit is basically Git saying “these two things happened at the same time.” Multiply that by a busy team and your history turns into a maze.
  • Cleaner pull requests — you can squash, reorder, and tidy up your commits before anyone else sees them, so the PR tells a coherent story instead of a stream of “wip”, “fix typo”, “actually fix it” commits.

None of this means merge commits are bad — they have their place, especially for marking when a long-lived branch (like a release branch) integrates into another. Rebase just optimizes for readability over “preserving exactly what happened, when.”

When to Rebase and When to Avoid?

This is the part people get burned on, so let’s be direct about it.

    flowchart TD
    A["Is the branch shared with others?"] -->|"Yes, others have it"| B["Don't rebase it"]
    A -->|"No, it's local/mine only"| C["Has it already been pushed?"]
    C -->|"Yes, but only I use it"| D["Safe to rebase, then force-push"]
    C -->|"No, still local"| E["Safe to rebase freely"]
    B --> F["Use merge instead"]
  

Good times to rebase:

  • Updating your local feature branch with the latest main before opening a PR
  • Cleaning up your own commits (squashing, rewording, reordering) before sharing them
  • Keeping a long-running personal branch in sync without cluttering history with merge commits

Times to avoid it:

  • Any branch that other people have already pulled or based work on — rebasing rewrites history, and rewritten history doesn’t play nicely with anyone who has the old version
  • main, develop, or any shared/protected branch — full stop
  • When you’re not confident resolving conflicts — a rebase can ask you to resolve the same conflict multiple times, once per replayed commit, which can get messy fast

This boils down to one rule of thumb, often called the golden rule of rebasing: never rebase a branch that other people might have commits based on.

Best Practice

A few habits that will save you from a bad afternoon:

  • Never rebase public/shared branches. If it’s on the shared remote and others use it, treat it as immutable.
  • Rebase local branches often, before you push. The earlier you sync with main, the smaller and easier your conflicts will be.
  • Use git pull --rebase for your own feature branches instead of the default merge-pull, to avoid pointless merge commits from just staying up to date.
  • Communicate before force-pushing. If you must rewrite a branch others might have touched, give people a heads-up so they can safely reset their local copies.
  • Use --force-with-lease instead of --force. It refuses to overwrite work you haven’t seen yet, unlike a plain force-push.
  • Keep commits small and focused before rebasing. Interactive rebase is much easier to reason about when each commit does one clear thing.
  • Run your tests after rebasing. Even if there were no conflicts, replayed commits can behave differently in a new context — don’t assume “no conflicts” means “nothing changed functionally.”

Nuance of Using Rebasing

A few things worth internalizing before you start using rebase regularly:

  • Rebasing rewrites history, it doesn’t “move” it. Every replayed commit gets a new SHA, even if the diff is identical. This means any reference to the old commits (code review links, CI runs tied to old hashes, someone else’s branch based on your old commits) can become stale or orphaned.
  • Conflicts can repeat. Because each commit is replayed individually, a conflict in the same file might surface more than once if multiple commits touch that area — not because Git is broken, but because it’s genuinely reapplying each change in sequence.
  • It changes commit dates, not authorship (usually). The author and original commit date are typically preserved, but a new “committer date” is set at the time of the rebase — useful to know if you’re debugging why timestamps look odd.
  • Rebase is not “safer” or “more correct” than merge — it’s a different trade-off. Merge preserves exactly what happened and when; rebase optimizes for a readable, linear story. Neither is objectively right; it depends on what your team values.
  • git reflog is your safety net. If a rebase goes sideways, your original commits aren’t gone — they’re just unreferenced. reflog can get you back to where you started.

That’s the conceptual foundation. In the next chapter, we’ll actually get our hands dirty with git rebase, interactive rebasing, resolving conflicts step by step, and a few tricks like --onto and --autosquash that make rebasing much less scary in practice.

Diagramatically: Before and After Rebase

    flowchart TB
    subgraph before["Before Rebase"]
        direction RL
        B1((B)) --> A1((A))
        E1((E)) ----> B1
        C1((C)) --> B1
        D1((D)) --> C1
        main1["main"] -.-> E1
        feat1["feature"] -.-> D1
    end

    subgraph after["After Rebase — feature onto main"]
        direction RL
        main2["main"] -.-> E2
            subgraph feat
            B2((B)) --> A2((A))
            E2((E)) --> B2
            C2((C')) --> E2
            D2((D')) --> C2
            feat2["feature"] -.-> D2
            end
    end

    before -.->|"git rebase main"| after

    style main1 fill:#888,stroke:#888,color:#fff
    style feat1 fill:#c2255c,stroke:#c2255c,color:#fff
    style main2 fill:#888,stroke:#888,color:#fff
    style feat2 fill:#c2255c,stroke:#c2255c,color:#fff
  
Last updated on