In Git, why does a file show every line as changed after a Windows teammate edits it?
answer
- The change is invisible in the editor
- Two bytes at the end of each line
- Git diffs whole lines, terminator included
- LF versus CRLF
- git ls-files --eol shows i/ and w/
basics
~10 sThe file's line endings switched between LF and CRLF. Git compares whole lines, so an invisible carriage return at the end of every line makes every line differ and the whole file looks rewritten.
solid answer
~40 sGit stores file content as bytes, and a line ending is part of those bytes. Unix and macOS editors terminate lines with a single LF (`\n`); many Windows tools write CRLF (`\r\n`). When a teammate opens a LF file in an editor that saves CRLF, every line gains a carriage return, so Git's line-based diff reports every line as removed and re-added even though no visible text changed. You can confirm it with `git ls-files --eol`, which prints the index and working-tree endings for a path, or by noticing that `git diff` flags the trailing carriage returns as whitespace errors. The fix is not to argue about editors: normalise the repository with a committed `.gitattributes` rule so Git converts on its own.
code
console · 6 lines$ git ls-files --eol -- src/app.js
i/lf w/crlf attr/text=auto src/app.js
$ git diff --stat
src/app.js | 842 ++++++++++++++++++++--------------------
1 file changed, 421 insertions(+), 421 deletions(-)go deeper
Be ready to name the two conventions (LF on Unix, CRLF on Windows) and say why an invisible byte makes every line differ in a Git diff.
Explain how you would confirm it — git ls-files --eol, whitespace highlighting — and why the fix belongs in a committed .gitattributes rather than in each developer's config.
Show the downstream damage you have actually seen: unreviewable diffs, blame destroyed, whole-file merge conflicts, CRLF shebangs failing in CI, and how you sequenced a repository-wide normalisation.
Frame it as a policy that must live in the repository and be enforced on every new clone and CI checkout, and weigh the one-time cost of a repo-wide normalisation commit against ongoing review noise.
## What a line ending is A text file is a byte stream, and "lines" exist only because certain bytes are agreed to terminate them. Unix, Linux and macOS use a single LF byte (`0x0A`, written `\n`). DOS and Windows conventions use two bytes, CR followed by LF (`0x0D 0x0A`, written `\r\n`). Some Windows editors, and many Windows-native tools, write CRLF by default. Git does not have a notion of "the same line with a different terminator". A blob is the exact bytes; a diff is computed by splitting on LF and comparing the resulting lines. A line that ends `foo\r` is simply not the same line as `foo`. ## Why the diff explodes If a file is committed with LF endings and a teammate's editor rewrites it with CRLF, the saved file differs from the committed blob on **every single line**. Git therefore reports the entire file as changed: `git diff` shows the whole file removed and re-added, `git diff --stat` shows a line count equal to the file size in lines, and any review of that change is useless because the one real edit is buried in thousands of noise lines. `git blame` on that file afterwards attributes every line to the person who saved it, destroying the authorship history. The reverse happens too: a repository whose blobs contain CRLF will show whole-file churn the moment a Unix editor or a formatter strips the carriage returns. ## How to confirm it The carriage return is invisible in most editors, so diagnose it with tools that show bytes: - `git ls-files --eol -- path` prints something like `i/lf w/crlf attr/text=auto path` — `i/` is what is stored in the index, `w/` is what is on disk, `attr/` is the effective `text` attribute. - `git diff` highlights a trailing CR as a whitespace error under Git's default whitespace rules, so the ends of lines light up. - `git show HEAD:path | cat -A` (or an editor's "show invisibles") reveals `^M` at line ends. A common variant is a file where *some* lines are CRLF and some are LF — mixed endings, usually the result of a partial edit or a merge of two differently-normalised copies. ## Why the invisible byte matters beyond diffs CRLF churn is not merely cosmetic. Shell scripts with CRLF endings fail on Unix because the interpreter line becomes `#!/bin/sh\r`. Some parsers, checksum comparisons, and tools that hash file content see two different files. Merges conflict on every line when one side has been re-terminated, because the three-way merge sees two sides that each rewrote the whole file. ## The right layer to fix it The wrong fix is to ask each developer to configure their editor, or to have one person strip the carriage returns in a cleanup commit and hope it stays clean. Both depend on every machine, forever. The durable fix is to let Git own the conversion and to express the policy in a file that is committed with the code. A `.gitattributes` at the repository root containing `* text=auto` tells Git to detect text files, store them with LF in the object database, and convert on checkout according to the platform or an explicit `eol` attribute. Because `.gitattributes` is versioned, it applies to every clone, every new hire, and every CI checkout without anybody setting anything locally. Per-machine `core.autocrlf` can do a similar conversion, but only on the machines where somebody remembered to set it. Once the rule exists, an existing repository still needs a one-time normalisation pass (`git add --renormalize .` followed by a commit) so the stored blobs actually match the policy. After that, a file edited on Windows and a file edited on Linux produce the same blob, and a diff again contains only the lines a human changed. ## What to say in an interview Name the bytes (LF versus CRLF), say that Git diffs whole lines so a terminator change alters every line, show that you would confirm it with `git ls-files --eol` rather than guessing, and land on the repository-level `.gitattributes` fix rather than a per-developer setting.
- Why can a CRLF file break a shell script on Linux even though it looks fine?The shebang line becomes `#!/bin/sh\r`, so the kernel looks for an interpreter whose name ends in a carriage return and fails. The same trailing byte leaks into variable values and here-documents, producing errors that make no sense when you read the file. Marking such paths with an `eol=lf` attribute keeps them LF on every platform.
- How would you keep the normalisation commit from ruining git blame?Record the normalisation commit's hash in a file and point `blame.ignoreRevsFile` at it, or pass `git blame --ignore-rev <sha>` ad hoc. Blame then attributes each line to the commit that last changed it meaningfully instead of to the whitespace sweep.
It is like retyping a document in a different font: the words are identical, but a byte-level comparison sees a completely new file.
saying these in an interview costs you the question
- Claims Git ignores whitespace and line endings by default
- Blames the diff tool rather than the stored bytes
- Says fix it by telling everyone to change their editor
- Thinks a one-off cleanup commit prevents future churn
- Confuses trailing spaces with the carriage return byte