You made a two-line change to src/api/routes.ts on a Windows machine, ran git diff, and saw 3,000 lines changed — every line in the file. Or a teammate’s PR shows a massive red-green diff where the only real change is a single import, but every green line has a ^M at the end in raw view. Git’s core.autocrlf and .gitattributes rules converted CRLF to LF (or the reverse) when staging, so the diff touches every line. The repo now holds files in mixed line-ending states, and every commit from a Windows machine risks another explosion.
Fastest fix (most cases): commit a .gitattributes with * text=auto, then run git add --renormalize . and commit the result once. That single normalization commit converts the whole repo to LF in the index; future diffs stay clean. The rest of this page is for confirming you’re in that bucket and for the edge cases (binary files mis-typed, batch scripts that need CRLF, blame pollution).
First: which bucket are you in?
Run these three commands and match the output. They tell you whether the mismatch is index-vs-worktree, an editor problem, or a binary file being mangled.
| Command | Output you see | Diagnosis | Jump to |
|---|---|---|---|
git config core.autocrlf | true on Windows, unset/false elsewhere | Per-machine config mismatch | Cause 1 |
git ls-files --eol src/api/routes.ts | i/lf w/crlf attr/text=auto | Index has LF, working tree has CRLF (stale checkout) | Cause 2 |
file src/api/routes.ts | ... with CRLF line terminators while .gitattributes says eol=lf | Editor saved CRLF, overriding the attribute | Cause 3 |
git show HEAD:assets/font.woff2 | file - | ASCII text (for a real binary) | Binary file mis-classified as text | Cause 4 |
git diff --check | whitespace errors on nearly every line | Mixed endings already committed | Cause 5 |
git ls-files --eol is the single most useful diagnostic here. The format is i/<eol> w/<eol> attr/<attr> <file>: i/ is the index (committed) version, w/ is your working tree, attr/ is the active .gitattributes rule. i/lf w/crlf means the index is correct and your local file drifted — the most common cause of “diff shows everything changed.”
Common causes
Ordered by hit rate, highest first.
1. core.autocrlf = true on Windows, false/unset elsewhere
Windows Git installers default core.autocrlf to true: convert LF to CRLF on checkout, CRLF back to LF on commit. When a macOS developer commits LF files and a Windows developer checks them out (now CRLF on disk) then runs git add with a non-matching config, Git stages the CRLF bytes as new content — every line is “changed.”
How to spot it: git config core.autocrlf returns true on the Windows machine. git diff --check reports whitespace errors on every line of the affected file. git diff --ignore-cr-at-eol HEAD showing an empty diff confirms the only difference is the trailing carriage returns.
2. .gitattributes was added or changed after files were already committed
Someone committed * text=auto to normalize going forward but never re-normalized the files already in the repo. Now git status shows hundreds of files as modified: a checkout converted the working tree to CRLF per the rule, but the index still holds the original bytes.
How to spot it: git ls-files --eol prints i/lf w/crlf for many files — index LF, working tree CRLF. The fix is one git add --renormalize . commit (Step 2 below), not a per-file battle.
3. Editor configured to use CRLF on Windows
VS Code on Windows has files.eol set to \r\n globally (or the file was opened once with Auto detection and re-saved as CRLF). Every save acquires CRLF even when .gitattributes specifies text eol=lf. .gitattributes controls what Git stores, not what the editor writes to disk — so an editor that saves CRLF still produces a dirty working tree.
How to spot it: file src/api/routes.ts returns “with CRLF line terminators” while the matching .gitattributes rule says eol=lf. The status bar in VS Code shows CRLF in the lower-right; click it to switch the open file to LF.
4. Binary file missing from .gitattributes, treated as text
A .png or .woff2 file was not marked binary. Git’s text=auto heuristic guessed it was text and tried to normalize its line endings, corrupting the binary and producing a full-file diff.
How to spot it: file assets/font.woff2 returns the real binary type, but git show HEAD:assets/font.woff2 | file - returns ASCII text (wrong — Git rewrote the bytes). Any 0x0D byte inside a binary that Git “fixed” is now lost.
5. Mixed line endings already committed
The committed file already mixes LF and CRLF lines (multiple contributors, a bad merge). When Git normalizes on staging, the output differs from the input on every mixed line, producing a noisy diff.
How to spot it: git grep -c $'\r' -- src/api/routes.ts returns a non-zero count for a file that should be pure LF, or cat -A src/api/routes.ts shows ^M at line ends.
Shortest path to fix
Step 1: Create or fix .gitattributes at the repo root
cat > .gitattributes << 'EOF'
# Default: let Git auto-detect text vs binary and store text as LF.
# This is GitHub's recommended baseline.
* text=auto
# Force LF in the working tree too (optional, stricter).
# Use "* text=auto eol=lf" instead of the line above ONLY if you want
# Windows checkouts to also be LF on disk. Supported since Git 2.10.
# Files that MUST be checked out with CRLF on every OS.
*.bat text eol=crlf
*.cmd text eol=crlf
*.sln text eol=crlf
*.ps1 text eol=crlf
# Files that MUST be LF on every OS (break otherwise).
*.sh text eol=lf
# Truly binary — never touch line endings.
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary
*.pdf binary
*.zip binary
*.gz binary
*.tar binary
EOF
git add .gitattributes
git commit -m "chore: add .gitattributes to normalize line endings"
* text=auto (GitHub’s documented default) stores text as LF in the repo and checks it out with native endings — Windows users still get CRLF on disk, which is usually what you want. * text=auto eol=lf is the stricter variant that also forces LF on disk everywhere; use it only if your team has standardized on LF in editors. Don’t set both forms.
Step 2: Re-normalize all existing files (the GitHub-recommended way)
# Save any uncommitted work first so the renormalize commit is clean.
git add -u
git commit -m "chore: save work before line-ending renormalize"
# Apply the new .gitattributes rules to every tracked file.
git add --renormalize .
# Inspect what changed — these should be line-ending-only diffs.
git status
git diff --cached --stat
# Commit the normalization as ONE isolated commit (important for blame).
git commit -m "chore: normalize all line endings per .gitattributes"
git add --renormalize . is the modern, safe command for this — it re-stages every tracked file through the .gitattributes filters without deleting anything. (The older git rm --cached -r . && git add . works too, but it’s noisier and can drop files that are git-ignored after the change. Prefer --renormalize.)
Keep this normalization as a single, isolated commit and record its SHA — Step 5 uses it to keep git blame clean.
Step 3: Set core.autocrlf = false on all developer machines
git config --global core.autocrlf false
git config --global core.eol lf
With .gitattributes controlling endings, autocrlf is redundant and reintroduces the per-machine conflicts above. false tells Git to defer entirely to .gitattributes.
Step 4: Fix Windows editor settings (commit them)
In the workspace .vscode/settings.json, committed to the repo so every contributor inherits it:
{
"files.eol": "\n"
}
This makes VS Code save with LF on all platforms. Pair it with an .editorconfig so JetBrains, Sublime, and Vim follow the same rule:
root = true
[*]
end_of_line = lf
insert_final_newline = true
Step 5: Keep git blame clean after the normalization commit
A repo-wide renormalize rewrites every line of many files, so git blame would attribute them all to that one chore commit. Suppress it:
# Record the normalization commit SHA in a tracked file.
echo "<normalize-commit-sha>" >> .git-blame-ignore-revs
git add .git-blame-ignore-revs
git commit -m "chore: ignore line-ending renormalize in blame"
# Make local blame and the GitHub UI use it.
git config blame.ignoreRevsFile .git-blame-ignore-revs
GitHub, GitLab, and modern git blame all honor .git-blame-ignore-revs, so the real author of each line is preserved.
Step 6: Verify the fix with a clean checkout
git ls-files --eol | grep -v 'w/lf' | grep -v binary # expect no text files left
git diff --check # expect no output
# Sanity check one file end-to-end:
git stash && git checkout -- . && git diff --check && git stash pop
If git ls-files --eol shows only w/lf for text files (plus your intentional eol=crlf exceptions) and git diff --check is silent, the repo is normalized.
Prevention
- Commit
.gitattributesin the first commit of every new repo, before any mixed-ending files can land. - Standardize
core.autocrlf = falsevia a Git config template so new clones start correct:git config --global init.templateDir ~/.git-templates. - Ship
.editorconfigwithend_of_line = lfalongside.gitattributes; VS Code, JetBrains, Sublime, and Vim respect it. - Add
git diff --check(or agit ls-files --eolassertion) as a CI step or pre-commit hook to catch regressions before they merge. - Document the required
core.autocrlf = falseinCONTRIBUTING.mdfor Windows contributors. - When a new
.gitattributesrule lands, tell everyone to rungit add --renormalize .once so working copies converge without losing unstaged work.
FAQ
Q: After the normalization commit, git diff still shows thousands of lines for some files. Why?
A: Those files have new LF content in the index but the working tree still has CRLF from a prior checkout (git ls-files --eol shows i/lf w/crlf). Run git checkout -- <file> to re-checkout with correct endings, or git add --renormalize . again to re-stage with normalization applied.
Q: We have a Windows build tool that requires CRLF batch scripts. How do we keep those?
A: Add *.bat text eol=crlf and *.cmd text eol=crlf (and *.ps1, *.sln if needed). Those files are always checked out with CRLF on every OS, including macOS and Linux, while everything else stays LF.
Q: Is text=auto safe for all projects?
A: For most, yes — it uses the same binary-vs-text heuristic as git diff. The risk is text-like binaries (some .svg, .dxf, or proprietary formats). Mark those explicitly as binary so the heuristic can’t mangle them.
Q: Best way to review a PR that’s drowning in CRLF noise?
A: Use git diff --ignore-cr-at-eol (and git log -p --ignore-cr-at-eol). It treats CRLF and LF as equal, so you see only real content changes — more precise than --ignore-all-space, which also hides genuine indentation edits. Reviewing is a stopgap, though: fix the root cause with .gitattributes so blame, git log -S, and bisect stay accurate.
Q: Does WSL on Windows hit this?
A: Git inside WSL behaves like Linux — core.autocrlf defaults to false, so it won’t auto-convert. The trap is editing files on the Windows host filesystem (/mnt/c/...) where Windows tools touch them. Keep WSL projects under the Linux filesystem (~/...) to avoid cross-OS ending drift.