Skip to content

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:

Terminal window
git blame

The basic syntax is:

Terminal window
git blame <file>

For example:

Terminal window
git blame app/Services/AuthService.php

The command annotates each current line in the file with information about the commit that last changed or introduced that line.

git blame connects 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.

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:

Terminal window
git blame app/Services/AuthService.php

An illustrative result may look like:

a81c42f1 (Yousef Ahmed 2026-08-02 10:15:22 +0300 1) <?php
a81c42f1 (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): bool
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;
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.

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)) {
f4a918b2

This 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.

Sara Adel

This 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.

2026-08-12 09:20:41 +0300

This shows the recorded time information associated with the attributed commit. The exact date format may differ based on Git configuration.

11

This is the line’s position in the current displayed version of the file.

if (!Auth::attempt($credentials)) {

This is the current content of the annotated line.

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:

Terminal window
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:

Terminal window
git blame app/Services/AuthService.php

This 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?

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:

Terminal window
git blame -L 8,15 app/Services/AuthService.php

The structure is:

Terminal window
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): bool
63ef201c (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 -n to find the relevant line, then use git blame -L to investigate a small surrounding range.

Example workflow:

Terminal window
git grep -n "Auth::attempt" -- '*.php'

Then:

Terminal window
git blame -L 8,15 app/Services/AuthService.php

git grep does not automatically pass its result to git blame — these are two separate commands in this beginner example.

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:

Terminal window
git blame -w app/Services/AuthService.php
  • -w means 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.

By default, the blame output normally displays the author’s name. To display the author’s email instead, use:

Terminal window
git blame -e app/Services/AuthService.php

Illustrative output:

f4a918b2 (<sara@example.com> 2026-08-12 09:20:41 +0300 11) if (!Auth::attempt($credentials)) {
  • -e means 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.

Options can be combined. To inspect lines 8 through 15 while ignoring whitespace changes:

Terminal window
git blame -w -L 8,15 app/Services/AuthService.php

To display author emails for the same range:

Terminal window
git blame -e -L 8,15 app/Services/AuthService.php

Suppose we edit a line in AuthService.php but do not create a commit. Running:

Terminal window
git blame app/Services/AuthService.php

may 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 Yet refers to the current uncommitted content — it is not a real author name.
  • The exact display may vary across Git versions and configurations.

git blame is useful, but its result is limited. It does not provide the full story behind a line.

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.

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.

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.

A user reports that login always returns false.

Terminal window
git grep -n "return false" -- app/Services/

Illustrative result:

app/Services/AuthService.php:12:return false;

This tells us where the text currently appears.

Terminal window
git blame -L 9,15 app/Services/AuthService.php

Illustrative result:

63ef201c (Omar Hassan 2026-08-10 18:42:03 +0300 9) public function login(array $credentials): bool
63ef201c (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;
  • Commit f4a918b2 is 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 blame gave us a useful starting point, not the complete answer.
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 blame tells us which commit last changed each current line. It gives us a place to begin investigating — not a person to accuse.