CRLF vs LF: Fix Line Ending Diffs with Git
Running git diff after pulling code can reveal a frustrating surprise: every single line in a file appears modified despite zero visible text changes! This issue stems from CRLF (\r\n) vs LF (\n) line ending mismatches. This article covers root causes and Git fixes.
1. Problem: whole-file false diffs in cross-platform collaboration
Windows uses Carriage Return Line Feed (CRLF, \r\n, ASCII 0x0D 0x0A), while Linux and macOS use Line Feed (LF, \n, ASCII 0x0A). In cross-platform teams, if an IDE converts LF files to CRLF, line-by-line diff engines evaluate trailing \r characters as additions on every line, flagging the entire file as modified.
2. Minimal reproduction: CRLF vs. LF byte-level false diff comparison
This snippet demonstrates how identical visible text produces false diffs due to line ending mismatches:
/* 1. Source File A (Linux/macOS format, LF \n): */
const fileLF = "function hello() {\n console.log('world');\n}";
/* 2. Target File B (Windows format, CRLF \r\n): */
const fileCRLF = "function hello()\r\n console.log('world');\r\n}";
const linesA = fileLF.split("\n");
const linesB = fileCRLF.split("\n");
console.log("Line Count A:", linesA.length, "Line Count B:", linesB.length);
console.log("Line 0 Byte Length A:", linesA[0].length); // 18
console.log("Line 0 Byte Length B:", linesB[0].length); // 19 (extra \r!)
console.log("Byte Comparison Match:", linesA[0] === linesB[0]); // false -> Triggers whole-file false diff!3. Root cause: carriage return \r byte entity and autocrlf conflicts
1. Carriage Return `\r` Byte Entity: line endings are physical byte codes. \r\n adds 1 byte per line compared to \n. Line diff algorithms evaluate line strings as tokens; trailing \r destroys string equality. 2. `core.autocrlf` Misconfigurations: setting core.autocrlf = true on Windows converts LF to CRLF on checkout and back to LF on commit. If editors overwrite line endings without triggering conversion, CRLF slips into repositories. 3. Missing `.gitattributes`: lacking repository-level line ending rules leaves line ending handling to local OS defaults.
4. Recommendation: full-stack line ending normalization and .gitattributes
1. Repository .gitattributes: add a .gitattributes file in project roots containing * text=auto eol=lf to enforce LF normalization on checkout and commit. 2. Normalize Repository History: run git add --renormalize . to re-normalize staged files. 3. Diff Option Whitespace Ignore: enable 'Ignore Whitespace' options in diff tools, stripping \r prior to comparisons.
5. Complete code: line ending normalization and \r diagnostic helper
This TypeScript snippet demonstrates diagnosing CRLF line endings in raw strings and performing non-destructive LF normalization.
interface LineEndingAnalysis {
hasCRLF: boolean;
hasLF: boolean;
crlfCount: number;
lfCount: number;
normalizedText: string;
}
function analyzeAndNormalizeLineEndings(rawText: string): LineEndingAnalysis {
const crlfMatches = rawText.match(/\r\n/g) || [];
const lfMatches = rawText.match(/(?<!\r)\n/g) || [];
const normalizedText = rawText.replace(/\r\n/g, "\n");
return {
hasCRLF: crlfMatches.length > 0,
hasLF: lfMatches.length > 0,
crlfCount: crlfMatches.length,
lfCount: lfMatches.length,
normalizedText
};
}
const mixedText = "Header\r\nLine 1\r\nLine 2\nFooter";
const result = analyzeAndNormalizeLineEndings(mixedText);
console.log("CRLF Count:", result.crlfCount, "Normalized Length:", result.normalizedText.length);6. Incorrect approaches
Globally setting git config --global core.autocrlf false on Windows (causing CRLF commits that break Linux CI/CD builds). Manually stripping \r line by line. Misconfiguring binary files in .gitattributes, corrupting binary byte streams.
7. Boundaries: protecting binary files from line ending conversion
Always exclude binary files when configuring .gitattributes. Explicitly declare binary files (*.png binary, *.jpg binary, *.woff2 binary) to prevent Git from converting 0x0D 0x0A byte sequences in binary streams.
8. Verification
Declare the repository EOL policy in .gitattributes and configure a matching linter rule. Keep fixtures for LF, CRLF, and mixed endings, and assert normalization changes only permitted text file types.
9. FAQ
Why does git diff mark every line modified when code looks identical? File line endings changed from \n to \r\n, adding invisible 0x0D carriage return bytes to every line.
What does * text=auto eol=lf in .gitattributes mean? text=auto enables automatic text file detection, while eol=lf forces LF line endings on checkout and commit.
How do I fix CRLF line endings across an entire repository at once? Add .gitattributes, run git add --renormalize ., commit, and push.
10. Summary
Whole-file false diffs stem from cross-platform line ending mismatches. Standardizing repository line endings via .gitattributes and ignoring whitespace in diff tools eliminates false diff noise.
Sources and further reading
Specifications and references for checking the article’s technical details.
- Git Documentation
Git project
Related articles
Large File Diffs: Web Workers and DOM Virtualization
Learn how to optimize web-based large file diffs by offloading CPU-bound tasks to Web Workers and applying DOM virtualization for zero UI lag.
How it worksCode Diff Algorithms: LCS, Myers and Patience
Examine the evolution of sequence comparison algorithms from dynamic programming LCS to Myers Diff and Patience Diff in version control systems.
Best practicesGit Guide: Branches, Merge, Rebase and Recovery
Master Git 3-area architecture, modern git switch/restore commands, branching strategies, Merge vs Rebase trade-offs, conflict resolution, and git reflog recovery.
Keep exploring
Use the related browser tool to test the workflow with your own sample data.
Open related tool