Git and GitHubgit 2.54 · GitHub · macOS, Linux or Windows
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
26 small wins to finish your pathNext lesson →

Seeing what changed with git diff

git diff is the command that prints the lines that changed, comparing the files in your folder with what you have staged for the next commit.

Last updated: 12 Oct, 2026 · git 2.54

Status names the files that changed. Diff shows what changed inside them. Running it before every commit is the habit that keeps a debugging line, a hard-coded key or a half-finished edit out of your history.

What each form of git diff compares. git diff with no arguments compares the working tree with the staging area. git diff --staged compares the staging area with the last commit. git diff HEAD compares the working tree with the last commit, so it covers both steps at once.

Syntax: the command takes no arguments for the common case, and a flag to move the comparison back a step.

bash
git diff            # the folder against the staging area
git diff --staged   # the staging area against the last commit
git diff HEAD       # the folder against the last commit
Project files used on this pageThis lesson builds on a project from earlier lessons. The code below imports this file. Click a file to see its code, or follow the link to the lesson that wrote it. To run the code yourself, keep it in the same folder.
View the code here
.gitconfig
[user]
	name = Reader
	email = reader@example.com

Reading one change

The repository below has one commit holding a prompt and a loader. Those first five lines are the throwaway repository for the rest of this page, and every block after this one starts from them. This one changes the prompt and asks what is different.

ExampleOne edited line, as git diff prints it
mkdir ask-docs && cd ask-docs && git init -q -b main
export GIT_AUTHOR_DATE="2026-10-12T10:00:00+00:00" GIT_COMMITTER_DATE="2026-10-12T10:00:00+00:00"
printf 'You are a support assistant.\n' > prompt.txt
printf 'chunk_size = 500\n' > loader.py
git add . && git commit -q -m "Add the prompt and the loader"
printf 'You are a support assistant. Answer only from the documents.\n' > prompt.txt
git diff
  • diff --git a/prompt.txt b/prompt.txt names the file twice: a/ is the old side, the staging area, and b/ is the new side, your folder.
  • index 900f445..6809cab 100644 names the stored content of each side and the file's permissions. You can ignore this line.
  • --- a/prompt.txt and +++ b/prompt.txt label the two sides, so a line marked - comes from the old one and a line marked + from the new one.
  • @@ -1 +1 @@ is the hunk header: it says this block of lines starts at line 1 on both sides. A bigger change has several hunks, each with its own header.
  • The - line and the + line are the change. Git has no idea you edited one word; it stores whole lines, so a changed line is a removal and an addition.

After git add, the change moves to the other side

git diff compares the folder with the staging area. Once you stage the change the two agree, and the answer is empty, which surprises everyone the first time.

ExampleAfter staging, git diff has nothing left to report
git add prompt.txt
echo '--- git diff ---'
git diff

The marker line printed and nothing came after it. The folder and the staging area hold the same content, so there is no difference to show. --staged asks the other question: what is in the staging area that the last commit does not have.

ExampleThe staged change against the last commit
git diff --staged

The change is printed, because the staging area differs from the last commit. --cached is an older name for the same flag.

ExampleThe folder against the last commit
git diff HEAD

The same change again. git diff HEAD compares the folder with the last commit and ignores the staging area in between, so staging makes no difference to it.

Smaller answers for bigger changes

A diff across twenty files is unreadable. Two flags cut it down, and two commits can be compared the same way as two states of a file.

ExampleTwo changed files, counted
printf 'chunk_size = 800\n' > loader.py
printf 'You are a support assistant. Answer only from the documents.\n' > prompt.txt
git diff --stat

One line per file with the number of changed lines, then a total across the pair. --name-only cuts the answer down to the names.

ExampleThe names of the changed files
git diff --name-only

Names and nothing more, which is what a script wants. A path after two dashes narrows the diff to one file.

ExampleOne file on its own
git diff -- loader.py

git diff <commit> <commit> compares two points in the history the same way, so git diff HEAD~1 HEAD is the change the last commit made, and git diff main experiment is the difference between two branches.

Why a notebook diffs badly

A Jupyter notebook is not what Jupyter shows you. The .ipynb file is JSON, and the JSON holds the code, the output of every cell and an execution count that goes up each time you run one. Change a single number and the diff reports the code line, the stored output and the counter.

ExampleA one-cell notebook, committed
cat > explore.ipynb <<'EOF'
{"cells": [{"cell_type": "code", "execution_count": 1,
  "outputs": [{"output_type": "stream", "text": ["chunks: 12\n"]}],
  "source": ["chunk_size = 500\n", "print('chunks:', 6000 // chunk_size)"]}],
 "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
EOF
git add . && git commit -m "Add the exploration notebook"

Four lines of JSON hold the whole notebook. source is the code, outputs is what the cell printed, and execution_count is the number Jupyter shows next to the cell. Raising the chunk size changes all three.

ExampleOne changed number in a notebook, as Git sees it
cat > explore.ipynb <<'EOF'
{"cells": [{"cell_type": "code", "execution_count": 7,
  "outputs": [{"output_type": "stream", "text": ["chunks: 7\n"]}],
  "source": ["chunk_size = 800\n", "print('chunks:', 6000 // chunk_size)"]}],
 "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
EOF
git diff -- explore.ipynb

Three changed lines for one changed number, and only one of them is code. A real notebook is worse: a chart cell stores its image as thousands of characters of base64 text, so one re-run produces a diff nobody can read and two people editing the same notebook conflict on every cell. Two habits deal with it:

  • Clear the outputs before committing. Cell, then All Output, then Clear in Jupyter, or the nbstripout tool run for you at commit time. The diff then shows the code only.
  • Keep the real code in a .py file. The notebook stays for exploring, and the functions that matter move into loader.py where a diff is one line, a review is possible and a test can import them.

The three comparisons side by side

CommandOld sideNew sideAnswers
git diffstaging areayour folderwhat have I not staged yet
git diff --stagedlast commitstaging areawhat is about to be committed
git diff HEADlast commityour foldereverything I changed since the commit
git diff A Bcommit or branch Acommit or branch Bhow two points differ

Where you run git diff

  • Before every commit. git diff --staged is the last chance to see what you are recording.
  • When a change stopped working. git diff HEAD lists every edit you made since the last good commit.
  • Reviewing someone else's branch. git diff main their-branch is the same view a pull request shows.
Watch out. git diff says nothing about untracked files. A brand new file has no old side to compare with, so it appears in git status and never in a diff until you add it. If a diff looks empty when you expect a change, check status first.
Try it yourself
  • Add three lines to loader.py in three different places and read how many hunks the diff has.
  • Run git diff --word-diff after changing one word in the prompt. Git marks the words instead of the whole lines.
  • Create a new file, run git diff, then git add -N newfile and run it again. The second form tells Git the file exists without staging its content.

You understood something today that you didn't yesterday.