Trace File Lines Back to Their Git Commits with git annotate
You will finish with a repeatable way to identify the commit, author and date associated with each line in a tracked file. You will also narrow the report to a small range and produce machine-readable output when a script needs the result.
The route
Jump straight to the step you need, or tick off Done means at the end.
Allow about fifteen minutes. You need Git and a checked-out repository with at least one committed file. The examples use Git 2.43.0 from Ubuntu package git 1:2.43.0-1ubuntu7.3, matching the installed git-annotate(1) manual. This is a read-only investigation: git annotate does not edit files, commits or repository configuration, so it does not need sudo.
1. Check the installed command
Run this from the repository that contains the file:
$ git --version
git version 2.43.0
$ git rev-parse --show-toplevel
/path/to/project
If the second command fails with "not a git repository", change directory to a clone or worktree before continuing. Confirm the path you intend to inspect:
$ git ls-files --error-unmatch path/to/file
path/to/file
Replace path/to/file with a path relative to the repository root. Keeping the path explicit avoids accidentally annotating a similarly named file in another directory.
2. Read the normal annotation
Run the command with the file after --. The separator makes it clear that the remaining argument is a path, even if it resembles a revision:
$ git annotate -- path/to/file
Each output line contains a shortened object name, author and date, a line number, and the current line text. A small fixture might look like this:
d9f341eb ( Guide 2026-09-24 00:59:05 +0100 1)first line
3db02276 ( Guide 2026-09-24 00:59:05 +0100 2)changed second line
d9f341eb ( Guide 2026-09-24 00:59:05 +0100 3)third line
Your object names, author fields and dates will differ. The useful result is the association: line 2 came from commit 3db02276, while the other two lines came from d9f341eb.
Checkpoint: copy one object name from the report and inspect it:
$ git show --stat --oneline 3db02276
3db02276 Change second line
notes.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
The abbreviated name is normally enough for Git to resolve the commit. If it is ambiguous, use the full name from a porcelain report or ask Git to show the commit history.
3. Restrict the report to the lines you need
Use -L start,end when a whole-file report is distracting or expensive:
$ git annotate -L 40,65 -- path/to/file
Line numbers start at 1. You can omit one side, so -L 40 covers line 40 to the end and -L ,65 covers the start through line 65. You may pass multiple -L ranges, including overlapping ranges.
For source files, a function-based range can be easier to maintain than fixed line numbers:
$ git annotate -L :function_name -- path/to/file
The function name is interpreted using the same hunk-header rules that Git uses for diffs. If it finds no matching function, treat that as a range-selection problem and try a numbered range. Do not assume that every language has equally useful function detection.
4. Start from a revision when current history is misleading
The optional revision tells Git where to stop its backward walk:
$ git annotate RELEASE_TAG -- path/to/file
Use a real tag, branch or commit name in place of RELEASE_TAG. This asks which commits had introduced the lines by that revision, rather than by the current checkout. Verify the selected revision before drawing a conclusion:
$ git rev-parse --verify RELEASE_TAG^{commit}
0123456789abcdef0123456789abcdef01234567
A line can be attributed differently depending on the revision and path history. Annotation is evidence about Git's history walk, not proof that the named author wrote every character on the line.
5. Handle movement, copies and merge history deliberately
Git can spend extra work looking for moved or copied text. Start with -M for moves within the same file, then add -C when code may have come from another file:
$ git annotate -M -C -- path/to/file
The default similarity thresholds are 20 alphanumeric characters for -M and 40 for -C. A lower explicit threshold can find smaller fragments, but it also increases the chance of a weak attribution. For a refactor review, compare the ordinary and move-aware reports instead of treating the second one as automatically correct.
When a merge is involved, --first-parent follows only the first parent of merge commits:
$ git annotate --first-parent -- path/to/file
This answers a narrower question: when did the line arrive on the integration branch? Without it, Git follows the history in which the line was originally introduced. State which question you are answering in a review note.
6. Use porcelain output for automation
Do not parse the aligned human format in a script. Use --porcelain:
$ git annotate --porcelain -L 2,2 -- path/to/file
3db0227666d02a5a4381a97d29c58b692d3dab79 2 2 1
author Guide
author-mail <[email protected]>
author-time 1790207945
author-tz +0100
committer Guide
committer-mail <[email protected]>
committer-time 1790207945
committer-tz +0100
summary Change second line
filename path/to/file
changed second line
The exact metadata and commit vary. Porcelain output repeats commit metadata only when a commit is first referenced, so a parser must understand the format rather than assume one complete record per line. Use --line-porcelain when each line needs its own commit metadata. Both forms are intended for machine consumption.
7. Investigate common false leads
A boundary commit can appear with a caret or a shortened identifier, depending on the options and history being examined. -b shows a blank object name for boundary commits, while --root treats root commits as ordinary sources instead of boundaries:
$ git annotate -b -- path/to/file
$ git annotate --root -- path/to/file
These options change how the boundary is displayed or treated. They do not rewrite history. If the first commit is the answer you need, prefer --root and verify it with git show.
For an audit trail, --show-stats appends counts such as blobs read, patches retrieved and commits examined. Keep it separate from parsers that expect only annotation records. -l requests full revision names and -t requests raw timestamps; use them when abbreviated identifiers or formatted dates are insufficient.
Done means
- You ran
git annotate -- path/to/filein the intended repository. - You verified at least one reported commit with
git show. - You used
-Lfor a focused range and recorded whether line numbers or function detection selected it. - You used
-M,-Cor--first-parentonly when that history question matched the investigation. - Any automation uses porcelain output, and any appended statistics are kept out of its parser.