Hi, I'm ThadeusB.

I code, I play, I love, I innovate

Stop Squash Merging My Commits

I really hate squash merges.

That's the whole thesis. If your team's default merge strategy is "squash and merge," you are actively making everyone's life harder and destroying useful information in the process. Let me explain why.

The stacked PR nightmare

This is the scenario that makes me lose it every time. You're working on a feature. You open a PR. Your team is busy, so it sits in review for a few days. Totally normal. You don't want to sit around waiting, so you branch off your feature branch and start working on the next piece.

main
  └── feature/auth
        └── feature/auth-roles

You open a second PR, pointing it at your first branch. This is a completely standard git workflow. Stacked PRs. Nothing exotic here.

Your first PR finally gets approved. Sweet. Someone clicks merge.

But it's a squash merge.

Now your second branch is based on commits that no longer exist in the target branch. The entire commit lineage your branch was built on has been replaced by a single squashed commit with a different hash. Git has no idea that those changes already landed. As far as git is concerned, your second branch is full of conflicts against work that was already merged.

So now you get to spend the next 30 minutes rebasing, resolving conflicts that aren't real conflicts, double-checking you didn't lose anything, and re-requesting review because the diff changed. All because someone wanted a "clean" history.

Multiply this by every developer on your team who works on anything non-trivial, and you start to see the problem. This isn't a theoretical edge case. This happens constantly on teams that default to squash merge.

With --no-ff merge commits, none of this happens. The original commits are still there. The lineage is preserved. Your dependent branches just work.

"Clean history" is not a real goal

People squash for aesthetics. They want a nice linear history where every commit is a tidy little summary. "Add user authentication." "Fix payment flow." One commit per PR.

The problem though? Version control isn't a changelog. It's a diagnostic tool. When something breaks at 2am and you're digging through git log trying to figure out what happened, you don't want a pretty summary. You want the actual sequence of changes that led to this state. You want to see that someone tried approach A, backed it out, tried approach B, fixed a bug they introduced along the way, and then landed on the final version.

A messy but real history is infinitely more useful than a clean but fictional one.

Merge commits give you both. You get a single merge commit that says "this feature landed" and you can drill into it to see every individual commit that was part of it. Best of both worlds. git log --first-parent gives you the clean view if you want it. The detail is there when you need it.

AI works better with full history

This one is newer but it matters more every day. AI coding agents and LLMs are significantly better at understanding what happened in your codebase when they have the full commit history to work with.

When an AI tool is trying to diagnose a bug, understand why a file looks the way it does, or figure out the intent behind a change, it needs context. Full merge commits preserve the intellectual journey. The individual commits, the messages, the sequence of changes, all of it gives the AI more signal to work with.

Squash that down to "Add feature X" and the AI has almost nothing. It can see what changed but not why, not how, not what was tried and abandoned. You're throwing away exactly the kind of context that makes AI-assisted development actually useful.

Reverting is straightforward

With a merge commit, reverting an entire feature is one command:

git revert -m 1 <merge-commit>

That undoes every change from the feature branch in a single revert commit. Clean, surgical, done.

With squashed commits, if someone bundled multiple logical changes into one squash (which they almost always do, because that's the whole point), you can't partially revert. It's all or nothing. And if you need to revert one of three features that all got squashed separately, you'd better hope the squash messages are descriptive enough to figure out which commit hash you need.

Attribution gets destroyed

Squash merges collapse all commits into one, authored by whoever clicked the merge button. If three people contributed commits to a feature branch, that history is gone. The squashed commit shows one author.

In open source this is especially bad. People contribute to projects to build a track record. Squashing their work away is disrespectful, even if it's unintentional.

On teams it's less dramatic but still annoying. git blame becomes less useful when half the codebase is attributed to whoever happened to merge each PR.

When I use rebase

Basically never.

The only time I'll rebase is on a local branch that has never been pushed. If I've made a few messy commits while figuring something out and I haven't pushed yet, sure, I'll interactive rebase to clean things up before the first push.

Once it's been pushed, rewriting history is off the table. Other people might have pulled it. Other branches might be based on it. The moment you push, those commits are shared state, and rewriting shared state is how you end up in the stacked PR nightmare I described above.

Just use --no-ff

Set it as your default. Configure your repo hosting to use merge commits instead of squash. Your future self debugging a production issue at midnight will thank you. Your teammates working on dependent branches will thank you. The AI agent trying to understand your codebase will thank you.

Stop optimizing for a pretty git log and start optimizing for a useful one.