git log, diff, show and blame: How to Read Your Project's History in Git
git log lists commits and can filter them by author, date, file and text; git diff compares the working tree, the index and any commits; git show prints a single commit in full; git blame shows which commit last changed each line of a file.
Four commands cover almost every question about a project's history. git log answers "what happened", git diff — "what exactly changed", git show — "what's inside this particular commit", and git blame — "who wrote this line and when". Below are practical techniques for each, with the flags people actually use in day-to-day development.
git log: listing commits
With no arguments, git log lists the commits on the current branch from newest to oldest: the full SHA-1 hash, author, date and message. It opens in a pager (usually less): space for the next page, /text to search, q to quit.
commit 7be41d0c9a2e4f1b8d6c3e5a7f9b0c2d4e6f8a1b
Author: Anna Miller <anna@example.com>
Date: Mon Oct 5 14:22:10 2026 +0300
feat: cache API responses
You don't need to copy the whole hash: Git accepts any unique prefix of 4 or more characters, and in practice 7 is enough.
Compact output: --oneline
git log --oneline -10
# 7be41d0 (HEAD -> main, origin/main) feat: cache API responses
# 1c02e8f fix: request timeout
# 9a4b3c2 refactor: move client into its own module
The -10 flag (or -n 10) limits the number of commits. The parentheses show refs: where HEAD, local and remote branches, and tags point.
Branch graph: git log --graph
When people search for "git graph", this is usually what they mean: a visualization of branches and merges right in the terminal.
git log --oneline --graph --all
# * 3e1f0aa (feature/auth) feat: token login
# | * 7be41d0 (HEAD -> main) feat: cache API responses
# |/
# * 1c02e8f fix: request timeout
# * 5d2c7b1 Merge branch 'feature/search'
# |\
# | * 8f3a9e0 feat: search
# |/
# * 9a4b3c2 refactor: move client into its own module
--all shows all branches, not just the current one. It's worth setting up an alias: git config --global alias.lg "log --oneline --graph --all --decorate", and then git lg is all you need.
Custom format: --pretty=format
git log --pretty=format:"%h %ad %an %s" --date=short
# 7be41d0 2026-10-05 Anna Miller feat: cache API responses
The main placeholders: %h — short hash, %H — full hash, %an — author, %ae — author e-mail, %ad — author date, %ar — relative date ("2 days ago"), %s — subject line.
How to filter history with git log
By author and date
git log --author="Anna"
git log --since="2 weeks ago" --until="2026-10-01"
git log --since=yesterday --oneline
--author takes a regular expression and matches both name and e-mail. Git understands dates in free form as well as ISO.
By commit message
git log --grep="fix" -i --oneline
-i makes the search case-insensitive. If your team puts ticket numbers in commit messages, this quickly finds every commit for a ticket.
By file or folder
git log --oneline -- src/api/client.py
git log --follow -p -- src/api/client.py
The double dash separates paths from branch names. --follow continues a file's history across renames (works for a single file), and -p shows each commit's patch.
By changed code: the "pickaxe" search
git log -S "retry_count" --oneline # commits where the number of occurrences of the string changed
git log -G "timeout\s*=" --oneline # commits whose diff contains lines matching the regex
-S finds the commit where a function or variable was added or removed — indispensable when you need to figure out where a piece of code went.
Commit ranges
git log main..feature # what's in feature but not in main
git log origin/main..HEAD # my local commits that haven't been pushed yet
git log --no-merges # exclude merge commits
git log --stat # which files changed and by how many lines
git diff: what exactly changed
The key thing to understand about git diff is which two states it compares. Confusion here is behind the question "why is my diff empty when I clearly changed things?"
| Command | What it compares |
|---|---|
git diff | working tree vs. index (unstaged changes) |
git diff --staged (--cached) | index vs. last commit (what will go into the commit) |
git diff HEAD | working tree vs. last commit (everything together) |
git diff A B | two commits, branches or tags |
git diff A...B | changes in B since it branched off from A |
Once you run git add, a plain git diff goes empty — the changes are already in the index, so use --staged. New files that have never been added don't show up in diff at all.
How to read diff output
diff --git a/config.py b/config.py
index 3b18e51..a1c4f02 100644
--- a/config.py
+++ b/config.py
@@ -10,7 +10,7 @@ DEFAULTS = {
"host": "localhost",
- "timeout": 5,
+ "timeout": 30,
"retries": 3,
Lines starting with - were removed, lines with + were added, and the rest is context. The @@ -10,7 +10,7 @@ header means the hunk starts at line 10 and spans 7 lines in both the old and the new version.
Useful git diff flags
git diff --stat main feature # per-file summary
git diff --name-only HEAD~3 # only the names of changed files
git diff --word-diff # word-level changes, handy for prose
git diff -w # ignore whitespace changes
git diff main feature -- src/ # limit to a folder
It's a good habit to check git diff --staged before every commit: it's your last chance to spot a debug print or a file you added by accident.
git show: a single commit in full
git show prints a commit's metadata and its diff against the parent. With no arguments it shows the latest commit.
git show 7be41d0
git show HEAD~2 --stat # no patch, summary only
git show v1.2.0 # tag: annotation and commit
git show main:src/config.py # the file as it was in that commit
The last form, <revision>:<path>, is especially useful: you can view an old version of a file without checking it out. Redirect the output to a file and you've recovered deleted content without a checkout.
git blame: who changed this line
git blame file shows, for each line, the last commit that changed it, plus the author and date. The name sounds accusatory, but in practice it's a tool for finding context: locate the commit, read its message, and understand why the code is the way it is.
git blame -L 40,60 src/api/client.py
# 1c02e8f3 (Anna Miller 2026-09-28 11:04:51 +0300 41) timeout = settings.timeout
# 9a4b3c2e (John Smith 2026-09-14 18:30:02 +0300 42) session = build_session()
-L 40,60— only a range of lines;-L :function_nameworks too.-w— ignore whitespace changes so you don't land on formatting commits.-C— track lines moved from other files.--ignore-rev <hash>or a.git-blame-ignore-revsfile — skip bulk commits such as auto-formatting.
A typical investigation: git blame finds the commit, git show shows it in full, and git log on the file gives you the surrounding changes.
Navigating the past: HEAD~ and HEAD^
You don't have to refer to commits by hash. HEAD~1 is the parent of the current commit, HEAD~3 is its great-grandparent. HEAD^2 is the second parent of a merge commit. Branch names, tags and remote branches work too: origin/main~2. To look at the whole project at an old commit, use git switch --detach <hash>, and go back with git switch -.
Good history vs. bad history
All these commands are only as useful as the history is meaningful. Messages like "fix", "wip" or "more changes" make git log --grep and git blame useless. A good commit is one logical change with a subject that explains what and why: fix: raise API timeout to 30s due to slow gateway. Six months later, that's the commit blame will find, and it'll save you an hour.
In practice: tracking down a bug through history
Say that after the latest release, API requests started timing out. Here's how this set of commands gets you to the cause in a couple of minutes.
- See what went into the release:
git log v1.4.0..v1.5.0 --oneline. The list is short, but the culprit isn't obvious from the subjects. - Narrow it down by path:
git log v1.4.0..v1.5.0 --oneline -- src/api/. Two commits remain. - Search the code:
git log -S "timeout" --oneline -- src/api/finds the commit where the timeout value changed. - Look at it in full:
git show 1c02e8f. The diff shows the timeout was cut from 30 to 5 seconds, and the message explains: "speed up UI response". - Check who else touched that line:
git blame -L :build_session src/api/client.py.
From here the fix is an engineering decision: restore the value, move the timeout into settings, or undo the commit with git revert. And if there are hundreds of commits and you can't spot the culprit by eye, git bisect comes to the rescue — a binary search through history that pinpoints the commit that broke the behavior in a few steps.
Settings that make life easier
git config --global log.date iso # date format in log
git config --global diff.colorMoved zebra # highlight moved lines in diff
git config --global core.pager "less -FRX" # don't open the pager for short output
If a script needs to process the output, turn off colors and the pager: git --no-pager log --format="%H %s" --no-color. Use the full %H hash for machine processing: short prefixes can become ambiguous over time in a large repository.
Summary
git log with --oneline, --graph, --author, -S and paths answers "what happened". git diff compares the working tree, the index and commits — the main thing is to remember what it compares by default. git show opens up a single commit or an old version of a file, and git blame leads you from a line of code to the decision behind it. Reading history and navigating commits are covered hands-on in the Git course.
FAQ
How do I exit git log?
Press q — the output opens in the less pager; to print without a pager, use git --no-pager log or the -n flag with a small number.
How do I see the history of a deleted file?
Run git log --oneline -- path/to/file (with the double dash), and get its content before deletion with git show <hash>^:path/to/file.
How do I compare a local branch with the remote one?
Run git fetch first, then git diff origin/main or git log origin/main..HEAD to see your unpushed commits.
Is there a graphical history viewer?
Yes, Git ships with gitk, and a history graph is built into VS Code, JetBrains IDEs and the GitHub and GitLab web interfaces.
Why does git diff show nothing?
Most likely the changes are already staged with git add — use git diff --staged; new untracked files never show up in git diff.
This topic is covered in depth with practice in the course “Git from Scratch to Confident Developer”.
Go to course