How do you find the commit that introduced or removed a specific string with git log?
answer
- grep sees the present, history needs a different tool
- Count the occurrences on each side of a diff
- Two flags: one counts, one matches patch lines
- Moves and reindents change the diff, not the count
- Scope by path, because every commit gets diffed
basics
~20 sUse the pickaxe: git log -S'<string>' lists commits where the number of occurrences of that string changed, so the commits that added or deleted it surface. git log -G'<regex>' instead matches any added or removed line against a regular expression.
solid answer
~50 s`git log -S'MAX_RETRIES' --oneline -- src/` is the pickaxe search: for every commit it compares occurrence counts of the string between parent and child, and reports the commit only when the count changed. That is exactly the "who added this" and "who deleted this" question, and it works for code that no longer exists in the tree — which plain grep cannot answer. `-G` is the sibling: it matches a regular expression against the added and removed lines of the patch, so it also catches commits where a line containing the string merely moved or was reformatted, since the count is unchanged but the diff text is not. Add `-p` to see the change, `--all` to search every ref rather than just HEAD's ancestry, and a pathspec to keep it fast, because both options diff every commit they walk.
code
bash · 8 lines# When did MAX_RETRIES appear, and when did it vanish?
git log -S'MAX_RETRIES' --oneline --all -- src/
# Show the actual change for each hit
git log -S'MAX_RETRIES' -p -- src/client.go
# Every commit touching lines that look like this, moves included
git log -G'retry.*backoff' --oneline -- src/go deeper
Know that git log -S'<string>' exists and finds when a string was added or removed, including for code that is no longer in the tree. Adding -p to see the change is the natural next step.
Explain the mechanism — occurrence counts compared across each commit's diff — and contrast it with -G, which matches the added and removed lines themselves and therefore also catches moves.
Drive a real investigation: scope by path and refs, use --all so branch-only history is covered, read the removal commit's message for intent, and recognise the cost of diffing every commit.
Treat history as an auditable record. The pickaxe is only as useful as the commit hygiene behind it, so argue for focused commits and meaningful messages that make a removal explainable years later.
## The question the pickaxe answers `git grep` searches a snapshot: it tells you where a string is *now*. The archaeology question is different — when did this string appear, and when did it disappear? For deleted code, grep over the working tree finds nothing at all, and scrolling `git log -p` looking for it is hopeless on any real history. The pickaxe options exist for exactly this. ## `-S<string>`: occurrence counting `git log -S'MAX_RETRIES'` walks history and, for each commit, computes the diff against its parent and counts occurrences of the string on each side. The commit is listed only if the count differs. In practice that means you get the commit that introduced the string and the commit that removed it, and nothing in between. By default the argument is a literal string. `--pickaxe-regex` reinterprets it as a regular expression, which is what you want for something like a function signature pattern rather than an exact token. Because the criterion is a change in *count*, `-S` deliberately ignores commits that shuffle the string around: moving a line from one place to another within a file, or reindenting it, leaves the count the same and produces no output. That is a feature — it filters out the churn and leaves the two commits that actually matter. ## `-G<regex>`: matching the patch text `git log -G'retry.*backoff'` takes a regular expression and matches it against the *added and removed lines* of each commit's patch. Any commit whose diff contains a matching line, in either direction, is reported. So `-G` catches the moves and reformattings that `-S` suppresses, and it is the right tool when you want every commit that touched a construct rather than the two that changed whether it exists. The practical rule: `-S` for "when did this appear or vanish", `-G` for "every commit that touched lines like this". ## Making the output useful - `-p` (or `--patch`) prints the diff alongside each hit, which is usually the point — you want to see the change, not just its hash. - `--pickaxe-all` shows the entire changeset of a matching commit rather than only the files that matched, which matters when the interesting context is in a neighbouring file. - `--oneline` keeps a survey readable before you drill in with `-p`. - `--all` starts the walk from every ref instead of HEAD's ancestry, which is essential when the code was removed on a branch, or you are hunting through history that was merged from elsewhere. - A pathspec after `--` restricts both the walk and the diffing, which is the single biggest performance lever. ## Cost Both options require Git to compute a diff for every commit it walks, which is dramatically more expensive than a plain log traversal. On a large repository an unscoped pickaxe search over all history can run for a long time. Scope it: limit by path, by date range, or by a revision range you already suspect. The workflow that scales is to narrow first with a cheap query, then pickaxe within it. ## Interpreting the results A few things regularly confuse people: - The pickaxe reports commits where the *diff* changed, so a string that arrived as part of a bulk import shows the import commit, not the original authorship elsewhere. - Merge commits are not diffed against both parents by default, so a change that only appears in a merge resolution may not surface; combine with the appropriate merge-diff handling if you suspect that. - Because `-S` counts occurrences, a commit that both adds and removes the same number of occurrences of the string cancels out and is not listed — another reason `-G` is the fallback when `-S` finds nothing you expected. ## The scenario in an interview A typical prompt is: a configuration key or feature flag is referenced in a runbook but does not exist anywhere in the codebase — find out what happened to it. The strong answer is `git log -S'<key>' --oneline --all -- <likely paths>`, then `git show` on the removal commit to read the message and the surrounding change, and finally the observation that the commit message and the removed code together explain whether this was an intentional deletion or an accident. The weak answer is grep, which cannot see deleted content, or a manual scroll through history.
- When would -G find a commit that -S misses?When the total occurrence count does not change. Moving a line within a file, reindenting it, or removing one instance while adding another leaves the count identical, so -S stays silent, but the patch text still contains matching added and removed lines, so -G reports it.
- Why is a pickaxe search so much slower than an ordinary git log?Because it must compute a diff for every commit it walks rather than just reading commit metadata. Limiting by pathspec is the biggest lever, since it restricts both the traversal and the diffing; bounding by date or by a revision range helps too.
- Why add --all to a pickaxe search?Without it the walk starts at HEAD and covers only its ancestry, so code that lived on a branch which was never merged, or was removed before merging, is invisible. --all starts from every ref, which is usually what you want when hunting for something that no longer exists anywhere in the current tree.
- How do you make -S treat its argument as a pattern rather than a literal?Pass --pickaxe-regex, which reinterprets the -S argument as a regular expression while keeping the occurrence-counting semantics. Without it the string is matched literally, which is generally what you want for an exact identifier and safer when the text contains punctuation.
The pickaxe is a metal detector for code: it does not care what the ground looks like today, only where something entered or left it.
saying these in an interview costs you the question
- Suggests git grep to find code that was deleted
- Confuses -S and -G, expecting -S to report line moves
- Runs an unscoped pickaxe over a huge repository and calls Git slow
- Assumes -S matches a regular expression by default
- Forgets --all and concludes the code never existed