Skip to content

check-shebang-eol's remediation message repairs the index only — and lands the developer at git status clean with a still-CRLF file #1809

Description

@bradygaster

Follow-up from Flight's ⚠️ gate on #1805. The gate's detection is sound — this is the remediation text only. But it is worse than "incomplete": the advised sequence lands the developer in a state where git status reports clean while the file on disk is still CRLF, so the original defect (dead suite, broken shebang) survives with no remaining signal.

Current text (scripts/check-shebang-eol.mjs, merged dev)

console.error(`  ${unpinned.length > 0 ? '2' : '1'}. Run: git add --renormalize -- <path>   (a rule alone does not rewrite committed blobs)`);

That repairs the index. It does not repair the working tree. Result: i/lf w/crlf — the #1793 condition.

Measured, not reasoned

Scratch repos reproducing the exact production condition (CRLF blob committed with no rule, then *.js text eol=lf added in a second commit, pre-existing working tree). Blob state read as raw bytes in Node — never pipe git cat-file through PowerShell, it re-encodes line endings and gives a confidently wrong answer.

# Sequence HEAD index worktree git status
A git add --renormalize ← what we ship CRLF LF CRLF M partial
B A + git checkout -- <path> CRLF LF CRLF M silent no-op
C A + commit + git checkout -- <path> LF LF CRLF (clean) silently wrong
H A + commit + git checkout --force -- <path> LF LF CRLF (clean) --force does not force
E A + commit + delete file + git checkout LF LF LF (clean) ✅ repaired
F A + commit + git rm --cached -r . + git reset --hard LF LF LF (clean) ✅ repaired
G A + delete file + git checkout (no commit) CRLF LF LF M ✅ repaired, uncommitted

Three findings, in increasing order of nastiness

  1. git checkout -- <path> is a no-op here (row B). It looks like the obvious second step. Git skips the write because the stat cache says the file is current.
  2. --force does not force it either (row H). This is the one nobody would guess, and it is the natural escalation after B fails.
  3. Once the blob is LF, git status is structurally incapable of reporting a CRLF worktree (rows C/H). The clean filter normalizes CRLF→LF on the way in, so git compares LF-to-LF and sees no difference. The remediation we ship converts a visible, permanently-dirty file into an invisible broken one. That is a strict downgrade — the churn was annoying but at least it was a signal.

Only deleting the file first works. That is the single non-obvious step, and it is absent from the message.

Proposed replacement

console.error(`  ${unpinned.length > 0 ? '2' : '1'}. Repair the committed blob:`);
console.error('       git add --renormalize -- <path>');
console.error(`  ${unpinned.length > 0 ? '3' : '2'}. Force the working tree to be rewritten -- the step above fixes`);
console.error('     the index only, and the working tree keeps its CRLF copy:');
console.error('       rm <path> && git checkout -- <path>');
console.error('     Deleting first is REQUIRED. `git checkout` alone is a silent no-op (stat');
console.error('     cache), and `--force` does not override it. Once the blob is LF, `git');
console.error('     status` reports clean whether the working tree is CRLF or not -- the clean');
console.error('     filter normalizes on read -- so skipping this step leaves a broken file');
console.error('     with no remaining signal. Verify with a byte check, not `git status`.');

Verification that is actually valid (PowerShell-safe):

node -e "const b=require('fs').readFileSync(process.argv[1]);console.log([...b].filter((c,i)=>c===10&&b[i-1]===13).length+' CRLF')" <path>

Why this belongs on the record beyond the text fix

This is tooling reproducing the defect it was built to catch — third occurrence in this family (the CRLF-shebang lint authored with CRLF; check-shebang-eol's enumeration blind to its own churn class, #1804; now its remediation manufacturing the invisible variant). It is also the sixth instance of a silent no-op masquerading as success in this area, alongside --renormalize staging nothing for the LF-blob/CRLF-worktree mirror case (*.snap).

The generalization worth keeping: for text eol=lf files, git status is not a valid oracle for working-tree line endings in either direction. Any runbook step, gate message, or repair block in this repo that verifies with git status is verifying nothing. Byte-count or blob-SHA only.

Related: #1793 (same condition, reached a different way), #1804, #1805, #1790.

Metadata

Metadata

Assignees

No one assigned

    Labels

    squadSquad triage inbox — Lead will assign to a membersquad:boosterAssigned to Booster (CI/CD Engineer)

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions