Free lesson · Everyday Git: Committing and Reading History

Reading history: log, show, diff and blame

You will read history far more often than you write it — understanding an unfamiliar codebase, hunting a regression, working out why a line exists. These are the tools that make history searchable rather than merely present.

git log, usefully

Bare git log is a wall of text. The options are the point:

# The one to memorise
git log --oneline --graph --all --decorate

# Bound the search
git log --since="2 weeks ago" --until="yesterday"
git log --author="priya"
git log -20

# What changed
git log -p                      # with patches
git log --stat                  # files and line counts
git log --name-only             # just file names

# Narrow to a path (-- separates paths from revisions)
git log -- src/auth/
git log -p -- src/auth/session.js

# A range of commits
git log main..feature           # in feature, not in main
git log main...feature          # in either, but not both
Two dots versus three

main..feature means "commits reachable from feature but not main" — what your branch adds, and the right question before opening a pull request. main...feature is the symmetric difference, showing where the two have diverged from each other. In git diff the three-dot form means something subtly different again: diff against the merge base.

The pickaxe: searching history by content

This is the most underused tool in Git, and the one that feels like a superpower the first time it lands.

# Commits where the number of occurrences of this string changed
git log -S "processPayment"

# Commits whose diff contains a line matching this regex
git log -G "TODO.*urgent"

# Combine with a path and show the patches
git log -S "API_KEY" -p -- config/

-S answers "when was this introduced, and when was it removed?". Note that git grep searches only the current tree — it cannot find something that was added and later deleted, which is exactly the case when hunting a leaked secret or a removed feature.

git show

git show                      # HEAD, with its diff
git show a3f9c2e              # a specific commit
git show v1.2.0               # what a tag points at
git show HEAD~3:src/auth.js   # a file as it was, three commits ago

That last form is quietly excellent: it prints an old version of a file to stdout without touching your working directory, so you can diff it, pipe it, or just look.

git blame

git blame src/auth.js
git blame -L 40,60 src/auth.js      # only lines 40-60
git blame -w src/auth.js            # ignore whitespace-only changes
git blame -C src/auth.js            # follow code moved between files
Blame lies after reformatting

A repository-wide Prettier or gofmt run makes blame attribute every line to whoever ran the formatter. The fix is .git-blame-ignore-revs: list the formatting commit hashes in that file, then

git config blame.ignoreRevsFile .git-blame-ignore-revs

GitHub honours the same file automatically. Worth setting up before you do a big reformat, not after.

git bisect

A binary search for the commit that introduced a behaviour. Over 200 commits it takes about eight tests instead of up to 200.

git bisect start
git bisect bad                  # current commit is broken
git bisect good v1.4.0          # this tag was fine

# Git checks out a midpoint. Test it, then:
git bisect good     # or: git bisect bad
# repeat until Git names the first bad commit

git bisect reset                # return to where you started

If you can express "is it broken?" as a script exiting non-zero on failure, automate the whole thing:

git bisect start HEAD v1.4.0
git bisect run ./scripts/reproduce.sh
Where commit hygiene pays off

Bisect is precisely why "one commit, one idea, always builds" matters. If half your commits are broken intermediate states, bisect keeps landing on them and you have to mark them git bisect skip, which blunts the search. A clean history makes bisect nearly magic; a messy one makes it frustrating.

git diff in its useful forms

git diff                        # working dir vs index
git diff --staged               # index vs HEAD — review before committing
git diff HEAD                   # everything uncommitted
git diff main feature           # between two branches
git diff main...feature         # feature's changes since it left main
git diff --stat                 # summary only
git diff --word-diff            # word-level, good for prose
git diff --check                # flag whitespace errors

Aliases worth having

git config --global alias.lg "log --oneline --graph --all --decorate"
git config --global alias.st "status --short --branch"
git config --global alias.last "log -1 --stat"
git config --global alias.unstage "restore --staged"

git lg in particular you will use dozens of times a day.

This is one lesson of 52

Git & GitHub Actions: Version Control and CI/CD in Depth continues from here — 5 lessons are free to read like this one, and the rest come with the course. Enrolled readers also get an AI tutor that has read the lesson they are on.

See the full course