git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH 0/2] status: agree with diff and add when conversion is active

From
CSCurtis Allen Smith <curtis.allen.smith@gmail.com>
Date
Oct 8, 2026, 20:45 UTC
Message-ID
<20261008204603.1988-1-curtis.allen.smith@gmail.com>

"git status", "git diff" and "git add" can disagree about whether a file has been modified. Under

	* text eol=lf

a tool that rewrites an otherwise unchanged file with CRLF endings makes "git status" report it as modified, while "git diff" shows nothing and "git add" stages nothing: the two commands that actually run the clean filter both conclude that the contents did not change. That contradiction, rather than the line endings as such, is what this series is about.

The cause is the size comparison in ie_match_stat(). ie_modified() takes a difference between the file's size and the size recorded in the index as proof of a content change and returns without reading the file. That was sound in 2005, when the working tree file and the blob were the same bytes. Conversion made it unsound -- the whole point of a clean filter is that the two representations differ in their bytes and agree on their content -- and the shortcut was never revisited. The mtime branch of the very same function already reads the file and applies the conversion before deciding, so Git pays for the conversion-aware check in one branch and refuses to in the other.

Patch 1 makes the size branch behave like the mtime branch whenever the path is subject to conversion. Paths with no conversion take the existing early return untouched. It also stops ce_compare_data() hashing a file whose converted length already differs from the size of its blob, since equal contents must have equal length; that is where most of the cost of the new check would otherwise go, and it helps the pre-existing mtime path as well. When the end-of-line conversion is the only one that applies, that length comes from a scan for CR, and the file is not converted either.

Patch 2 adds core.convertAwareStatus for people who would rather keep the old shortcut, either everywhere (false) or only for paths with an expensive clean filter such as Git LFS (no-filter). I defaulted it to on, including filters: the measurements below say reading is not what costs, and the 2005 performance argument should not be re-applied in 2026 without evidence. Being ordinary configuration it also works per command, as "git -c core.convertAwareStatus=no-filter status".

Note that there is no way to opt out of the size comparison today -- core.checkStat=minimal drops ctime, uid/gid and inode but still compares the size -- which is why patch 2 adds a variable instead of extending an existing one.

Numbers -------

Linux (WSL2, ext4) on an i9-14900K, warm page cache, fastest of 5 runs, "status -uno" to separate the refresh from untracked scanning. One binary for both columns with core.convertAwareStatus flipped; "false" is the pre-series code path.

	10000 files x 2.6 KB, "* text=auto eol=lf"
	                                   false      true
	  clean tree                         4 ms      4 ms
	  10000 genuinely modified          19 ms     49 ms
	  10000 CRLF-rewritten, 1st run     20 ms    148 ms
	  the same, steady state            20 ms      6 ms
	200 files x 1 MB, same attributes
	                                   false      true
	  clean tree                         2 ms      2 ms
	  200 genuinely modified             2 ms     26 ms
	  200 CRLF-rewritten, 1st run        2 ms    611 ms
	  the same, steady state             2 ms      3 ms
	10000 files x 2.6 KB, no conversion configured
	  clean tree                         4 ms      4 ms
	  10000 genuinely modified          19 ms     20 ms

A clean tree and a repository without conversion are unaffected. What is paid for is stat-dirty converted paths. A file that was really edited is read and scanned for CR, but neither converted nor hashed, because its length already rules out a match. Without that the "genuinely modified" rows read 142 ms and 517 ms rather than 49 ms and 26 ms. Of the 214 MB in the second corpus, reading costs 6 ms from page cache, the conversion about 180 ms, and SHA-1 about 280 ms.

The last row of the first block is the case the series exists for: the patched build settles at 6 ms where the unpatched one pays 20 ms on every invocation and still reports the files as modified, because it never refreshes their recorded sizes. The 1st-run rows are the one-time cost of discovering that. Their lengths match, so they are converted and hashed in full.

This was reported against Git for Windows [1], where Torsten suggested bringing it to the list. It is not Windows-specific; anything with a clean filter runs into it, and Git LFS users on Linux see the same contradiction.

Built with gcc 15.2 on top of 6de20f6; each commit builds and passes on its own. t0020 (with the new tests), t0021, t0026, t0027, t1300, t2106, t2200, t3700, t7508 and t0008 pass.

[1] https://github.com/git-for-windows/git/issues/6410
Curtis Allen Smith (2):
  read-cache: do not trust a size change when conversion is active
  core: add core.convertAwareStatus to opt out of the content check
 Documentation/config/core.adoc |  22 +++++
 environment.c                  |  14 ++++
 environment.h                  |   7 ++
 read-cache.c                   | 141 ++++++++++++++++++++++++++++++++-
 t/t0020-crlf.sh                |  92 +++++++++++++++++++++
 5 files changed, 273 insertions(+), 3 deletions(-)
-- 
2.53.0
Next: Curtis Allen Smith
Message 1 of 5 in “status: agree with diff and add when conversion is active”
  1. 0/2 status: agree with diff and add when conversion is activeCurtis Allen Smith, Oct 8, 2026
  2. 1/2 read-cache: do not trust a size change when conversion is activeCurtis Allen Smith, Oct 8, 2026
  3. Junio C HamanoOct 9, 2026
  4. Curtis SmithOct 10, 2026
  5. 2/2 core: add core.convertAwareStatus to opt out of the content checkCurtis Allen Smith, Oct 8, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.