Module 13 — Searching and Debugging History
Updated 7 September 2026
🧠 concept → 🧩 analogy → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (hidden)
Part A — git blame: who last touched this line
A1. Line-by-line authorship
git blame <file> annotates every line with the commit that last changed it, plus that commit's author and date. It answers "who wrote this line, and in which commit?" — the starting move of almost every debugging session, because the commit it names is where you go to learn why the line is the way it is (its message, its diff, its author to ask). git blame -L <start>,<end> <file> restricts the output to a line range — essential on large files where you only care about the function that's misbehaving.
The exercises use a converter script with a bug planted in its history: someone changed a density constant from 120 to 100, breaking every conversion. Blame is how you find the hand on the knife.
🧪 Exercise 13.1 — set up the scene, then blame the file
mkdir -p ~/git-course && cd ~/git-course
git init -q kitchen && cd kitchen
cat > convert.sh <<'EOF'
#!/bin/bash
# convert grams to cups (flour)
grams_to_cups() {
echo "scale=2; $1 / 120" | bc
}
EOF
git add convert.sh && git commit -q -m "Add grams_to_cups converter" --author="Ben <[email protected]>"
echo "# added usage note" >> convert.sh && git commit -qa -m "Document converter usage"
echo "# tweak comment" >> convert.sh && git commit -qa -m "Clarify comment"
sed -i 's|/ 120|/ 100|' convert.sh && git commit -qa -m "Adjust flour density constant" # THE BUG
echo "# more docs" >> convert.sh && git commit -qa -m "Expand docs"
echo 'extra() { echo hi; }' >> convert.sh && git commit -qa -m "Add helper"
git blame -L 3,5 convert.sh # blame just the function's lines✅ Expected result — click to reveal
^76da286 (Ben 2026-09-07 22:26:43 +0000 3) grams_to_cups() {
cd2d1c18 (Aisha Rahman 2026-09-07 22:26:43 +0000 4) echo "scale=2; $1 / 100" | bc
^76da286 (Ben 2026-09-07 22:26:43 +0000 5) }What to read out of it: each line shows the commit that last changed it, its author, date, and line number. Lines 3 and 5 trace to ^76da286 — the ^ marks the root commit (Ben's original), meaning "unchanged since the beginning." But line 4 — the buggy / 100 — traces to a different commit, cd2d1c18 by Aisha: that is your suspect. Blame just pointed straight at the commit that introduced the bad constant, without reading a single other commit. Now git show cd2d1c18 (Module 4) reveals why it was changed — the message Adjust flour density constant and whatever context the author left. Blame finds the where; the commit it names supplies the why.
git blame is a textbook where every sentence has a tiny margin note: "revised by Aisha, March, see revision #47." You don't read the whole book to find who rewrote the definition on page 30 — you glance at the margin of that one line. The note points you to the full revision, where the reasoning lives.
Where the analogy stops working. Margin notes credit whoever touched the line, and so does blame — which is exactly its blind spot. If someone reformatted the whole file (re-indented, moved a block), blame credits them for every line, burying the person who wrote the actual logic. The bug's real author hides behind a later cosmetic change. That's why "blame" is a misleading name — it finds the last editor, not the responsible author — and why the pros reach for blame -w (ignore whitespace) and, when that's not enough, the pickaxe (Part B), which follows the content rather than the line.
🎯 Interview questions — Part A
🎯 "What is git blame?" — asked verbatim in GeeksforGeeks' 70+ Git Interview Questions, updated Jul 2026; also as "What does git annotate command do?" in InterviewBit, 2025
git blame <file> shows, for each line, the commit that last modified it along with that commit's author, timestamp, and short hash — so you can see who changed a line and jump to the commit for the full reason. (git annotate is a near-identical, older-style variant.) It's a debugging and archaeology tool: land on a suspicious line, blame it, git show the named commit for its message and diff. -L <start>,<end> scopes it to a line range; -w ignores whitespace-only changes; -C/-M detect lines moved or copied from elsewhere.
The details that separate candidates: an average answer says "shows who changed each line." A strong answer names blame's core weakness — it credits the last toucher, so a reformat or file-move masks the real author — and the mitigations (-w, -C, -M, blaming an earlier revision like git blame <commit>~1 -- <file> to see past a known cosmetic change). The senior framing: blame answers "who touched this line last"; to answer "when did this value actually enter the code," you need the pickaxe — knowing blame's limits is what the question is really probing.
Part B — The pickaxe: when did this string appear or vanish?
B1. log -S and log -G
Blame finds the last change to a line; the pickaxe finds every commit that changed how many times a string appears anywhere in history. git log -S "<string>" lists commits where the count of that string went up or down — i.e. where it was added or removed. This is the tool for "when did this config key first appear?", "which commit deleted that function?", or "who introduced this magic number?" — questions blame can't answer because the code may have been added, deleted, and re-added across many lines. git log -G "<regex>" is the broader cousin: it matches commits whose diff contains a line matching the regex (any change touching such a line), where -S only fires when the number of occurrences changes. Add -- <path> to scope to a file, and -p to see the actual diffs.
🧪 Exercise 13.2 — pickaxe the density constant
cd ~/git-course/kitchen
git log -S "120" --oneline -- convert.sh # commits that added/removed "120"
git log -S "/ 100" --oneline -- convert.sh # commits that added/removed the bad value
git log -G "/ 1[0-9]0" --oneline -- convert.sh # regex: any change to a "/ 1x0" line✅ Expected result — click to reveal
cd2d1c1 Adjust flour density constant
76da286 Add grams_to_cups converter
cd2d1c1 Adjust flour density constant
76da286 Add grams_to_cups converterWhat to read out of it: -S "120" returned two commits — 76da286 (where 120 first appeared, count 0→1) and cd2d1c1 (where it was removed, count 1→0, when someone changed it to 100). That pair is the whole life story of the constant: born here, killed there. -S "/ 100" returns just cd2d1c1 (where 100 was born; it's still alive, so no removal commit). -G "/ 1[0-9]0" (regex) returns both commits that touched a / 1x0 line. The pickaxe found the exact commit that swapped the constant — even though blame would have worked here, the pickaxe also finds changes that were later overwritten, deleted code, and strings that moved between lines: history blame cannot see.
🎯 Interview questions — Part B
🎯 Corpus note — the pickaxe (git log -S / -G)
Across the surveyed corpus (GeeksforGeeks, DataCamp, InterviewBit), the pickaxe appears only inside answers about finding when code changed, never as a verbatim standalone question — so, a corpus note, not a fabricated block. What to be able to say cold: git log -S "<string>" finds commits where a string was added or removed (occurrence count changed); git log -G "<regex>" finds commits whose diff touches a line matching the pattern; both accept -- <path> and -p. The interview-worthy nuance is knowing when blame fails and pickaxe wins: blame shows only the current line's last edit, while the pickaxe reconstructs a string's entire birth-and-death history across the whole repo — the tool for "when did this value first appear / who deleted this function," which comes up constantly in real debugging.
Part C — git grep and shortlog: searching the tree and the authors
C1. git grep — search code at any point in time
git grep "<pattern>" searches the tracked files of your working tree — faster and cleaner than plain grep -r because it skips .git, ignored files, and untracked clutter, searching only what Git manages. Its superpower over ordinary grep: it can search any commit or tree. git grep "<pattern>" <commit> searches the codebase as it was at that commit — "did this function exist in v1.0.0?", "what did the config look like three releases ago?" — reading straight from the object database (Module 3's snapshots) without checking anything out. -n adds line numbers; -i case-insensitive; -l lists only filenames.
🧪 Exercise 13.3 — search now, and search the past
cd ~/git-course/kitchen
git grep -n "grams_to_cups" # where is this defined, in the current tree?
git grep -n "120" HEAD~5 -- convert.sh # what was the constant 5 commits ago?
git grep -n "100" HEAD -- convert.sh # ...and now?✅ Expected result — click to reveal
convert.sh:3:grams_to_cups() {
HEAD~5:convert.sh:4: echo "scale=2; $1 / 120" | bc
HEAD:convert.sh:4: echo "scale=2; $1 / 100" | bcWhat to read out of it: the first git grep -n searched the current tree — one hit (convert.sh:3: — the -n gives the line number), no .git noise. The second and third are the trick worth remembering: git grep <pattern> <commit> prefixes each hit with the revision (HEAD~5:convert.sh:4: vs HEAD:convert.sh:4:), letting you diff a value across time at a glance — 120 five commits ago, 100 now, the bug caught by direct before/after inspection of the same line without a single checkout. This is Module 4's git show <commit>:<file> (read one file) generalized to search all files at that commit — history is queryable, not just readable.
C2. git shortlog — who did how much
git shortlog summarizes commits grouped by author — by default a per-author list of commit subjects; with -sn a ranked count (-s summary count, -n sort by number). It's how release notes get drafted ("changes since v1.0.0, by contributor") and how you gauge who knows a codebase. One gotcha worth knowing now: run interactively it reads the current branch, but in a script or pipe it expects a revision argument (git shortlog -sn HEAD) or input piped in — otherwise it waits on stdin.
🧪 Exercise 13.4 — rank the contributors
cd ~/git-course/kitchen
git shortlog -sn HEAD # count per author, sorted
git shortlog -sne HEAD # ...with emails, to spot identity duplicates✅ Expected result — click to reveal
5 Aisha Rahman
1 Ben
5 Aisha Rahman <[email protected]>
1 Ben <[email protected]>What to read out of it: five commits by Aisha, one by Ben (the original converter, via the --author flag in Exercise 13.1) — a contribution ranking in one line. The -e variant adds emails, which is how you catch the common real-world mess of one person committing under two identities ([email protected] and [email protected] would show as two rows, inflating the author count — a symptom of the per-repo-identity discipline from Module 1). For release notes you'd add a range: git shortlog -sn v1.0.0..HEAD lists exactly who contributed since the last release.
🎯 Interview questions — Part C
🎯 Corpus note — git grep and git shortlog
Neither git grep nor git shortlog appears as a verbatim standalone question in the surveyed corpus — both show up only inside broader "how do you search history" answers — so this Part carries a corpus note rather than a fabricated block. The cold-recall facts: git grep <pattern> [<commit>] searches tracked files (skipping .git/ignored/untracked), and crucially can search any historical commit's tree without checkout; git shortlog -sn gives a per-author commit count, the basis of release-note attribution. The differentiator when these come up as follow-ups is knowing why git grep beats grep -r (it searches only tracked content and can time-travel to any revision) — the same "history is a queryable database" theme running through Modules 4 and 13.
Part D — git bisect: binary-searching for the breaking commit
D1. The idea, and a manual hunt
When a bug appeared somewhere in the last 500 commits and blame/pickaxe can't pin it (the symptom is behavioral, not a single obvious line), git bisect finds the exact culprit with a binary search: mark one known-good commit and one known-bad commit, and Git repeatedly checks out the midpoint for you to test, halving the search space each time. 500 commits → ~9 tests, not 500. You start with git bisect start, mark the current broken state git bisect bad, mark a known-working commit git bisect good <commit>; Git checks out the middle commit (in detached HEAD, Module 5 — that's why it teaches detached HEAD as a feature); you test, say git bisect good or git bisect bad, and it narrows until one commit remains: <hash> is the first bad commit. Always finish with git bisect reset to return to where you started.
🧪 Exercise 13.4b — bisect the density bug by hand
The bug: grams_to_cups 120 should print 1.00, but the constant was changed to 100. HEAD~5 (the original) is good; HEAD is bad.
cd ~/git-course/kitchen
git bisect start
git bisect bad HEAD # current state is broken
git bisect good HEAD~5 # the original converter worked
# Git checks out a midpoint. Test THIS checkout: does convert.sh divide by 120 (good) or 100 (bad)?
grep "/ 1" convert.sh # inspect the checked-out midpoint
git bisect good # this midpoint still had /120 → good
grep "/ 1" convert.sh # next midpoint
git bisect bad # this one had /100 → bad
git bisect bad # the final candidate is also bad → found
git bisect reset # ALWAYS return to the start✅ Expected result — click to reveal
Bisecting: 2 revisions left to test after this (roughly 1 step)
[3142068e...] Clarify comment
echo "scale=2; $1 / 120" | bc
Bisecting: 0 revisions left to test after this (roughly 1 step)
[8a8d1733...] Expand docs
echo "scale=2; $1 / 100" | bc
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[cd2d1c18...] Adjust flour density constant
cd2d1c18... is the first bad commit
commit cd2d1c18...
Adjust flour density constant
convert.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)What to read out of it: each step Git announces how many revisions remain and roughly how many tests are left, then checks out a midpoint (in detached HEAD — note the bare [hash]). You inspect the actual checked-out files each time — / 120 = good, / 100 = bad — and mark accordingly; Git halves the range every answer. It lands on cd2d1c18 Adjust flour density constant as the first bad commit, showing its full diff: the 120→100 change, red-handed. That's the exact commit blame and pickaxe also found — but bisect gets there by testing behavior, so it works even when no single line obviously screams "bug." Your hashes and the exact midpoints differ; the shape (halve, test, narrow, found) is invariant.
D2. bisect run — automate the whole search
The real power: if you can script the good/bad test, git bisect run <command> does the entire search unattended. Git checks out each midpoint, runs your command, and reads its exit code — 0 means good, 1–127 (except 125) means bad, 125 means "skip, can't test this commit" — marking automatically until it finds the culprit. This is bisect at its most valuable: point it at a test script and walk away; it pinpoints the breaking commit across hundreds of revisions while you get coffee. The command is often a single test (git bisect run make test, git bisect run ./repro.sh).
🧪 Exercise 13.5 — let bisect find it automatically
cd ~/git-course/kitchen
git bisect start HEAD HEAD~5 # bad=HEAD, good=HEAD~5, in one line
git bisect run bash -c 'test "$(source convert.sh; grams_to_cups 120)" = "1.00"'
git bisect reset✅ Expected result — click to reveal
Bisecting: 2 revisions left to test after this (roughly 1 step)
[3142068e...] Clarify comment
...
Author: Aisha Rahman <[email protected]>
Adjust flour density constant
convert.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
cd2d1c18... is the first bad commit
bisect found first bad commitWhat to read out of it: one command replaced the whole manual dance. The test grams_to_cups 120 == 1.00 exits 0 (good) on commits with the correct / 120 and 1 (bad) on the buggy / 100 ones; git bisect run fed that exit code back to bisect at every midpoint and drove the search to completion — landing on the same cd2d1c18 ... is the first bad commit, fully automated. git bisect start HEAD HEAD~5 even set both endpoints in one line (bad first, good second). This is the production pattern: write a one-line reproduction test, hand it to bisect run, and Git finds the regression across any number of commits by itself.
🎯 Interview questions — Part D
🎯 "What is git bisect and what is it used for?" — asked verbatim in DataCamp's Top 25 Git Interview Questions, updated Jun 2026; also "How do you perform a Git bisect to find the commit that introduced a bug?" in GeeksforGeeks (updated Jul 2026)
git bisect finds the commit that introduced a bug via binary search over history. You give it a known-good commit and a known-bad commit; it checks out the midpoint (detached HEAD) for you to test, and each good/bad answer halves the remaining range — N commits take ~log₂(N) tests. The flow: git bisect start, git bisect bad (current broken commit), git bisect good <hash> (last known working), then test each checkout and mark it, until Git reports <hash> is the first bad commit; finish with git bisect reset. If the test is scriptable, git bisect run <cmd> automates the whole search via the command's exit code (0 good, 125 skip, else bad).
The details that separate candidates: an average answer describes the manual good/bad loop. A strong answer states the logarithmic payoff (500 commits → ~9 tests), the run automation with its exit-code convention (and 125 for un-testable commits), and when to reach for bisect versus blame/pickaxe — bisect for behavioral regressions where no single line is obviously wrong; blame/pickaxe when you can already see the offending line or string. Mentioning git bisect reset to restore state, and that bisect assumes a clean good→bad transition (a flaky test breaks it), signals real use rather than recitation.
🎯 "How do you find a commit which broke something after a merge operation?" — asked verbatim in InterviewBit's 30+ Git Interview Questions, 2025
git bisect — it doesn't care whether the break came from a plain commit or a merge; it binary-searches the commit graph between a known-good and known-bad point and lands on the first bad commit, which may itself be a merge. Mark git bisect good <last-working> and git bisect bad <current>, test each midpoint (or automate with git bisect run <test>), and Git reports the culprit. If it turns out to be a merge commit, git show <merge> and the per-parent diffs (Module 6's ^1/^2) show what the merge brought in; you can then bisect within the merged branch's own commits if needed.
The details that separate candidates: an average answer just says "use bisect." A strong answer adds the merge-specific nuances: bisect can finger a merge commit as the first-bad (the bug was latent on both sides and only their combination broke — a genuinely hard case blame can't touch), and git bisect run plus a reproduction test is how you'd actually do it across a big post-merge range. Tying it back to "which parent introduced it" via ^1/^2 and a follow-up bisect on the branch shows the debugging maturity the question is fishing for.
Part E — Production practice
E1. Symptom → cause → diagnosis → fix
| Symptom | What is really happening | What to run | The fix / next step |
|---|---|---|---|
| git blame credits a reformat/move for logic someone else wrote | Blame shows the last toucher; a cosmetic change buried the real author | git blame -w -C <file> · blame an earlier rev: git blame <badcommit>~1 -- <file> | Use -w/-C/-M; when still masked, switch to the pickaxe to follow the content |
| "When did this string first appear / get deleted?" — blame can't say | The content was added/removed across commits; blame only sees the current line | git log -S "<string>" -- <path> (add/remove) or -G "<regex>" (any touch) | Read the returned commits with -p; the first/last are birth/death of the string |
| git log -S misses a change you know happened | The edit changed a line containing the string without changing its occurrence count | Re-run with git log -G "<regex>" | -S = count changed (add/delete); -G = line matching pattern touched — pick the right one |
| A regression appeared "sometime last month" across hundreds of commits | Behavioral break with no single obvious line — blame/pickaxe won't find it | git bisect start; git bisect bad; git bisect good <old> then test | Or automate: git bisect run <test-cmd>; git bisect reset when done |
| git bisect run gives wrong/erratic results | A flaky test, or commits that don't build being marked bad instead of skipped | Run the test manually on a midpoint to check determinism | Make the test reliable; return exit 125 from the script for un-buildable commits (skip) |
| Stuck in detached HEAD / weird state after debugging | A bisect was never reset — you're parked on a midpoint checkout | git bisect log (are you mid-bisect?) · git status | git bisect reset returns to the original branch and commit |
| git shortlog hangs in a script | Without a revision arg in a non-interactive context it waits on stdin | — | Pass a rev: git shortlog -sn HEAD (or a range v1.0.0..HEAD) |
E2. Capstone — four tickets
Worked answer: the symptom is behavioral (wrong output, not an obvious bad line), so bisect is the tool — but layer the cheap checks first. (1) git log -S "120" -- convert.sh (pickaxe) — if the magic constant is the culprit, this names the two commits (birth and change) in one shot, faster than bisecting. (2) If the pickaxe doesn't obviously reveal it, git blame -L <func> convert.sh on the function to see if one line changed recently. (3) If neither pins it (the bug is subtler — a changed call order, a removed guard), bisect: git bisect start; git bisect bad; git bisect good <last-release-tag> (Module 12 tags earn their keep here — a known-good anchor), then git bisect run bash -c '<repro test exits 0 when correct>' and let it find the commit across all 40 automatically. The senior instinct is escalating from cheap targeted tools (pickaxe/blame) to the systematic one (bisect) — not reaching for bisect first when a log -S would answer in a second.
Worked answer: blame's last-toucher blind spot, and the standard escape ladder. Start with git blame -w convert.sh (ignore whitespace — defeats reformats). If a real logic move still masks authorship, git blame -C -C -C convert.sh (repeated -C detects lines copied/moved from other files, not just within). When a specific commit (the linter run) is the wall, blame before it: git blame <linter-commit>~1 -- convert.sh shows authorship as it stood before the cosmetic change. And to trace one tricky line's value rather than its formatting, pickaxe it: git log -S "<distinctive substring>" -- convert.sh walks straight to where that logic entered, ignoring every reformat since. Present it as: blame answers "last edited by," which is often not "authored by" — knowing the ladder from -w → -C → earlier-revision → pickaxe is the difference between finding the right person and blaming the linter-runner.
Worked answer: git bisect run wired to the failing test. The pipeline: on a red nightly, fetch the range since the last green run (green build's commit = known good, current = known bad), then git bisect start <bad> <good>; git bisect run <the failing test> in a clean checkout. Bisect drives ~log₂(N) automated builds and emits the first bad commit; post it to the report with git show. Critical robustness details for the writeup: the test must be deterministic (flaky tests make bisect lie — gate on a stable subset); the script returns 125 for commits that fail to build (vs fail the test) so infrastructure breakage doesn't get blamed as the bug; run in isolated worktrees (Module 11) so parallel bisects don't collide; and cap the range with a known-good tag so a bisect never wanders into ancient history. This is the fleet-scale callout made concrete — logarithmic search means even an 800-commit range costs ~10 builds, cheap enough to run automatically.
Worked answer (the guide): match the question shape to the tool. "Who last changed this specific line, and why?" → git blame <file> (-L to scope, -w/-C to see past cosmetics) → then git show the named commit. "When did this exact string/value enter or leave the code?" → pickaxe git log -S "<string>" (or -G "<regex>" for any line matching) — finds births, deaths, and moves blame can't. "Where in the current or a past codebase does this pattern appear?" → git grep "<pattern>" [<commit>] — searches tracked files at any revision, no checkout. "Which of many commits introduced this behavior change?" → git bisect (or bisect run <test> to automate) — binary search when no single line is obviously the cause. "Who contributed what, for release notes?" → git shortlog -sn <range>. Pin the meta-rule on top: escalate from targeted (blame/grep/pickaxe — instant, when you can name the line or string) to systematic (bisect — when you can only describe the broken behavior). Most stuck debugging sessions are someone using blame for a question that needed the pickaxe or bisect.
E3. Documentation reference
| Topic | Official source | What it covers |
|---|---|---|
| Debugging with Git (tutorial) | Git Book §7.10 — Debugging with Git | blame and bisect, worked examples, bisect run |
| git blame | git-blame manual | -L, -w, -C/-M, porcelain output |
| git bisect | git-bisect manual | start/good/bad/skip/reset/run, exit-code convention |
| Pickaxe (-S/-G) | git-log manual | -S, -G, --pickaxe-regex, combined with -p/paths |
| git grep | git-grep manual | Searching trees/commits, -n/-i/-l, regex options |
| git shortlog | git-shortlog manual | -s/-n/-e, ranges for release notes |
E4. Self-assessment
Answer each aloud, from memory, before moving on. Every one is answered on this page.
- What exactly does git blame credit for each line — and why is "blame" a slightly misleading name?
- Name three flags that help blame see past cosmetic changes, and one way to blame a file before a known reformat.
- What does git log -S "foo" find, precisely? How does -G differ?
- Give a concrete edit that -G catches but -S misses, and say why.
- Why can the pickaxe answer "who deleted this function?" when blame cannot?
- What does git grep <pattern> <commit> do that plain grep -r cannot?
- What does git shortlog -sn produce, and why add -e?
- State the bisect payoff in one sentence (commits vs tests), and why it uses detached HEAD.
- Recite the manual bisect command sequence, start to reset.
- In git bisect run, what do exit codes 0, 125, and (say) 1 mean?
- When do you reach for bisect instead of blame or pickaxe? Give the distinguishing property of the bug.
- Match each question to its tool: "who edited this line," "when did this string appear," "which commit broke the behavior," "who contributed since v1.0."
E5. Sources
🗒️ Cheat sheet — Module 13
| Command | What it does |
|---|---|
| git blame <file> · -L <a>,<b> · -w · -C/-M | Last commit per line · scope to a range · ignore whitespace · detect moves/copies |
| git blame <commit>~1 -- <file> | Blame as of before a known commit — see past a reformat |
| git log -S "<string>" -- <path> | Pickaxe: commits where the string was added or removed (count changed) |
| git log -G "<regex>" -- <path> · add -p | Commits whose diff touches a line matching the regex · with patches |
| git grep -n "<pattern>" · git grep "<pattern>" <commit> | Search tracked files now · search the tree at any commit (no checkout) |
| git shortlog -sn <rev> · -sne · <range> | Per-author commit counts · with emails · since a tag (release notes) |
| git bisect start · bad [<c>] · good <c> · reset | Begin · mark broken · mark working · restore original state (always finish with reset) |
| git bisect start <bad> <good> · git bisect run <cmd> | Set both endpoints in one line · fully automate via the command's exit code |
| git bisect skip · exit 125 in a run script | Can't test this commit (won't build) — skip rather than judge |
Key concepts: blame = last commit to touch each line (author + why-via-git show); its blind spot is reformats/moves — defeat with -w/-C or by blaming an earlier revision · pickaxe -S finds where a string was added/removed; -G finds any commit touching a matching line — -S for births/deaths, -G for modifications · git grep searches tracked files and, uniquely, any historical tree without checkout · git shortlog -sn ranks authors (release notes; -e catches identity dupes) · bisect binary-searches good→bad, ~log₂(N) tests, in detached HEAD; bisect run <cmd> automates via exit codes (0 good / 125 skip / else bad); always bisect reset · escalate from targeted (blame/grep/pickaxe — you can name the line/string) to systematic (bisect — you can only describe the broken behavior).