The Clean History Delusion: Why Rebase-Only Policies are a Maintenance Nightmare
The Clean History Delusion: Why Rebase-Only Policies are a Maintenance Nightmare
There’s a specific kind of satisfaction in looking at a Git graph that is a perfectly straight line. It feels like progress, untainted by the messy reality of three different developers touching the same file on a Tuesday afternoon. We’ve been told for years that "clean history" is the hallmark of a professional team, and usually, that means one thing: a strict rebase-only policy.
But here’s the thing about a perfectly straight line: it’s often a lie. And in software engineering, lies are expensive.
The 3 AM Reality Check
Imagine it’s 3:00 AM. A critical service is throwing 500s. You’re scanning logs, and you find a stack trace pointing to a logic error in a core utility function. You run git blame to see when this changed and why.
If your team enforces a "squash and rebase" policy, you’ll likely see a single, massive commit titled Feature: Refactor Auth logic (#402). That commit contains 40 changed files and 2,000 lines of code. The timestamp? It’s from yesterday, when the lead dev did the final rebase and click-merged the PR.
The original context—the iterative steps, the "tried this but it failed" commits, the actual time the bug was introduced—is gone. You’ve sacrificed the forensic trail for a prettier graph.
Rebasing Destroys Truth
When you rebase, you aren't "moving" commits. You are creating entirely new commits that look like the old ones but have different hashes and, crucially, different metadata.
The Committer and Author fields in Git are different for a reason. Rebasing often updates the commit timestamp to the moment the rebase happened. This might seem trivial until you're trying to correlate a bug with a specific deployment or a spike in error rates. If the code was written on Monday but rebased and merged on Friday, your history says the change happened on Friday. If the bug started appearing on Wednesday, your Git history is now actively gaslighting you.
The git bisect Trap
People love rebasing because it makes git bisect "easier" by removing merge commits. But strict rebase-only workflows often go hand-in-hand with squashing.
Squashing a feature branch into a single commit is a death sentence for effective bisecting. If I have a bug, and git bisect tells me the culprit is a commit that touches 50 files because the team wanted a "clean" history, the tool has failed me.
I’d much rather bisect through ten small, messy-but-functional commits than one "clean" mega-commit.
# The dream:
$ git bisect good
# The reality in a squash-only world:
# Bisect identifies a 2,000 line commit.
# You still have to manually hunt the bug.The "First Parent" Middle Ground
The biggest argument for rebasing is that merge commits make the git log look like a subway map designed by a toddler.
* Merge branch 'feature-x'
|\
| * Add validation
| * Fix typo
| * Fix typo again
| * Realized I broke validation
* | Add logging to production
|/Yes, that’s ugly. But you don't need to rewrite history to fix it. You just need to learn how to view it.
The --first-parent flag is the secret weapon of maintainable history. It tells Git to follow only the first parent of merge commits, effectively showing you the history of the "main" branch while ignoring the noise of the feature branches.
git log --oneline --graph --first-parentThis gives you a linear view of high-level changes (the merges) without destroying the granular data sitting inside those merges. You get the best of both worlds: a clean high-level summary and a detailed forensic record when you actually need to dig.
Use Rebase for You, Merge for Us
I’m not saying git rebase is evil. I use it every day. But I use it locally.
Rebase is a tool for cleaning up your workspace. Use it to fix your "oops" commits, reorder your thoughts, and stay up to date with main while you’re working.
# While working on a branch
git commit -m "fix typo"
git commit -m "actually fix typo"
git rebase -i HEAD~2 # Clean this up before anyone sees itBut once that code is shared? Once it represents the collective history of the project? Stop rebasing. A merge --no-ff (no-fast-forward) creates a clear record that a feature started, lived for a while, and was integrated at a specific point in time.
Context is King
We don't write Git messages for the compiler; we write them for the person who has to fix our code in two years. That person is usually us, and we are usually tired.
A "clean" history that lacks context is just a well-organized cemetery. It looks nice from the road, but it doesn't help the living. Stop obsessing over the straight line and start valuing the metadata that helps you understand *why* the code changed. Your future self, standing over a broken production server at 3 AM, will thank you.