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
dispelled