fix(update): detect EOL-only churn via numstat, not name-only
_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.
This commit is contained in:
@@ -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: "<added>\t<deleted>\t<path>". 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
|
||||
|
||||
Reference in New Issue
Block a user