Resources / Field guide

The phantom "300 files changed" diff

You open a repo in a different environment, run git status, and it claims hundreds of files changed — while Visual Studio shows a clean tree. Nothing is actually wrong. It's CRLF vs LF line endings, and one committed file makes it stop.

I opened a repo through a Linux environment recently, ran git status, and got told that 300-plus files had changed. I hadn't touched any of them. Visual Studio, looking at the same repo on Windows, showed a clean tree. Nothing was actually wrong — but if I'd trusted that diff and committed it, I'd have rewritten half the codebase for no reason and made the next person's review impossible.

This is one of those problems that looks alarming and is actually boring, which is the best kind. Here's what's happening and the one file that makes it stop.

What you're actually looking at

Git was telling the truth, in a narrow sense: from where it sat, those files were different. But the difference wasn't your code. It was line endings.

Windows ends lines with a carriage return and a line feed (CRLF, or \r\n). Linux and macOS use a line feed alone (LF, or \n). When a repo gets committed on Windows and then opened in a Linux environment — a WSL shell, a container, a mounted drive — git can see every Windows line ending as a change, because to that environment, it is one. Every line of every text file looks modified, so a whole file shows up as rewritten.

The tell is in the diff shape. A real change touches a few lines. A line-ending flip rewrites the entire file — you'll see something like Program.cs with +149 / −122 on a file you never opened, and the same lopsided whole-file signature repeated across hundreds of files. When the "changes" are that uniform and that total, it's almost never your work. It's CRLFLF.

The fastest confirmation: look at the same repo in the tool that committed it. If Visual Studio (or a Windows git client) shows a clean tree while the Linux side shows 300 changed files, the files on disk are fine and the Linux view is the one that's distorted.

The fix: commit a .gitattributes

The reason this happens at all is that the repo has no committed .gitattributes. That file is how you tell git, explicitly, how to handle line endings — so it stops guessing based on whatever OS happens to be looking. Add one at the root, on your main branch:

* text=auto
*.cs     text eol=crlf
*.razor  text eol=crlf
*.css    text eol=crlf
*.js     text eol=crlf
*.json   text eol=crlf
*.csproj text eol=crlf

What each part does:

  • * text=auto is the baseline: let git normalize line endings to LF inside the repository, for every text file, regardless of who commits. This alone kills most of the noise.
  • The per-extension eol=crlf lines pin specific file types to Windows line endings in the working tree. For a .NET codebase that lives primarily in Visual Studio, this keeps your checked-out files consistent with what the IDE and tooling expect.

Pick the eol rule that matches your team. A team that's all-Windows can pin to crlf; a mixed or Linux-leaning team is usually happier letting text=auto normalize to LF and leaving the per-type pins off. The important part isn't which one you pick — it's that the repo states the rule instead of leaving it to chance.

A subtlety worth knowing: adding .gitattributes doesn't retroactively fix files already committed with the "wrong" endings. To normalize the existing tree once, commit the .gitattributes, then run:

git add --renormalize .
git commit -m "Normalize line endings"

Do that on the OS your team actually develops on, deliberately, as its own commit — not by accident in the middle of a feature.

The trap to avoid in the meantime

Until that .gitattributes is in place, don't run git operations against the repo from the environment showing the phantom diff. If you stage and commit from the Linux side while it's seeing every file as changed, you'll sweep all those bogus line-ending rewrites into history. That's a commit that touches hundreds of files, buries the three real ones, and forces a painful normalization later to undo. Do your branching and commits from the side that shows a clean tree (Visual Studio or a Windows terminal) until the .gitattributes lands.

Stale lock after poking from two places. If you've been running git against the repo from two environments at once, you can end up with a leftover index.lock in .git, and Visual Studio will warn that "a git process may be running or may have crashed." If nothing is actually mid-operation, delete .git\index.lock and refresh to clear it.

Why it's worth a two-minute fix

A .gitattributes is six lines and you write it once per repo. Skip it and the cost shows up later and falls on other people: reviews where the real change is hidden in a 300-file diff, merge conflicts that are pure whitespace, and that creeping doubt about whether the repo is actually clean. Commit the file, normalize once, and line endings go back to being something you never have to think about — which is exactly where they belong.

Inherited a repo that fights you?

Phantom diffs, mystery build breaks, "works on my machine" — the boring stuff that quietly eats a team's time. I clean up real .NET codebases and the workflow around them. If you want senior eyes on yours, let's talk.

Work with me