How to adopt a formatter in an old codebase.

•

•

Reformat Everything in One Commit

I once spent a full Tuesday afternoon chasing a ghost in our deployment pipeline, only to realize the “bug” was actually just a massive, three-thousand-line diff caused by a developer’s IDE deciding to re-indent an entire module on a single save. It was a nightmare of merge conflicts and wasted cognitive load that had nothing to do with logic and everything to do with chaos. Most people will tell you that solving this requires a massive, company-wide “refactoring sprint” or some complex, multi-stage migration strategy that takes months to perfect. They are wrong. Learning how to adopt a formatter in an old codebase isn’t about achieving aesthetic perfection; it’s about stopping the bleeding so you can actually see the code changes that matter during a code review.

I’m not going to give you a lecture on the philosophical benefits of clean code or suggest you spend three weeks debating semicolon placement. Instead, I’m going to show you the practical, slightly messy way I’ve handled this in production environments without breaking everyone’s workflow. I’ll walk you through the trade-offs of a “big bang” reformat versus a gradual approach and, most importantly, how to keep your git history readable so you don’t lose the ability to track why a line of code was changed three years ago.

Table of Contents

The Nightmare of Minimizing Merge Conflicts During Reformatting

The Nightmare of Minimizing Merge Conflicts During Reformatting

The biggest fear when you finally pull the trigger on an automated code style migration isn’t the tool itself—it’s the inevitable tidal wave of merge conflicts. I’ve seen it happen: a developer spends three days on a critical hotfix in a feature branch, only to find that the main branch was reformatted overnight. Suddenly, their diff is a sea of red and green that looks like a crime scene, and they can’t tell if the logic actually changed or if it’s just a missing semicolon. If you don’t have a plan, you aren’t just cleaning code; you’re sabotaging your teammates’ ability to ship.

To survive this, I’ve learned to favor an incremental code formatting strategy over the “big bang” approach. Instead of reformatting the entire monolith in one massive, terrifying PR, I try to target specific modules or even individual files as they come up in the sprint. It’s slower, sure, but it keeps the blast radius manageable. Even when you do go big, you have to protect your ability to audit the past. This is where you need to lean on `.git-blame-ignore-revs`. By adding your massive reformatting commit to that file, you ensure that `git blame` still points to the person who actually wrote the logic, rather than just the person who ran the formatter. It’s a small step, but it’s how you keep maintaining git history integrity while still cleaning up the mess.

Why Pre Commit Hooks for Legacy Projects Fail Without Strategy

Why Pre Commit Hooks for Legacy Projects Fail Without Strategy

The temptation to just slap a pre-commit hook onto a legacy repo is high, especially when you’re tired of seeing inconsistent indentation in pull requests. But if you just turn it on and walk away, you’re essentially setting a trap for every other engineer on the team. The moment someone touches a file that hasn’t been edited in three years, the hook will trigger, reformat the entire file, and suddenly their PR is a sea of white-space changes. This is how you end up with a massive, unreviewable diff that makes it impossible to see the actual logic change. Without a deliberate incremental code formatting strategy, you aren’t improving the codebase; you’re just creating a minefield of noise.

The real danger isn’t just the noise, though—it’s the loss of context. When you run an automated code style migration across a thousand files, you effectively “reset” the history of those files. If a bug is discovered six months from now, `git blame` will point to the person who ran the formatter, not the person who actually wrote the logic. To avoid this, you have to be proactive about maintaining git history integrity. I’ve learned the hard way that you need to pair your formatting runs with a `.git-blame-ignore-revs` file. If you don’t teach Git to skip over your “formatting-only” commits, you’re essentially burning your breadcrumbs for the next person who has to debug a production outage.

Five ways to do this without setting the repo on fire

  • Don’t try to fix the whole world at once. I’ve seen teams try to run a global format on a 500k line monolith on a Friday afternoon; it’s a great way to ensure nobody sleeps for a week. Pick one module, one directory, or even just one package, and win there first.
  • Use `.git-blame-ignore-revs`. This is the single most important piece of advice I can give. If you don’t add your massive formatting commit to this file, `git blame` becomes useless, and your coworkers will spend their afternoons hunting for the “real” author of a line that was actually changed by a semicolon move.
  • The “Format-on-Touch” rule is a trap for legacy systems. It sounds clean in theory to only format files you’re already editing, but it creates a weird, inconsistent codebase where half the files look like 2014 and the other half look like 2024. It’s better to be consistent, even if the consistency is achieved through a slow, deliberate rollout.
  • Coordinate with the CI pipeline before you touch the local environment. If your local formatter is version 1.2 and the build server is running 1.1, you’re going to enter a loop of “ghost commits” where the CI keeps re-formatting your code every time you push. Match the versions exactly.
  • Accept that the first month will be noisy. There will be merge conflicts, there will be grumpy comments in PRs, and there will be a few accidental regressions. I’ve learned that it’s better to deal with the friction of a clean codebase now than the friction of a “broken-at-2-AM” codebase later.

The Survival Checklist

Don’t try to fix the whole repo in one go; pick a single directory, format it, and commit it separately so the diffs don’t swallow the actual logic changes.

Use a `.git-blame-ignore-revs` file immediately after the big reformat, otherwise your teammates will spend their entire afternoon wondering why you “deleted” lines of code that were actually just moved by a semicolon.

Skip the mandatory pre-commit hooks for the whole team until the rules are stable; forcing a broken or aggressive formatter on a legacy build is just a fast way to ensure no one uses it.

The Long Game

Strategies for winning The Long Game.

At the end of the day, adopting a formatter isn’t a “one and done” task you check off a Jira ticket; it’s a surgical operation on a moving target. We’ve looked at why you can’t just run a global reformat and hope for the best, why pre-commit hooks can actually become a bottleneck if they aren’t tuned, and how to navigate those massive, headache-inducing merge conflicts. The goal isn’t to achieve aesthetic perfection across ten years of legacy code overnight. It’s about incremental consistency and ensuring that when we do touch a file, we aren’t fighting the editor as much as we are solving the actual business logic. If you follow the strategy of small, targeted batches, you avoid the 2:00 AM realization that a formatting change accidentally wiped out a critical configuration line.

I’ve seen enough production incidents to know that the “perfect” technical solution often fails because it ignores the humans actually typing the code. Don’t get caught up in the holy wars over tabs versus spaces or which specific linter rule is more pedantic. Just pick a standard, automate the boring parts, and protect the diffs. We write these things down so that the engineers coming after us—including the version of me five years from now—don’t have to suffer through the same preventable friction. Build a system that works with the team, not one that fights them every time they try to push a hotfix.

Frequently Asked Questions

How do I handle the "git blame" problem so my name doesn't suddenly appear on every single line of code in the repository?

The “git blame” problem is the most common reason people delay reformatting. If you run a global format, you become the “author” of every line, making it impossible to see who actually wrote the logic.

Is it better to run the formatter on the whole project at once, or should I just do it file-by-file as we touch them?

I’ve seen both approaches fail, but usually for different reasons. Doing it file-by-file feels safer, but it’s a lie; you end up with a codebase that looks like a patchwork quilt, and your diffs become a nightmare of stylistic inconsistency. I prefer the “big bang” approach—format the whole thing in one massive, dedicated commit. It’s painful for about an hour of merge conflict resolution, but then the noise stops forever.

If I'm in the middle of a massive feature branch, how do I rebase against the new formatted master without losing my mind?

The “rebase hell” happens because Git sees every single line change as a conflict, even if you didn’t touch the logic. Don’t try to rebase your feature branch against the new master directly; you’ll spend all day resolving whitespace. Instead, format your feature branch first using the exact same config. Once your branch matches the new master’s style, the rebase becomes a boring, automated task rather than a manual nightmare.

What do I do if the formatter's "standard" style actually makes certain complex logic or mathematical blocks harder to read?

I’ve run into this. You’ve got a dense matrix multiplication or a complex bitwise operation, and the formatter turns it into a vertical tower of single digits that looks like a broken staircase. It’s technically “correct,” but it’s unreadable.

Tomasz Wierzbicki

About Tomasz Wierzbicki

I am not interested in arguing about frameworks. I am interested in why the thing broke at two in the morning and what would have prevented it. Most of what I know came from being wrong in production and writing it down afterwards.