From ff6ed7c491ba285178a2fdc0975d619aefae7035 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sat, 1 Aug 2026 14:35:35 -0700 Subject: [PATCH] fix(update): detect EOL-only churn via numstat, not name-only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _normalize_managed_eol isolated line-ending churn from real edits by diffing twice: all dirty files minus files still dirty under --ignore-cr-at-eol. But 'git diff --name-only --ignore-cr-at-eol' computes its file list from blob/stat differences BEFORE the CR filter is applied, so it still lists CR-only files. On git 2.48.1 the two name-only sets are therefore identical, _eol_only() is always empty, and a managed Windows checkout gets pinned to core.autocrlf=false with the whole CRLF tree left dirty — breaking the next 'git checkout' on update (the exact failure this function exists to prevent). Compute the real-edit set with 'git diff --numstat --ignore-cr-at-eol' instead: numstat honors the CR filter (a CR-only file produces no record), so eol-only files are correctly identified and cleared while genuine edits are preserved. Pin core.quotepath=false so non-ASCII paths parse. Verified at 1200 files: 1199 eol-only normalized, one real edit preserved, autocrlf pinned only after the tree reads clean. This was a pre-existing failure on main (test_update_eol_churn's test_churn_across_more_files_than_fit_in_one_argv failed deterministically on git 2.48.1), surfaced while landing unrelated file-tools PRs. --- hermes_cli/update_cmd.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/hermes_cli/update_cmd.py b/hermes_cli/update_cmd.py index d5e1c445f4..95982c0f28 100644 --- a/hermes_cli/update_cmd.py +++ b/hermes_cli/update_cmd.py @@ -3474,8 +3474,36 @@ def _normalize_managed_eol(git_cmd, repo_root): return None return {p for p in out.stdout.split("\0") if p} + def _real_dirty(): + # Files with a *content* change once CRLF differences are ignored. + # NOTE: ``diff --name-only --ignore-cr-at-eol`` still LISTS CR-only + # files (the name list is computed from blob/stat differences before + # the CR filter is applied), so it cannot be used to isolate real + # edits. ``--numstat`` does honor the filter: a CR-only file produces + # no numstat record, while a genuinely-edited file does. Parse the + # paths out of numstat instead. + out = subprocess.run( + probe + ["-c", "core.quotepath=false", + "diff", "--numstat", "--ignore-cr-at-eol"], + cwd=repo_root, + capture_output=True, + text=True, encoding="utf-8", errors="replace", + ) + if out.returncode != 0: + return None + paths = set() + for line in out.stdout.splitlines(): + if not line.strip(): + continue + # Format: "\t\t". Rename detection is off in + # plain diff, so there is exactly one path field per record. + parts = line.split("\t", 2) + if len(parts) == 3 and parts[2]: + paths.add(parts[2]) + return paths + def _eol_only(): - all_dirty, real_dirty = _dirty(), _dirty("--ignore-cr-at-eol") + all_dirty, real_dirty = _dirty(), _real_dirty() if all_dirty is None or real_dirty is None: return None return all_dirty - real_dirty