Reading history: log, diff, blame and bisect

Format your log instead of scrolling it, search history with the pickaxe, find which commit last touched a line, and let bisect run find the breaking change.

Log that answers questions

git log --oneline --graph --decorate --all
git log --oneline -20
git log --author="Ada" --since="3 weeks ago" --until=yesterday
git log --grep="clamp" --regexp-ignore-case
git log -p -- src/api/retry.ts          # patches for one path
git log --follow -- src/api/retry.ts    # keep following it through renames
git log --stat --oneline
git log --pretty=format:"%h %ad %an %s" --date=short
git log --merges
git log --no-merges
git shortlog -sne                        # commit counts per author
Range or refMeans
A..BCommits reachable from B but not from A
A...BCommits reachable from either but not from both, which is what a pull request shows
origin/main..HEADYour local commits that have not been pushed
HEAD~3..HEADThe last three commits
HEAD~2Two commits before the current one
--allEvery ref, including other branches and tags
--first-parentFollow only the main line, skipping inside a merged branch
# pickaxe: commits whose diff added or removed this exact string
git log -S "retryDelay" --oneline
git log -G "retry.*Delay" --oneline          # regex over the patch content

# history for a line range rather than the whole file
git log -L 20,40:src/api/retry.ts

# what actually changed, in a readable shape
git diff --word-diff main feature
git diff --stat main HEAD
git diff --name-only HEAD~5
git diff --cached
git diff --ignore-space-at-eol

# exclude generated noise while reviewing a big change
git diff -- . ":(exclude)package-lock.json"
⚠️
A..B and A...B are not the same thing, and git diff A...B compares against the merge base rather than against A directly. Run git merge-base A B when the output looks wrong, because it usually means you asked a different question than you intended.

Blame and bisect

git blame src/api/retry.ts
git blame -L 20,40 src/api/retry.ts
git blame -w -C -C src/api/retry.ts               # ignore whitespace, follow moved code
git blame --ignore-rev 9f3c1ab src/api/retry.ts   # skip a reformat commit
git blame --ignore-revs-file .git-blame-ignore-revs

# binary search for the commit that broke something
git bisect start
git bisect bad                 # the current commit is broken
git bisect good v2.4.0         # this tag was fine
# Git checks out the middle; test it, then say which it was
git bisect good
git bisect bad
git bisect skip                # cannot test this one
git bisect reset

# or let a command decide: exit 0 means good, non-zero means bad
git bisect run npm test
  • blame shows the last commit that touched each line, not the person who had the idea — follow the commit, not the name.
  • Keep a .git-blame-ignore-revs file listing bulk formatting commits, then set blame.ignoreRevsFile once; it makes blame usable again on a reformatted codebase.
  • bisect run needs a command whose exit status means pass or fail. Wrap flaky checks so a timeout is reported as a skip instead of blaming an innocent commit.
  • A bisect over a thousand commits needs about ten tests, so it is usually faster than reading the log.

FAQ

How do I find when a file was deleted?
git log --diff-filter=D --name-only lists deleted files, and git log --all --full-history -- path/to/file finds one that exists on no current branch. Then git show <sha>^:path/to/file prints the last version that existed.
Why does blame show one huge commit for every line?
A formatting or line-ending change rewrote the file. Add that commit to .git-blame-ignore-revs and configure git config blame.ignoreRevsFile .git-blame-ignore-revs, and blame skips it.

Clean history: interactive rebase, squash and amend Installing Git and configuring your environment

Last refreshed 2026-09-18.