Git: Searching and Debugging

Git's history is a searchable record of every change ever made. git grep searches file content across commits. git blame and git annotate show who last changed each line. git bisect performs a binary search through history to find the exact commit that introduced a bug — invaluable when tracking down regressions.

Commands Covered

git grep · git blame · git annotate · git bisect · git log (advanced search)

git grep

Searches working tree files (or any tree object) for a pattern. Faster than system grep because it respects .gitignore, only searches tracked files, and can search historical commits.

# Search working tree for a string
$ git grep "functionName"
$ git grep "TODO"

# Case-insensitive search
$ git grep -i "error"

# Show line numbers
$ git grep -n "session_start"

# Show only filenames (not the matching lines)
$ git grep -l "deprecated"

# Count matches per file
$ git grep -c "import"

# Search with a regex
$ git grep -E "def [a-z_]+"       # extended regex
$ git grep -P "\d{3}-\d{4}"       # Perl-compatible regex

# Search within a specific commit or branch
$ git grep "functionName" HEAD
$ git grep "functionName" main
$ git grep "functionName" v1.0.0

# Search across all commits (slow on large repos)
$ git grep "secret" $(git rev-list --all)

# Limit search to specific files
$ git grep "TODO" -- "*.php"
$ git grep "error" -- src/

# Show surrounding context lines
$ git grep -A 3 "findUser"     # 3 lines after
$ git grep -B 2 "findUser"     # 2 lines before
$ git grep -C 2 "findUser"     # 2 lines before and after

# Search for a string that spans word boundaries
$ git grep -w "log"     # matches "log" but not "logger" or "catalog"

git blame

Shows the commit and author that last modified each line of a file. Essential for understanding why a line of code exists and who to ask about it.

# Show blame for a file
$ git blame src/auth.c

# Output format:
# abc12345 (Jason Smith 2024-03-15 14:22:31 -0500 42) if (user == null) {
# ^SHA     ^author      ^date/time               ^line ^content

# Show only specific lines (useful for large files)
$ git blame -L 40,60 src/auth.c      # lines 40 to 60
$ git blame -L 40,+20 src/auth.c     # 20 lines starting at line 40

# Blame a file at a specific commit (how it looked historically)
$ git blame v1.0.0 -- src/auth.c
$ git blame abc1234 -- src/auth.c

# Ignore whitespace changes (blame the last meaningful change)
$ git blame -w src/auth.c

# Follow line movement between files (detect when code was moved/copied)
$ git blame -M src/auth.c    # lines moved within the same commit
$ git blame -C src/auth.c    # lines copied from other files in the same commit
$ git blame -CCC src/auth.c  # very aggressive copy detection

# Short output (abbreviated SHA)
$ git blame --abbrev=8 src/auth.c

# Show the email address instead of name
$ git blame -e src/auth.c

# Ignore specific commits (e.g. a bulk reformatting commit)
# Create a file listing commits to ignore:
$ echo "abc1234" > .git-blame-ignore-revs
$ git blame --ignore-revs-file .git-blame-ignore-revs src/auth.c
# Or set it globally in config:
$ git config blame.ignoreRevsFile .git-blame-ignore-revs

git annotate

Similar to git blame but uses a slightly different output format. Mostly a legacy command — git blame is preferred.

$ git annotate src/auth.c

# Output format (different from blame):
# abc12345        (Jason Smith      2024-03-15 14:22:31 -0500 42)if (user == null) {

# The data is the same; the column layout differs.
# git blame -p gives more structured output for scripting.

git bisect

Binary search through commit history to find the commit that introduced a bug. You mark commits as "good" or "bad" and Git checks out the midpoint — in O(log N) steps you find the exact culprit out of thousands of commits.

# Start a bisect session
$ git bisect start

# Mark the current commit as bad (has the bug)
$ git bisect bad

# Mark a known-good commit (before the bug existed)
$ git bisect good v1.0.0
$ git bisect good abc1234

# Git checks out the midpoint commit.
# Test whether the bug exists, then mark it:
$ git bisect bad    # this commit has the bug
$ git bisect good   # this commit is fine

# Repeat until Git reports:
# abc1234 is the first bad commit
# commit abc1234
# Author: ...
# Date: ...
#     The commit message of the culprit

# End the bisect session (returns to original branch)
$ git bisect reset

# Bisect a specific range from the start:
$ git bisect start HEAD v1.0.0   # shorthand: bad then good

Automated Bisect

# If you have a test script that exits 0 on good, non-zero on bad:
$ git bisect start
$ git bisect bad HEAD
$ git bisect good v1.0.0
$ git bisect run ./test-script.sh

# Git runs the script at each midpoint automatically.
# Much faster than manual bisect for large ranges.

# Example test script:
#!/bin/sh
make >/dev/null 2>&1 || exit 125  # exit 125 = skip this commit (can't build)
./run-tests 2>/dev/null | grep -q "FAIL" && exit 1  # bad
exit 0  # good

# Exit codes for git bisect run:
# 0     = good (no bug)
# 1-124 = bad (bug present)
# 125   = skip this commit (e.g. doesn't build)
# 126+  = bisect error (abort)

Advanced Log Searching

# Find commits that changed a specific string (the "pickaxe")
$ git log -S "password_hash"        # commits that added or removed this string
$ git log -G "session.*token"       # commits where diff matches this regex

# Search commit messages
$ git log --grep="closes #"
$ git log --grep="fix" --grep="bug" --all-match   # both patterns must match
$ git log --grep="fix" -i            # case-insensitive

# Find who last changed a specific function
$ git log -L :functionName:src/auth.c    # history of a function
$ git log -L 40,60:src/auth.c            # history of lines 40-60

# Find commits that touched a specific file
$ git log -- src/auth.c
$ git log --follow -- src/old-name.c    # follow through renames

# Find commits by author in a date range
$ git log --author="Jason" --since="2024-01-01" --until="2024-06-01"

# Find the commit that deleted a line containing a string
$ git log -S "the deleted string" -p

difftool

# Open diffs in a visual tool instead of the terminal
$ git difftool
$ git difftool HEAD~3 HEAD

# Configure a diff tool
$ git config --global diff.tool vimdiff
$ git config --global diff.tool meld
$ git config --global diff.tool code   # VS Code

# Don't prompt before opening each file
$ git config --global difftool.prompt false

# List available diff tools
$ git difftool --tool-help

References