Without Playing the Blame Game
Many teams treat git blame as a tool for pointing fingers, when in reality it delivers valuable context: who changed a line, when, and why. This article shows practically how to get sharper results with flags like -w, -C, and -M, cleanly exclude mass reformatting commits, and correctly read blame output in both PhpStorm and the command line.
Table of Contents
- 1. Why git blame was never meant for pointing fingers
- 2. Basic syntax and reading blame output correctly
- 3. Filtering whitespace noise from history with -w
- 4. Tracking moved and copied code with -C
- 5. Detecting lines moved within the same commit using -M
- 6. Time-boxing blame with --since
- 7. Hiding mass reformats with .git-blame-ignore-revs
- 8. Reading blame output: PhpStorm gutter vs. the command line
- 9. When git blame isn't enough: git log -p and --follow
- 10. Summary
- 11. FAQ
1. Why git blame was never meant for pointing fingers
The name git blame is one of the most unfortunate choices in the entire Git vocabulary, because it suggests the exact opposite of what the command is actually for. Inherited historically from CVS and SVN's annotate, git blame shows the commit, author, and timestamp of the last change for every line of a file. This is code archaeology, not a tribunal: the output answers the question "in what context did this line come to be", not "who screwed this up".
In teams with a healthy code review culture, git blame is mainly used to find the right person to ask about a change, someone who knows the original requirement, the bugfix, or the design decision behind a line. Anyone who uses git blame as a tool for public shaming instead poisons collaboration and ensures developers would rather leave messy code alone than risk a visible change with their name attached. Some teams deliberately set up an alias like git config alias.context blame to anchor this mindset in language too.
What matters is the perspective the output is read with. A line written three years ago by someone who has long since left the company is not an accusation, it's a signal that the associated commit message, the linked pull request, or the ticket system is now the only reliable source of context. git blame is the starting point of an investigation, not its endpoint.
2. Basic syntax and reading blame output correctly
The simplest call, git blame file.php, shows four core pieces of information for every line: the abbreviated commit hash, the author's name, the date and time of the change, and the line number, followed by the actual line content. By default, Git sorts by line number in the current file version, not by commit order, which can make the output look confusing at first glance in frequently edited files. Reaching for the hash with git show <hash> immediately gives you the full commit, message and diff included.
For deeper analysis, git blame -e is worth knowing: it shows email addresses instead of names and avoids mix-ups when multiple authors share the same name. git blame -s hides author and date entirely and shows only the hash and line content, handy when only the change history matters, not the person behind it. The example below shows a typical output for a PHP class in a Magento module, narrowed with -L to a specific line range.
$ git blame -L 10,18 src/app/code/Mironsoft/SeoSuite/Model/MetaGenerator.php
^a1b2c3d4 (Jane Miller 2024-03-11 14:22:07 +0100 10) class MetaGenerator
a1b2c3d4 (Jane Miller 2024-03-11 14:22:07 +0100 11) {
f9e8d7c6 (Tom Weber 2024-06-02 09:15:41 +0200 12) public function __construct(
f9e8d7c6 (Tom Weber 2024-06-02 09:15:41 +0200 13) private readonly StoreManagerInterface $storeManager,
3c4d5e6f (Jane Miller 2025-01-19 16:40:12 +0100 14) private readonly ScopeConfigInterface $scopeConfig,
3c4d5e6f (Jane Miller 2025-01-19 16:40:12 +0100 15) ) {
7a8b9c0d (Tom Weber 2025-11-04 11:03:55 +0100 16) }
7a8b9c0d (Tom Weber 2025-11-04 11:03:55 +0100 17)
a1b2c3d4 (Jane Miller 2024-03-11 14:22:07 +0100 18) public function generate(): string
# ^a1b2c3d4: the caret marks the commit that first created the file (boundary commit)
3. Filtering whitespace noise from history with -w
Mass commits that switch indentation from tabs to spaces, apply a PHP-CS-Fixer or Prettier rule retroactively, or normalize line endings are the most common reason git blame appears to point at the wrong author. Without a countermeasure, every affected line ends up attributed to the reformat commit, even if the actual code hasn't changed in years. The -w flag tells Git to ignore whitespace-only changes when attributing lines and instead keep walking back to the last substantive change.
git blame -w file.php still compares every version as usual, but skips commits where a line differs from its predecessor only by spaces, tabs, or line breaks. The result is a much more meaningful history, especially in legacy modules that have been run through automatic formatting tools multiple times. Combined with -C for moved code, -w provides the foundation for a blame output that actually says something about authorship instead of just the last formatting pass.
# Without -w: a whitespace-only reformat commit dominates the blame
$ git blame -L 40,42 app/code/Mironsoft/Core/Helper/Data.php
9f1a2b3c (Tom Weber 2026-02-14 08:00:00 +0100 40) if ($value === null) {
9f1a2b3c (Tom Weber 2026-02-14 08:00:00 +0100 41) return $default;
9f1a2b3c (Tom Weber 2026-02-14 08:00:00 +0100 42) }
# Commit 9f1a2b3c only re-indented the file, it did not write this logic
# With -w: whitespace-only commit is skipped, the real author surfaces
$ git blame -w -L 40,42 app/code/Mironsoft/Core/Helper/Data.php
4d5e6f7a (Jane Miller 2023-08-30 12:11:03 +0200 40) if ($value === null) {
4d5e6f7a (Jane Miller 2023-08-30 12:11:03 +0200 41) return $default;
4d5e6f7a (Jane Miller 2023-08-30 12:11:03 +0200 42) }
4. Tracking moved and copied code with -C
When a method is extracted from a class into a new, dedicated service class, git blame without additional flags by default shows exactly that extraction commit as the origin of every moved line, even if the logic hasn't changed in years. The -C flag solves this by telling Git to also search for code copied or moved from other files within the same commit. There are three escalation levels: a plain -C only searches files that were also changed in the same commit.
-C -C (also written -CC) extends the search to all files newly added in the commit, even if they were otherwise untouched. -C -C -C (-CCC) goes furthest and searches the entire commit history for the origin, regardless of whether the source file even still exists, the most thorough but also the most computationally expensive variant. For large Magento modules with frequent refactoring, -C -C -C is often the only way to reconstruct actual authorship across file and class boundaries.
# A method was extracted into a new service class, find its real origin
$ git blame -C -C -C -L 5,7 app/code/Mironsoft/SeoSuite/Service/SchemaBuilder.php
e2f3a4b5 app/code/Mironsoft/SeoSuite/Model/MetaGenerator.php (Jane Miller 2024-03-11 5) public function buildProductSchema(
e2f3a4b5 app/code/Mironsoft/SeoSuite/Model/MetaGenerator.php (Jane Miller 2024-03-11 6) ProductInterface $product
e2f3a4b5 app/code/Mironsoft/SeoSuite/Model/MetaGenerator.php (Jane Miller 2024-03-11 7) ): array
# Original file and author of the logic are preserved across the extraction,
# instead of showing the refactoring commit as the "author" of the logic
5. Detecting lines moved within the same commit using -M
While -C tracks code across file boundaries, -M handles the simpler but equally common case: lines that were moved up or down within the same file, for example because a method was reordered or a constant was pulled to the top of the file. Without -M, Git marks every moved line as "newly written" in the current commit, which makes the blame history useless, especially for larger cleanup commits.
Detection is based on a configurable similarity threshold, 20 alphanumeric characters by default: a code block shorter than this minimum length is not recognized as a move and is still counted as a new line. Using -M<n>, for example -M10, lowers this threshold for short but content-identical lines like individual array entries or short conditions. In practice, -M is almost always combined with -C -C -C as git blame -M -C -C -C, to correctly attribute both lines moved within a file and lines copied across file boundaries.
6. Time-boxing blame with --since
Not every blame query needs the full history of a file back to its very first commit. When the actual question is "who changed this line last quarter" or "is this regression part of the last release cycle", git blame --since=<date> gives you a time-boxed result that ignores older changes and instead stops at the most recent change within the chosen period. Accepted values range from absolute dates like --since=2026-01-01 to relative expressions like --since="3 months ago".
A related, often overlooked alternative is the revision-range syntax git blame <old>..<new> -- file.php, which explicitly limits blame to commits between two reference points, for example between the last stable tag and HEAD. This is especially valuable when debugging after a deployment: instead of searching the entire history, the narrowed blame shows only lines that were actually changed within the suspect release window, drastically reducing the number of candidate commits.
7. Hiding mass reformats with .git-blame-ignore-revs
The -w flag helps against pure whitespace changes, but fails for commits that also make real formatting decisions, such as line-wrapping long method calls via PHP-CS-Fixer or a complete switch to PSR-12. For exactly this case, Git has offered the .git-blame-ignore-revs file in the repository root since version 2.23: a simple list of commit hashes that git blame skips entirely when attributing lines, regardless of what they actually changed.
For Git to honor this file automatically, it must be registered via the blame.ignoreRevsFile config, once per clone, or project-wide via a versioned .gitconfig recommendation in the README. GitHub and GitLab now also recognize .git-blame-ignore-revs automatically and hide the listed commits in their web blame view. Important: every new mass reformat commit must be added to the file manually, ideally as a fixed step in the pull request checklist right after merging.
# .git-blame-ignore-revs (lives in the repository root)
# Commits listed here are skipped by git blame and by GitHub/GitLab blame view.
# Add the full commit hash plus a comment explaining why it is ignored.
# Applied PSR-12 formatting across the entire app/code directory
a3f9c21e8b4d6f0912ab34cd56ef7890a1b2c3d4
# Migrated indentation from tabs to 4 spaces (editorconfig change)
7b2e4f61c9a80d3e5f6178902b3c4d5e6f7a8b9c
# Ran php-cs-fixer with the new house style ruleset
c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0
# Register the ignore file once per local clone
$ git config blame.ignoreRevsFile .git-blame-ignore-revs
# From then on, git blame automatically skips the listed commits
$ git blame -L 1,5 app/code/Mironsoft/Core/Helper/Data.php
# Verify the config value was actually set
$ git config --get blame.ignoreRevsFile
.git-blame-ignore-revs
8. Reading blame output: PhpStorm gutter vs. the command line
PhpStorm shows blame information by default as an Annotate column on the left edge of the editor: right-clicking the line number gutter and choosing "Annotate" reveals author, date, and a color-coded age indicator, with recent changes shaded warmer. Clicking an entry opens the full commit directly in the integrated VCS log, including the commit message, diff, and linked pull request, provided the git hosting integration is configured. For quick context while reading unfamiliar code, this is hard to beat.
The command line plays to its strengths when it comes to automation and targeted queries. git blame -L 120,145 file.php restricts output to a specific line range and is noticeably faster than waiting for a full IDE annotation on a file that's a thousand lines long. For tooling and scripts, --porcelain or --line-porcelain is the right choice: a machine-readable format with one data block per line that outputs commit metadata like author, email, and timestamp as individual key-value pairs, and can be parsed reliably without fragile regex handling of the human-readable default output.
9. When git blame isn't enough: git log -p and --follow
git blame only shows the lines currently present in a file and each one's most recent change commit, deleted code never appears in its output at all. When the actual question is "why was this function removed three months ago and now needed again", only git log -p -- file.php helps, which prints the full commit-by-commit diff history of a file, including every deleted and later restored section.
If a file was renamed or moved to a different folder, git blame without -C flags typically loses the connection to history before the rename. git log --follow -- file.php solves exactly this problem for the commit list by automatically detecting renames and continuing the history across the rename point. The combination git log --follow -p therefore delivers the complete change history of a file across every rename, while git blame always shows only a snapshot of the currently existing lines. The table below compares both approaches for typical scenarios.
| Scenario | Less useful | Recommended approach |
|---|---|---|
| Whitespace-heavy history | plain git blame |
git blame -w |
| Mass reformat commit | blame shows the reformat commit | blame.ignoreRevsFile |
| Targeted line query | blame on the whole file | git blame -L for the line range |
| Deleted then recreated | blame alone isn't enough | combine git log -p / --follow |
| Team mindset | blame as "who's to blame" | blame to find the right person to ask |
In practice, the tools complement each other: git blame quickly answers "who last touched this line", while git log -p and --follow fill the gaps that a pure line-level snapshot inherently can't cover. Combine both tools routinely, and you find in minutes the context that used to require a follow-up thread in the team chat.
Mironsoft
Git workflows, code reviews, and developer processes for Magento and Hyvä teams
Ready to establish clean Git history and code reviews on your team?
We help Magento and Hyvä teams build traceable Git workflows, from commit conventions through blame-ignore configuration to code review processes that put context ahead of blame.
Git workflow audit
Analyzing commit history, branching strategy, and blame hygiene in your repository
Onboarding & training
Training teams in the productive use of git blame, log, and reviews
CI/CD & tooling
Setting up blame.ignoreRevsFile, hooks, and review automation for Magento projects
10. Summary
git blame solves a core problem in everyday development: understanding the context behind a line of code quickly, without needing to ask in the team chat. The -w flag filters out whitespace noise, -C in its three escalation levels tracks copied and moved code across file boundaries, -M detects moves within the same file, and --since narrows the search by time. Together, these flags turn a superficially useless blame output into a precise tool for code archaeology.
Mass reformats belong consistently outside the history via .git-blame-ignore-revs and the blame.ignoreRevsFile config, instead of being tediously skipped by hand on every blame query. PhpStorm's Annotate view and the command line with -L and --porcelain complement each other depending on the use case, and where git blame reaches its limits, for example with deleted or renamed code, git log -p and --follow provide the missing depth. The most important principle remains true regardless of which flag you reach for: git blame is a tool for context, not a weapon for assigning blame.
Using git blame Correctly, The Essentials at a Glance
Context instead of blame
git blame answers "why is this the way it is", not "who was it". Find the right person to ask, don't shame them.
Combine -w, -C, -M deliberately
Ignore whitespace, track moved/copied code via -C -C -C, detect line moves with -M.
Cleanly hide reformat commits
Register .git-blame-ignore-revs plus blame.ignoreRevsFile so mass formatting doesn't distort history.
Combine IDE and CLI
PhpStorm Annotate for quick context, git blame -L and --porcelain for targeted queries and tooling.