Setup
- Git version: reproduced on
git version 2.42.0.windows.2; the behavior is by design in read-cache.c and present in all current versions.
- OS: Windows 11 Pro (10.0.26200), but the defect is in cross-platform core behavior — it affects any platform whenever a clean/smudge or EOL conversion is active (git-lfs users on Linux hit the same thing).
- Options: repository uses
.gitattributes with * text=auto eol=lf; no relevant non-default config.
Minimal reproduction
git init t && cd t
printf '* text eol=lf\n' > .gitattributes
printf 'one\ntwo\nthree\n' > file.txt
git add . && git commit -m init
# A tool (javadoc, code generators, many Windows editors) rewrites the file
# with CRLF but IDENTICAL content:
sed -i 's/$/\r/' file.txt
git status --short # -> M file.txt
git diff # -> empty; content is identical after the clean filter
git add file.txt # -> stages nothing (clean filter finds content identical)
git status --short # -> clean again, file still CRLF on disk
What happens and why
git status reports the file as modified, while git diff and git add — which run the clean filter — both agree the content is unchanged. The cause is the fast path in ie_modified() (read-cache.c): when the working file's size differs from the cached stat size, the entry is declared modified immediately, without reading the file, so the clean filter never runs. Because CRLF adds one byte per line, a generator that rewrites unchanged files in the other line-ending flavor changes every file's size and trips this path for the entire output tree.
Notably, the mtime-changed-size-same path does read the file and apply the clean filter before deciding. So git already accepts paying for the conversion-aware check in one branch of the same function; the size branch is the only place where physical difference is treated as content difference.
Why this deserves re-evaluation rather than the usual wontfix
This is documented behavior (gitfaq covers "always modified" files, remedy git add --renormalize), but I'd ask the maintainers to re-weigh the decision, for four reasons:
-
It is an oversight of layering, not a designed semantic. The size shortcut dates to 2005, when working-tree bytes and blob bytes were identical and a size mismatch mathematically proved modification. Content conversion (autocrlf 2007, text/eol attributes 2010) invalidated that invariant — "physically different" stopped implying "different," which is the entire premise of the conversion feature. The fast path was never re-audited against the feature layered on top of it. The result is git contradicting itself: status says modified; add and diff, running the real comparison, say identical.
-
The 2005 performance justification no longer holds in 2026. The extra work is bounded and tiny: it applies only to files that (a) have conversion active AND (b) changed size since the last index refresh — in a typical status, a handful of small text files, on SSDs, often under fsmonitor. Reading and cleaning those few files is unmeasurable next to what status already does.
-
The affected population is large and the annoyance is chronic. Everyone working across Windows and Linux with normalized line endings — arguably the majority of professional users — sees phantom modifications whenever any tool (javadoc, formatters, generators, editors) rewrites unchanged files in native line endings. The failure mode is maximally confusing: files flagged modified with empty diffs. The documented remedy (--renormalize / re-add) does not stick: every checkout, stash, or branch switch rewrites the checkout-form bytes and resets the cached sizes, so the next generator run re-flags everything.
-
A fix affects no one who doesn't currently experience the problem. Proposal: when a path has conversion active (the predicate already exists in convert.c) and the size check fires, fall through to the content check — exactly what the mtime branch already does — instead of returning early. Paths without conversion keep the identical fast path, byte-for-byte; repositories that never see the bug never pay a cycle. If even that is unacceptable as a default, gate it behind a config (e.g. core.eolAwareStatus or an extension of core.checkStat) so those of us affected can opt in. An optional micro-optimization: only take the slow path when the size delta is consistent with EOL inflation (0 < delta <= line count of the indexed blob), which filters out virtually all genuine edits before any read happens.
Expected behavior
git status, git diff, and git add should agree on whether a file is modified. If the clean filter defines content identity for add and diff, status should not report a contradictory answer purely because it declined to run the filter.
Actual behavior
git status reports files as modified based on the physical size of bytes the clean filter is configured to normalize away, producing modified files with empty diffs that git add refuses to stage.
Setup
git version 2.42.0.windows.2; the behavior is by design inread-cache.cand present in all current versions..gitattributeswith* text=auto eol=lf; no relevant non-default config.Minimal reproduction
What happens and why
git statusreports the file as modified, whilegit diffandgit add— which run the clean filter — both agree the content is unchanged. The cause is the fast path inie_modified()(read-cache.c): when the working file's size differs from the cached stat size, the entry is declared modified immediately, without reading the file, so the clean filter never runs. Because CRLF adds one byte per line, a generator that rewrites unchanged files in the other line-ending flavor changes every file's size and trips this path for the entire output tree.Notably, the mtime-changed-size-same path does read the file and apply the clean filter before deciding. So git already accepts paying for the conversion-aware check in one branch of the same function; the size branch is the only place where physical difference is treated as content difference.
Why this deserves re-evaluation rather than the usual wontfix
This is documented behavior (gitfaq covers "always modified" files, remedy
git add --renormalize), but I'd ask the maintainers to re-weigh the decision, for four reasons:It is an oversight of layering, not a designed semantic. The size shortcut dates to 2005, when working-tree bytes and blob bytes were identical and a size mismatch mathematically proved modification. Content conversion (autocrlf 2007,
text/eolattributes 2010) invalidated that invariant — "physically different" stopped implying "different," which is the entire premise of the conversion feature. The fast path was never re-audited against the feature layered on top of it. The result is git contradicting itself:statussays modified;addanddiff, running the real comparison, say identical.The 2005 performance justification no longer holds in 2026. The extra work is bounded and tiny: it applies only to files that (a) have conversion active AND (b) changed size since the last index refresh — in a typical status, a handful of small text files, on SSDs, often under fsmonitor. Reading and cleaning those few files is unmeasurable next to what status already does.
The affected population is large and the annoyance is chronic. Everyone working across Windows and Linux with normalized line endings — arguably the majority of professional users — sees phantom modifications whenever any tool (javadoc, formatters, generators, editors) rewrites unchanged files in native line endings. The failure mode is maximally confusing: files flagged modified with empty diffs. The documented remedy (
--renormalize/ re-add) does not stick: every checkout, stash, or branch switch rewrites the checkout-form bytes and resets the cached sizes, so the next generator run re-flags everything.A fix affects no one who doesn't currently experience the problem. Proposal: when a path has conversion active (the predicate already exists in
convert.c) and the size check fires, fall through to the content check — exactly what the mtime branch already does — instead of returning early. Paths without conversion keep the identical fast path, byte-for-byte; repositories that never see the bug never pay a cycle. If even that is unacceptable as a default, gate it behind a config (e.g.core.eolAwareStatusor an extension ofcore.checkStat) so those of us affected can opt in. An optional micro-optimization: only take the slow path when the size delta is consistent with EOL inflation (0 < delta <= line count of the indexed blob), which filters out virtually all genuine edits before any read happens.Expected behavior
git status,git diff, andgit addshould agree on whether a file is modified. If the clean filter defines content identity foraddanddiff,statusshould not report a contradictory answer purely because it declined to run the filter.Actual behavior
git statusreports files as modified based on the physical size of bytes the clean filter is configured to normalize away, producing modified files with empty diffs thatgit addrefuses to stage.