Tracing a function history with git log -L Jump to heading

When a function behaves strangely, the useful question is usually “how did it get like this?” — every change to that function, in order, with its message. git log -- file lists every commit that touched the file, most of them irrelevant. git blame shows only the latest change per line. git log -L does what you actually want: give it a function name or a line range in a file, and it shows each commit that changed those lines, with just the diff of that region, following the region as surrounding code shifts it up and down. It is the closest Git gets to a biography of a piece of code. This page shows the forms of -L, how to make function-name matching work for your languages, and its limits, within bisect and history forensics.

When to use this approach Jump to heading

  • You need the full history of one function or block, not of the whole file.
  • git blame keeps pointing at refactoring or formatting commits.
  • You are backporting a fix and need to know what changed around the fixed code since the release, as in resolving cherry-pick conflicts on older branches.
  • You are reviewing a risky function before changing it and want to know which past changes broke it.

Step 1 — Trace a function by name Jump to heading

The :funcname:file form finds the function using the file’s diff driver and traces its body.

git log -L ':apply_discount:src/billing/invoice.py' --format='%h %ad %an%n  %s' --date=short

The output is one entry per commit that changed the function, each followed by the diff of just that function. Read from the bottom up to see the function grow.

One function's history, from git log -Lgit log -L on a discount function shows it being introduced, gaining a currency parameter, being moved further down the file during an unrelated refactor without changes, being fixed for credit notes, and finally having a guard added for zero values.Introducedflat discount2025-02Currency addednew parameter2025-07Moved downno change shown2025-11Credit-note fixskip_discount2026-09Zero guardedge case2026-10-L follows the function when it moves, so unrelated refactors drop out of the story

Step 2 — Make function matching work for your language Jump to heading

Git finds function boundaries with built-in patterns per language, enabled through .gitattributes. Without one, :funcname: may match the wrong line or fail. Set the diff driver for each language you use.

# .gitattributes
*.py    diff=python
*.go    diff=golang
*.rs    diff=rust
*.java  diff=java
*.ts    diff=javascript
*.rb    diff=ruby
# Verification: hunk headers now name the enclosing function
git diff HEAD~5 -- src/billing/invoice.py | grep '^@@' | head -3
# @@ -40,7 +40,9 @@ def apply_discount(lines, customer, currency):

Correct hunk headers are a sign that -L :funcname: will find functions correctly too. For languages without a built-in driver, define diff.<name>.xfuncname with a regular expression for function definitions.

Step 3 — Trace a line range or a regex range Jump to heading

When the code is not a function — a configuration block, a SQL query, a class attribute — give a range. Ranges can be line numbers or regular expressions marking the start and end.

# Lines 120 to 150 of a file, as they are now
git log -L 120,150:src/billing/invoice.py --oneline

# From a regex to a number of lines after it
git log -L '/^DISCOUNT_RULES = /,+25:src/billing/rules.py' --oneline

# Between two regexes
git log -L '/^class InvoiceTotals/,/^class /:src/billing/totals.py' --oneline
Ways to name the range for git log -LA function name uses the language's diff driver to find the body. Line numbers pick a fixed range in the current version. A regex plus a line count starts at a pattern. Two regexes bound the range by patterns at both ends.:funcname:fileneeds diff driverstart,end:filefixed lines, now/re/,+N:filepattern + length/re1/,/re2/:filepattern boundsranges are evaluated on the newest version, then followed backwards

Step 4 — Combine -L with other filters Jump to heading

-L accepts most git log options, which helps on busy histories.

# Only changes since the last release, with full messages
git log -L ':apply_discount:src/billing/invoice.py' v2.4.0..main

# Only one author's changes to the function
git log -L ':apply_discount:src/billing/invoice.py' --author=priya

# Just the list of commits, without diffs
git log -L ':apply_discount:src/billing/invoice.py' -s --oneline

The -s form is useful as a quick index: list the commits, then open the interesting ones with git show.

Step 5 — Know the limits Jump to heading

-L follows a range within one file. If the function was moved to a different file, the trace stops at the commit that added it to the current file, which looks like the function’s creation.

# When the trace starts with an "introduced" commit, check whether it was moved from elsewhere
first=$(git log -L ':apply_discount:src/billing/invoice.py' -s --format=%h | tail -1)
git show --stat "$first" | head          # a deletion elsewhere in the same commit suggests a move
git log -S'def apply_discount' --oneline --all

The pickaxe search in the last line finds the function’s definition anywhere in history, which bridges moves between files. It is covered in finding when code appeared with the pickaxe.

blame, log -- file and log -Lblame gives the latest change per line, which often lands on refactors. log on a file lists every commit touching the file, mostly irrelevant. log -L lists exactly the commits that changed the chosen function or range, with only that region's diff.AnswersNoisegit blamelast change per linerefactors hide origingit log -- fileevery commit on the filemost unrelatedgit log -Levery change to the rangelittle-L is the right tool when the question is about one piece of code

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Why does -L show a commit where the function did not visibly change? Jump to heading

It may have changed only whitespace, or a line at the range’s edge. Add -w to ignore whitespace, or tighten the range.

Is -L slow? Jump to heading

It is slower than plain log because it computes diffs for every commit touching the file. On very long histories, restrict the revision range; the commit-graph file also helps.

Can I use -L on several ranges at once? Jump to heading

Yes. Pass -L more than once, for the same or different files, and Git shows commits that changed any of them.