git status and git diff: Understanding Changes and Comparisons

Understanding git status output, git diff options, comparing branches, and reading diffs effectively for version control and code review.

published: reading time: 17 min read updated: March 31, 2026

Introduction

git status and git diff are your eyes into Git’s internal state. While other commands modify the repository, these two commands observe and report. They tell you exactly what has changed, where those changes live, and how they differ from the committed versions. Without mastering these commands, you are committing blindly.

git status gives you a high-level overview — which files are modified, staged, or untracked. git diff gives you the line-by-line details — exactly what was added, removed, or changed. Together, they form the foundation of every Git workflow. Every commit should be preceded by these commands, and every code review starts with understanding a diff.

This guide covers every important form of git status and git diff, from basic usage to advanced comparison patterns. Understanding these tools transforms Git from a mysterious black box into a transparent, predictable system. For the conceptual foundation, see The Three States.

When to Use / When Not to Use

Use git status when:

  • Before every commit to verify what will be included
  • After pulling or merging to understand the resulting state
  • When confused about which branch you are on or what has changed
  • After resolving merge conflicts to confirm all conflicts are resolved

Use git diff when:

  • Reviewing changes before staging or committing
  • Understanding what a colleague changed in a pull request
  • Comparing branches to see what will be merged
  • Investigating when a bug was introduced
  • Preparing release notes by reviewing changes since the last tag

Less critical when:

  • Using a GUI Git client that shows changes visually
  • Working on throwaway experimental code
  • You trust your IDE’s Git integration completely

Core Concepts

git status reads the .git/index file (staging area) and compares it against the HEAD commit and the working directory. It reports three categories of files: staged changes (ready to commit), unstaged changes (modified but not staged), and untracked files (new files Git does not know about).

git diff compares two snapshots and outputs the differences in unified diff format. By default, it compares the working directory against the staging area. With flags, it can compare any two commits, branches, or tree objects.


graph LR
    A[HEAD Commit] -->|git diff HEAD| B[Working Directory]
    A -->|git diff --staged| C[Staging Area]
    C -->|git diff| B

The Three Comparison Planes


graph TD
    A[HEAD<br/>Last commit] -->|git diff HEAD<br/>or git diff HEAD --| B[Working Directory<br/>Current files]
    A -->|git diff --staged<br/>or git diff --cached| C[Staging Area<br/>Prepared for commit]
    C -->|git diff<br/>default| B

Architecture or Flow Diagram

git status Output Structure


graph TD
    A[git status] --> B[Branch Info<br/>On branch main]
    A --> C[Staged Changes<br/>Changes to be committed]
    A --> D[Unstaged Changes<br/>Changes not staged for commit]
    A --> E[Untracked Files<br/>Untracked files]

    C --> C1[new file]
    C --> C2[modified]
    C --> C3[deleted]
    C --> C4[renamed]

    D --> D1[modified]
    D --> D2[deleted]

Diff Comparison Matrix


graph LR
    A[Command] --> B[Compares]
    B --> C[Purpose]

    A --> D[git diff]
    D --> E[Working vs Staging]
    E --> F[Review unstaged changes]

    A --> G[git diff --staged]
    G --> H[Staging vs HEAD]
    H --> I[Review what will commit]

    A --> J[git diff HEAD]
    J --> K[Working vs HEAD]
    K --> L[See all changes]

    A --> M[git diff branch1..branch2]
    M --> N[Branch comparison]
    N --> O[Review before merge]

Step-by-Step Guide / Deep Dive

git status Basics


# Standard status output
git status

Output:


On branch main
Your branch is ahead of 'origin/main' by 2 commits.
  (use "git push" to publish your local commits)

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
 modified:   src/app.py
 new file:   src/auth.py

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
 modified:   README.md

Untracked files:
  (use "git add <file>..." to include in what will be committed)
 notes.txt

Short Status Format


git status -s

Output:


M  src/app.py
A  src/auth.py
 M README.md
?? notes.txt

Status codes:

CodeMeaning
??Untracked file
ANew file staged
MModified file staged
DDeleted file (staged)
MModified file (unstaged)
MMModified in both staging and working directory
RRenamed file
CCopied file
!!Ignored file (with -uall)

Ignoring Untracked Files in Status


# Do not show untracked files
git status -uno

# Show only untracked files in current directory (not subdirectories)
git status -unormal

# Show all untracked files including those in subdirectories
git status -uall

git diff Basics


# See unstaged changes (working directory vs staging area)
git diff

# See staged changes (staging area vs HEAD)
git diff --staged
# or
git diff --cached

# See ALL changes (working directory vs HEAD)
git diff HEAD

Understanding Diff Output


diff --git a/src/app.py b/src/app.py
index abc1234..def5678 100644
--- a/src/app.py
+++ b/src/app.py
@@ -10,7 +10,9 @@ def process_data(data):
     result = validate(data)
-    return transform(result)
+    cleaned = clean(result)
+    log.info(f"Processed {len(cleaned)} items")
+    return transform(cleaned)

 def transform(data):

Reading the diff:

  • --- is the old version, +++ is the new version
  • Lines starting with - were removed
  • Lines starting with + were added
  • Lines without prefix are context (unchanged)
  • @@ -10,7 +10,9 @@ means: in the old file, start at line 10, show 7 lines; in the new file, start at line 10, show 9 lines

Comparing Branches


# See what changed in feature compared to main
git diff main..feature

# See what will be merged INTO main FROM feature
git diff feature..main

# See only file names that differ
git diff --name-only main..feature

# See a summary of changes (renames, mode changes)
git diff --stat main..feature

# See diff with word-level highlighting
git diff --word-diff main..feature

Comparing with Specific Commits


# Compare working directory with a specific commit
git diff abc1234

# Compare two commits
git diff abc1234 def5678

# Compare current branch with a tag
git diff v1.0..HEAD

# See what changed between the last two commits
git diff HEAD~1..HEAD

# Compare a file across two commits
git diff abc1234:def5678 -- src/app.py

Filtering Diffs


# Show only changed file names
git diff --name-only

# Show only added/deleted file names
git diff --name-status

# Show diff statistics (files changed, insertions, deletions)
git diff --stat

# Show only changes in specific files
git diff -- src/app.py src/utils.py

# Show only changes in a directory
git diff -- src/

# Ignore whitespace changes
git diff -w
git diff --ignore-space-change

# Ignore all whitespace including blank lines
git diff --ignore-all-space

# Show only the names of changed files with status
git diff --name-status

Color and Format Options


# Enable colored diff output (usually on by default)
git diff --color

# Show diff with function context
git diff --function-context

# Generate a patch file
git diff > changes.patch

# Apply a patch file
git apply changes.patch

# Show diff in raw format (for scripting)
git diff --raw

Production Failure Scenarios + Mitigations

| Scenario | Impact | Mitigation | | ----------------------------------------------- | ------------------------------------------------ | ---------------------------------------------------------------- | ---------------------------------------------- | | Not reviewing git diff --staged before commit | Accidental commit of debug code or wrong changes | Make git diff --staged a mandatory step before every commit | | Misreading diff direction in branch comparison | Merging unexpected changes | Remember: git diff A..B shows what B has that A does not | | Ignoring whitespace-only diffs | Missing real changes among noise | Use git diff -w to filter whitespace noise when reviewing | | Not checking git status after merge | Unresolved conflicts or unintended staged files | Always run git status after merge/rebase to verify clean state | | Diff output truncated in terminal | Missing critical changes at end of large diffs | Use git diff | lessorgit diff > diff.txt for large changes | | Comparing wrong branches | Reviewing irrelevant changes | Verify branch names with git branch before comparing |

Trade-offs

ApproachAdvantagesDisadvantagesWhen to Use
git diff (default)Shows unstaged changes, most common use caseDoes not show staged changesReviewing work in progress
git diff --stagedShows exactly what will be committedDoes not show unstaged changesFinal review before commit
git diff HEADShows all changes at onceCan be overwhelming with many changesQuick overview of everything
git diff --statQuick summary of scopeNo detail about actual changesAssessing impact, release notes
git diff --name-onlyFast, scriptableNo change detailsCI/CD pipelines, automation
git diff -wIgnores whitespace noiseMay hide meaningful whitespace changesReviewing refactored code

Implementation Snippets

The Pre-Commit Review Ritual


# Step 1: Overview
git status -s

# Step 2: Review unstaged changes
git diff

# Step 3: Stage intended changes
git add src/feature.py

# Step 4: Review what will be committed
git diff --staged

# Step 5: See everything together
git diff HEAD

# Step 6: Commit
git commit -m "feat: add feature"

Branch Comparison Before Merge


# What will main gain if we merge feature?
git diff main..feature --stat

# Detailed view of changes
git diff main..feature

# Only the files that changed
git diff --name-only main..feature

# Files added in feature
git diff --diff-filter=A --name-only main..feature

# Files deleted in feature
git diff --diff-filter=D --name-only main..feature

Generating a Patch


# Create a patch from uncommitted changes
git diff > my-changes.patch

# Create a patch from staged changes
git diff --staged > staged-changes.patch

# Create a patch between two commits
git diff abc1234 def5678 > commit-diff.patch

# Apply the patch
git apply my-changes.patch

# Apply with verification
git apply --check my-changes.patch
git apply --stat my-changes.patch

Diff with External Tools


# Use vimdiff
git diff --ext-diff=vimdiff

# Use meld (visual diff tool)
git difftool -t meld

# Use beyond compare
git difftool -t bc3

# Configure default difftool
git config --global diff.tool meld
git config --global difftool.prompt false
git difftool

Observability Checklist

  • Logs: Use git status -s for quick daily checks of repository state
  • Metrics: Track git diff --stat output to measure change velocity per commit
  • Traces: Use git diff --name-status to trace which files were added, modified, or deleted
  • Alerts: Pre-commit hooks should reject commits exceeding file count or line change thresholds
  • Audit: Use git diff tag1..tag2 --stat to audit all changes between releases
  • Health: Periodically run git status to ensure no long-running uncommitted work accumulates
  • Validation: Always run git diff --staged before committing to verify exact contents

Security/Compliance Notes

  • Diff output may contain secrets: Be careful when sharing diff output or posting it in public channels. It may include API keys, passwords, or internal URLs
  • Patches are executable: git apply applies patches without verification. Always review patches with git apply --check before applying
  • Audit trails: git diff between releases provides a complete audit trail of what changed. This is essential for compliance in regulated industries
  • Signed tags for releases: Combine git diff with signed tags to create verifiable release change records
  • Data in diffs: Deleted files still appear in diffs. If you delete a file containing secrets, the secret is still visible in the diff of the commit that deleted it

Common Pitfalls / Anti-Patterns

  • Confusing git diff and git diff --staged: The default git diff shows unstaged changes, not what will be committed. This is the most common source of confusion
  • Misreading branch comparison direction: git diff A..B shows what B has that A does not. Reversing the order reverses the diff
  • Ignoring the --stat output: The diff stat (files changed, insertions, deletions) gives you immediate context about the scope of changes. Skipping it means reviewing blindly
  • Not using git diff HEAD: When you have both staged and unstaged changes, neither git diff nor git diff --staged shows the complete picture. git diff HEAD does
  • Reading diffs without context: A diff shows what changed but not why. Always read the commit message alongside the diff to understand the intent
  • Assuming diff order matches file order: Git sorts diff output by file path, not by the order you staged files. Do not rely on diff order for understanding commit structure

Quick Recap Checklist

  • git status shows branch, staged, unstaged, and untracked files
  • git status -s shows compact single-line status
  • git diff compares working directory vs staging area (unstaged changes)
  • git diff --staged compares staging area vs HEAD (what will be committed)
  • git diff HEAD compares working directory vs HEAD (all changes)
  • git diff branch1..branch2 compares two branches
  • git diff --stat shows summary of changes
  • git diff --name-only shows only changed file names
  • git diff -w ignores whitespace changes
  • Lines with + are additions, lines with - are deletions
  • Always review git diff --staged before committing
  • Use git difftool for visual diff with external tools

Interview Q&A

What is the difference between `git diff`, `git diff --staged`, and `git diff HEAD`?

git diff compares the working directory against the staging area, showing changes you have made but not yet staged. git diff --staged (or --cached) compares the staging area against HEAD, showing what will be included in the next commit. git diff HEAD compares the working directory directly against HEAD, showing all changes regardless of staging state. When you have both staged and unstaged changes, only git diff HEAD shows the complete picture.

How do you read a unified diff output?

A unified diff has several parts. The diff --git line identifies the files being compared. The index line shows blob hashes. The --- line is the old file, +++ is the new file. The @@ hunk header shows line numbers: @@ -old_start,old_count +new_start,new_count @@. Lines starting with - were removed, lines with + were added, and lines without a prefix are context (unchanged lines shown for reference). The function name in the hunk header shows which function the change belongs to.

What does `git diff main..feature` show?

It shows the changes that feature has that main does not — in other words, what would be added to main if you merged feature into it. The syntax A..B means "show me what B has that A does not." A common mistake is reversing the order. If you want to see what main has that feature does not, use git diff feature..main. For a symmetric difference (changes unique to either branch), use git diff main...feature (three dots).

How can you see only the names of changed files without the full diff?

Use git diff --name-only to see just the file paths that have changed. Use git diff --name-status to see file paths with their change type (A for added, M for modified, D for deleted, R for renamed). Use git diff --stat to see a summary with file names and the number of insertions and deletions per file. These options are useful for quick overviews, scripts, and CI/CD pipelines where you need to know what changed without the full diff content.

How git diff Computes Deltas

git diff does not compare files directly. It compares tree objects — Git’s internal representation of directory snapshots. The process:

  1. Resolve both commit references to their tree objects
  2. Walk both trees recursively, matching file paths
  3. For each matching file, compare blob hashes — if identical, skip
  4. For differing files, compute a line-by-line diff using the Myers diff algorithm
  5. Output results in unified diff format

graph LR
    A[Tree Object A<br/>Commit snapshot] --> B[Walk trees]
    C[Tree Object B<br/>Commit snapshot] --> B
    B --> D{Same blob hash?}
    D -->|Yes| E[Skip — no changes]
    D -->|No| F[Myers diff algorithm]
    F --> G[Unified diff output]

When blob hashes match, Git skips content comparison entirely — this is why git diff is fast even on large repositories with mostly unchanged files.

Production Failure: Misleading Diff Output from Whitespace

A code review approves a PR based on git diff output. The diff shows only whitespace changes — tabs converted to spaces, trailing spaces removed, line ending normalization (CRLF to LF). The reviewer assumes the change is harmless formatting.

What was missed: buried among 200 whitespace changes, a single line changed if (user.isAdmin) to if (user.isAdmin || user.isModerator) — a privilege escalation that was not caught because the reviewer skimmed the noisy diff.

Mitigation:


# Ignore whitespace when reviewing formatting-heavy changes
git diff -w          # Ignore all whitespace
git diff --ignore-space-at-eol   # Ignore trailing whitespace only

# Word-level diff to see actual content changes clearly
git diff --word-diff=color

# See only non-whitespace changes
git diff --ignore-all-space --stat

Always review diffs with whitespace filtering as a second pass, especially when the diff is large.

Implementation: Useful Diff Configurations

Add these to your ~/.gitconfig for a better diff experience:


# Always use color in diff output
git config --global color.diff auto

# Show word-level changes inline (not just whole lines)
git config --global diff.colorMoved zebra

# Detect renames and copies
git config --global diff.renames copies

# Show function context in diffs
git config --global diff.funcContext 5

# Use a better diff algorithm
git config --global diff.algorithm histogram

# Show unified context lines (default is 3)
git config --global diff.context 5

Useful diff aliases:


git config --global alias.d 'diff --color-words'
git config --global alias.ds 'diff --stat'
git config --global alias.dn 'diff --name-status'
git config --global alias.dc 'diff --cached'
git config --global alias.dw 'diff -w'

Quick Recap: Pre-Commit Diff Review Checklist

  • Run git status -s for a quick overview of changed files
  • Run git diff to review unstaged changes
  • Run git diff --staged to review exactly what will be committed
  • Run git diff --stat --staged to verify the scope of changes
  • Check for accidental debug code, console.log, or print statements
  • Verify no secrets, API keys, or credentials are in the diff
  • Use git diff -w as a second pass if whitespace changes are noisy
  • Confirm the diff matches your commit message description
  • Check that no unintended files are included (build artifacts, IDE configs)
  • For large diffs, review with git diff --staged | less to avoid terminal truncation

Resources

Category

Related Posts

Git Blame and Annotate: Line-by-Line Code Attribution

Master git blame for line-by-line code attribution, understanding code history, finding when code changed, and using blame effectively for code comprehension.

#git #version-control #git-blame

Pull Requests and Code Review: Git Collaboration Best Practices

Master pull request workflows and code review — writing effective PR descriptions, review best practices, collaboration patterns, and team workflows.

#git #version-control #pull-requests

Centralized vs Distributed VCS: Architecture, Trade-offs, and When to Use Each

Compare centralized (SVN, CVS) vs distributed (Git, Mercurial) version control systems — their architectures, trade-offs, and when to use each approach.

#git #version-control #svn