Skip to content
Field Notes
Go back

Git Blame Beyond the Basics

git blame <file> is one of those commands most developers learn once and never revisit. But the defaults are conservative — they optimize for simplicity, not accuracy. The flags that change what blame attributes and how it searches for origins are where the real value is.


TL;DR

git blame has flags for restricting output to line ranges (-L), detecting code that moved between files (-C), ignoring whitespace changes (-w), and producing machine-readable output (-p). The combination -w -C -C gives accurate attribution for most real-world codebases where modules have been extracted and formatters have run. Start there; escalate only when needed.


Context

The default git blame output is: who last touched each line, in the entire file, with no attempt to look through moves or copies. That works fine for a file that has never been refactored. It breaks down the moment any of the following is true:

In each case, the blame output will point at the refactor, the formatter, or the renaming commit — not the commit where the logic was actually written. The signal you want is buried.

The flags below address each of these cases specifically.


Analysis / Key Findings

Line Ranges with -L

The most immediately useful flag. Instead of blaming an entire file, restrict the output to the lines you actually care about.

# Blame only lines 40–60
git blame -L 40,60 src/auth.ts

# Blame from line 40 to end of file
git blame -L 40, src/auth.ts

# Blame a function by name — Git searches for the function boundary
git blame -L :validateToken src/auth.ts

The function-name form (-L :functionName) is particularly useful: Git uses its own heuristics to find the start of the named function and attributes only the lines within it. This means you can ask “who last touched validateToken?” without knowing or caring which line numbers it spans. The heuristics are language-aware enough to work reliably on most common languages.

-L accepts a range, a start with no end, an end with no start, or a function name. The function name variant can also take a regex: -L '/^function foo/,/^}/.

Copy Detection with -C and -M

This is the flag that changes the most. By default, blame will attribute lines to the commit that added them to the file you are looking at — regardless of where those lines came from. If a function was cut and pasted from another file, blame shows the paste, not the original write.

-M and -C instruct blame to look through moves and copies:

The practical implication: when a module is extracted — utils.ts is split off from app.ts — the extraction commit is what blame shows by default. With -C, blame traces back through the extraction and attributes lines to whoever originally wrote them.

# Standard copy detection — covers most extraction scenarios
git blame -C src/utils.ts

# Broader search — handles copies across different commits
git blame -C -C src/utils.ts

Gotcha: -C -C -C can be very slow on large repositories with long history. It searches every file in every commit, which is a lot of work. Start with -C and escalate to -C -C only if the results still point at an extraction commit rather than the original author. Reach for -C -C -C rarely and deliberately.

Ignoring Whitespace with -w

-w tells blame to ignore whitespace when comparing lines to their prior versions. Practically: a line that was only re-indented, had trailing spaces removed, or had its internal spacing adjusted will be attributed to the commit that last changed meaningful content, not the commit that adjusted the whitespace.

git blame -w src/file.ts

This is a lighter-weight alternative to .git-blame-ignore-revs for cases where you haven’t set that up yet, or where only some lines were affected by formatting. The two are complementary: -w handles whitespace-only changes at the line level; .git-blame-ignore-revs handles entire commits that should be skipped. For the full picture on hiding formatting commits from blame, see the companion article on .git-blame-ignore-revs.

Email Output with -e

By default, blame shows the committer’s username (the user.name Git config value). -e switches this to the email address:

git blame -e src/file.ts

Useful when names are ambiguous, when you are correlating blame output against a user directory indexed by email, or when two contributors have similar display names.

Porcelain Format for Scripting with -p

The default blame output is formatted for human reading — it is not stable across Git versions and not easy to parse. -p (porcelain) produces a machine-readable format with complete commit metadata.

git blame -p src/file.ts

Each group of lines in the output starts with a 40-character SHA, followed by line number information, then the commit metadata (author, author-mail, author-time, committer, etc.) as key-value pairs, then the line content prefixed with a tab. The format is designed to be parsed by scripts: each field has a defined prefix, and the structure is stable.

When building tooling on top of blame — dashboards, ownership reports, automated review routing — -p is the correct flag to use.

--since and --until

Blame normally walks the full history. --since restricts it to commits after a given date; --until restricts it to commits before one.

# Show blame considering only commits from the last 6 months
git blame --since="6 months ago" src/file.ts

This is useful for answering questions like “who has been maintaining this file recently?” when you know that historical authorship is not relevant to your investigation. Note that lines last touched before the cutoff will show as “not yet committed” or fall back to the oldest visible commit — check the Git docs for the exact behavior in your version.

Combining Flags: The Power User Invocation

The flags compose. The most generally useful combination for a codebase with extracted modules and a formatter in its history:

git blame -w -C -C src/some-module.ts

-w handles whitespace changes; -C -C handles lines that were copied from other files at any point. Together they give you accurate attribution for the large majority of real-world cases without the performance cost of -C -C -C.

Add -L when you only need a specific function:

git blame -w -C -C -L :handleRequest src/server.ts

Add -e if you need email addresses instead of names:

git blame -w -C -C -e src/some-module.ts

Conclusion

The default git blame invocation is a starting point, not a finished tool. -L makes it focused; -C makes it accurate across file moves; -w makes it resilient to formatting noise; -p makes it scriptable. Using these flags in combination transforms blame from “who touched this file last?” into “who is actually responsible for this logic?” — which is the question worth asking.

The one discipline these flags do not replace is keeping formatting changes in isolated commits. -w catches whitespace at the line level, but a commit that mixed significant logic changes with a bulk reformat cannot be attributed correctly regardless of flags. That problem is better addressed at the commit level, which is what .git-blame-ignore-revs is designed for.


References


Share this post on:

Previous Post
Transporting Git Repositories Offline with git bundle
Next Post
Hiding Formatting Commits from Git Blame with .git-blame-ignore-revs