Finding when code appeared with the pickaxe Jump to heading

git blame answers “who last touched this line”, which is often not the question. You want to know when a function call first appeared, when a configuration key was removed, when a magic number entered the codebase — and blame stops at the most recent reformatting or rename. Git’s “pickaxe” options search history by content change instead of by line. git log -S<string> finds commits where the number of occurrences of a string changed: where it was added or removed. git log -G<regex> finds commits whose diff contains a line matching a pattern. Together they answer “when did this get here, and when did that go away” across the whole repository in seconds. This page shows how to use both precisely, within bisect and history forensics.

When to use this approach Jump to heading

  • You need the commit that introduced or removed a specific call, constant or configuration key.
  • git blame points at a reformatting commit, a rename or a mass edit rather than the real origin.
  • The code you are looking for no longer exists, so there is no line to blame.
  • You have a symptom but not a reproduction script; with a reproduction, automating git bisect with a test script may be faster.

Step 1 — Find additions and removals with -S Jump to heading

-S lists commits that changed the number of times the exact string appears. Adding it, removing it, or adding one more copy all count. Moving a line within a file does not, because the count stays the same — which is exactly what makes -S good at ignoring reformatting.

git log -S'retry_with_backoff(' --oneline
# 9d1e0a2 Remove retry_with_backoff from export client
# 4f7c11b Add retry_with_backoff to export client
# 2a03e98 Introduce retry_with_backoff helper
# Show the change itself, limited to the hunks that matter
git log -S'retry_with_backoff(' -p --pickaxe-all -1 4f7c11b | less

--pickaxe-all shows the whole commit’s diff instead of only the file containing the match, which helps when the reason for a change is in another file.

-S against -G-S matches commits where the count of an exact string changed, so it finds additions and removals and ignores lines that merely moved. -G matches commits whose diff contains an added or removed line matching a regular expression, so it also finds edits to lines containing the pattern.git log -S'text'git log -G'regex'matches whencount of text changesdiff line matchesline moved, unchangednot reportedreportedargument edited on same linenot reportedreportedpattern supportliteral (or --pickaxe-regex)regex-S for 'when did this appear or vanish'; -G for 'when was this touched at all'

Step 2 — Find edits to matching lines with -G Jump to heading

When the string’s count did not change — someone edited the arguments of a call, or changed a value next to a key — -S finds nothing. -G matches any commit whose diff contains a changed line matching a regular expression.

# When did the timeout passed to retry_with_backoff change?
git log -G'retry_with_backoff\(.*timeout=' --oneline -p -- src/export/

Because -G matches moved lines too, it is noisier. Narrow it with a path and a date range.

git log -G'MAX_EXPORT_ROWS *=' --since=2025-01-01 --format='%h %ad %an %s' --date=short -- src/

Step 3 — Follow the search across renames and branches Jump to heading

Pickaxe searches cover whatever revisions you give them. By default that is the current branch’s history; add --all to include every branch and tag, which finds changes that were made on a branch and never merged.

# Every ref, including unmerged branches
git log --all -S'LEGACY_BILLING_ENABLED' --format='%h %d %s'

# Restrict to one file, following it through renames
git log --follow -S'LEGACY_BILLING_ENABLED' --oneline -- config/flags.yml
A feature flag's life, reconstructed with -SSearching all refs for a feature flag's name finds the commit that introduced it, the commit that enabled it by default, a rename of the file containing it, and the commit that removed it — a complete history even though the flag no longer exists anywhere in the code.Introducedflag added, off2025-03Enableddefault on2025-06File renamed--follow keeps it2025-11Removedcode path deleted2026-04blame cannot show any of this — the line no longer exists

--follow works for a single path only. For a repository-wide search without a path, renames do not matter, because the search looks at every file’s diff anyway.

Step 4 — Search in merge commits when needed Jump to heading

By default, git log does not show diffs for merge commits, so a change introduced while resolving a merge conflict is invisible to the pickaxe. Add a merge diff format to include them.

# Include changes made in conflict resolutions (Git 2.36+)
git log -S'skip_discount' --diff-merges=remerge --oneline
# Or treat each merge's diff against its first parent
git log -S'skip_discount' --diff-merges=first-parent --oneline

remerge shows only what the person resolving the merge changed beyond Git’s own automatic merge, which is precisely where unexpected code tends to come from. It is explained further in reviewing conflict resolutions with remerge-diff.

Step 5 — Turn the answer into context Jump to heading

The commit is the start, not the end. From it, find the pull request, the issue and the people involved.

c=4f7c11b
git log -1 --format='%H%n%an <%ae>%n%ad%n%n%B' "$c"
# Which merge brought it into main, and therefore which pull request?
git log --ancestry-path --merges --format='%h %s' "$c..main" | tail -1
gh pr list --state merged --search "$c" --json number,title --jq '.[] | "#\(.number) \(.title)"'
From a string to the reason it existsSearch history with -S or -G to find the commit, read its message and diff, find the merge that brought it into main and the pull request behind it, and follow links to the issue that motivated the change.Pickaxe-S / -GCommitmessage + diffMerge into main--ancestry-pathPull requestreview discussionIssuethe whythe commit tells you when; the pull request usually tells you why

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Why is -S slow on a large repository? Jump to heading

It must diff every commit in the range. Limit it with a path, a date range or a revision range, and it becomes fast. The commit-graph file, covered in enabling commit-graph and multi-pack-index, also helps.

Can -S take a regular expression? Jump to heading

Yes, with --pickaxe-regex, which applies the count rule to regex matches. It is useful for patterns like retry_with_backoff\( where literal search would also match unrelated names.

Does the pickaxe search binary files? Jump to heading

Only if Git can diff them. With a textconv driver configured, as in diffing binary formats with textconv, add --textconv and the search sees the converted text.