Skip to content
masterclass Buddy

Git · Command

git blame

git blame shows, for every line of a file, which commit last changed that line, along with its author and date. Use it to find out where a specific piece of code came from.

Last updated

How it works

Git starts from a revision (HEAD by default, or the <rev> you pass) and walks back through history. At each step it compares a commit's version of the file with its parent's version. Lines that are the same in both are passed on to the parent. Lines that differ are credited to that commit. This continues until every line has a commit or reaches a boundary, such as a range limit or the root commit. The default output puts the short commit hash, author, date and line number in front of each line. Options can limit the line range, detect moved or copied code, or skip commits you choose.

Syntax

git blame [-c] [-b] [-l] [--root] [-t] [-f] [-n] [-s] [-e] [-p] [-w] [--incremental] [-L <range>] [-S <revs-file>] [-M] [-C] [-C] [-C] [--since=<date>] [--ignore-rev <rev>] [--ignore-revs-file <file>] [--color-lines] [--color-by-age] [--progress] [--abbrev=<n>] [ --contents <file> ] [<rev> | --reverse <rev>..<rev>] [--] <file>

Parameters

NameTypeDescription
<file>pathThe file to annotate (required).
<rev>revision or rangeThe revision to start annotating from (HEAD if omitted). A range such as v2.6.18.. limits how far back blame looks.
-L <start>,<end> | -L :<funcname>line rangeAnnotates only the given lines. Each end can be a line number, a /regex/ or a +/-offset. A function-name regex can be used instead of a range.
-M[<num>] / -C[<num>]flag with optional thresholdDetect lines moved or copied within the file (-M, default threshold 20 alphanumeric characters) or from other files (-C, default threshold 40). -C can be repeated to search more widely.
--ignore-rev <rev> / --ignore-revs-file <file>revision / pathSkip the given commits when assigning blame.
-p, --porcelain / --line-porcelain / --incrementalflagMachine-readable output formats.
--contents <file>path or -Annotate the given contents (use - for stdin) instead of the version stored in the repository.

Returns

Writes to stdout one annotation per line of the file. By default each line has the abbreviated commit hash, the author ident and the line number, followed by the line's content. Porcelain and incremental modes print structured headers instead.

Key points

  • Renamed files are followed automatically, and this can't be turned off. To follow code that was moved or copied between different files, you have to use -C.
  • -L can be given more than once, and the ranges may overlap. This lets you annotate several separate parts of a file in one run.
  • -C looks for lines that came from other files changed in the same commit. Giving it twice also checks the commit that created the file, and three times checks every commit.
  • Commits you ignore (for example, bulk reformatting commits) don't hide history. Their lines are credited to the commit that changed them before.
  • --first-parent shows when a line arrived on an integration branch, not when it was first written on a side branch.
  • --line-porcelain repeats the full commit details on every line. This makes it easy to process with tools like sed or sort, for example to count lines per author.
  • -w ignores whitespace changes, so commits that only re-indent lines don't take the credit for them.

Common mistakes

  • Blame only covers lines that still exist. To find deleted or replaced code, use git diff or git log -S (the 'pickaxe' search).
  • When you limit the history with a revision range or --since, lines older than the limit are credited to the boundary commit, not to the commit that actually wrote them.
  • Plain --porcelain prints a commit's details only the first time that commit appears. A parser that expects details on every line will miss them.
  • --incremental output does not come in line order, so tools reading it must reorder the results themselves.

Example: Blame a function while skipping formatting commits

#!/usr/bin/env bash
set -euo pipefail

# Commit hashes of bulk reformatting commits to ignore (one per line)
cat > .git-blame-ignore-revs <<'EOF'
# example: prettier reformat
# 0123456789abcdef0123456789abcdef01234567
EOF

# Make every git blame in this repository use the ignore list
git config blame.ignoreRevsFile .git-blame-ignore-revs

# Blame only the body of main(); ignore whitespace; detect moved/copied code
git blame -w -C -L ':main' -- src/app.c

# Count how many lines each author is credited with
git blame --line-porcelain -- src/app.c \
  | sed -n 's/^author //p' | sort | uniq -c | sort -rn

The script adds an ignore-revs file to the repository config, blames a single function with whitespace changes ignored and copy detection on, then counts lines per author from the line-porcelain output.

When to use it

Finding who to ask about a line

While fixing a bug, find the commit and author behind a suspicious line, then read that commit's message to understand why it was written.

Skipping reformatting commits

After a project-wide formatter run, add that commit to an ignore file so blame keeps crediting the real authors.

Spotting copy-paste in a new file

Run git blame -C -C -f $commit^! -- file to see which lines of a newly added file were copied from existing files.

Seeing when a line was removed

Use --reverse <start>..HEAD to find the last commit in which a line still existed.

Interview questions

QA team ran a formatter across the whole codebase, and now git blame credits that one commit for most lines. How do you fix this?

Pass the commit with --ignore-rev, or list its hash in a file and point blame.ignoreRevsFile or --ignore-revs-file at that file. Blame then treats that commit as if it never happened and credits the earlier commit that changed each line. Adding -w also helps when the changes were whitespace-only.

Follow-up: How can you see which lines were affected by the ignored commit? Set blame.markIgnoredLines to mark lines that were re-credited with '?', and blame.markUnblamableLines to mark lines that couldn't be credited to any other commit with '*'.

QWhat is the difference between -M and -C?

-M finds blocks of lines that were moved or copied within the same file. -C does that too, and also finds lines that came from other files changed in the same commit. Each takes an optional minimum number of alphanumeric characters that must match: 20 by default for -M and 40 for -C.

Follow-up: What do -C -C and -C -C -C add? Given twice, blame also looks at other files in the commit that created the file. Given three times, it looks at other files in every commit, which is the most thorough but the slowest.

QHow would you use git blame output in a script?

Use --porcelain or --line-porcelain. Each line gets a header with the full 40-character hash, the line number in the original and final file, and fields such as author and summary, and then the line's content after a tab. --line-porcelain repeats the commit details on every line, so the script doesn't need to remember them.

Follow-up: Why might an interactive viewer use --incremental instead? It prints results while blame is still running, newest commits first, and every entry ends with a filename line, so a viewer can display results gradually.

  • git annotate
  • git log
  • git diff
  • git rev-list
  • git config
  • gitattributes
  • gitmailmap
  • git bisect

Source: Git - git-blame Documentation. Checked 2026-11-02.