In the previous lesson we learned to decide precisely what goes into each commit. But there is an earlier question that git status does not answer: what exactly have I changed?
git status tells you which files have changed and which area they are in. It does not tell you which lines. That difference is enormous: committing without having read your own changes is the commonest way of getting a debugging console.log, a test password, a commented-out block of code or a change you thought you had undone into the history.
git diff is the tool that answers the question. In this lesson we will look at its three fundamental forms — and why they produce different results depending on which areas they compare — learn to read the unified diff format line by line (a format that shows up in Git, in code reviews, in patches and across half of computing), and go through the options that make a difficult output readable.
By the end you should have picked up one habit: git diff --staged right before every git commit.
Contents
- The three forms of
git diffand what each one compares git diff: working tree against the staging areagit diff --staged: staging area against the last commitgit diff HEAD: everything that has changed- Reading the unified diff format step by step
- Comparing specific commits
- Limiting the comparison to files or paths
- Options that make the output readable
git difftool: comparing with a visual tool- The habit of reviewing before committing
- The three forms of
git diff and what each one compares
git diff and what each one comparesThe whole apparent mystery of git diff dissolves with one idea: it always compares two of the three areas, and the form you use decides which two.
graph LR
WT["WORKING<br/>TREE"]
IDX["STAGING<br/>AREA"]
REPO["LAST COMMIT<br/>(HEAD)"]
WT ---|"git diff"| IDX
IDX ---|"git diff --staged"| REPO
WT -.-|"git diff HEAD"| REPO
As a table:
| Command | Compares | Answers the question |
|---|---|---|
git diff |
Working tree ↔ Staging area | What have I changed and not yet staged? |
git diff --staged |
Staging area ↔ HEAD |
What is going into the next commit, exactly? |
git diff --cached |
Identical to --staged |
(An older synonym, still working) |
git diff HEAD |
Working tree ↔ HEAD |
What has changed in total since the last commit? |
git diff <sha1> <sha2> |
Two commits | What changed between these two versions? |
Two consequences worth taking on board from the start:
-
A bare
git diffdoes not show staged changes. If you stage everything withgit add -Aand then rungit diff, the output will be empty. It is not that you have changed nothing: it is that there is no difference between your disk and the staging area. This is bewilderment number one withgit diff. -
git diff HEADis the sum of the other two. Staged or not, if it differs from the last commit, it shows up. -
None of the three shows untracked files. A new file you have never added has no "previous version" to compare against, so
git diffignores it completely. That is whatgit statusis for.
git diff: working tree against the staging area
git diff: working tree against the staging areaBack to Ana's repository. She has just tweaked styles.css and has staged nothing yet:
diff --git a/styles.css b/styles.css
index 2c9d4e6..8f1a3b7 100644
--- a/styles.css
+++ b/styles.css
@@ -12,4 +12,5 @@ body {
#list li {
padding: 0.5rem 0;
- border-bottom: 1px solid #ddd;
+ border-bottom: 1px solid #e5e7eb;
+ cursor: pointer;
}Three seconds and she knows what she did: she softened the border colour and added the pointer cursor. No surprises, no debugging leftovers.
Now she stages the change and repeats:
Precisely: the disk and the staging area agree, so there is nothing to show. To see that change she has to ask for the other comparison.
git diff --staged: staging area against the last commit
git diff --staged: staging area against the last commitThis is the most important form of all, because it shows literally what you are about to commit:
diff --git a/styles.css b/styles.css
index 2c9d4e6..8f1a3b7 100644
--- a/styles.css
+++ b/styles.css
@@ -12,4 +12,5 @@ body {
#list li {
padding: 0.5rem 0;
- border-bottom: 1px solid #ddd;
+ border-bottom: 1px solid #e5e7eb;
+ cursor: pointer;
}--cached is an exact and older synonym. --staged was added in Git 1.6 because it read more clearly; use whichever you prefer, though --staged is the plainer of the two.
The MM case seen through diff
This is where git diff proves its worth. Ana carries on working and adds one more line to styles.css after staging:
#list li {
padding: 0.5rem 0;
border-bottom: 1px solid #e5e7eb;
cursor: pointer;
transition: opacity 0.2s;
}Now the three forms give three different answers:
@@ -12,4 +12,5 @@
#list li {
padding: 0.5rem 0;
- border-bottom: 1px solid #ddd;
+ border-bottom: 1px solid #e5e7eb;
+ cursor: pointer;
}@@ -12,4 +12,6 @@
#list li {
padding: 0.5rem 0;
- border-bottom: 1px solid #ddd;
+ border-bottom: 1px solid #e5e7eb;
+ cursor: pointer;
+ transition: opacity 0.2s;
}Three questions, three precise answers. The MM from git status warns you that something odd is going on; git diff tells you exactly what.
git diff HEAD: everything that has changed
git diff HEAD: everything that has changedIt compares your working tree with the last commit, ignoring the staging area. It answers "what have I touched since the last commit?", which is the question you ask yourself coming back from lunch or picking up work in the morning.
There is nothing special about HEAD here: it is simply a reference to a commit, and git diff accepts any of them. These variants are just as valid:
git diff HEAD~1 # against the commit before the last one
git diff HEAD~5 # against the one five commits back
git diff 4e7f2a9 # against a specific commitThe HEAD~n notation and the other ways of referring to a commit are covered in detail in Viewing Commit History.
- Reading the unified diff format step by step
The format git diff produces is called unified diff and has been a computing standard since the eighties. You will see it in Git, in GitHub and GitLab code reviews, in email patches and in the output of dozens of tools. Being able to read it is a transferable skill.
Let us dissect a complete output, line by line. Ana has modified app.js:
diff --git a/app.js b/app.js
index 7b2e8f1..3c9d4a2 100644
--- a/app.js
+++ b/app.js
@@ -28,8 +28,12 @@ function renderList() {
list.innerHTML = '';
for (const task of tasks) {
const li = document.createElement('li');
li.textContent = task.text;
- li.className = 'task';
+ li.className = task.done ? 'task done' : 'task';
+ li.addEventListener('click', function () {
+ task.done = !task.done;
+ renderList();
+ });
list.appendChild(li);
}
}Line 1: the header
It signals the start of a file comparison. a/ is the old version and b/ the new one; they are conventional prefixes, not real directories. In a rename you would see different names on each side.
If the diff covers several files, you will see one of these lines per file: it is the separator that tells you where each block begins.
Line 2: the object identifiers
The abbreviated hashes of the old and new blobs, and the file mode (100644 = a normal file). This connects straight back to The Git Data Model: the diff is not an entity stored in Git, it is something Git computes on the fly by comparing two blobs. If the mode changed — say, on making a file executable — you would see old mode and new mode on separate lines.
Lines 3 and 4: the file markers
--- marks the old version and +++ the new one. That is why, in the body of the diff, - means "it was in the old version" and + means "it is in the new one".
Two special cases clear a lot up:
Line 5: the hunk header (@@)
This is the line that takes most getting used to and the one that carries most information. You read it like this:
Applied to the example:
| Part | Value | Meaning |
|---|---|---|
-28,8 |
old | In the old version this block starts at line 28 and spans 8 lines |
+28,12 |
new | In the new version it starts at line 28 and spans 12 lines |
function renderList() { |
context | The function or section the change sits in |
The numbers tell you everything: 8 lines have become 12, so the block has grown by 4. And sure enough, counting the body: 7 context lines, 1 removed and 5 added. That is how you verify any @@ header:
- Old lines = context + removed → 7 + 1 = 8.
- New lines = context + added → 7 + 5 = 12.
The text at the end is not a line of the file: Git extracts it by searching upwards for the last line that looks like a function or section declaration. It helps you get your bearings when the diff is long. It can be tuned per language with .gitattributes, which we cover in File Attributes with .gitattributes.
When the number of lines is 1, it is left out: @@ -12 +12 @@ means "one line on each side".
The body: the lines of the hunk
Every line in the body begins with a character telling you its nature:
| First character | Means |
|---|---|
(space) |
Context: the line exists identically in both versions |
- |
Removed: it was in the old version, it is gone |
+ |
Added: it was not there before, it is now |
\ |
A special note, almost always \ No newline at end of file |
Applied to our example:
list.innerHTML = ''; ← context
for (const task of tasks) { ← context
const li = document.createElement('li'); ← context
li.textContent = task.text; ← context
- li.className = 'task'; ← REMOVED
+ li.className = task.done ? 'task done' : 'task'; ← ADDED
+ li.addEventListener('click', function () { ← ADDED
+ task.done = !task.done; ← ADDED
+ renderList(); ← ADDED
+ }); ← ADDED
list.appendChild(li); ← contextTwo important observations:
- Git has no notion of "modified lines". A modification is always represented as a removal followed by an addition. That is why the first pair of lines shows up as
-and+even though conceptually it is "the same line, changed". - The context lines are there for a reason. By default Git shows 3 lines of context on each side of the change, so you can place it. You adjust it with
-U<n>:
The detail of \ No newline at end of file
It means that the file does not end with a line break. That is a POSIX convention many tools take for granted, and its absence produces noisy diffs: if somebody adds the trailing break, every nearby line can show up as changed. Setting your editor to always add the final break avoids that noise.
A quick thought exercise
Before moving on, read this hunk and answer: how many lines did the block have before, and how many does it have now?
@@ -45,5 +45,3 @@ function updateCounter() {
const pending = tasks.filter(t => !t.done).length;
- console.log('DEBUG pending:', pending);
- console.log('DEBUG total:', tasks.length);
document.querySelector('#counter').textContent = pending;
}Five before, three after: two debugging lines have been removed. This is exactly the kind of find that justifies reviewing the diff before committing.
- Comparing specific commits
git diff also compares any two points in the history:
It shows everything that changed between those two commits, aggregated into a single diff. It makes no difference how many commits sit in between: it compares the two end states.
Equivalent and much used forms:
# What the last commit introduced
git diff HEAD~1 HEAD
# The changes of the last three commits, together
git diff HEAD~3 HEAD
# From a commit up to the current state of the disk
git diff 1a4c8d6That last one deserves a note: given a single argument, git diff compares that commit with your working tree, not with HEAD. It is the same logic as git diff HEAD.
Seeing what one particular commit introduced
To inspect a single commit, the idiomatic way is:
which shows the metadata (author, date, message) and the diff. That is the subject of the next lesson.
A note about branches
git diff takes branch names exactly as it takes hashes:
There is also a three-dot notation, git diff main...develop, which compares from the point where the two branches parted ways. Since we work on main throughout this module, we will just note it here: it is developed in module 3.
- Limiting the comparison to files or paths
In a change touching fifteen files, reading the whole diff is unmanageable. You narrow it with -- followed by paths:
# One specific file
git diff -- app.js
# Several
git diff -- app.js styles.css
# A whole directory
git diff -- src/
# A pattern
git diff -- "*.css"The double dash -- separates options from paths. It is optional when there is no ambiguity, so git diff app.js works just as well. It becomes mandatory when a file name could be mistaken for a branch or commit name:
Without the --, Git would try to read main as a reference and, if the branch existed, you would get something completely different from what you asked for. Getting into the habit of putting -- before paths is a good one.
It combines with everything above:
- Options that make the output readable
--stat: the summary
app.js | 27 +++++++++++++++++++++----- styles.css | 9 ++++++++- index.html | 3 ++- 3 files changed, 33 insertions(+), 6 deletions(-)
Each line shows the file, the total number of lines affected and a proportional bar of + (additions) and - (deletions). It is the first view worth looking at when facing a large change: it tells you where to look next.
Variants:
git diff --shortstat HEAD~3 HEAD
# → 3 files changed, 33 insertions(+), 6 deletions(-)
git diff --numstat HEAD~3 HEAD
# → 22 5 app.js
# → 8 1 styles.css
# → 2 1 index.html--numstat gives additions, deletions and name separated by tabs: ideal for processing in scripts.
--name-only and --name-status: the files alone
--name-status adds a letter per file: M modified, A added, D deleted, R renamed (the number is the similarity percentage: R100 is a rename with no content change).
These options are very practical chained with other commands:
--word-diff: comparing by words
In a text file (documentation, README, HTML content), changing one word makes the whole line show up as removed and added. It is unreadable:
-The task manager lets you add and list the team's pending tasks.
+The task manager lets you add, list and delete the team's pending tasks.With --word-diff the real change jumps out:
What was removed goes between [- -] and what was added between {+ +}. With colour in the terminal it is clearer still.
Variants:
It is indispensable for reviewing text and very useful in CSS and HTML.
-w: ignoring whitespace
An automatic reformat, a switch of indentation from tabs to spaces or an editor setting can produce a two-hundred-line diff where the real change is two lines. The whitespace family of options solves it:
| Option | Effect |
|---|---|
-w, --ignore-all-space |
Ignores all whitespace, wherever it is |
-b, --ignore-space-change |
Ignores changes in the amount of whitespace, not its appearance or removal |
--ignore-space-at-eol |
Ignores whitespace at the end of a line |
--ignore-blank-lines |
Ignores blank lines added or removed |
A word of warning: -w is for reading, not for deciding. If the file is indentation-sensitive (Python, YAML, a Makefile), hiding the spacing changes may hide a real bug from you. Use it to locate the substantive change and then go back to the full diff.
Other useful options
# Detect moved code and show it in a different colour
git diff --color-moved
# Differences between characters rather than words
git diff --word-diff-regex=.
# Force colour even when the output goes to a file or a pipe
git diff --color=always
# Show binary files as a binary difference too
git diff --binary
# Higher-quality comparison (the patience algorithm)
git diff --patience--color-moved is a little-known gem: when you refactor by moving a block from one place to another, it visually distinguishes "this has moved" from "this is new".
git difftool: comparing with a visual tool
git difftool: comparing with a visual toolFor large diffs, or for people who get on better with a two-column view, Git can hand the job over to an external tool:
It accepts exactly the same arguments as git diff.
Seeing which tools you have available
'git difftool --tool=<tool>' may be set to one of the following: vimdiff vimdiff2 nvimdiff The following tools are valid, but not currently available: araxis bc kdiff3 meld opendiff vscode ...
Setting it up
For Visual Studio Code, which is what Ana uses:
git config --global diff.tool vscode
git config --global difftool.vscode.cmd 'code --wait --diff "$LOCAL" "$REMOTE"'For Meld (cross-platform, free and very clear):
For macOS's opendiff, which suits Bruno:
And one setting almost everybody ends up adding:
Without it, Git asks before opening each file, which is exhausting when the change affects ten of them.
The $LOCAL and $REMOTE variables in that configuration are temporary files Git creates with each version and hands to the tool. All of this is stored in ~/.gitconfig through the mechanism we saw in Configuring Git:
[diff]
tool = vscode
[difftool]
prompt = false
[difftool "vscode"]
cmd = code --wait --diff "$LOCAL" "$REMOTE"Terminal or visual tool?
git diff in the terminal |
git difftool |
|
|---|---|---|
| Speed | Instant | Opens a window per file |
| Small changes | Perfect | Overkill |
| Large refactorings | Hard to follow | Much clearer |
| Use in scripts | Yes | No |
| Works over SSH with no desktop | Yes | No |
The practical recommendation: use git diff as your default tool — it is faster and always there — and keep difftool for the big changes.
Worth a separate mention: there is also
git mergetool, the equivalent for resolving merge conflicts. It is configured along the same lines and we cover it in Resolving Merge Conflicts.
- The habit of reviewing before committing
Everything above condenses into a thirty-second routine worth making automatic:
git status -s # which files are in play?
git diff # what have I left unstaged?
git add -p # stage with judgement
git diff --staged # is this EXACTLY what I want to commit?
git commit -m "..."The genuinely valuable step is the second to last. What it usually catches:
- Debugging leftovers:
console.log,print, breakpoints. - Credentials or test URLs.
- Commented-out blocks of code you meant to delete.
- Accidental changes from the editor's autoformatting in files you were not touching.
- Files that slipped in through a hasty
git add -A. - Changes you thought you had made and had not.
If you would rather not run a separate command, the -v option of git commit includes the diff in the message template:
And to leave it switched on permanently:
It is probably the best effort-to-benefit setting in the whole of Git.
Common Mistakes and Tips
- Running
git diffaftergit addand concluding there are no changes. Empty output means "the disk matches the staging area". What you want isgit diff --staged. - Expecting to see new files in
git diff. An untracked file has no previous version, so it does not appear. That is whatgit statusis for. If you want to see it, stage it first (git add -N filerecords it as empty and makes its contents show up in the diff). - Reading the
@@header backwards. The first number belongs to the old version and the second to the new one. Mixing them up leads to misreading the direction of the change. - Thinking Git stores diffs. It does not: it stores snapshots and computes the diff when you ask for it, as we saw in the data model. That is why you can compare any two commits, however far apart they are.
- Overusing
-w. In indentation-sensitive languages, ignoring whitespace can hide the very bug you are hunting. It is a reading tool, not a final-review one. - Not using
--before an ambiguous path. If you have a file with the same name as a branch,git diff mainwill not do what you think. - Reviewing a 40-file change through the full diff. Start with
--statto see the map, and drop into the detail only where it matters. - Tip: set aliases for whatever you use most:
git config --global alias.d diffandgit config --global alias.ds "diff --staged". - Tip: if the output of
git diffseems to "trap" you in the pager, remember that you move through it with the arrow keys or the space bar and leave withq. To turn it off just once,git --no-pager diff. - Tip:
git diff --statright aftergit add -Ais the quickest way to spot that you have added something you did not mean to.
Exercises
Exercise 1: Three areas, three different diffs
Set up this scenario and reason out your answer before running each command:
mkdir -p ~/practice/diffs && cd ~/practice/diffs
git init
cat > app.js <<'EOF'
const tasks = [];
function addTask(text) {
tasks.push(text);
}
EOF
git add app.js && git commit -m "Initial version"
# Change 1: staged
sed -i 's/tasks.push(text);/tasks.push({ text: text, done: false });/' app.js
git add app.js
# Change 2: NOT staged
echo "" >> app.js
echo "function countTasks() { return tasks.length; }" >> app.js- What will
git status -sshow? - What will
git diffshow? How many+lines will it have? - What will
git diff --stagedshow? - What will
git diff HEADshow? - If you commit now with no further
git add, what will the file in the history contain? - Check your answers by running the lot.
Exercise 2: Reading a diff without running it
Interpret this output and answer the questions:
diff --git a/styles.css b/styles.css
index a1b2c3d..d4e5f6a 100644
--- a/styles.css
+++ b/styles.css
@@ -8,10 +8,8 @@ body {
h1 {
font-size: 1.8rem;
- color: #333;
- margin-bottom: 1rem;
+ color: #1f2937;
}
-.hidden { display: none; }
#counter {
color: #6b7280;
}
diff --git a/config.js b/config.js
new file mode 100644
index 0000000..7a8b9c0
--- /dev/null
+++ b/config.js
@@ -0,0 +1,4 @@
+const API_URL = 'https://api.example.com';
+const TIMEOUT = 5000;
+const DEBUG_KEY = 'test-1234';
+export { API_URL, TIMEOUT, DEBUG_KEY };- How many files does this change affect, and what happens to each one?
- In
styles.css, how many lines have been removed and how many added? How does the total number of lines in the block change? - What exactly does
--- /dev/nullmean in the second file? - What does
index 0000000..7a8b9c0mean? - Is there anything in this diff you should not commit? Explain why, and what you would do.
- Write the command that would show only the per-file summary of this change.
Exercise 3: Finding the real change among the noise
Simulate a file being reindented at the same time as a line of logic changes:
mkdir -p ~/practice/noise && cd ~/practice/noise
git init
cat > app.js <<'EOF'
function calculateTotal(tasks) {
let total = 0;
for (const task of tasks) {
if (!task.done) {
total = total + 1;
}
}
return total;
}
EOF
git add app.js && git commit -m "Initial version"
# Reindent from 4 spaces to 2 AND change the condition
cat > app.js <<'EOF'
function calculateTotal(tasks) {
let total = 0;
for (const task of tasks) {
if (!task.done && !task.archived) {
total = total + 1;
}
}
return total;
}
EOF- Run
git diffand count how many lines show up as changed. - Use the right option to see only the logic change.
- Explain why the results of the two commands are so different.
- What would have been better to do from the outset so that the history stayed readable? Write the correct sequence of commands.
- Run
--word-diffon the same change and comment on whether it adds anything here.
Solutions
Solution to Exercise 1
1. git status -s:
There is a staged version (change 1) and the disk differs from it (change 2).
2. git diff — the unstaged part. It shows change 2 only:
@@ -3,3 +3,5 @@ const tasks = [];
function addTask(text) {
tasks.push({ text: text, done: false });
}
+
+function countTasks() { return tasks.length; }Two + lines: the blank line and the function. The tasks.push({...}) line shows up as context, not as an addition, because the staging area already holds it that way.
3. git diff --staged — what is going in. Change 1 only:
@@ -1,5 +1,5 @@
const tasks = [];
function addTask(text) {
- tasks.push(text);
+ tasks.push({ text: text, done: false });
}4. git diff HEAD — the total, both changes together:
@@ -1,5 +1,7 @@
const tasks = [];
function addTask(text) {
- tasks.push(text);
+ tasks.push({ text: text, done: false });
}
+
+function countTasks() { return tasks.length; }5. Committing with no further git add would bring in change 1 only. The file in the history would have the push with the object, but not the countTasks function, which would still be pending in the working tree.
Checking:
Solution to Exercise 2
1. Two files:
styles.css: modified (there is a--- a/+++ b/pair with the same name and mode100644).config.js: a new file, asnew file mode 100644and--- /dev/nullshow.
2. In styles.css: 3 lines removed (color: #333;, margin-bottom: 1rem; and .hidden { display: none; }) and 1 added (color: #1f2937;).
The count matches the @@ -8,10 +8,8 @@ header: the block goes from 10 to 8 lines, losing 2 (−3 +1 = −2). And you can verify it by counting the hunk's lines by hand:
- Old version = context lines +
-lines → 7 context + 3 removed = 10. - New version = context lines +
+lines → 7 context + 1 added = 8.
This checking exercise is the best way to make sure you have understood the @@ header: the two numbers must always tally with what you see in the body.
3. --- /dev/null says that the old version of the file does not exist: it is a new file. /dev/null is the "null device" of Unix systems, and here it stands for "nothing at all". The mirror case, +++ /dev/null, would mean a deletion.
4. index 0000000..7a8b9c0 are the hashes of the old and new blobs. The left-hand one is all zeros because there is no old blob: consistent with the file being new.
5. Yes, there is a problem: DEBUG_KEY = 'test-1234' in config.js. Test key or not, it is a secret in the code, and committing it leaves it permanently in the history for anybody who clones the repository. Harmless as it may be today, it sets a bad precedent and it is exactly the mechanism by which real keys end up leaking.
What I would do:
# Take the constant out of the file and move it to an environment variable
# or to an ignored local configuration file.
# Then unstage config.js:
git restore --staged config.js
# Edit it, and stage only the legitimate part again:
git add -p config.jsIf config.js should not be versioned at all:
6. The per-file summary:
Solution to Exercise 3
1. The full diff:
@@ -1,9 +1,9 @@
function calculateTotal(tasks) {
- let total = 0;
- for (const task of tasks) {
- if (!task.done) {
- total = total + 1;
- }
- }
- return total;
+ let total = 0;
+ for (const task of tasks) {
+ if (!task.done && !task.archived) {
+ total = total + 1;
+ }
+ }
+ return total;
}Fourteen lines show up as changed (7 removed and 7 added), when the real change in behaviour is just one.
2. Seeing the logic alone:
@@ -1,7 +1,7 @@
function calculateTotal(tasks) {
let total = 0;
for (const task of tasks) {
- if (!task.done) {
+ if (!task.done && !task.archived) {
total = total + 1;
}
}Now there is only one -/+ pair: the condition. The indentation of the other lines is ignored, so the real change stands on its own.
3. Why the difference is so large. Git compares whole lines, character by character. Swapping four spaces for two at the start of a line makes it, as far as Git is concerned, a different line: the old one is removed and the new one added. Since the reformat touches all seven lines of the function body, all seven show up as changed and the logic change is lost among them. -w tells Git to normalise whitespace before comparing, leaving only the real difference.
4. The right approach from the outset: two separate commits. Mixing reformatting with functional changes is a well-known bad practice, because it makes the change impossible to review and ruins git blame for the whole block.
# Commit 1: the reformat ONLY
# (reindent the file, without touching the logic)
git add app.js
git commit -m "Reindent calculateTotal to 2 spaces"
# Commit 2: the behaviour change ONLY
# (add the !task.archived condition)
git add app.js
git commit -m "Exclude archived tasks from the total"The diff of the second commit is two lines and can be reviewed in five seconds. On top of that, if the logic change has to be undone tomorrow, it comes out without dragging the reformat with it.
If the two changes are already mixed on disk, git add -p with the e option lets you separate them, though in this particular case — where each line contains both changes at once — redoing the work in two steps would be more practical.
5. With --word-diff:
@@ -1,9 +1,9 @@
function calculateTotal(tasks) {
let total = 0;
for (const task of tasks) {
if (!task.done[- -]{+ && !task.archived +}) {It helps a good deal: comparing by words rather than by lines all but removes the noise from the reindentation, and the change to the condition is pinpointed surgically. In text changes and in cases like this one, --word-diff and -w complement each other well:
Conclusion
You can now read changes before they enter the project's story. To recap:
git diffalways compares two areas, and the form you use decides which:git diff(disk ↔ staging),git diff --staged(staging ↔HEAD) andgit diff HEAD(disk ↔HEAD, that is, the total).- Empty output from
git diffdoes not mean "I have changed nothing": nearly always it means you have already staged everything. - No
git diffshows untracked files. That is whatgit statusis for. - The unified diff format has a fixed structure: a
diff --githeader, blob hashes,---/+++markers,@@ -a,b +c,d @@hunk headers with their context, and context, removed (-) and added (+) lines. Git does not represent "modified lines": a modification is a removal plus an addition. - Any two commits can be compared (
git diff <sha1> <sha2>) and narrowed by path with-- <path>, using--whenever the name could be ambiguous. - The options change readability radically:
--statfor the overall map,--name-only/--name-statusfor the list,--word-difffor text and-wfor separating the real change from spacing noise. git difftoolhands over to a visual tool and takes the same arguments; it pays off on large changes.- The habit that matters:
git diff --stagedbefore everygit commit, or simplycommit.verbose = true.
With git status you know where you are, with git diff you know what you have changed, and with git add/git commit you decide what gets recorded. One piece of the basic cycle is missing: looking backwards.
A history is only worth as much as your ability to query it. After two hundred commits, how do you find when a function was introduced? Who touched styles.css last week? In which commit did that line you swear you wrote disappear?
In the next lesson, Viewing Commit History, we will look at git log with all its formats and filters, custom formats with --pretty, the "pickaxe" search that finds when a particular piece of text appeared or vanished, git show for inspecting a commit, and the various ways of referring to a commit — HEAD, HEAD~3, HEAD^ — that we have been using in passing and that are worth understanding properly.
Mastering Git: From Beginner to Advanced
Module 1: Introduction to Git
- What Is Git?
- Installing Git
- Basic Git Terminology
- The Git Data Model
- Configuring Git
- Initial Configuration
Module 2: Basic Git Operations
- Creating a Repository
- Cloning a Repository
- The Basic Git Workflow
- Staging and Committing Changes
- Inspecting Changes with git diff
- Viewing Commit History
Module 3: Branching and Merging
- Understanding Branches
- Creating and Switching Branches
- Merging Branches
- Merge Strategies
- Resolving Merge Conflicts
- Branch Management
Module 4: Working with Remote Repositories
- Understanding Remote Repositories
- Adding a Remote Repository
- Authenticating with Remote Repositories
- Fetching and Pulling Changes
- Pushing Changes
- Tracking Branches
Module 5: Advanced Git Operations
Module 6: Git Tools and Techniques
- Using Git Hooks
- Git Bisect
- Git Blame
- Git Log and Aliases
- Git Submodules
- Multiple Working Copies with git worktree
Module 7: Collaboration and Workflow Strategies
- Forks and Pull Requests
- Code Reviews with Git
- The Git Flow Workflow
- GitHub Flow
- Trunk Based Development
- Continuous Integration with Git
Module 8: Git Best Practices and Tips
- Writing Good Commit Messages
- Keeping a Clean History
- Ignoring Files with .gitignore
- File Attributes with .gitattributes
- Security Best Practices
- Performance Tips
Module 9: Troubleshooting and Debugging
- Common Git Problems
- Undoing Changes
- Resolving Divergence with the Remote
- Recovering Lost Commits
- Dealing with Corrupted Repositories
- Advanced Debugging Techniques
