Trace a Line's History with git blame

A line of code makes no sense, and you need to know who touched it last and what they were thinking. You will finish with a practical way to find the commit and author currently tied to a line with git blame, inspect the relevant change, and deal with moved or reformatted code. The examples use Git 2.43.0, matching the installed git-blame(1) manpage.

Allow about fifteen minutes. You need Git and a local clone with history.

Safety boundary: the commands are ordinary read-only queries. They need no elevated privileges, do not alter the working tree, and do not rewrite commits.

1. Check the repository and file

Run the command from the repository that contains the file. Check the installed version and confirm that Git can see the path:

$ git --version
git version 2.43.0
$ git status --short
$ test -f src/example.c && echo "file found"
file found

Replace src/example.c with a tracked file in your clone. A path after -- is unambiguously treated as a path, which matters when a filename could otherwise be mistaken for a revision.

Checkpoint: you should have a cleanly identified repository path. If test -f fails, find the path first with git ls-files | less; do not guess a filename.

2. Read the default annotation

Annotate the complete current file:

$ git blame -- src/example.c
3f2a1c4a (Alex Smith 2024-06-18 09:41:12 +0100  1) #include <stdio.h>
8b7d9e21 (Priya Shah 2024-07-02 14:08:33 +0100  2)
8b7d9e21 (Priya Shah 2024-07-02 14:08:33 +0100  3) int main(void) {
...

Each line carries four things:

This output answers "which revision last changed this line?" It does not show deleted lines, and it does not by itself explain why the change was made. A line may also have been moved or copied, so treat the first result as a lead to investigate.

Checkpoint: copy the commit ID beside the suspicious line, then inspect its change.

$ git show --stat --oneline 8b7d9e21
$ git show --format=fuller --find-renames 8b7d9e21 -- src/example.c

3. Narrow the output to useful lines

For a large file, limit the annotation with -L. Line numbers start at 1, and the range is inclusive:

$ git blame -L 40,60 -- src/example.c
$ git blame -L 40,+21 -- src/example.c

These two commands request the same 21 lines. You can also select a range by matching a function or another POSIX regular expression. Quote the expression so the shell does not interpret its punctuation:

$ git blame -L ':main' -- src/example.c

The function-name form uses the same hunk-header rules as git diff. If it does not match your language's function syntax, use explicit line numbers or a regular-expression range instead. A range that matches nothing is a search problem, not permission evidence.

4. Follow movement within and between files

The basic command follows a whole-file rename automatically. It does not automatically prove that a particular block was moved inside a file or copied from another file. Add -M to detect moved or copied lines within the file:

$ git blame -M -- src/example.c

Use -C as well when code may have come from another file changed in the same commit:

$ git blame -M -C -- src/example.c

Repeated -C options search more widely:

These passes cost more time and still use similarity heuristics, so a copied fragment can be missed or attributed differently after substantial editing.

The default similarity thresholds are 20 alphanumeric characters for -M and 40 for -C. You can provide a lower bound when the fragment is short, for example -M10, but lower thresholds increase the chance of coincidental matches. Verify any surprising attribution with git show.

5. Hide mechanical commits

Formatting-only commits can obscure the change that introduced the logic. If the repository maintains a reviewed ignore list, pass it explicitly:

$ git blame --ignore-revs-file .git-blame-ignore-revs -- src/example.c

The file contains one full revision ID per line; whitespace and comments beginning with # are ignored. Git can also read a configured blame.ignoreRevsFile. If you are unsure whether an ignore list exists, check without changing configuration:

$ git config --show-origin --get-all blame.ignoreRevsFile

Tip: an ignored revision is not erased from history. Git tries to attribute its changed lines to an earlier revision, and some lines may be marked with ? or * when the corresponding configuration is enabled. Record the ignore file used in any report, because it changes the question being answered.

6. Produce stable data for a script

For tooling, use porcelain output rather than parsing the human-readable columns:

$ git blame --porcelain -- src/example.c | sed -n '1,16p'
8b7d9e21... 1 1 2
author Priya Shah
author-mail <[email protected]>
author-time 1720000000
...

The exact metadata and commit IDs depend on the repository. In porcelain mode, the first header line gives the commit, original line, final line and group length. Later metadata lines identify the author, committer, filename and summary. Git suppresses repeated commit metadata after its first appearance. Use --line-porcelain when a simple line-by-line consumer needs the full metadata repeated for every line.

Do not assume the display width of the default format is fixed, because names, dates and commit abbreviations vary.

Warning: avoid exposing author email addresses unnecessarily. -e and porcelain output can include them, so keep generated reports within the repository's privacy expectations.

7. Check an attribution before acting on it

Use the commit as a starting point for evidence, not as a verdict about a person. Inspect its message and complete patch, then look at parent history if the line came through a merge:

$ COMMIT='8b7d9e21'
$ git show --format=fuller --stat "$COMMIT"
$ git show --format=fuller --find-copies-harder "$COMMIT" -- src/example.c
$ git log --follow --oneline -- src/example.c

If you need to ask when a line last existed in a forward revision range, --reverse START..END changes the question: it reports the last revision in which the line existed rather than the revision in which it appeared. The path must exist at START. This is useful for locating the removal of a line, but it is easy to read backwards if you forget which direction you requested.

Recovery: there is no undo step because every example only reads history. Do not replace git blame with a commit rewrite or a checkout while investigating unless that is a separate, deliberate operation.

Done means