PHP Architect logo

Want to check out an issue? Sign up to receive a special offer.

How to use git rebase without breaking your team’s history

Posted by on June 10, 2026

Video version: https://youtu.be/5n2VZS_YPv0

Let’s say you’re working on a feature branch called add-discount-codes. You’ve been at it for a couple of days, and you’ve got five commits with all of your work done. A team member mentions that git rebase can make sure you have the most recent changes from the “main” branch. So you run git rebase main, and everything looks fine locally, so you push it.

Then your teammate messages you that Git is saying the histories diverged, and they have a bunch of conflicts that don’t make sense. If you’re like most developers, this is where you stop using rebase and stick to merging main into your branch over and over again, making a mess of your git history.

In this video, we’ll explain what happens when you rebase and how to use it safely to keep you git history clean.

Hello developers, and welcome to the PHP Architect Channel! I’m Scott Keck-Warren, and today we’re talking about git rebase: how it works, when it’s the right tool, and when you should back away slowly.

If you’re new here, we cover topics across the PHP ecosystem. Hit subscribe so you don’t miss the next one.

Merge vs. Rebase

Say you have a main branch with commits A and B. You branched off from B and made commits D and E. While you were working, someone merged commit C into main.

Now you want commit C in your feature branch because it has a fix to some shared code you’re both working on. You have two options for how to resolve this.

Merging

The first option and generally default option is to run git merge main from your feature branch. In this mode, Git creates a merge commit that ties the two histories together.

git checkout feature/discount
git merge main

Your history now has a merge commit (F) that joins both lines of development. The original commits and the merge point are all visible.

Rebase

The second option is to run git rebase main from your feature branch. Git then takes commits D and E, temporarily “sets them aside,” moves your branch pointer to the tip of main, and replays your commits on top.

git checkout feature/discount
git rebase main

The result is that the history looks like you started your work after C was already there.

I wrote D’ and E’, not D and E. That distinction matters a lot, and it’s exactly why rebase can cause problems.

Why Use Rebase Then?

You might be asking yourself, if rebase rewrites history and causes problems when working on a shared branch, why would anyone use it at all? The answer is that linear history is genuinely easier to work with.

Run git log --oneline on a project that uses merge commits heavily:

f1a2b3c Merge branch 'feature/payments' into main
d4e5f6a Merge branch 'hotfix/tax-rate' into main
a7b8c9d Merge branch 'feature/discount-codes' into main
1b2c3d4 Merge branch 'bugfix/email-validation' into main

Every entry is a merge commit, and to see what actually changed, you have to dig into each one, and it can be a real challenge to revert those changes.

Now compare that to a team that rebases before merging:

f1a2b3c Add Stripe webhook handler
d4e5f6a Validate discount codes before applying
a7b8c9d Fix tax rate for Canadian provinces
1b2c3d4 Add email format validation on checkout

You can read that history like a changelog, and each commit tells you what changed and why, in order, without noise.

Linear history also makes git bisect more reliable. git bisect is a command that does a binary search through your commits to find which one introduced a bug. Merge commits complicate that process because they bundle multiple lines of development together. With a clean linear history, bisect can walk the commits one at a time and find the exact commit that broke something.

Rebase also lets you clean up your own commits before anyone else sees them. Maybe you made five commits while working on a feature, and three of them are “fix typo”, “oops wrong file”, and “testing”. You can use an interactive rebase (check back for this in the future) to squash those into one clean commit before you open a pull request. Your teammates see a tidy history, and they never have to know about the chaos.

The key phrase there is “before anyone else sees them”. That is exactly where rebase belongs: on your local branch, before you share it.

How Rebase Works Under the Hood

When you run git rebase main, Git doesn’t move your existing commits. It creates brand new ones:

  1. Git finds the common ancestor of your branch and main (commit B).
  2. Git saves the diffs from each of your commits (D and E) as temporary patches.
  3. Git resets your branch to point at the tip of main (commit C).
  4. Git applies each patch one at a time, creating a new commit for each one.

It’s important to note that each new commit gets a new SHA hash. In our example before, D became D’ and E became E’. They contain the same code changes, but as far as Git is concerned, they’re different commits because they have different parents now. The original D and E still exist in Git’s object store, but no branch points to them anymore.

This is what people mean when they say rebase rewrites history. You’re replacing old commits with new ones. If anyone else had a copy of those old commits, their history and yours no longer agree.

You can see it yourself. Before a rebase, grab the SHA of your latest commit:

git log --oneline -1
a1b2c3d Add discount validation logic

After the rebase, check it again:

git log --oneline -1
f4e5d6a Add discount validation logic

Notice how it’s the same message with the same code change but different commit SHAs.

We’ll have more after this word from our partners.

The Golden Rule: Never Rebase Shared History

If anyone else has created commits based on your commits, don’t rebase those commits. EVER. Or at least without making sure you have those commits first and communicate this to the other people involved.

Again, when you rebase, you replace commits with new ones. This means if your teammate already has the old commits in their local branch, Git sees them as two totally different histories for the same branch. The next time they pull, they’ll get conflicts or duplicate commits, even though the actual code changes are identical. The only way to fix it is a force push (git push --force), which overwrites the history and makes things worse for everyone.

# After rebasing a shared branch, a normal push fails
git push origin feature/discount
 ! [rejected]        feature/discount -> feature/discount (non-fast-forward)
error: failed to push some refs to 'origin'

When we get a rejection, it’s Git’s way of protecting you from doing something terrible. It’s saying “the remote has commits you don’t have, so maybe we all chill out for a minute”. If you force push past that on a branch other people are using, you’re going to ruin someone’s day, if not their whole week. It might feel like I’m saying this too often, but it’s for a good reason.

Rebase is safe when the commits live only on your local machine, and nobody else has seen them, or it’s on a branch you’re the only one adding commits to.

Gotchas

Because rebase replays commits one at a time, you might resolve the same conflict over and over again if several commits touch the same lines. Contrast this to if you’re doing a merge where you resolve it once and you’re done.

Force pushing to your own branch still overwrites the remote. Even on a branch only you use, git push --force can destroy work you pushed from another machine. To help with this, always use git push --force-with-lease instead. It checks that the remote hasn’t changed since your last fetch and refuses the push if it has. I like to include this in my Makefile, so I just have to run make git-push and it will remember for me.

# Safer alternative to --force
git push --force-with-lease origin feature/discount

Another gotcha is that old commits don’t disappear immediately. They hang around in Git’s reflog for about 30 days, so if you rebase and regret it, git reflog can help you recover. That said, don’t treat this as a safety net for rebasing shared branches because it can be a headache.

Never ever rebase main! Rebasing main and force-pushing it rewrites the history that every branch in your repo is based on. Recovery from that is extremely painful for the whole team (been there, don’t recommend it).

What You Need to Know

  1. git merge preserves history as it happened and creates a merge commit.
  2. git rebase replays your commits one by one on top of another branch.
  3. The original commits are replaced, not moved.
  4. Never rebase commits other people have worked off.
  5. Use git push --force-with-lease instead of git push --force when pushing after a rebase.

 

Leave a comment

Use the form below to leave a comment:

 

Our Partners

Collaborating with industry leaders to bring you the best PHP resources and expertise