Understanding Who Changed Each Line with git blame
Suppose we find an important line inside a Laravel login feature:
if (!Auth::attempt($credentials)) {We may want to understand:
- Which commit last changed this line?
- Who authored that commit?
- When was it changed?
- What other commit information can help explain the reason?
- Who may have useful context about this code?
Git provides:
git blameThe basic syntax is:
git blame <file>For example:
git blame app/Services/AuthService.phpThe command annotates each current line in the file with information about the commit that last changed or introduced that line.
git blameconnects each current line of a file to the commit that Git considers responsible for its most recent change.
Despite its name, git blame is an investigation tool, not a tool for
accusing developers:
git grep → Where does this text appear?git blame → Which commit last changed each current line?The word “blame” is only the command’s historical name. It helps us find the most relevant commit, understand why a line changed, and discover the context behind a decision — not automatically decide who is responsible for a bug.
Practical Laravel example
Section titled “Practical Laravel example”Suppose app/Services/AuthService.php contains:
<?php
namespace App\Services;
use Illuminate\Support\Facades\Auth;
class AuthService{ public function login(array $credentials): bool { if (!Auth::attempt($credentials)) { return false; }
return true; }}Run:
git blame app/Services/AuthService.phpAn illustrative result may look like:
a81c42f1 (Yousef Ahmed 2026-08-02 10:15:22 +0300 1) <?phpa81c42f1 (Yousef Ahmed 2026-08-02 10:15:22 +0300 3) namespace App\Services;9d52b7aa (Mona Ali 2026-08-07 14:30:10 +0300 5) use Illuminate\Support\Facades\Auth;63ef201c (Omar Hassan 2026-08-10 18:42:03 +0300 9) public function login(array $credentials): boolf4a918b2 (Sara Adel 2026-08-12 09:20:41 +0300 11) if (!Auth::attempt($credentials)) {f4a918b2 (Sara Adel 2026-08-12 09:20:41 +0300 12) return false;63ef201c (Omar Hassan 2026-08-10 18:42:03 +0300 15) return true;This output is illustrative. The real commit IDs, names, dates, timezone, line numbers, alignment, and file content depend on the repository.
How to read one blame line
Section titled “How to read one blame line”Take this line:
f4a918b2 (Sara Adel 2026-08-12 09:20:41 +0300 11) if (!Auth::attempt($credentials)) {Commit ID
- Value
- f4a918b2
Author
- Value
- Sara Adel
Date, time, timezone
- Value
- 2026-08-12 09:20:41 +0300
Current line number
- Value
- 11
Current line content
- Value
- if (!Auth::attempt($credentials)) {
Commit ID
Section titled “Commit ID”f4a918b2This identifies the commit to which Git attributes the latest change to this line. It is usually displayed in abbreviated form — the displayed hash is not always exactly eight characters; the abbreviation length can vary.
Author
Section titled “Author”Sara AdelThis is the author recorded in the attributed commit. It does not automatically mean:
- Sara created the entire file.
- Sara designed the entire login feature.
- Sara introduced the bug currently being investigated.
- Sara is the only person who understands the code.
It means Git attributes the last change to this current line to a commit authored by Sara.
Date and time
Section titled “Date and time”2026-08-12 09:20:41 +0300This shows the recorded time information associated with the attributed commit. The exact date format may differ based on Git configuration.
Current line number
Section titled “Current line number”11This is the line’s position in the current displayed version of the file.
Current content
Section titled “Current content”if (!Auth::attempt($credentials)) {This is the current content of the annotated line.
What question does git blame answer?
Section titled “What question does git blame answer?”git blame helps answer:
Which commit last changed each line that currently exists in this file?
It does not fully answer “Who originally invented this logic?”, and it does not prove “Who caused the bug?”
A line may have been introduced in one commit, moved during a refactor, reformatted later, changed only to fix whitespace, copied from another part of the project, or modified by several different commits over time. The normal blame output shows the latest attribution Git calculates for the current line.
The difference between git grep and git blame
Section titled “The difference between git grep and git blame”First, we want to find where Auth::attempt appears:
git grep -n "Auth::attempt"Illustrative output:
app/Services/AuthService.php:11:if (!Auth::attempt($credentials)) {This answers “Where does this text appear?” Next, we inspect the relevant file:
git blame app/Services/AuthService.phpThis answers “Which commit last changed each current line in this file?”
| Command | Main question |
|---|---|
git grep |
Where does this text appear? |
git blame |
Which commit last changed each current line? |
git log |
What commits are in the reachable project history? |
Inspecting only a line range with -L
Section titled “Inspecting only a line range with -L”Large files may contain hundreds of lines. If the login logic is between lines 8 and 15, we do not need to display blame information for the complete file. Use:
git blame -L 8,15 app/Services/AuthService.phpThe structure is:
git blame -L <start>,<end> <file>In this example, 8 is the first line included, 15 is the last line
included, and app/Services/AuthService.php is the file being inspected.
Illustrative output:
63ef201c (Omar Hassan 2026-08-10 18:42:03 +0300 8) {63ef201c (Omar Hassan 2026-08-10 18:42:03 +0300 9) public function login(array $credentials): bool63ef201c (Omar Hassan 2026-08-10 18:42:03 +0300 10) {f4a918b2 (Sara Adel 2026-08-12 09:20:41 +0300 11) if (!Auth::attempt($credentials)) {f4a918b2 (Sara Adel 2026-08-12 09:20:41 +0300 12) return false;f4a918b2 (Sara Adel 2026-08-12 09:20:41 +0300 13) }63ef201c (Omar Hassan 2026-08-10 18:42:03 +0300 15) return true;Use
git grep -nto find the relevant line, then usegit blame -Lto investigate a small surrounding range.
Example workflow:
git grep -n "Auth::attempt" -- '*.php'Then:
git blame -L 8,15 app/Services/AuthService.phpgit grep does not automatically pass its result to git blame — these are
two separate commands in this beginner example.
Ignoring whitespace changes with -w
Section titled “Ignoring whitespace changes with -w”A commit may reformat a file without intentionally changing its logic. For example, it may change:
if(!Auth::attempt($credentials)){into:
if (!Auth::attempt($credentials)) {A normal blame calculation may attribute the line to the formatting commit. To ask Git to ignore whitespace differences while tracing line attribution, use:
git blame -w app/Services/AuthService.php-wmeans ignore whitespace while comparing versions.- It can help look past formatting-only changes.
- It does not remove whitespace from the actual file.
- It does not modify any commit.
- It does not guarantee that Git will recover the complete conceptual origin of every line.
-w ignores whitespace differences specifically — it does not ignore all
refactoring or every non-functional change.
Displaying author email with -e
Section titled “Displaying author email with -e”By default, the blame output normally displays the author’s name. To display the author’s email instead, use:
git blame -e app/Services/AuthService.phpIllustrative output:
f4a918b2 (<sara@example.com> 2026-08-12 09:20:41 +0300 11) if (!Auth::attempt($credentials)) {-emeans show author email.- The email comes from the author information recorded in the commit.
- It does not contact or notify the author.
- It does not prove that the email is still active.
- It does not modify the file or repository.
Combining useful options
Section titled “Combining useful options”Options can be combined. To inspect lines 8 through 15 while ignoring whitespace changes:
git blame -w -L 8,15 app/Services/AuthService.phpTo display author emails for the same range:
git blame -e -L 8,15 app/Services/AuthService.phpWhat about uncommitted changes?
Section titled “What about uncommitted changes?”Suppose we edit a line in AuthService.php but do not create a commit.
Running:
git blame app/Services/AuthService.phpmay display the uncommitted line using a zero-like commit ID and a label such as:
00000000 (Not Committed Yet 2026-09-11 12:15:00 +0300 11) if (!Auth::attempt($credentials)) {- Git cannot attribute that current line to a completed commit because the change is not committed.
Not Committed Yetrefers to the current uncommitted content — it is not a real author name.- The exact display may vary across Git versions and configurations.
What git blame cannot show by itself
Section titled “What git blame cannot show by itself”git blame is useful, but its result is limited. It does not provide the
full story behind a line.
It does not show deleted lines
Section titled “It does not show deleted lines”The normal command annotates lines that currently exist in the selected file. If a line was deleted, it is not part of the current file and will not appear in the normal output.
It does not show every change to a line
Section titled “It does not show every change to a line”If five commits changed the same line, normal blame output usually shows the latest commit to which Git attributes the current line. It does not display the complete sequence of all five changes.
It does not explain why the change happened
Section titled “It does not explain why the change happened”The blame output shows attribution information, but the commit ID is only the beginning of the investigation. To understand the reason, developers usually inspect the commit message, the changes inside that commit, related issue or pull request context when available, nearby code, tests, and documentation.
It cannot determine responsibility
Section titled “It cannot determine responsibility”The author shown may have fixed an earlier problem, reformatted the file, renamed a variable, moved code, applied a team decision, or committed code written collaboratively. The command should not be treated as proof of responsibility for a bug.
Practical investigation scenario
Section titled “Practical investigation scenario”A user reports that login always returns false.
Step 1: Find the relevant text
Section titled “Step 1: Find the relevant text”git grep -n "return false" -- app/Services/Illustrative result:
app/Services/AuthService.php:12:return false;This tells us where the text currently appears.
Step 2: Inspect the surrounding lines
Section titled “Step 2: Inspect the surrounding lines”git blame -L 9,15 app/Services/AuthService.phpIllustrative result:
63ef201c (Omar Hassan 2026-08-10 18:42:03 +0300 9) public function login(array $credentials): bool63ef201c (Omar Hassan 2026-08-10 18:42:03 +0300 10) {f4a918b2 (Sara Adel 2026-08-12 09:20:41 +0300 11) if (!Auth::attempt($credentials)) {f4a918b2 (Sara Adel 2026-08-12 09:20:41 +0300 12) return false;f4a918b2 (Sara Adel 2026-08-12 09:20:41 +0300 13) }63ef201c (Omar Hassan 2026-08-10 18:42:03 +0300 15) return true;Step 3: Interpret the result correctly
Section titled “Step 3: Interpret the result correctly”- Commit
f4a918b2is the latest commit Git attributes to lines 11 through 13. - Sara is the author recorded in that commit.
- This does not prove that Sara caused the reported problem.
- The next investigation step would be understanding the commit’s context and testing the behavior.
git blamegave us a useful starting point, not the complete answer.
Command summary
Section titled “Command summary”| Goal | Command |
|---|---|
| Annotate every current line | git blame app/Services/AuthService.php |
| Inspect lines 8 through 15 | git blame -L 8,15 app/Services/AuthService.php |
| Ignore whitespace changes | git blame -w app/Services/AuthService.php |
| Show author emails | git blame -e app/Services/AuthService.php |
| Combine a range with ignore whitespace | git blame -w -L 8,15 app/Services/AuthService.php |
git blametells us which commit last changed each current line. It gives us a place to begin investigating — not a person to accuse.