Running git diff between two files is one of the most frequent operations in
any developer's day — and also one of the most misunderstood. Most tutorials stop at the
basic syntax and leave you staring at a wall of @@ markers and +/-
lines wondering what actually changed. This guide goes further: it covers every flag you
actually need, explains how to read unified diff output, and — critically — covers the cases
where git compare two files in the terminal is the wrong tool for the job
and what to use instead. No other guide in this space covers the visual/CLI handoff. We own
that gap here.
What git diff Actually Does
git diff is not a simple file comparison tool. It is a command that computes the
minimal edit distance between two text snapshots and outputs those edits in unified diff
format — the same format that patch, GitHub pull requests, and most code review
systems consume. Understanding what it is comparing depends on which snapshots you give it.
--staged = index vs HEAD; HEAD = all changes combined.Git tracks three distinct trees at any moment:
- Working tree: The actual files on disk as you edit them. Not yet staged, not committed.
- Index (staging area): A snapshot of what will be in the next commit. Files
land here after
git add. - Commits (object database): Permanent snapshots stored in
.git/objects. Referenced by SHA-1 hashes, branch names, tags, and relative refs likeHEAD~1.
git diff with no arguments compares the working tree against the index. Add
--staged and it compares the index against HEAD. Add a commit hash
and it compares that commit against another. Add --no-index and it escapes the
git object database entirely, letting you diff any two paths on disk — including files in
completely different directories or outside any repository. This flexibility is why a single
comparison request can mean five different things depending on context — working tree,
staged, between commits, between branches, or arbitrary paths on disk.
Internally, the git diff command uses the Myers diff algorithm by default (the same algorithm the unix diff command popularized). It produces the shortest edit script that transforms snapshot A into snapshot B. The official git-diff reference documents every flag in detail. The output is unified diff format — headers, hunk markers, and context lines — which we decode in detail in Section 4.
Basic Syntax for Comparing Two Files
The cleanest way to compare two specific files depends on what you are comparing. Here are the five forms you will reach for most often.
Working tree vs last commit (one file)
# Show unstaged changes to a single file
git diff -- src/app.ts
# Equivalent: working tree vs HEAD for that file
git diff HEAD -- src/app.ts Two arbitrary files (no-index mode)
# Compare any two files — even outside a git repository
git diff --no-index file-a.txt file-b.txt
# Useful for comparing a vendor file vs your modified copy
git diff --no-index vendor/original.js src/modified.js
The --no-index flag is under-documented. It instructs git to skip the repository
entirely and run a pure file comparison — you get git's formatting, color support, and all
the flags you know, without needing either file to be tracked. This is the most direct answer
to "how do I git compare two files that aren't in the same commit history."
Two specific commits or refs
# Compare a file between two commits
git diff abc1234 def5678 -- src/config.ts
# Compare a file between two branches
git diff main..feature/login -- src/auth.ts
# Three-dot syntax: compare from divergence point
git diff main...feature/login -- src/auth.ts
When you git diff branches, the two-dot form (main..feature)
compares the exact tips of both refs. The three-dot form (main...feature) finds
the common ancestor of both refs and compares feature's tip against that ancestor — this is
what GitHub uses in pull request diffs and what you want when reviewing what a branch added
independent of changes on main. The Pro Git book branching chapter covers the semantics in
full.
Staged changes only
# See what you've staged (what the next commit will include)
git diff --staged -- src/app.ts
# --cached is an alias for --staged
git diff --cached -- src/app.ts 8 Common Scenarios
These cover the scenarios that come up repeatedly when doing a git diff between two files in real projects. Each one has a concrete command and notes on when to use it.
1. Working tree vs last commit
# All unstaged changes across the repo
git diff
# Narrow to one file
git diff -- path/to/file.ts 2. Staged changes (pre-commit review)
git diff --staged
# or
git diff --cached Run this before every commit. It shows exactly what will land in the object database — no surprises from accidentally staged debug lines.
3. Between two commits
# Two specific SHAs
git diff a1b2c3d e4f5g6h -- src/server.ts
# One commit back vs current HEAD
git diff HEAD~1 HEAD -- src/server.ts
# A range using double-dot notation
git diff HEAD~5..HEAD -- src/ 4. Between two branches
# Branch tips (two-dot)
git diff main..staging -- config/database.yml
# From divergence point (three-dot — preferred for PR review)
git diff main...feature/payments -- src/billing/ 5. Specific paths only
# Limit diff to a subdirectory
git diff HEAD~1 -- src/components/
# Exclude a path using pathspec magic
git diff HEAD~1 -- ':!*.lock' ':!dist/'
# Multiple specific files
git diff main -- src/auth.ts src/users.ts
The ':!*.lock' pathspec exclusion is worth memorizing. Lock file changes are
almost never what you want to review, and they add noise to every diff. The
:(exclude) pathspec (longhand) or ':!' (shorthand) filters them out
without touching .gitignore.
6. Ignore whitespace
# Ignore all whitespace differences (indentation, trailing spaces)
git diff -w -- src/formatter.ts
# Ignore changes in amount of whitespace (not presence)
git diff -b -- src/formatter.ts
# Ignore trailing whitespace only
git diff --ignore-space-at-eol -- src/formatter.ts 7. Summary only (--stat)
# File-level summary: which files changed and by how much
git diff --stat HEAD~1
# Example output:
# src/auth.ts | 24 ++++++--
# src/users.ts | 3 +-
# 2 files changed, 22 insertions(+), 5 deletions(-) 8. File names only (--name-only)
# List only changed file paths — useful for scripting
git diff --name-only HEAD~1
# For machine-readable output (NUL-separated, safe for paths with spaces)
git diff --name-only -z HEAD~1 | xargs -0 ... Reading Diff Output: Headers, Hunks, Context Lines
The output of git diff follows unified diff
format. Once you understand the anatomy, you can read any diff instantly.
@@ line tells you exactly where each change is in both the old and new file.Here is a representative git diff output with every component labeled:
diff --git a/src/auth.ts b/src/auth.ts ← diff header
index 9a3f2c1..4d8e7b0 100644 ← blob SHAs + file mode
--- a/src/auth.ts ← old file path
+++ b/src/auth.ts ← new file path
@@ -12,7 +12,9 @@ export function validateToken( ← hunk header
import { verify } from 'jsonwebtoken'; ← context line (unchanged)
import { config } from '../config'; ← context line
-const TOKEN_EXPIRY = 3600; ← deleted line
+const TOKEN_EXPIRY = config.tokenExpiry; ← added line
+const REFRESH_EXPIRY = config.refreshExpiry; ← added line
export function validateToken(token: string) { ← context line Decoding the @@ hunk header
The @@ -12,7 +12,9 @@ line is the hunk header. The numbers tell you exactly
where in each file this change occurs:
-12,7— in the old file, this hunk starts at line 12 and spans 7 lines+12,9— in the new file, this hunk starts at line 12 and spans 9 lines
The text after @@ (the function name or nearby identifier) is the "function
context" that git adds for readability. Git determines this via language-specific heuristics
or a custom .gitattributes pattern. If the context line is wrong or missing,
you can tune it with --function-context or the
diff.funcname attribute.
Useful output modifiers
# Word-level diff instead of line-level (great for prose and config)
git diff --word-diff -- docs/README.md
# Color-word diff (color only, no [+/-] markers)
git diff --color-words -- src/styles.css
# Show function context around each hunk
git diff --function-context -- src/parser.ts
# Expand context from 3 lines (default) to 10 lines
git diff -U10 -- src/complex.ts --word-diff and --color-words are rarely documented but enormously
useful when reviewing documentation or configuration changes where most of a line is
unchanged. Instead of seeing an entire line red and green, you see only the changed words
highlighted — dramatically easier to scan.
Cheat Sheet: 12 Most Useful git diff Variations
git diff invocation based on what you are comparing, then layer on modifiers.| Command | Use Case | Output Type |
|---|---|---|
git diff | Unstaged changes in working tree vs index | Unified diff, all changed files |
git diff --staged | Staged changes (index vs HEAD) | Unified diff of what will be committed |
git diff HEAD | All changes (staged + unstaged) vs last commit | Unified diff |
git diff HEAD~1 HEAD -- file | One file between two consecutive commits | Unified diff for that file only |
git diff branch1..branch2 | Tip-to-tip comparison across branches | Unified diff of all changes between branch tips |
git diff branch1...branch2 | What branch2 added since diverging from branch1 | Unified diff from common ancestor |
git diff --no-index a b | Compare any two arbitrary files on disk | Unified diff, no git history required |
git diff -w -- file | Ignore all whitespace differences | Unified diff excluding whitespace-only hunks |
git diff --word-diff | Word-level diffs for prose and config | Inline word diff with [{-old-}] / {+new+} markers |
git diff --stat | Summary of which files changed and by how much | File list with +++/--- counts |
git diff --name-only | List only changed file paths (for scripting) | Newline-separated file paths |
git diff -- ':!*.lock' | Exclude lock files or generated directories | Unified diff with specified paths excluded |
When Terminal git diff Hits Its Limits
Every guide to git diff between two files stops at the CLI. That is a problem because there are real, common situations where the terminal is the wrong tool for the job. Recognizing those situations saves time and prevents review errors.
Long files with scattered changes
Terminal diff shows changes linearly — you scroll through the entire output top to bottom. When a 3,000-line file has 15 changes spread across it, you spend most of your time navigating between hunks rather than understanding them. Visual tools show the file in two panes and let you jump between change locations via a minimap or navigation arrows. For files over ~500 lines with non-contiguous changes, the cognitive overhead of terminal diff exceeds the friction of opening a GUI tool.
Binary files
# git diff output for a binary file — not helpful
Binary files a/assets/logo.png and b/assets/logo.png differ Terminal diff tells you binary files differ but nothing about how. Visual tools — Beyond Compare, Kaleidoscope, and the Diff Checker extension — can render images side by side or highlight changed regions in certain binary formats. For everything else, a hex viewer comparison is needed. See our side-by-side diff guide for tools that handle binary content.
Sharing diffs with non-developers
A product manager, legal reviewer, or technical writer asked to review a diff cannot parse
unified diff format. The +/- convention, hunk headers, and line number math are
opaque without training. Options:
- GitHub's pull request diff view — visual, annotatable, but requires the changes to be pushed
- A browser-based diff tool where you paste the two file versions — no git knowledge required
- HTML-formatted diff output via
git diff --word-diff=htmlpiped to a file
Side-by-side review of large hunks
To get side-by-side output, you can use an external diff tool configured with
git difftool or set GIT_EXTERNAL_DIFF=diff -y (the standard
diff command's -y flag produces side-by-side format). However, the
columnar output breaks at terminal width — 80 or 120 characters per column, with wrapping
that destroys alignment. For reviewing a function that was heavily refactored, a visual
side-by-side view that scrolls horizontally rather than wrapping is significantly easier to read.
Reviewing diffs across multiple files at once
A feature branch might touch 20 files. Terminal git diff concatenates all 20
diffs into one long stream. Visual tools present a file tree on the left and let you click
through files individually, mark files as reviewed, and skip binary or auto-generated files.
This structured navigation is not possible in terminal output.
Visual & GUI Alternatives
These tools all integrate with git — either via git difftool, native git
support, or by connecting to your repository directly. Each covers a different point on the
friction/capability spectrum.
VS Code built-in diff viewer
VS Code's diff view is the lowest-friction option for developers already working in the editor. It activates automatically when you click a changed file in the Source Control panel, or you can invoke it from the command line:
# Open two files in VS Code side-by-side diff
code --diff file-a.ts file-b.ts
# Set VS Code as git's difftool
git config --global diff.tool vscode
git config --global difftool.vscode.cmd 'code --wait --diff $LOCAL $REMOTE'
git difftool HEAD~1 -- src/auth.ts For a full walkthrough of VS Code's comparison features, see our how to compare two files in VS Code guide. The built-in viewer handles text files well but does not support binary diff or directory-level navigation as cleanly as dedicated tools.
GitLens (VS Code extension)
GitLens extends VS Code's diff with inline blame, commit history per line, interactive rebase, and a full repository timeline. The "File History" view shows every commit that touched a file and lets you compare any two revisions with a click — no command line needed. For teams already on VS Code, GitLens is the most integrated path to visual git diff.
JetBrains IDEs (IntelliJ, WebStorm, PyCharm)
JetBrains tools have arguably the best built-in diff and merge tooling in the IDE space. The three-way merge editor, the "Compare with Branch" dialog, and the changelist system let you review, annotate, and partially stage changes without leaving the IDE. For IntelliJ family users, there is no reason to reach for an external diff tool.
GitHub web UI
For review workflows tied to pull requests, the GitHub web diff view is the standard. It
supports inline comments, suggestion blocks (direct code edits proposed in a comment), file
tree navigation, and "viewed" status per file. It consumes the same unified diff format that
git diff generates — git pushes the commits, GitHub renders them visually.
Diffchecker.pro (browser extension)
When you need to compare two versions of a file without the overhead of a git commit or a full IDE — pasting two variants of a config, comparing outputs from two branches of logic, or reviewing a change before staging — the Diff Checker extension works directly in the browser. You paste both versions, get a side-by-side highlighted diff immediately, with no uploads and no server. It is the tool to reach for when you are working outside a git context or sharing a diff with someone who does not have git installed.
For Python-based file comparison workflows, see our guide on
how to compare two files in Python using
difflib and filecmp — useful when you need to automate diff
generation as part of a script or CI step.
The Hybrid Workflow: CLI + Visual Together
The most effective developers do not choose between CLI and visual — they use both at different stages of the same workflow.
Stage 1: Orient with the terminal
# Get a high-level view of what changed
git diff --stat HEAD~1
git diff --name-only HEAD~1 These two commands give you a file list and a rough magnitude of change in under a second. They answer "which files are involved and how big is this diff" before you invest time in reviewing individual hunks.
Stage 2: Review specifics visually
Once you know which files matter, open them in your visual tool of choice for per-file
review. For in-IDE review, git difftool HEAD~1 -- src/auth.ts opens the file
directly in VS Code or your configured difftool. For sharing with a non-developer reviewer,
paste the two file versions into a browser diff tool.
Stage 3: Selective staging with the terminal
# Stage specific hunks interactively
git add -p src/auth.ts
# Review staged result before commit
git diff --staged git add -p (interactive patch staging) is the command that bridges visual review
back to the terminal. After reviewing a file visually, you know which hunks you want to
include in this commit and which belong in a later one. The -p flag walks you
through each hunk and asks y/n/s/e — yes, no, split, or edit.
Stage 4: Verify with the terminal
# Final check: exactly what goes into the commit
git diff --staged
# Or with word-level precision on prose
git diff --staged --word-diff The terminal diff is the authoritative source of what will be committed. Visual tools are for comprehension; terminal output is for verification. Combining both eliminates the failure modes of each.
Configuring GIT_EXTERNAL_DIFF
For maximum flexibility, you can set the GIT_EXTERNAL_DIFF environment variable
to any diff binary and git will call it instead of its built-in differ:
# Use difft (difftastic) as the external differ for one command
GIT_EXTERNAL_DIFF=difft git diff HEAD~1
# Or set it permanently via difftool
git config --global diff.tool difftastic
git config --global difftool.difftastic.cmd 'difft "$LOCAL" "$REMOTE"'
Difftastic, delta, and diff-so-fancy are popular drop-in replacements that understand
language syntax and produce output that is significantly more readable than raw unified diff
for code. They work with all the same git diff flags you already know.
For a comparison of Linux-side diff tools including these, see the
Linux diff tool roundup.
For PowerShell-based workflows on Windows, Compare-Object covers many of the
same scenarios — see the PowerShell diff guide for
a full walkthrough.
Troubleshooting & Gotchas
CRLF vs LF — false positives everywhere
On Windows, line endings default to CRLF (\r\n). On Linux and macOS, they are
LF (\n). When a file is checked out on Windows and git is not configured to
normalize endings, git diff reports every line as changed — because every line
has a different ending byte. The fix:
# Check current setting
git config core.autocrlf
# Windows: convert to LF in the repo, restore CRLF on checkout
git config --global core.autocrlf true
# Linux/macOS: reject CRLF in the repo
git config --global core.autocrlf input
# Or add a .gitattributes file to the repo
# .gitattributes
* text=auto BOM (Byte Order Mark) causing spurious diffs
Files saved with a BOM (common with UTF-8 files from Windows editors like Notepad) will show
a leading invisible character difference in every diff. The BOM is the bytes EF BB BF
at the start of the file. To detect it:
# Check for BOM on the first bytes
xxd src/config.ts | head -1
# BOM present: 0000000: efbb bf...
# BOM absent: 0000000: 696d 706f... (normal "impo" for "import")
Remove BOM with most modern editors (VS Code: "Change File Encoding → UTF-8 without BOM"),
or with sed -i '1s/^\xEF\xBB\xBF//' file.ts on Linux/macOS.
"No newline at end of file"
-last line of file
\ No newline at end of file
+last line of file
\ No newline at end of file
This warning appears when a file lacks a trailing newline. POSIX requires text files to end
with a newline; many linters and editors enforce this. The diff is technically correct —
the file differs from POSIX convention — but it is often noise. There is no native git flag
to suppress this message (it is informational, not an error). The real fix is to configure
your editor to add a final newline on save (VS Code: "files.insertFinalNewline": true
in settings).
Binary files showing no meaningful diff
Beyond the "Binary files differ" message, git can be configured to run a custom text converter for specific file types. For example, to diff PDF content:
# .gitattributes
*.pdf diff=pdf
# .git/config or ~/.gitconfig
[diff "pdf"]
textconv = pdftotext -layout -nopgbrk
Similar patterns work for Word documents (docx2txt), SQLite databases
(sqlite3 $1 .dump), and other binary formats. The textconv output is then
diffed as plain text, giving you meaningful hunks instead of the binary placeholder.
Diff showing too much / too little context
# Increase context lines from 3 (default) to 10
git diff -U10
# Show the entire file as context (not useful for review, but useful for patches)
git diff -U99999
# Reduce context to 0 lines (useful for generating minimal patches)
git diff -U0 Frequently Asked Questions
How do I compare two files in git?
Run git diff -- path/to/file-a path/to/file-b to compare two tracked files
in the working tree. To compare across commits, use
git diff commit1 commit2 -- filename. For files outside git history entirely,
git diff --no-index file-a file-b applies git's diff engine to any two paths
on disk without requiring a repository.
How do I git diff between branches?
Use git diff branch1..branch2 to compare the tips of two branches. Use
git diff branch1...branch2 (three dots) to compare from the point where
branch2 diverged from branch1 — the three-dot form is what GitHub uses in pull requests.
To narrow to a specific file: git diff main..feature -- src/app.ts.
How do I run git diff without committing?
git diff (no arguments) shows unstaged changes — working tree vs the index.
git diff --staged shows staged changes — index vs HEAD. Neither
requires a commit. These are the two forms you run most often during active development
to see what has changed before deciding what to commit.
How do I save git diff output to a file?
Redirect with >: git diff > changes.patch. To apply the
patch later: git apply changes.patch. The --output flag also
works: git diff --output=changes.patch HEAD~1. To preserve ANSI color
codes in the saved file, add --color=always.
How do I ignore whitespace in git diff?
git diff -w ignores all whitespace differences, including indentation.
git diff -b ignores changes in the amount of whitespace but not
its presence. git diff --ignore-space-at-eol targets only trailing
whitespace. All three flags work in every context — working tree, staged, between commits,
and --no-index mode.
Compare Files Visually — No Terminal Required
Paste any two file versions into Diff Checker and get an instant side-by-side highlighted diff — additions green, deletions red, unchanged lines collapsed. Runs entirely in your browser: no uploads, no signup, no git required. The right tool when git diff output is hard to share or hard to read.