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: 19 min read author: Geek Workbench updated: March 31, 2026

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

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

Reference: 1. Basic Blame Usage

Reference: 4. Track Moved and Copied Lines

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. Blame with Color Output

Highlight author boundaries and age with color:


# Show colored output to distinguish authors
git blame --color-by-age src/auth/middleware.ts

# Show age of lines (newer vs older)
git blame --age-format src/auth/middleware.ts

# Color by email for easier author identification
git blame --color-by-author src/auth/middleware.ts

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

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-off Analysis

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 / 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 Questions

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

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

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

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

5. How do you set up .git-blame-ignore-revs to ignore formatting commits?

Create a .git-blame-ignore-revs file with commit hashes (one per line): echo "abc12345" > .git-blame-ignore-revs. Then configure Git to use it: git config blame.ignoreRevsFile .git-blame-ignore-revs. Find formatting commits with: git log --oneline --all -- '*.ts' '*.js' | grep -i 'format\|prettier\|lint'. Commit the file so the whole team benefits.

6. What is the --porcelain format in git blame and when would you use it?

git blame --porcelain <file> outputs a machine-readable format with each line as: <hash> <author> <author-mail> <author-time> <author-tz> <committer> <committer-mail> <committer-time> <committer-tz> <filename> <boundary> <lines>. Parse this format in scripts to extract structured attribution data for dashboards, code ownership metrics, or compliance reports.

7. How does git blame -L (line range) work and what patterns can you use?

git blame -L 10,30 file.txt shows blame for lines 10 through 30 only. git blame -L '/function/,/^}/' file.txt uses regex: it blames from the line containing "function" to the next line starting with "}". This focuses attribution on specific functions or code blocks without annotating the entire file.

8. What is the difference between git blame and git log -p for understanding code history?

git blame shows who last modified each line in the current file version. git log -p -- <file> shows the complete history of changes to a file — every version, every diff, every commit that touched it. Blame is for attribution; log -p is for understanding how code evolved over time.

9. How do you interpret blame output that shows a reformatting commit for every line?

This happens when a mass-reformat commit (like running Prettier) modified every line in the file. Use git blame -w to ignore whitespace changes, and set up .git-blame-ignore-revs to exclude the reformatting commit from blame output. This reveals the original code authors underneath the formatting changes.

10. How does git blame handle code that was moved between files?

Without flags, blame attributes lines to their last modifier within the current file only. Use git blame -C <file> to detect lines copied from other files, or git blame -M <file> to track lines moved within the same file. The output shows the original commit where the line was introduced, even if it was moved.

11. What is the age-format option in git blame?

git blame --age-format <format> <file> shows the age (time since the line was last modified) in a custom format. Use %ar for relative dates like "3 weeks ago". Combine with --color-by-age to highlight older lines in one color and newer lines in another — useful for identifying code that hasn't been touched recently.

12. How do you trace a specific line of code back through renames?

git log --follow --oneline -- <file> traces a file across renames, showing its history even after it was renamed. git blame --follow is not supported — follow only works with log. For rename tracing in blame, use -M to detect moved lines within the renamed file.

13. What information does git show <commit> provide after getting a hash from blame?

After getting a commit hash from blame, git show <hash> displays the full commit: author, date, commit message, and the diff of all files changed. This gives context on why the change was made. Use git log -1 --format='%h %s' <hash> for a one-line summary without the full diff.

14. How do IDE integrations display git blame information?

VS Code with GitLens shows inline blame above each line, displaying the author, date, and commit message. Neovim with gitsigns.nvim uses :Gitsigns toggle_current_line_blame. IntelliJ has a built-in Annotate feature (right-click → Git → Annotate). Emacs uses git-timemachine. These integrations make blame information available without leaving your editor.

15. What are the limitations of git blame for understanding code ownership?

Blame only shows the last modifier, not original authorship. A file that was reformatted shows the formatter as author of every line. Blame doesn't explain why code was written — only when and by whom. Code that was copied from elsewhere may show the copier as author. For true code ownership, combine blame with --ignore-revs and cross-reference with PR history.

16. How does git blame work with binary files?

Git blame cannot annotate binary files — binary content cannot be meaningfully split into lines for attribution. For binary files, use git log -- <file> to see when the file was changed, and git show <hash>:<file> to see historical versions. Store binary files in Git LFS for better performance and use blame only on text files.

17. What is the show-name flag in git blame?

git blame --show-name <file> adds the filename to each line of blame output. This is useful when a file has been renamed but blame still processes it. The filename shown is the one from the commit that last modified that line, which may differ from the current filename if the file was renamed.

18. How do you use git blame to find when a specific configuration value was introduced?

Use git blame <file> and look for the line in question to see when it was last modified. Or use git log -S "CONFIG_VALUE" --oneline -- <file> to find the exact commit that introduced or changed the value. Combine with git show <hash> for the commit message explaining why it was changed.

19. What is the difference between git blame -e and git blame -l?

git blame -e shows the author email instead of author name in blame output. git blame -l shows the full 40-character commit hash instead of the abbreviated hash. These flags customize the output format for different use cases — email for easier identification, full hash for scripting.

20. How does git blame handle whitespace-only changes?

By default, blame attributes lines with whitespace-only changes to the commit that made those changes. Use git blame -w <file> to ignore whitespace when attributing lines. This skips commits that only changed indentation, formatting, or line endings, showing instead who last changed the actual content.

Further Reading

Conclusion

Git blame is a context tool, not a blame tool — it answers “when and why did this line get here?” Used constructively, it accelerates code comprehension, debugging, and impact analysis.

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