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: 24 min read author: Geek Workbench 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

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

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

| 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-off Analysis

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 Considerations

  • 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

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

Interview Questions

1. 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.

2. 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.

3. 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).

4. 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.

5. What do the short status codes in `git status -s` mean?

The first column shows the staging area status: A (added), M (modified), D (deleted), R (renamed), C (copied). The second column shows the working directory status. ?? means untracked, !! means ignored. MM means the file was modified in both staging and working directory. For example, M (space then M) means modified in working directory only, while M means staged modification.

6. What is the three-dot syntax in git diff and when should you use it?

The three-dot syntax git diff A...B produces a symmetric difference — it shows changes that exist in either branch but not both, essentially what each branch uniquely contributed since they diverged. In contrast, A..B (two dots) shows all changes B has that A does not have, as if A was fully included. Use three dots when comparing branches to understand what each independently introduced; use two dots when preparing a merge to see exactly what would be merged in.

7. How does `git diff` internally compute differences between files?

Git resolves both commit references to tree objects, then recursively walks both trees matching file paths by name. For each matching path, it compares the blob hashes — if identical, the file is skipped entirely. For files with different hashes, Git runs the Myers diff algorithm to compute line-by-line differences, then outputs results in unified diff format. This approach is why Git can diff large repositories quickly: identical files are skipped at the hash level without content comparison.

8. What flags can you use to ignore whitespace in diffs?

git diff -w ignores all whitespace. git diff --ignore-space-change ignores trailing whitespace and line-ending differences. git diff --ignore-all-space ignores all whitespace including internal spaces. git diff --ignore-space-at-eol ignores only end-of-line whitespace. These are essential when reviewing refactored code or changes from auto-formatters where whitespace noise obscures actual content changes.

9. What is the difference between `git diff` and `git diff --raw`?

git diff produces human-readable output with context lines, hunk headers, and +/- prefixes. git diff --raw produces machine-readable output showing only the mode/type changes and blob hashes without actual content diffs. Raw format is designed for scripting and hooks — you get the file names and change types but not the actual line-by-line content. git diff --raw is what drives many Git hooks and automation pipelines.

10. How can external diff tools be configured with git difftool?

Configure with git config --global diff.tool [name] and optionally git config --global difftool.prompt false to disable the confirmation prompt. Common setups: meld for a visual merge tool, vimdiff for terminal-based diffing, bc3 for Beyond Compare. Invoke with git difftool instead of git diff. You can also set git config --global difftool.trustExitCode true to have Git interpret the external tool's exit code properly.

11. Why might `git status` show a file as modified even after running `git add`?

This happens when a file has been modified in both the staging area and the working directory after staging. The index holds one version (staged), HEAD holds another, and the working directory holds a third. This commonly occurs during merge conflicts or when an editor auto-formats a file after you have staged it. Use git diff --staged to see what is staged, git diff to see unstaged changes, and git diff HEAD to see everything together.

12. What does `git diff --stat` show and why is it useful?

git diff --stat shows a summary of changes: the number of files changed plus total insertions and deletions, per file. It is useful for quickly assessing the scope of changes — for example, when reviewing a pull request to determine if it is a minor fix or a major refactor. It does not show the actual content changes, making it ideal for release note generation and change velocity tracking.

13. How do you diff a specific file across two commits?

Use git diff commit1 commit2 -- path/to/file or the shorthand git diff commit1:path/to/file commit2:path/to/file. The colon syntax lets you diff a file as it existed in two different tree objects. This is useful when investigating when a specific bug was introduced or comparing how a file looked at different points in history.

14. What are the risks of sharing `git diff` output publicly?

Diff output can inadvertently expose secrets: API keys, passwords, internal URLs, and tokens often appear in diffs even when developers intend to remove them. Deleted files are still visible in the diff of the commit that removed them — if you delete a file containing secrets, the secret remains visible in history. Additionally, patches applied with git apply can execute arbitrary code if the patch contains malicious modifications. Always review diffs before sharing or posting publicly.

15. How can you audit all changes between two release tags?

Use git diff tag1..tag2 to see all changes between releases. Add --stat for a summary overview, --name-only for just file names, or --name-status for file names with change types. For compliance audits, combine with signed tags: git diff v1.0..v1.1 --stat gives you a verifiable record of what changed between releases, which is essential in regulated industries.

16. What does the `--diff-filter` option do in git diff?

The --diff-filter=[A|C|D|M|R|T|U|X] option limits the diff to specific change types: A (added), C (copied), D (deleted), M (modified), R (renamed), T (type changed), U (unmerged), X (unknown). For example, git diff --diff-filter=A --name-only main..feature shows only files added in feature relative to main. Combine with other options like --stat to get filtered summaries.

17. Why is it important to run `git diff --staged` before every commit?

Because git diff only shows unstaged changes. If you rely only on the unstaged diff and skip git diff --staged, you are committing blind — you will not see what is actually staged and ready to commit. Debug code, accidental changes, or even wrong files can slip through. Making git diff --staged a mandatory pre-commit step is the most effective mitigation against unintended commits.

18. How does `git diff --color-words` differ from standard word-level diff?

git diff --word-diff marks changed words with [ and ] delimiters inline. git diff --color-words (often used as git diff --color-words or via alias git d) highlights changed portions with color rather than brackets, making it easier to scan visually. Both show intra-line word differences rather than whole-line differences, which is useful when reviewing refactored code where lines shifted but content mostly stayed the same.

19. What information does the `index` line in a diff provide?

The index line shows two blob hashes separated by ..: the old file hash and the new file hash. These are SHA-1 (or SHA-256 in newer Git) hashes of the file content at that stage. The first hash is the blob in the parent commit (or staging area for staged diffs), the second is the blob in the current version. You can use these hashes directly with git show <hash> to inspect the actual blob content at any stage.

20. What is the practical difference between `git diff` and `git diff HEAD` when you have staged changes?

When you have both staged and unstaged changes, git diff shows only the unstaged portion — what you modified after staging. git diff --staged shows only what you staged. Only git diff HEAD reveals the complete picture: all modifications regardless of staging state. Many developers mistakenly think they have seen everything with git diff alone, only to discover after commit that staged changes they forgot about were included.

Further Reading

Conclusion

Mastering their output is the key to committing with confidence. Before every git add, run git diff. Before every git commit, run git status and git diff --staged. Make these commands a reflex, and you will never commit blindly again.

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