Reformat everything, keep git blame honest: .git-blame-ignore-revs
This morning I noticed the Scala backend of this site had never been through a formatter. The Spring-to-Play migration produced working code, but some of it was written as one-liners: one line over 800 characters wide, a seven-field case class on a single line. So I added scalafmt, ran it once over the whole backend, and shipped the result as one commit.
That commit touched 80 files, with 5,924 lines added and 2,571 removed. It changed no behaviour, and it would have taken over git blame for most of the backend.

What a reformat does to blame
git blame answers "which commit last changed this line?" A formatter changes almost every line, so after the reformat the honest answer is nearly always "the reformat". That is true, and it is useless when you want to know why a line exists.
I measured it on one file. VisitService.scala has 260 lines after formatting. A plain git blame attributed 207 of them to the scalafmt commit. The decisions behind those lines were made weeks earlier, in the migration and in a later move from the Cake pattern to Guice. None of that was visible any more.
The fix is a list of commits
Git has supported ignoring revisions in blame since 2.23. You can pass a commit with --ignore-rev, or keep a list in a file and pass that with --ignore-revs-file. By convention the file is called .git-blame-ignore-revs and lives at the repository root:
# Formatting-only commits; GitHub blame skips these. Locally: git config blame.ignoreRevsFile .git-blame-ignore-revs
# Format the backend with scalafmt
d6c2d54617fdb4c774a3b1bf71767f5524bc3b25
Use the full 40-character SHA, one per line. Comment lines are allowed, so write down what each commit was. Someone will ask later.
With the file in place, the same blame on VisitService.scala attributes only 22 lines to the reformat. The other 185 go back to the commits that actually wrote them.

Lines that move are still traced
I expected the ignore list to work only for lines that kept their shape. It handles more than that. scalafmt split RecruiterVisit from three long lines into one parameter per line, and git still maps those new lines back to the migration commit. When git ignores a commit, it looks for a matching line in the parent version of the file and continues blaming from there.
Two settings make this visible locally:
git config blame.markIgnoredLines true
git config blame.markUnblamableLines true
With them, a line that blame re-attributed past an ignored commit gets a ? prefix. A line that could not be traced gets a *. When I checked the 22 lines still blamed on the reformat, they were all * lines: blank lines and lone closing parentheses that scalafmt added. No earlier commit contains them, so the reformat is the correct answer for those.
GitHub reads the file automatically
I didn't configure anything on GitHub. A .git-blame-ignore-revs at the root of the repository is enough. The blame view then shows a banner saying it is ignoring the revisions in that file:

Further down the same file, the case classes scalafmt rewrapped are still blamed on the migration from 28 days ago. Only the blank separator lines between them are blamed on the reformat:

Local git does not read the file by default. Each clone needs it set once:
git config blame.ignoreRevsFile .git-blame-ignore-revs
Three things to get right
Keep the reformat in its own commit
The ignore list works per commit. If the reformat also contains a real change, ignoring it hides that change from blame too. My PR had three commits: the tooling (config, sbt plugin, CI check), the reformat, and the ignore file. One line in the reformat commit was not formatting: it restored an import anorm.postgresql._ that an IDE "optimize imports" pass had dropped. I described it in the commit message so that ignoring the commit doesn't hide it.
The SHA has to survive the merge
The ignore file can only name the reformat after it has been committed, so it goes in a follow-up commit. That only works if the SHA reaches master unchanged. This repository uses merge commits, so it does. A squash merge or a rebase merge would create new commits with different SHAs, and the file would point at a commit that isn't on the main branch. With squash merging, add the SHA after the merge, in a separate small PR.
Make it the last reformat
An ignore list should stay short. CI now fails if a file isn't formatted, and the editor hook formats each file as it is changed, so there shouldn't be a second commit like this one. If there is, it goes on the next line of the file.
Summary
A formatting commit is easy to justify and leaves a lasting cost in blame. Listing its SHA in .git-blame-ignore-revs removes most of that cost: 207 of 260 lines attributed to the reformat became 22, and those 22 are lines the formatter actually created.