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 blamekeeps 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.
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 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.
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.
Related Jump to heading
- Bisect & History Forensics — the parent topic.
- Finding Who Changed a Line with log and blame — the line-level view.
- Bisecting Across Merges with --first-parent — when you have a reproduction rather than a suspect function.
- Standardising Line Endings with gitattributes — the attributes file that also sets diff drivers.