Reading What Changed
Answer what is different and who changed this line. git diff and --staged, reading a unified diff hunk by hunk, commit references, git show, log filtering, and blame.
Answer what is different and who changed this line. git diff and --staged, reading a unified diff hunk by hunk, commit references, git show, log filtering, and blame.
A discount code that should have taken 10% off was taking 90% off instead. The line responsible had been in the file for four months, and nobody on the team remembered writing it.
Two commands found the answer in under a minute: one that showed which commit last touched that line, and one that showed what that commit was trying to do. The information had been sitting in the repository the whole time. Nobody had asked for it.
By the end of this lesson you can compare any two points in your project, read a diff without squinting, narrow a long history down to the commits that matter, and trace a suspicious line back to the commit that wrote it.
A diff is a description of the difference between two
versions of your files. The command is git diff, and what
trips up almost everyone is that with no arguments it compares
one very specific pair of things.
git diffThat compares your working directory — the files as they sit
on disk right now — against the staging area, the changes
you marked with git add. Picture staging as a parcel you are
packing: plain git diff shows only what is still lying on the
desk. The moment you git add something it moves into the
parcel and vanishes from the output. Nothing is lost; you are
asking the wrong question.
To see what is inside the parcel — what you are about to commit — add the flag:
git diff --staged # what is staged, vs the last commit
git diff --cached # identical; older name for the same thing--cached is the original spelling and still works everywhere;
--staged was added later to match the word everyone actually
uses. Pick one and stay consistent.
To see everything since your last commit, staged or not, compare against the commit itself:
git diff HEADRun that right before committing, when you want a final look at the whole change.
Three commands, three different questions. Almost every "why is
git diff empty?" moment is one of these asked in place of
another.
What is still on the desk.
Working directory against the staging area.
Goes empty as you stage things. That is correct behaviour, not a lost change.
What is inside the parcel.
Staging area against the last commit.
This is the exact content of the commit you are about to make.
Everything, either way.
Working directory against the last commit, staged or not.
The one to run when you want a final look before committing.
Git prints diffs in a format called unified diff. It looks cryptic for about five minutes and then it never does again. Here is a real one:
Take it a piece at a time.
diff --git a/src/pricing.js b/src/pricing.js is the header for
one file. Git calls the old version a/ and the new version
b/; when a file is renamed the two paths differ, but usually
they are the same file twice.
index 7c9f0a1..b2e4d33 100644 is internal bookkeeping: the
abbreviated IDs of the old and new contents, plus the file mode
(100644 means an ordinary non-executable file). Ignore it.
--- a/src/pricing.js and +++ b/src/pricing.js label the two
sides. Three minus signs mark the old version, three plus
signs mark the new one — which is why removed lines below
start with - and added lines start with +.
Then comes the hunk header, @@ -12,7 +12,9 @@. A hunk is
one contiguous region of change. It says: on the old side this
region begins at line 12 and covers 7 lines; on the new side it
begins at line 12 and covers 9 lines. Seven became nine, so the
file grew by two. The trailing function subtotal(items) { is
the nearest enclosing declaration, printed as a signpost so you
know where in the file you are standing.
Below the header, every line carries a one-character marker in column one:
- means the line was removed from the old version.+ means the line was added in the new version.A modified line always appears as a - followed by a +. Git
has no notion of "edited" — only removed and added.
Press q to leave the pager when you have read enough.
Sometimes you do not want the lines, you want the shape of the
change. --stat gives you a summary:
Each row is a file, a count of touched lines, and a bar showing
the balance of additions and deletions. If you meant to fix one
typo and --stat reports nine files, stop and look.
Unified diff is line-based, which makes it clumsy for prose. Fix one word and Git reports the whole paragraph as deleted and re-added. For Markdown, documentation, or anything written in sentences, ask for word granularity:
Removed words appear in [-brackets-] and added words in
{+braces+}, so a one-word fix reads as a one-word fix.
To compare two points in history you need a way to name them.
Every commit has a unique hash — a long hexadecimal string
like b2e4d33f9c… — and Git accepts the first seven or so
characters as long as they are unambiguous.
Typing hashes is tedious, so Git gives you relative names.
HEAD is a bookmark pointing at the commit you have checked
out, almost always the newest on your branch. From there you
walk backwards:
HEAD~1 — one commit before HEAD, the parent.HEAD~2 — two commits back, the grandparent.HEAD^ — also the parent, identical to HEAD~1.The difference between ~ and ^ only shows up at a merge
commit, which has two parents: HEAD^2 means "the second
parent", while HEAD~2 means "walk back two generations, always
taking the first parent". Day to day, HEAD~n is the one you
want.
Now you can compare anything to anything:
Order matters: the first argument is the "old" side. Swap them
and every + becomes a -.
When you want everything about one commit — author, date,
message, and full diff — reach for git show:
Its most underused trick is printing a file exactly as it was at some point in history:
The syntax is <commit>:<path>, with the path relative to the
repository root. This is purely a read — nothing on disk
changes, so you can inspect an old version while your current
edits stay exactly where they are.
git log --oneline is where most people stop, and the log gets
far more useful once you narrow it. Every one of these flags
combines with the others:
--graph --all is the one to remember when a repository feels
confusing: it draws the branch lines as ASCII art, so you can
see which commits happened where.
To scope the log to a single file, put the path last, behind a double dash:
The -- is a separator meaning "everything after this is a
path, not a revision". Git needs it because branch names and
file names share a namespace: with a branch called docs and
a folder called docs, git log docs is genuinely ambiguous
while git log -- docs is not. Use the separator whenever you
pass a path and you will never meet that error.
--pretty=format: lets you build your own log line out of
placeholders. Each starts with %:
%h is the abbreviated hash, %ad the author date, %an the
author name, and %s the subject line. --date=short trims the
timestamp to a plain date.
Nobody wants to type that twice. Save it as an alias, a custom command Git stores in your config:
From now on git hist works in every repository on your
machine, and git hist -n 20 -- src/ still accepts extra
arguments on the end.
Diffs tell you what changed between two points. git blame
answers a different question: for every line in a file as it
stands right now, which commit last touched it?
Each row is one line of the file, prefixed with the commit hash,
the author, that commit's timestamp, and the line number. Feed a
hash you find here to git show to read the message and see the
change in context. That pair — blame to find the commit, show to
understand it — is the everyday workflow.
Blaming a large file is noisy, so limit the range:
Now the honest caveat. Blame reports the last commit to touch each line, which is not always the meaningful one. If someone ran a formatter or renamed a variable across the whole file, their commit sits on every line and buries the real history.
That is why the investigation is three steps rather than one, and why the third step exists.
git blame — which commit last touched it?
Use -L 10,20 to limit it to the lines you care about.
Copy the hash from the row you are interested in.
git show — what was that commit doing?
The message, the author, and the full diff. Usually this is where the answer is.
git log -S — when the blame commit is noise
If step one pointed at "reformat" or "lint", it is telling
you who moved the line, not who wrote it. Search the changes
themselves instead: git log -S "discountRules".
Every command from this lesson in one place:
You can now interrogate a repository instead of guessing at it, which is most of what "knowing Git" actually means. The natural next step is choosing what Git tracks: diffs get much quieter once your history holds only the files that belong in it, and the next lesson covers keeping build output, dependencies, and local clutter out of the way so that every diff you read is signal.
Before you move on, spend ten minutes in a repository you did
not write. Pick a file, run git log -p on it, follow the
changes backwards, and blame a line that looks strange. Reading
other people's history is the fastest way to make these commands
stick.
src/cart.js | 12 ++++++------
src/pricing.js | 4 +++-
tests/pricing.test.js | 31 +++++++++++++++++++++++
3 files changed, 42 insertions(+), 8 deletions(-)b2e4d33 2026-03-14 Marta Reyes Use discount rule table
7c9f0a1 2026-03-13 Sam Okafor Add tests for subtotal7c9f0a1 (Sam Okafor 2026-03-13 09:12:41 +0000 12) }
b2e4d33 (Marta Reyes 2026-03-14 16:04:02 +0000 14) function app
b2e4d33 (Marta Reyes 2026-03-14 16:04:02 +0000 15) const rulegit diff --statgit diff --word-diff README.mdgit diff HEAD~1 HEAD # what the last commit changed
git diff HEAD~2 HEAD # the last two commits combined
git diff 7c9f0a1 b2e4d33 # two specific commits
git diff main new-pricing # two branchesgit show b2e4d33
git show HEAD~3git show HEAD~5:src/pricing.jsgit log --oneline # one commit per line
git log --graph --all # draw branch structure, all branches
git log --stat # which files each commit touched
git log -p # full diff for every commit
git log -n 5 # only the five most recent
git log --since="2 weeks ago" # time-bounded
git log --until="2026-01-01" # up to a date
git log --author="Marta" # by one person (matches substrings)
git log --grep="timeout" # search commit messagesgit log --oneline -- src/pricing.js
git log -p -- README.mdgit log --pretty=format:"%h %ad %an %s" --date=short -n 10git config --global alias.hist \
"log --pretty=format:'%h %ad %an %s' --date=short --graph"git blame src/pricing.jsgit blame -L 10,20 src/pricing.js # only lines 10 through 20git diff # working tree vs staging area
git diff --staged # staged vs last commit (= --cached)
git diff HEAD # everything since the last commit
git diff --stat # summary of files and line counts
git diff --word-diff # word-level diff, good for prose
git diff HEAD~2 HEAD # compare two points in history
git diff main new-pricing # compare two branches
git show <commit> # message plus full diff of a commit
git show <commit>:<path> # a file exactly as it was then
git log --oneline # compact one-line history
git log --graph --all # draw branch structure
git log --stat # files touched per commit
git log -p # full diff per commit
git log -n 5 # limit to five commits
git log --since="2 weeks" # time-bounded (also --until)
git log --author="Marta" # commits by one person
git log --grep="timeout" # search commit messages
git log -- src/pricing.js # history of one path
git log -S "discountRules" # commits adding/removing a string
git blame <file> # last commit to touch each line
git blame -L 10,20 <file> # blame only a range of lines
git config --global alias.hist \
"log --pretty=format:'%h %ad %an %s' --date=short --graph"diff --git a/src/pricing.js b/src/pricing.js
index 7c9f0a1..b2e4d33 100644
--- a/src/pricing.js
+++ b/src/pricing.js
@@ -12,7 +12,9 @@ function subtotal(items) {
}
function applyDiscount(total, code) {
- return code === "WELCOME" ? total * 0.9 : total;
+ const rule = discountRules[code];
+ if (!rule) return total;
+ return total * (1 - rule.percent / 100);
}
module.exports = { subtotal, applyDiscount };