Why git diff and git diff --staged Show Different Changes

See exactly why one file can show two different diffs by comparing HEAD, the index, and the working tree in a disposable Git lab.

By Network Nuts · Published 2026-10-21

If git diff and git diff --staged seem to disagree about the same file, Git is usually doing exactly what it should. The two commands compare different snapshots: plain git diff compares your working tree to the index, while git diff --staged (same as --cached) compares the index to HEAD according to the official git diff documentation. This lab reproduces that state with one file so you can see why both outputs are correct.

Lab environment

This walkthrough uses the verified lab runtime exactly as captured:

  • Ubuntu 26.04 x86_64
  • 6 vCPU and about 12 GB RAM
  • Git 2.53.0
  • Python 3.14.4
  • A new disposable local Git repository
  • No remote and no credentials

The captured environment also had ansible-playbook [core 2.20.1] installed, but Ansible is not used in this lab.

Prerequisites

You need:

  • Git
  • Python 3
  • A new empty working directory outside any existing Git repository

Safety notes:

  • Run this only in a disposable folder. The script initializes a repository and writes settings.txt plus details.txt.
  • The lab does not contact any remote repository and does not require credentials.
  • The behavior shown here is standard Git snapshot behavior, but the exact object IDs in diff headers can vary between runs.

If you want deeper structured practice after this walkthrough, the GIT Version Control Training course covers the staging area and commit workflow in a more systematic way.

The mental model: three snapshots, not one file

The confusion comes from thinking about “the file” as one thing. Git tracks at least three relevant states here:

  1. HEAD: the version from the last commit.
  2. The index: also called the staging area, which Git uses to prepare the next commit according to git add and gitdatamodel.
  3. The working tree: the file as it exists on disk right now.

That means one path can legitimately have two different kinds of changes at once:

  • a change already staged in the index, and
  • a newer change still unstaged in the working tree.

Git’s own git status documentation describes this as differences between HEAD and the index, and differences between the working tree and the index.

Run the verified lab

Open the linked source folder from the publisher bundle, copy its files into a new empty disposable directory, and run the exact tested command:

python3 run.py

The important part of the runner is that it creates one baseline commit, stages one edit, and then edits the same file again without staging the second edit:

Path('settings.txt').write_text('workers=1\n')
git('add','settings.txt')
git('commit','-qm','Baseline')

Path('settings.txt').write_text('workers=2\n')
git('add','settings.txt')
Path('settings.txt').write_text('workers=3\n')

That sequence matters:

  • workers=1 is committed, so it becomes HEAD.
  • workers=2 is written and staged, so it becomes the index version.
  • workers=3 is written afterward, so it exists only in the working tree.

This matches the official git add behavior: it stages the file contents at the time git add runs, and later edits are not automatically included until you add them again, as described in the git add documentation.

Read the observed status first

The runner printed this verification output:

Recorded git-index command output

Rendered output from actual lab commands. Captured 21 October 2026, 09:01 IST. Open full-size image

This result is the fastest way to confirm the repository state before you even inspect a diff.

The key line is:

MM settings.txt

In Git short status, X is the index state and Y is the working tree state, as documented in git status. So MM means:

  • the file is modified in the index relative to HEAD, and
  • the file is modified again in the working tree relative to the index.

That one line already tells you why two diff commands can disagree: they are not comparing the same pair of snapshots.

The other useful line is:

?? run.py

That simply means run.py is untracked. It is not part of the staged-versus-unstaged confusion; it is just the lab script sitting in the repository.

Inspect the two diffs side by side

The lab also captured the exact staged and unstaged diffs for settings.txt:

Recorded git-index command output

Rendered output from actual lab commands. Captured 21 October 2026, 09:01 IST. Open full-size image

Now the reason becomes concrete.

What git diff --cached showed

The staged diff was:

-workers=1
+workers=2

That is the difference between:

  • HEAD = workers=1
  • index = workers=2

This is why git diff --staged shows what would be included in the next commit. The official git diff documentation says --staged is a synonym for --cached, and that form shows staged changes.

What plain git diff showed

The unstaged diff was:

-workers=2
+workers=3

That is the difference between:

  • index = workers=2
  • working tree = workers=3

So plain git diff is not “wrong” and it is not ignoring the staged change. It is answering a different question: what changed since the last git add for this file?

A simple command map you can memorize

When one file has both staged and unstaged changes, this table is the quickest way to stay oriented:

CommandComparisonPractical question
git diffworking tree vs indexWhat have I changed that is not staged yet?
git diff --stagedindex vs HEADWhat is already prepared for the next commit?
git diff HEADworking tree vs HEADWhat is different from the last commit overall?

These comparisons follow the official git diff documentation.

In this lab, git diff HEAD -- settings.txt would effectively show the overall jump from workers=1 to workers=3, because it compares the working tree to the last commit. That is useful when you want the full picture, but it can hide the boundary between what is already staged and what still needs git add.

Why Git behaves this way

This behavior is not a quirk. It is what makes Git’s staging model useful.

Because the index is a separate snapshot, you can:

  • prepare a clean commit,
  • continue editing the same file,
  • and decide later whether the newer edits belong in the same commit.

That is why the index exists in the first place. Git’s data model uses the index to build the next commit tree, as described in gitdatamodel. If Git always treated the working tree and the staging area as the same thing, you would lose that control.

The practical takeaway during daily work

When you feel that Git is “showing different changes for the same file,” use this order:

  1. Run git status --short.
  2. Check whether you see two-layer state such as MM.
  3. Run git diff to inspect what is still unstaged.
  4. Run git diff --staged to inspect what the next commit would contain.

That sequence is usually faster than staring at one diff and guessing.

A useful rule of thumb is:

  • If you want to review the next commit, use git diff --staged.
  • If you want to review what still needs git add, use plain git diff.

If you discover that you staged the wrong thing, the safest follow-on topic is how to undo that cleanly without damaging shared history. See What to do after reviewing the wrong staged or unstaged changes.

Common misunderstanding to avoid

A common mistaken assumption is: “I edited one file, so there should be one diff.”

In Git, the better assumption is: “A path may differ across multiple snapshots.” Once you adopt that model, the outputs stop looking contradictory:

  • HEAD can contain one version,
  • the index can contain a second,
  • and the working tree can contain a third.

That is exactly what the verified lab proved:

  • HEAD: workers=1
  • Index: workers=2
  • Working tree: workers=3

The file path is the same, but the snapshots are different.

Cleanup

Remove only the disposable folder you created for this lab.

For example, from the parent directory:

rm -rf your-disposable-folder

Because the lab uses no remote and no credentials, cleanup is just local file removal.

Example source files

The supporting code and configuration on GitHub contain the complete example files discussed in this article.

Sources