Reviewing edits before a commit and comparing branches before a merge are what git diff gets used for most. It prints line-level differences between two states of a repository (the working tree, the index, two commits) and never modifies any of them.
The output is a unified diff with some Git-specific header lines added. As of Git 2.56, the synopsis in the manual lists six command forms, and one of them, –no-index, compares two paths on disk even outside a repository.
What Is git diff?

Read-only, mostly. Give it two states and it prints one patch-style text stream. It won’t stage a change, record anything in history or touch a byte in your files.
Because Git’s version control model stores snapshots of the project, a diff gets computed on request from two of them.
The output follows the unified diff format. According to the GNU diffutils manual, only GNU diff could produce that format in the early 1990s, and only GNU patch could apply it.
Git extends the traditional format with its own header lines. They carry blob hashes and file modes. Rename data goes in there too.
What Can git diff Compare?
Any pairing of the working tree, the index and commits works. Which pair you get depends on the command form you type.
| Command | Left side | Right side | What it shows |
|---|---|---|---|
| git diff | Index | Working tree | Unstaged changes |
| git diff –staged | HEAD | Index | What the next commit will contain |
| git diff HEAD | HEAD | Working tree | Tracked changes, staged or not |
| git diff A B | Commit A | Commit B | Everything that changed between them |
Staged and unstaged changes

Plain git diff only shows what you could still add.
It compares the working tree against the staging area, so a file you staged and haven’t touched since drops out of the output. That confuses people the first few times. You edit, you stage, you run git diff, and it prints nothing.
Once you stage an edit with git add, it moves over to the –staged view. –cached is a synonym for it. On an unborn branch with no HEAD yet, –staged lists every staged change.
Commits and branches

git diff abc1234 def5678 compares those two snapshots. git diff HEAD^ HEAD sets the version before the last commit against the last commit, and git diff topic master compares the two branch tips.
Argument order matters. Swap the two refs and every plus line becomes a minus line (the -R flag does the same swap).
Limiting the diff to a path
Put a path after a double dash to restrict the comparison. git diff HEAD — ./test compares only that file against the tip of the current branch, and directory names work the same way.
Path limits matter more as repositories grow. GitHub’s Git 2.56 highlights (September 28, 2026) report that removing quadratic scans from path-limited working-tree diffs cut one git diff on a Chromium checkout, with roughly 500,000 index entries, from about eight minutes to 0.07 seconds.
How Do You Read git diff Output?

From the top down. A file header comes first, then extended header lines, a pair of file markers, and finally the hunks.
Here’s a one-line change in a small Python file.
diff --git a/app.py b/app.py
index 3f2a1b7..8c9d0e4 100644
--- a/app.py
+++ b/app.py
@@ -10,7 +10,7 @@ def load_config():
retries = 3
verbose = False
debug = False
- timeout = 30
+ timeout = 60
host = "localhost"
port = 8080
return read(path)The first line, diff –git a/app.py b/app.py, names the file before (a/) and after (b/) the change. The two names only differ for renames and copies.
The index line, index 3f2a1b7..8c9d0e4 100644, gives abbreviated blob hashes for both versions, and the file mode too when it didn’t change.
Below that, the — line marks the old file and the +++ line marks the new one.
The @@ -10,7 +10,7 @@ line is the hunk header. It gives the start line and line count in the old file, then in the new one, and Git appends the nearest function name after it. Inside the hunk, a leading space marks context, a minus marks a removed line and a plus marks an added one.
Three context lines surround each change by default. Run -U5 to widen that to five, or set diff.context once, per the Git 2.56 manual.
A line reading \ No newline at end of file means the last line has no trailing newline.
Extended headers change with the kind of edit. Renames add rename from and rename to lines, new files add new file mode, and deleted files add deleted file mode.
The similarity index on a rename is the share of unchanged lines, rounded down. 100% means identical content.
Git keeps the a/ and b/ names even for created or deleted files, where a traditional diff would print /dev/null.
What Is the Difference Between Two-Dot and Three-Dot Ranges?
Two dots compare the tips of two refs directly. Three dots compare the second ref against its merge base with the first, so only the second side’s own work appears.
A..B is the same as git diff A B. Every difference between the two tips shows up, including work on A that B lacks, which appears as removals.
A…B is the same as git diff $(git merge-base A B) B. You only see what B changed since the two sides split, and anything added to A after the split gets ignored.
To review what a feature branch adds to main, run git diff main…feature. Two dots would mix in anything main gained after the split.
Leaving one side empty falls back to HEAD, so git diff main… works from a checked-out feature branch. The –merge-base option gives the same result as three dots in the two-commit form.
Despite the notation, these aren’t ranges in the git log sense. The manual says git diff compares two endpoints, and the dots only control how the left side is derived.
Three dots hide anything that exists only on the first ref. That suits a code review and misleads a sync check.
Which git diff Options Make the Output Easier to Use?

Summaries, filters, whitespace flags and word-level views cover most of what I reach for day to day.
Summary and filter options
- –stat draws a per-file graph of added and removed lines, as wide as the terminal, or 80 columns when output isn’t a terminal
- –name-only prints changed file names and nothing else
- –name-status adds a status letter to each name (A for added, M for modified, D for deleted, R for renamed)
- –diff-filter keeps only the statuses you list, so –diff-filter=MRC drops pure additions and deletions
Run –stat before opening a large diff. It tells you how much is coming.
For scripts, –numstat prints the same counts in a tab-separated form that is easier to parse.
Whitespace options
Reindented code buries the real change under noise, and a few flags cut it down. -w ignores all whitespace when comparing lines, while -b only ignores changes in the amount of whitespace. –ignore-space-at-eol skips trailing whitespace changes, and –ignore-blank-lines skips changes made only of blank lines.
-w is great for reviewing a formatter pass. It also hides real edits in whitespace-sensitive files such as YAML or a Makefile with tab-indented recipes.
Word-level output
Line diffs get noisy on prose and long config lines. –word-diff marks the changed words instead.
| Mode | Output |
|---|---|
| plain | [-removed-] and {added} markers inline |
| color | Colors only, no markers |
| porcelain | Line-based format for scripts, with ~ marking newlines |
| none | Turns word diff off again |
–color-words is shorthand for –word-diff=color, with an optional regex that defines what counts as a word.
By default a word is a run of non-whitespace characters, so a token like foo(bar) counts as one word. –word-diff-regex fixes that for code.
Word diff is computed from the normal line diff inside each hunk. The manual warns that the result can be larger than what a dedicated word-diff tool produces.
When Should You Use git diff Instead of git status, git log or git show?

When you want the content of a change between two states. The other commands answer neighboring questions.
git status tells you which files changed. git log tells you which commits exist, and git show tells you what one commit did. That leaves git diff for the question of which lines changed before you commit, with git diff –staged covering the staged side.
My usual loop is to run git diff to review edits, stage them, run git diff –staged to check the exact commit contents, then commit.
The manual describes git status as listing paths in groups: index versus HEAD, working tree versus index, and untracked files. It prints names, not content.
Run git status -v and it embeds the git diff –cached output. A second -v adds the plain git diff output too, which is handy but buries the file list.
git log prints no diff by default. Add -p and you get one patch per commit.
git show defaults to HEAD. For a commit it prints the log message plus the diff, and for a merge commit it switches to the combined format.
How Does git diff Behave in Scripts and CI?
By default git diff exits with 0 whether or not it finds differences. –exit-code changes that. 1 means differences and 0 means none, the same convention as diff(1).
–quiet prints nothing and implies –exit-code. –no-index does the same for two paths on disk. –check is a separate animal, returning non-zero on conflict markers or whitespace errors, and it can’t be combined with –exit-code.
git diff --quiet || echo "unstaged changes"
git diff --cached --quiet || echo "staged changes"A common step in a continuous integration job runs a formatter or code generator, then git diff –exit-code. If the tool touched anything, the job fails.
There’s a catch. Both flags only look at tracked files, so a brand-new generated file passes silently.
Marking new paths with git add -N makes them visible to git diff, though that command does change the index. For a read-only check, git status –porcelain is the safer choice, since the manual guarantees its format stays stable across versions and user configuration.
External diff drivers add one more wrinkle. diff.trustExitCode defaults to false, so Git expects such a driver to return 0 regardless of equality, and –quiet skips drivers whose exit code is not trusted.
Which Diff Algorithm Should You Choose?
Git’s default is Myers, per the Git 2.56 manual. Switch when hunks look wrong, not before.
| Algorithm | Pros | Cons |
|---|---|---|
| myers (default) | Fast when differences are small | Greedy, so output is not guaranteed minimal |
| minimal | Smallest possible diff | Takes extra time |
| patience | Anchors hunks on unique lines | Needs unique common lines to anchor on |
| histogram | Patience-style readability, copes with low-occurrence lines | Favors readable hunks over the fewest edits |
How to set the algorithm
For a single run, pass –diff-algorithm=histogram, or use the shortcuts –patience, –histogram and –minimal. To make the choice permanent, run git config diff.algorithm histogram. A configured choice can be undone for one run with –diff-algorithm=default.
–anchored takes a text argument and pins a line in place. A line that appears once on both sides and starts with that text won’t show up as deleted or added.
As of Git 2.54 (April 20, 2026), git blame accepts –diff-algorithm as well, according to GitHub’s release highlights.
What the published evidence says
The sources lean toward histogram, yet the default hasn’t moved.
Eugene Myers’ 1986 Algorithmica paper describes an O(ND) method that performs well when the two sequences are similar. The JGit documentation presents HistogramDiff as an extension of Bram Cohen’s patience diff that behaves exactly like patience whenever a unique common line exists and adds support for low-occurrence common lines.
A 2020 paper in Empirical Software Engineering found histogram more suitable than Myers for presenting code changes, recommends it for code, and notes it arrived in Git 1.7.7 in 2011. The Git 2.56 manual still lists Myers as the default.
Histogram is still being tuned. GitHub’s Git 2.54 highlights report a fix to its compaction phase, which could shift a change group across anchor lines and produce a technically correct but redundant diff.
Every algorithm produces a valid diff. They differ in how they group the changes.
When Does git diff Show Nothing or Too Little?
Nothing appears when the two sides match. Too little appears when the change lives somewhere git diff doesn’t look, like an untracked file, the index or binary content.
| Symptom | Cause | Fix |
|---|---|---|
| New file missing | Path is untracked | git add -N on the path, or stage it and use –staged |
| Empty output after editing | Changes already staged | git diff –staged |
| One “Binary files differ” line | Content is binary | –text, or a textconv filter |
| Only commit hashes for a submodule | Short submodule format is the default | –submodule=diff |
| Slow run on a big refactor | Exhaustive rename detection | –no-renames, or tune -l |
Untracked files
Untracked paths never appear in git diff.
git add -N marks a path as intent-to-add. It then shows as an empty existing file in git diff and as a new file in git diff –cached, per the manual.
git status also hides untracked paths that match gitignore rules, so nothing flags them.
Binary files and very large diffs

For binary files, –numstat prints two dashes instead of line counts. textconv filters set in gitattributes turn binary formats into readable text, and they are on by default for git diff.
On the large side, diff.renameLimit defaults to 1000 files, and the exhaustive comparison behind it is O(N^2). On a big change, run –stat before reading anything.
Textconv output is for human eyes only. A diff built that way cannot be applied as a patch.
git diff FAQ
Can git diff compare two files outside a Git repository?
Yes. Run git diff –no-index with two paths and Git compares them on disk. You can leave the flag out when you work outside a Git working tree, or when one path points outside it.
This form implies –exit-code, so the exit status reports whether the files differ.
How do you save a git diff as a patch file and apply it?
Redirect the output to a file, or pass –output with a file name. Apply it later with git apply.
For binary files, add –binary so the patch carries a binary diff that git apply can use.
How do you open a diff in an external tool?

Use git difftool, a frontend to git diff that accepts the same options and arguments. Pick a tool with –tool, or set diff.tool once.
git difftool –tool-help lists the valid tool names, and –dir-diff compares whole directories in a single window.
How does git diff detect renames?
Rename detection is on by default, since diff.renames is true. Git pairs a deleted file with an added one when at least 50% of the content matches, and -M90% raises that threshold.
Use -M100% for exact renames only, or –no-renames to turn detection off.
Can git diff highlight moved lines?
Yes. –color-moved colors moved blocks differently from true additions and deletions. With no mode given it uses zebra, which alternates colors between adjacent blocks.
The blocks mode detects moved text of at least 20 alphanumeric characters. diff.colorMoved makes the setting permanent.
How do you see what you changed while resolving a merge conflict?
Run git diff AUTO\_MERGE. The ort merge strategy writes that ref when it hits conflicts, so the command shows the edits you made so far to resolve textual conflicts.
To compare against one side only, –ours, –theirs and –base use stage 2, stage 3 and stage 1 of an unmerged path.
Turning a Reviewed git diff Into a Commit
Match the diff you review to the command you commit with. git diff –staged previews a plain commit, and git diff HEAD previews git commit -a, according to the Git 2.56 manual.
The trade-off sits in that choice. Committing with -a skips the staging step but sends every tracked edit, so partial commits need the staged route.
I run git diff –cached –check first, read git diff –staged next, and commit after that.
The machine check costs seconds and catches conflict markers and whitespace errors before anyone spends time reading. Reading comes second because a clean –check says nothing about whether the change is right.
Once the diff passes both, git commit records the staged snapshot, and the message carries the reason for the change.
- VS Code Extensions That Are Actually Worth Installing - October 13, 2026
- Google Play App Signing Explained - October 11, 2026
- 5 Best NinjaOne Alternatives for Focused Endpoint Management - October 10, 2026



