Skip to content

git status reports phantom modifications for files under line-ending conversion because the size fast-path skips the clean filter — please re-evaluate this trade-off #6410

Description

@Curtis-Allen-Smith

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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions