Automatic code formatting: where the process breaks down
Automatic code formatting is almost always postponed: the repository grows faster than anyone gets around to configuring rules. While the team is small, style rests on agreements and the reviewer's attentiveness — indentation, quotes, import order, line length.
But once a second language and a third repository appear, style diverges: each editor is configured differently, and there is no single source of truth.
The cost shows up not in debates about taste but in measurable things. Manual edits in review are the most frequent cause of comments: the discussion cycle goes into spaces and line breaks instead of the logic of the change.
Noise in diffs grows with the number of affected files — the real change has to be found among reformatted neighboring lines, time to merge increases, and the chance of missing a defect grows.
A separate category is build failures due to style. Prettier, Black, and gofmt are run at one pipeline step, style checks at another, and the rules on the developer's machine differ from the rules on the build server.
The result is a red build on a line the author did not touch, and the habit of restarting it instead of investigating the cause. Below we examine how this is addressed and why the formatter should keep a single run mode.
What changes in the team and pipeline after adopting auto-formatting
The formatter runs before review, not after it: a commit hook rewrites the file, and the pipeline check fails if the style diverges from the config. The reviewer sees only semantic changes — the tool has already handled whitespace rearrangement and line breaks. This changes not individual speed but the structure of the process itself.
The effect is measurable. Before adoption, fixing indentation and quotes took from a quarter to half of review time, and diffs added hundreds of lines of noise.
Afterward, the share of lines changed for style drops almost to zero, and contentious edits stop being a subject of discussion: they are agreed on once in the config.
A single style without manual edits
The config is versioned together with the project, so the rule works the same in the editor, on the local machine, and in the pipeline. Prettier with a fixed set of options removes variability: the file is brought to a single form regardless of who wrote it.
A new engineer does not need to learn someone else's formatting style — the formatter applies it automatically.
Less noise in review and fewer edits in diffs
Less noise in the diff means review converges faster: comments shift from formatting to logic and edge cases. Build stability grows for the same reason — style discrepancies drop out of the list of failure causes.
Before a mass run, formatting is put into a separate commit so that semantic and cosmetic changes do not mix in history.
Check and fix modes: from local hook to build step
The modes differ in who pays for the fix and when: locally, a fix costs seconds; in the pipeline, it costs a build, review, and rerun cycle. That is why modes are usually combined rather than choosing just one. Below is when each mode is appropriate and where it stops working.
Local fix before commit
Formatting on save in the editor keeps the working copy consistent: the author does not think about style, and diffs are not polluted by whitespace rearrangement.
A pre-commit fix via a pre-commit hook runs the formatter over changed files and stops the commit if anything remains unfixed. Selective running over files is important in large repositories: a full pass over all source files on every commit noticeably slows down work.
Check as a mandatory pipeline step
Here the formatter runs in check mode, without writing, and fails with a non-zero return code. This is the only mode that cannot be bypassed by local settings — and it serves as a common barrier for all branches. The price is that the fix must be made after the push, which means rerunning the build.
| Option | When it fits | Limitations |
|---|---|---|
| Formatting on save | Daily work, immediate feedback needed | Depends on each developer's settings, does not work for edits in the web interface |
| Fix before commit | Need to keep unformatted code out of history | Requires installing hooks for everyone, can be bypassed with a skip-checks flag |
| Pipeline step | Common barrier for all branches and external contributors | Error is visible only after push, rerunning the build is required |
| Selective run over changed files | Large repositories with a long full pass | Requires a correct branch comparison base, otherwise some files are missed |
How we add style checks to the pipeline: work stages
Formatting enabled in one commit across all repositories resets the edit history: it stops being clear who changed a line and why, and change review drowns in whitespace rearrangement.
That is why we implement formatting as a separate project with reversible steps that do not block releases.
Inventory of repositories and exclusions
First we build a map: languages and their shares, activity, generated code, vendor directories, files with intentional alignment — tables, migration schemas, markup.
Everything created by the build goes into exclusions; otherwise the formatter and generator would endlessly edit each other.
- Inventory of repositories: languages, activity, generated and vendor directories.
- Shared rule profile — a single set of settings per organization, with language differences defined by overrides rather than copies.
- Pilot on one repository in check mode without fixes — we estimate the share of files that will be affected.
- One-time run over the repository in a separate commit: formatting only, without semantic changes.
- Connecting the check on a branch — first a warning, then a mandatory merge condition.
- Gradual enablement of the remaining repositories in batches, from the most active to archived ones.
- Handover to the team: profile, run commands, auto-fix before commit in the local environment.
Gradual tightening of checks
We do not introduce mandatory status immediately: the repository lives in warning mode until the check stops triggering on generated files. Then it becomes blocking — code that fails formatting does not enter the main branch, and style stops being a subject of debate in reviews.
What the team is left with: configs, rules, regulations
Formatting in the pipeline is not a single command but a set of artifacts in the repository. Their purpose is for a new engineer six months later to get the same rules with one command, without messaging the person who wrote the settings.
Configuration files and shared rule profile
- A single rule profile at the repository root: one config for all languages, without local variations in subdirectories — settings divergence between modules disappears as a class.
- Alignment with the editor:
.editorconfigrepeats the same indentation and line breaks so that IDE edits do not conflict with the formatter during merge. - Exclusion lists: generated code, vendor directories, migrations — only what the team edits by hand is formatted.
- Pipeline check step: merge is blocked if a file is not formatted; auto-fix comes as a separate commit.
- Hook on the developer's machine: a pre-commit check keeps unformatted code out and saves pipeline runs.
Documentation and exception procedure
- A short guide in the repository: how to run formatting locally, where the config is, and what to do in case of a conflict with the linter.
- Procedure for deviations from rules: an exception is documented with a reason and owner; otherwise the list grows and the rules stop working.
- Regulation for profile changes: rule edits go as a separate change with a run across the entire repository, not "along the way" in a work task.
The result: formatting stops depending on agreements — review diffs contain meaning, and the check is reproducible on any machine.
Case: monorepo without manual style edits
The client came with a monorepo: a dozen services in several languages, and a unified style existed only as a verbal agreement.
Everyone ran formatting manually, not always before committing, and code review turned into an argument about indentation — about a third of comments concerned the look of the file rather than logic.
Before
Code review on an average branch took about 35 minutes, and almost every third comment was about formatting.
Roughly every fifth commit did not change system behavior but only fixed indentation and line breaks: such edits settled into the shared history and made it harder to understand changes during rollback.
Merge conflicts in files maintained by several teams arose precisely because of different formatting — resolving the merge took more time than the actual work.
After
Formatting was moved into the pipeline: the check runs before changes are discussed; if there is a discrepancy, the build fails and attaches a ready fixed version.
Historical files were run in a separate commit through Prettier — without logic changes, so that edits remained readable.
Code review time dropped from 35 to 12 minutes. The share of style-only commits is less than 2%. Merge conflicts over formatting almost disappeared: the number of cases fell by roughly five times, and identical content in two branches now produces an identical file. Discussion is again about logic, not spaces.
Formatting and linting: what is the difference and what to do with legacy
The formatter and linter answer different questions, and trying to cover both with one check creates endless disputes in review. The formatter works with appearance: indentation, line length, quotes, import order — it rewrites the file and always produces the same result.
The linter looks at meaning: unused variables, unreachable branches, suspicious comparisons — and fixes them only partially.
| Criterion | Formatter | Linter |
|---|---|---|
| Subject | code appearance | logic and potential errors |
| Result | deterministic, the same for everyone | findings with severity levels |
| Auto-fix | complete, file is rewritten | partial, some decisions remain with the engineer |
| Legacy code | enabled in one mass pass | enabled gradually, rule by rule |
| Source of settings | one config in the repository | a set of rules and a trigger threshold |
The checks in the pipeline are separated into steps: formatting is a separate step with auto-fix, lint is separate, with its own return code. A failure of one does not mask the other, and the developer immediately sees exactly what needs to be fixed.
On long-lived code, the order is the reverse of the usual one. First, one mass formatter pass, placed in a separate commit — after that, any edit goes through an aligned file, and the change history is not polluted with style edits.
The linter is enabled gradually: rule by rule, findings in old modules are converted to warnings, and only new ones are blocked.
Conflict-free rules are achieved when there is one source of truth: the formatter defines style, and everything that duplicates formatting is removed from the linter set.
Otherwise, the tools pull the same code in different directions, and the team disables checks entirely — along with the useful ones.
Technical concerns: history noise, other languages, exclusions
A one-time reformat is a commit touching hundreds of files, where the logic of the edits is unreadable, and git blame afterward shows the person who ran the formatter as the author. Both problems are resolved by procedure, not persuasion.
Four concerns that come up before enabling a mandatory check, and how each is addressed by configuration and process.
A one-time "reformat everything" commit will break history
The pass is done as one separate revision, and its hash is added to the ignore file — line history is preserved, and a regular comment on an edit remains meaningful. After that, formatting changes only in lines that were already being edited.
We have a mixed stack
The check is split by globs: each language has its own formatter and config, run only on its files. Anything not matched by any glob is not checked, so a new language does not require rewriting the whole rule.
There is generated code and vendor directories
Builds, migrations, schemas, and third-party libraries are ignored: we do not format generated content, otherwise the next generation will bring the edits back and the check will fail again. An edge case is handled by a targeted disable for a file or block with a comment about the reason.
Teams have different styles
Style moves from chat into one shared config — the dispute becomes an edit to one file, visible in review. The pipeline check makes the outcome non-negotiable: team styles converge to a fixed set of rules, not to someone's taste.
Let's discuss the task and come back with an estimate and work plan
For the conversation to be specific rather than general, a few inputs are enough for us.
Automatic formatting depends almost entirely on your repository: whether it is one or a dozen connected ones, which languages coexist in it, what is already fixed in the rules, and what prevents enabling the check today. Below is what helps us come back with an estimate and plan, rather than with follow-up questions.
- Repository composition: where there is a monorepo, where there are separate services, which languages and in what proportion, and whether there are shared library directories.
- Current formatting rules: formatter configs, editor settings, exclusions for generated and vendor code, and contentious files.
- Check requirements: what should block merge, what is sufficient to warn about, which branches are protected, and whether auto-fix is needed instead of failure.
- Share of legacy code: the volume of files that cannot be formatted in one commit, and the team's readiness for a large one-time edit.
- Rollout order: whether you plan to enable it immediately for all branches or in stages — first a report, then auto-fix, then blocking.
- Runtime environment: where build tasks run, whether there are access restrictions and isolated runners.
Describe the task in free form — repository composition, languages, and current rules. We will come back with an estimate and work plan where the rollout order is laid out step by step and it is clear what to check before enabling blocking.







