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.
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:
| Field | Description | Example |
|---|---|---|
| Commit hash | The commit that last modified this line | abc12345 |
| Author | Who made the commit | Jane Doe |
| Date | When the commit was made | 2026-03-15 |
| Line number | The line number in the current file | 42 |
| Content | The actual line of code | export 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
| Scenario | What Happens | Mitigation |
|---|---|---|
| Formatting commit pollution | A reformat commit appears as the “author” of every line | Use .git-blame-ignore-revs to exclude formatting commits |
| Code moved between files | Blame shows the move commit, not the original author | Use git blame -C to track copied lines across files |
| Wrong person blamed | The last modifier isn’t the person who understands the code | Check the commit history, not just the last commit |
| Deleted code | You can’t blame code that no longer exists | Use git log -p -- <file> to see historical versions |
| Binary files | Blame doesn’t work on binary files | Use git log -- <file> for binary file history |
| Large files | Blame output is overwhelming for files with 1000+ lines | Use line ranges (-L) to focus on specific sections |
Trade-offs
| Aspect | Advantage | Disadvantage |
|---|---|---|
| Speed | Instant attribution for any line | Only shows last modifier, not original author |
| Context | Links directly to commit message | Formatting commits can obscure real history |
| Precision | Line-level granularity | Doesn’t show why code was written, only when |
| Integration | Built into Git, no tools needed | Output can be overwhelming for large files |
| Tracking | Can follow moved/copied lines | -C flag is slow for large codebases |
| Historical | Shows complete modification history | Doesn’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
- Blame as Weapon — Using blame to shame developers creates a toxic culture. Use it for understanding, not accusation.
- Trusting the Last Modifier — The person who last touched a line may not understand it. Check the full history.
- Ignoring Formatting Commits — Without
.git-blame-ignore-revs, formatting commits make blame useless. Set it up. - Blame for Performance Reviews — Lines attributed to a person says nothing about code quality or contribution value.
- Not Following the History — Blame gives you a commit hash. Always run
git showto read the commit message. - Blaming Deleted Code — Blame only works on current files. Use
git log -pfor deleted code history. - 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,endto focus on specific line ranges - Use
-wto ignore whitespace changes - Set up
.git-blame-ignore-revsto exclude formatting commits - Use
-Cto 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 -pfor 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
| Tool | Granularity | Performance | Best Use Case |
|---|---|---|---|
git blame | Line-by-line | Fast | Who last touched this specific line |
git log -p | Commit-level | Medium | Full history of a file with diffs |
git log -L | Line-range | Medium | History of a specific function/section |
git log -S | String-level | Fast | When was this string added/removed |
git log -G | Regex-level | Medium | When did code matching a pattern change |
Interview Q&A
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.
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.
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.
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
- Git blame documentation — Official Git documentation
- Git blame ignore revs — Ignoring formatting commits
- Git pickaxe search — Finding when code was added or removed
- GitLens VS Code extension — Inline blame in VS Code
- Git log with -p — Full file history with diffs
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 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 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.