skip to content

How do you make git blame skip a mass-reformatting commit?

level: middleimportance: should knowfreq 40%

answer

  1. Formatter commit hides every real author
  2. Give Git a skip list
  3. Full object ids, one per line
  4. One config key makes it automatic
  5. Config is not cloned with the repo

basics

~20 s

List the reformatting commit's full id in a file and point Git at it: git blame --ignore-rev <sha>, or --ignore-revs-file <file>, or set blame.ignoreRevsFile so it applies automatically. Blamed lines are then attributed to the previous commit that touched them.

solid answer

~40 s

Git can be told to look through specific commits when blaming. `git blame --ignore-rev <sha>` skips one commit; `git blame --ignore-revs-file <path>` skips every commit id listed in a file, one full object id per line with `#` comments allowed. Commit the file — the conventional name is `.git-blame-ignore-revs` — and register it once with `git config blame.ignoreRevsFile .git-blame-ignore-revs` so every subsequent blame in that clone honours it. Lines whose only change came from an ignored commit are attributed to the commit that touched them before it. Two config keys control how the result is marked: `blame.markIgnoredLines` prefixes lines whose attribution was adjusted, and `blame.markUnblamableLines` marks lines Git could not attribute elsewhere. Note the config key is repository-local, so each clone must set it; the file being committed is what makes the *list* shared.

go deeper

for a junior

Know that a repository-wide reformat makes blame point at the wrong commit, and that Git has a supported way to skip such commits rather than requiring history rewrites.

for a middle

Name the flags and the config key, describe the file format with full object ids, and explain that lines fall through to the previously touching commit.

for a senior

Cover the operational details: the config is per clone and not cloned, a missing configured file breaks blame, and ignored commits must be strictly mechanical or attribution silently lies.

for a principal

Own the policy: mandate formatter rollouts as isolated commits recorded in the ignore file within the same change, and document the one-time config step so archaeology stays trustworthy for years.

## The problem Run a formatter across a codebase in one commit and you have just made `git blame` useless for that codebase: every line's last-touching commit is now the formatting commit, and every line's apparent author is whoever ran the tool. The information is not lost — it is one drill-down step away — but the default answer is now noise for every line of every file. ## The mechanism Git's answer is an explicit skip list. When blame reaches a commit on the ignore list, it does not attribute lines to it; instead it passes the attribution through to the commit that previously touched those lines. Three ways to supply the list: 1. **One-off, single commit:** `git blame --ignore-rev <sha> -- <path>`. Repeatable for several commits. 2. **One-off, from a file:** `git blame --ignore-revs-file <path> -- <file>`. 3. **Persistent:** `git config blame.ignoreRevsFile <path>` — after that, plain `git blame` uses the list automatically. ## The file format The file contains one **full** object id per line. Abbreviated ids are not accepted, which is deliberate: an ignore list must be unambiguous. Blank lines are allowed and `#` starts a comment, so the convention is to annotate each entry: ``` # Apply the project formatter across src/ 8f1a9c2b6d3e4f5a7b8c9d0e1f2a3b4c5d6e7f80 ``` By convention the file is named `.git-blame-ignore-revs` and lives at the repository root, committed like any other file, so the list travels with the project and everyone shares the same view of which commits are noise. ## Config is per clone A point candidates routinely miss: committing the file does **not** switch the behaviour on. `blame.ignoreRevsFile` is Git configuration, and repository-level configuration lives in `.git/config`, which is not part of the repository's content and is not cloned. So each developer runs the `git config` command once (or the project documents it in its contributing guide). The committed file gives everyone the same *list*; the config makes their Git *use* it. A robustness note: if the configured file does not exist, blame fails rather than silently ignoring the setting. Setting the key without committing the file — or after the file is later renamed — therefore breaks blame for anyone who followed the instructions. ## Marking the adjusted lines Skipping a commit changes what blame reports, and it is worth being able to see where that happened: - `blame.markIgnoredLines` — when true, lines whose attribution was moved because of an ignored commit are marked in the output, so you know the answer is approximate. - `blame.markUnblamableLines` — when true, lines that Git could not attribute to any non-ignored commit are marked. This happens when the ignored commit did more than reformat: if it genuinely introduced a line, there is no earlier commit to attribute it to. That second case is the important caveat. The mechanism is honest only if the ignored commits really are content-preserving. A commit that reformats *and* fixes a bug will have the fix quietly attributed to an older commit or flagged as unblamable, which is worse than not ignoring it. The discipline the mechanism implies is therefore: a formatting commit must contain formatting and nothing else. ## What it does and does not affect The ignore list affects blame only. It does not rewrite history, does not change any object, and does not affect `git log`, `git show` or diffs — the reformatting commit is still there and still fully inspectable. It is purely a lens over blame's attribution walk. ## Complementary options - `git blame -w` ignores whitespace when deciding whether a line changed, which handles pure indentation churn without needing a list at all. - `-M` and `-C` follow lines that moved within or between files. Use `-w` for incidental whitespace, the ignore list for named, deliberate, repository-wide rewrites. In practice a project that runs a formatter also adds each formatter-rollout commit to `.git-blame-ignore-revs` as part of the same change, so the list grows alongside the history it describes.

  • Why is committing .git-blame-ignore-revs not enough on its own?
    Because the behaviour is driven by configuration, and repository configuration lives in .git/config, which is not part of the repository content and is not cloned. Each developer must run git config blame.ignoreRevsFile .git-blame-ignore-revs once, or pass --ignore-revs-file explicitly.
  • What happens to a line the ignored commit genuinely introduced?
    There is no earlier commit to attribute it to, so blame either passes it to a neighbouring commit or reports it as unblamable; blame.markUnblamableLines makes those visible. This is why an ignored commit must be purely mechanical — a formatting commit that also fixes a bug corrupts the attribution.
  • Does the ignore list change the repository in any way?
    No. It is a lens applied while blame walks history. The ignored commit remains in the history, fully visible to git log, git show and every diff; nothing is rewritten and no object changes. Only blame's line attribution is affected.
  • When is -w sufficient instead of an ignore-revs file?
    When the churn is purely whitespace — reindentation, trailing-space cleanup — because -w makes blame ignore whitespace differences when deciding whether a line changed. An ignore list is for larger deliberate rewrites where content moved on the line, such as reflowing arguments or changing quote style.

saying these in an interview costs you the question

  • Thinks committing the file enables it automatically
  • Suggests rewriting history to remove the formatting commit
  • Uses abbreviated commit ids in the list
  • Assumes the ignore list also affects git log
  • Mixes a bug fix into an ignored formatting commit

context