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.

Working Tree files on disk Index (Stage) after git add HEAD (Commit) .git/objects git add git commit git diff (no flags) git diff --staged git diff HEAD
The three trees git diff compares. No flags = working tree vs index; --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 like HEAD~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.

diff --git a/src/auth.ts b/src/auth.ts index 9a3f2c1..4d8e7b0 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -12,7 +12,9 @@ export function validateToken( import { verify } from 'jsonwebtoken'; import { config } from '../config'; -const TOKEN_EXPIRY = 3600; +const TOKEN_EXPIRY = config.tokenExpiry; +const REFRESH_EXPIRY = config.refreshExpiry; export function validateToken(token: string) { diff header file paths + blob SHAs @@ hunk header -old start,count +new start,count deleted line (−) added lines (+) context line 3 lines by default
Anatomy of a unified diff hunk. The @@ 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

What are you comparing? Working tree vs last commit Staged changes before committing Commits or branches git diff git diff --staged Two commits/tags Branches git diff A B -- file main..feat main...feat Add modifiers to any command above: --no-index any two files on disk -w ignore whitespace --word-diff word-level changes --stat / --name-only summary output
Decision tree: pick the right 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.

Terminal git diff Visual side-by-side vs bash @@ -12,7 +12,9 @@ validateToken( const jwt = require('jsonwebtoken') import { config } from '../config' - const TOKEN_EXPIRY = 3600; + const TOKEN_EXPIRY = cfg.expiry; + const REFRESH_EXPIRY = cfg.refresh; export function validateToken(t) { @@ -28,4 +30,4 @@ checkExpiry( - return token.exp > Date.now(); + return token.exp > Date.now()/1000; BEFORE (a/src/auth.ts) AFTER (b/src/auth.ts) const jwt = require(…) const jwt = require(…) import { config } from … import { config } from … EXPIRY = 3600; EXPIRY = cfg.expiry; ──────────────── REFRESH = cfg.refresh; export function val… export function val… Scroll through all hunks Navigate per file
Terminal diff (left) scrolls all hunks linearly. Visual side-by-side tools (right) align old and new in columns and let you navigate changes individually.

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=html piped 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.

Hybrid CLI + Visual Workflow STAGE 1 --stat File overview + change count STAGE 2 --name-only File list for scripting/triage STAGE 3 — VISUAL difftool or browser Per-file deep review STAGE 4 git add -p Selective hunk staging STAGE 5 --staged Final verify before commit CLI (terminal) Visual tool
The hybrid workflow: use CLI for orientation and final verification; use visual tools for the per-file review stage in the middle.

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.

Get Diff Checker Free →