Git Blame and Annotate: Line-by-Line Code Attribution

Master git blame for line-by-line code attribution, understanding code history, finding when code changed, and using blame effectively for code comprehension.

published: reading time: 14 min read updated: March 31, 2026

Git Blame and Annotate: Line-by-Line Code Attribution

git blame answers the question every developer asks at some point: “Who wrote this, when, and why?” It annotates every line of a file with the commit that last modified it, the author, and the date. It’s the fastest way to understand the history of a specific piece of code.

Despite its name, git blame is not about pointing fingers. It’s about understanding context. When you see a strange piece of code, blame tells you which commit introduced it, which leads you to the commit message, which explains the reasoning. That context is invaluable for debugging, refactoring, and code review.

This post covers the complete blame toolkit: finding when code changed, tracking moved lines, integrating blame into your workflow, and the limitations that can mislead you.

When to Use / When Not to Use

Use Git Blame When

  • Understanding unfamiliar code — You need context about why code was written a certain way
  • Debugging regressions — You want to find when a specific line was last changed
  • Code review preparation — You want to understand the history of files you’re reviewing
  • Refactoring decisions — You need to know if code is actively maintained or legacy
  • Accountability — You need to find the right person to ask about specific code

Do Not Use Git Blame When

  • Assigning blame for bugs — The person who wrote the line may not be responsible for the bug
  • Performance reviews — Lines of code attributed to a person is a vanity metric
  • Finding the original author — Blame shows who last modified the line, not who wrote it originally
  • Understanding architecture — Blame is line-level; it doesn’t show system-level design decisions

Core Concepts

Git blame annotates each line with metadata from the last modifying commit:

FieldDescriptionExample
Commit hashThe commit that last modified this lineabc12345
AuthorWho made the commitJane Doe
DateWhen the commit was made2026-03-15
Line numberThe line number in the current file42
ContentThe actual line of codeexport function authenticate() {

The key insight: blame shows the last modifier, not the original author. If someone reformatted the file, their commit appears for every line.


graph LR
    A[Current File] --> B[Line 1: abc123 Jane 2026-01-15]
    A --> C[Line 2: def456 Bob 2026-02-20]
    A --> D[Line 3: abc123 Jane 2026-01-15]
    A --> E[Line 4: ghi789 Alice 2026-03-01]

Architecture and Flow Diagram

The git blame workflow from investigation to understanding:


graph TD
    A[Suspicious Code Line] --> B[git blame file.ts]
    B --> C[Get Commit Hash]
    C --> D[git show commit-hash]
    D --> E[Read Commit Message]
    E --> F{Understand Context?}
    F -->|Yes| G[Proceed with Fix/Refactor]
    F -->|No| H[git log -p --follow file.ts]
    H --> I[Review Full History]
    I --> G
    G --> J[Contact Author if Needed]

Step-by-Step Guide

1. Basic Blame Usage

The simplest form shows who last modified each line:


# Blame a file
git blame src/auth/middleware.ts

# Output format:
# abc12345 (Jane Doe     2026-03-15 14:30:00 -0500  1) import { Request, Response } from 'express';
# abc12345 (Jane Doe     2026-03-15 14:30:00 -0500  2) import { verifyToken } from './jwt';
# def67890 (Bob Smith    2026-03-20 09:15:00 -0500  3)
# def67890 (Bob Smith    2026-03-20 09:15:00 -0500  4) export async function authMiddleware(
# def67890 (Bob Smith    2026-03-20 09:15:00 -0500  5)   req: Request,
# def67890 (Bob Smith    2026-03-20 09:15:00 -0500  6)   res: Response,
# def67890 (Bob Smith    2026-03-20 09:15:00 -0500  7)   next: NextFunction
# def67890 (Bob Smith    2026-03-20 09:15:00 -0500  8) ) {
# ghi11111 (Alice Chen   2026-03-25 16:45:00 -0500  9)   const token = req.headers.authorization?.split(' ')[1];

2. Blame with Line Ranges

Focus on specific sections of a file:


# Blame lines 10-30
git blame -L 10,30 src/auth/middleware.ts

# Blame a specific function (by line range)
git blame -L 15,45 src/auth/middleware.ts

# Blame from a regex pattern
git blame -L '/export function authMiddleware/,/^}/' src/auth/middleware.ts

3. Ignore Formatting Changes

Formatting commits pollute blame output. Ignore them:


# Ignore whitespace changes
git blame -w src/auth/middleware.ts

# Use ignore revisions file (Git 2.23+)
# Create a file with commits to ignore (formatting, linting, etc.)
echo "abc12345" > .git-blame-ignore-revs
echo "def67890" >> .git-blame-ignore-revs

# Configure Git to use it
git config blame.ignoreRevsFile .git-blame-ignore-revs

# Now blame ignores those commits
git blame src/auth/middleware.ts

4. Track Moved and Copied Lines

When code is moved between files, blame can track it:


# Track lines moved within the same file
git blame -M src/auth/middleware.ts

# Track lines copied from other files
git blame -C src/auth/middleware.ts

# More aggressive copy detection (slower)
git blame -C -C src/auth/middleware.ts

5. Blame with Commit Details

Get the full commit message for each line:


# Show commit message summary
git blame --show-name src/auth/middleware.ts

# Porcelain format (machine-readable)
git blame --porcelain src/auth/middleware.ts

# Show email instead of author name
git blame -e src/auth/middleware.ts

# Show full commit hash
git blame -l src/auth/middleware.ts

6. Investigate a Specific Commit

Once you have the commit hash from blame, dig deeper:


# Show the full commit
git show abc12345

# Show just the commit message
git log -1 --format=full abc12345

# Show the diff for that commit
git show abc12345 --stat

# See what other files changed in that commit
git show abc12345 --name-only

Production Failure Scenarios + Mitigations

ScenarioWhat HappensMitigation
Formatting commit pollutionA reformat commit appears as the “author” of every lineUse .git-blame-ignore-revs to exclude formatting commits
Code moved between filesBlame shows the move commit, not the original authorUse git blame -C to track copied lines across files
Wrong person blamedThe last modifier isn’t the person who understands the codeCheck the commit history, not just the last commit
Deleted codeYou can’t blame code that no longer existsUse git log -p -- <file> to see historical versions
Binary filesBlame doesn’t work on binary filesUse git log -- <file> for binary file history
Large filesBlame output is overwhelming for files with 1000+ linesUse line ranges (-L) to focus on specific sections

Trade-offs

AspectAdvantageDisadvantage
SpeedInstant attribution for any lineOnly shows last modifier, not original author
ContextLinks directly to commit messageFormatting commits can obscure real history
PrecisionLine-level granularityDoesn’t show why code was written, only when
IntegrationBuilt into Git, no tools neededOutput can be overwhelming for large files
TrackingCan follow moved/copied lines-C flag is slow for large codebases
HistoricalShows complete modification historyDoesn’t work for deleted code

Implementation Snippets

Git Configuration for Better Blame


# ~/.gitconfig
[blame]
    # Ignore formatting commits
    ignoreRevsFile = .git-blame-ignore-revs
    # Show date in ISO format
    date = iso

[alias]
    # Quick blame with context
    bl = "!f() { git blame -w -L \"$1\" \"$2\"; }; f"
    # Blame with commit message
    blm = "!f() { git blame \"$2\" | head -n \"$1\" | awk '{print $1}' | xargs -I{} git log -1 --format='%h %s' {}; }; f"
    # Blame the current line in your editor
    blame-line = "!f() { git blame -L \"$(grep -n \"$1\" \"$2\" | cut -d: -f1),+5\" \"$2\"; }; f"

Ignore Revs File Setup


# .git-blame-ignore-revs
# Formatting commits to ignore in blame output
# Add commit hashes here, one per line

# Initial code formatting pass
a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2

# ESLint auto-fix across entire codebase
b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3

# Prettier formatting update
c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4

# To find formatting commits:
# git log --oneline --all -- '*.ts' '*.js' | grep -i 'format\|prettier\|lint'

Blame Analysis Script


#!/bin/bash
# scripts/blame-analysis.sh
# Analyze blame data for a file or directory

TARGET=${1:-.}

echo "=== Blame Analysis ==="
echo "Target: $TARGET"
echo ""

if [ -f "$TARGET" ]; then
  # Single file analysis
  echo "File: $TARGET"
  echo "Total lines: $(wc -l < "$TARGET")"
  echo "Unique authors: $(git blame --porcelain "$TARGET" | grep '^author ' | sort -u | wc -l)"
  echo ""
  echo "Authors by line count:"
  git blame --porcelain "$TARGET" | grep '^author ' | sort | uniq -c | sort -rn
else
  # Directory analysis
  echo "Directory: $TARGET"
  echo ""
  echo "Top contributors by lines:"
  git ls-files "$TARGET" | xargs git blame --porcelain 2>/dev/null | \
    grep '^author ' | sort | uniq -c | sort -rn | head -20
fi

IDE Integration


# VS Code: GitLens extension provides inline blame
# Install: ext install eamodio.gitlens

# Neovim: gitsigns.nvim provides inline blame
# Use: :Gitsigns toggle_current_line_blame

# IntelliJ: Built-in Annotate feature
# Right-click file -> Git -> Annotate

# Emacs: git-timemachine
# M-x git-timemachine

Blame-Based Code Review Script


#!/bin/bash
# scripts/blame-review.sh
# Show blame info for lines changed in a PR

PR_DIFF=$1

if [ -z "$PR_DIFF" ]; then
  echo "Usage: blame-review.sh <diff-file>"
  exit 1
fi

# Extract changed files and line numbers from diff
grep '^+++' "$PR_DIFF" | sed 's|+++ b/||' | while read -r file; do
  echo "=== $file ==="
  # Get the range of changed lines
  grep -A1 "^@@.*$file" "$PR_DIFF" | grep '^@@' | \
    sed 's/@@ -[0-9]*,[0-9]* +\([0-9]*\),\([0-9]*\).*/\1,\2/' | \
    while IFS=, read -r start count; do
      git blame -L "${start},$((start + count))" "$file" 2>/dev/null
    done
  echo ""
done

Observability Checklist

  • Logs: Not typically applicable — blame is a local investigation tool
  • Metrics: Track how often blame is used during incident response
  • Alerts: Not applicable for blame
  • Dashboards: Display code ownership metrics (lines per author) for team awareness
  • Code Review: Use blame data during PR reviews to understand file history and identify reviewers

Security and Compliance Notes

  • Author attribution: Blame shows commit author, which can be spoofed — use signed commits for verified attribution
  • Sensitive data: Blame output may reveal who wrote code containing secrets — audit blame data during security reviews
  • Access control: Blame requires read access to the repository — ensure proper permissions
  • Audit trail: Blame provides a line-level audit trail for compliance requirements

Common Pitfalls and Anti-Patterns

  1. Blame as Weapon — Using blame to shame developers creates a toxic culture. Use it for understanding, not accusation.
  2. Trusting the Last Modifier — The person who last touched a line may not understand it. Check the full history.
  3. Ignoring Formatting Commits — Without .git-blame-ignore-revs, formatting commits make blame useless. Set it up.
  4. Blame for Performance Reviews — Lines attributed to a person says nothing about code quality or contribution value.
  5. Not Following the History — Blame gives you a commit hash. Always run git show to read the commit message.
  6. Blaming Deleted Code — Blame only works on current files. Use git log -p for deleted code history.
  7. Over-relying on Blame — Blame tells you who and when, but not why. Talk to the author for full context.

Quick Recap Checklist

  • Use git blame <file> for line-by-line attribution
  • Use -L start,end to focus on specific line ranges
  • Use -w to ignore whitespace changes
  • Set up .git-blame-ignore-revs to exclude formatting commits
  • Use -C to track lines moved between files
  • Always follow up with git show <commit> for context
  • Use blame for understanding, not accusation
  • Check full history, not just the last modifier
  • Use IDE integrations for inline blame display
  • Combine blame with git log -p for complete code history

.git-blame-ignore-revs Setup


# Create the file
touch .git-blame-ignore-revs

# Add formatting commit hashes (one per line)
# Find them with: git log --oneline --all -- '*.ts' '*.js' | grep -iE 'format|prettier|lint|style'
cat > .git-blame-ignore-revs << 'EOF'
# Formatting commits to exclude from blame
# Run: git log --oneline --grep='prettier' --grep='format' --all
a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2  # Initial prettier pass
b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3  # ESLint auto-fix
c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4  # Import sort cleanup
EOF

# Configure Git to use it
git config blame.ignoreRevsFile .git-blame-ignore-revs

# Commit the file so the team shares the configuration
git add .git-blame-ignore-revs
git commit -m "chore: add blame ignore file for formatting commits"

# Update the file after each formatting commit
# Add the new commit hash and commit the updated file

Blame vs Log Comparison

ToolGranularityPerformanceBest Use Case
git blameLine-by-lineFastWho last touched this specific line
git log -pCommit-levelMediumFull history of a file with diffs
git log -LLine-rangeMediumHistory of a specific function/section
git log -SString-levelFastWhen was this string added/removed
git log -GRegex-levelMediumWhen did code matching a pattern change

Interview Q&A

What does git blame actually show — the original author or the last modifier?

Git blame shows the last person who modified each line, not the original author. If Alice wrote a function and Bob later reformatted it, Bob's name appears for every line in that function.

This is why setting up .git-blame-ignore-revs is critical — it excludes formatting commits from blame output, so you see the actual code authors rather than the person who ran Prettier.

How do you find out who wrote code that has been deleted?

Use git log -p -- to see the full history of a file, including deleted lines. For a specific deleted function, use:

git log -p -S "functionName" --

The -S flag (pickaxe) finds commits that added or removed the specified string. This works even if the code no longer exists in the current version of the file.

How does the -C flag in git blame work?

The -C flag tells blame to look for copied or moved lines from other files. By default, blame only tracks changes within the same file. With -C, it searches other files in the same commit for the origin of each line.

Using -C -C makes the search more aggressive, checking commits beyond the one where the line was copied. This is useful when code is refactored across multiple files, but it's significantly slower on large codebases.

What is the difference between git blame and git annotate?

They are the same command. git annotate is an alias for git blame with slightly different default output formatting. Both show the same information: commit hash, author, date, and line content for each line in a file.

The annotate name exists for compatibility with other version control systems that use that terminology. In practice, everyone uses git blame.

Resources

Category

Related Posts

Git Bisect for Bug Hunting: Binary Search Through Commit History

Master git bisect to find the exact commit that introduced a bug using binary search. Automate bug hunting with scripts and handle complex scenarios.

#git #version-control #debugging

git log: Master Commit History Navigation and Filtering

Master git log formatting, filtering, searching history, and navigating commit history effectively for version control debugging and auditing.

#git #git-log #history

Git Reflog and Recovery: Your Safety Net for Destructive Operations

Master git reflog to recover lost commits, undo destructive operations, and understand Git's safety net. Learn recovery techniques for reset, rebase, and merge disasters.

#git #version-control #git-reflog