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 ref | Means |
|---|---|
A..B | Commits reachable from B but not from A |
A...B | Commits reachable from either but not from both, which is what a pull request shows |
origin/main..HEAD | Your local commits that have not been pushed |
HEAD~3..HEAD | The last three commits |
HEAD~2 | Two commits before the current one |
--all | Every ref, including other branches and tags |
--first-parent | Follow only the main line, skipping inside a merged branch |
Searching content, not just messages
# 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 testblameshows 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-revsfile listing bulk formatting commits, then setblame.ignoreRevsFileonce; it makes blame usable again on a reformatted codebase. bisect runneeds 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.Related
Clean history: interactive rebase, squash and amend Installing Git and configuring your environment
Last refreshed 2026-09-18.