What you will achieve
You will use git blame and git log together to reconstruct the history of a buggy line, identify when and why a change was made, and find the right person to consult. The same techniques apply to bug-hunting, security incident response, and onboarding.
Set up a sandbox
mkdir blame-tutorial && cd blame-tutorial
git init
cat > calc.js <<'EOF'
function divide(a, b) {
return a / b;
}
module.exports = { divide };
EOF
git add . && git commit -m "Initial divide function"
# Add validation
sed -i.bak '2i\
if (b === 0) throw new Error("Division by zero");
' calc.js && rm calc.js.bak
git commit -am "Add zero check"
# Refactor (introduces a subtle bug)
sed -i.bak 's|if (b === 0)|if (b == 0)|' calc.js && rm calc.js.bak
git commit -am "Style: use loose equality for zero check"
Step 1: blame the suspicious line
git blame -L 2,3 calc.js
Output shows the SHA, author, date, and content of each line. The most recent change to line 2 is the loose-equality refactor.
Step 2: inspect the suspicious commit
git show <sha>
You see the full commit - message, author, diff. The "style" justification looks innocent, but loose equality has subtle behaviour around null and undefined.
Step 3: walk the file's history
git log --follow -p calc.js
--follow traces across renames. -p shows each commit's diff. You see the full evolution of the file.
Step 4: find when a string was introduced
git log -S 'Division by zero' --oneline -- calc.js
# finds commits that change the count of this string
-S is the "pickaxe" - find when text was added or removed. -G is regex-based:
git log -G 'b\\s*[!=]==?\\s*0' --oneline -- calc.js
Step 5: blame ignoring formatter commits
Mass-format commits (prettier, black) bury blame results. Maintain .git-blame-ignore-revs:
# .git-blame-ignore-revs
abc1234 # Apply prettier across codebase
def5678 # Apply black to all Python
git config blame.ignoreRevsFile .git-blame-ignore-revs
git blame -L 2,3 calc.js
Now blame skips the formatter commits and points at the real authors.
Step 6: blame across renames and moves
git blame -M calc.js # detect within-file moves
git blame -C calc.js # detect across-file copies
git blame -CCC calc.js # aggressive cross-file detection
Each C increases sensitivity. Three Cs catches even small moved fragments.
Step 7: log with detailed filters
git log --since='1 month' --author='Alice'
git log --grep='checkout'
git log -p -L :divide:calc.js # log of changes to the divide function
-L :function:file traces a specific function across history.
Step 8: log a region of a file
git log -L 1,5:calc.js
Shows every commit that touched lines 1-5 of calc.js, with diffs. Powerful for tracing the evolution of a small block.
Step 9: combine with bisect
If blame points at a refactor that touched many files, bisect narrows further:
git bisect start
git bisect bad HEAD
git bisect good v1.0.0
git bisect run npm test
Bisect identifies the exact commit; blame and log fill in the why.
Step 10: graph the divergence
git log --graph --oneline --all -20
Useful when a bug appears only on one branch - the graph shows where branches diverged.
Real-world workflow
- Reproduce the bug.
git blamethe suspicious line.git showthe commit; read the message.- If the commit is too coarse,
git log -p -- fileto walk file history. - If many files changed in tandem,
git log -- 'src/**'for cross-file context. - If commit message lacks context, ask the author (linked via blame).
- If the author has left, the message and PR description are your only source of context.
Etiquette
Blame is for understanding, not finger-pointing. The author of a line might have written it years ago in a context they no longer remember. Use blame to ask, not to accuse.
Tooling
VS Code's GitLens, JetBrains' built-in annotate, and fugitive.vim in Vim provide interactive blame UIs that make this workflow much faster than the CLI.
The result
You can navigate from a buggy line to the commit, the PR, the author, and the original intent in minutes. The investigative skills compound - the more often you do this, the faster you find the answer.