Git

What Is Git Diff? How to Compare File Changes

What Is Git Diff? How to Compare File Changes

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?

YouTube player

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.

CommandLeft sideRight sideWhat it shows
git diffIndexWorking treeUnstaged changes
git diff –stagedHEADIndexWhat the next commit will contain
git diff HEADHEADWorking treeTracked changes, staged or not
git diff A BCommit ACommit BEverything that changed between them

Staged and unstaged changes

YouTube player

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.

Why is GitHub the heart of open source?

Uncover GitHub statistics: developer community growth, repository trends, collaboration patterns, and the platform that powers modern software development.

Explore GitHub Data →

Commits and branches

YouTube player

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?

YouTube player

From the top down. A file header comes first, then extended header lines, a pair of file markers, and finally the hunks.

Looking to sharpen your Git skills? Branching, merging, rebasing, and everything else you need - including git rebase, git stash, and commit workflows - is on one page in the Git Cheat Sheet.

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?

YouTube player

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.

ModeOutput
plain[-removed-] and {added} markers inline
colorColors only, no markers
porcelainLine-based format for scripts, with ~ marking newlines
noneTurns 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?

YouTube player

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.

AlgorithmProsCons
myers (default)Fast when differences are smallGreedy, so output is not guaranteed minimal
minimalSmallest possible diffTakes extra time
patienceAnchors hunks on unique linesNeeds unique common lines to anchor on
histogramPatience-style readability, copes with low-occurrence linesFavors 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.

SymptomCauseFix
New file missingPath is untrackedgit add -N on the path, or stage it and use –staged
Empty output after editingChanges already stagedgit diff –staged
One “Binary files differ” lineContent is binary–text, or a textconv filter
Only commit hashes for a submoduleShort submodule format is the default–submodule=diff
Slow run on a big refactorExhaustive 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

YouTube player

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?

YouTube player

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.

Bogdan Sandu

Stay sharp. Ship better code.

Every week: one curated article, one tool worth knowing, one tip you can use tomorrow. No noise, no padding.