Skip to content

fix(update): repair managed checkouts still running core.autocrlf=true - #74487

Merged
OutThisLife merged 1 commit into
mainfrom
bb/update-eol-churn
Jul 30, 2026
Merged

fix(update): repair managed checkouts still running core.autocrlf=true#74487
OutThisLife merged 1 commit into
mainfrom
bb/update-eol-churn

Conversation

@OutThisLife

Copy link
Copy Markdown
Collaborator

A Windows install from May 2026 still runs the May install.ps1 no matter how many times it updates. That means the core.autocrlf=false pin added in #67730 never reaches the installs that need it most, and those checkouts keep every tracked text file rewritten to CRLF in the working tree. hermes update ships with the checkout itself, so it is the only path left that can repair them.

Why the existing fix can't get there

install.ps1 pins core.autocrlf=false on the managed clone in all three paths — in-place update, ZIP-init, and the shared post-clone block. That landed 19 Jul 2026. It is correct, and it is unreachable for anyone who installed earlier:

hermes-setup.exe bakes BUILD_PIN_COMMIT into the binary and resolves install.ps1 from it. A commit pin is treated as immutable, so cache_plan returns Reuse and the installer executes its cached script forever:

pub(crate) fn cache_plan(immutable: bool, cached_exists: bool) -> CachePlan {
    if immutable && cached_exists {
        CachePlan::Reuse

From a real user's bootstrap-installer.log, three separate runs today:

[bootstrap] using cached install.ps1 for 3ef97a61b9f1
[bootstrap] script …\bootstrap-cache\install-3ef97a61b9f1….ps1 via cached

That script is dated 31 May and contains no autocrlf handling at all. Their .git/config has no pin, so the portable Git's system config wins:

$ cat ~/.hermes/git/etc/gitconfig
[core]
	autocrlf = true

Result: 6,382 tracked files reading as modified against origin/main with insertions matching deletions almost exactly, and a real diff of two regenerable artifacts once --ignore-cr-at-eol is applied.

Why the pin and the cleanup are one operation

This is the part that makes a naive version of this fix a regression rather than a fix.

Under autocrlf=true git compares normalized content, so a CRLF working tree reads clean. Pinning autocrlf=false on its own does not repair anything — it reveals thousands of files as modified, and the very next hermes update autostashes and pops the entire tree. Strictly worse than the state it set out to fix.

So _normalize_managed_eol does both, in an order that cannot half-apply:

  1. Evaluate the tree as it would look pinned, via git -c core.autocrlf=false — nothing persisted yet.
  2. Restore only the files that are clean under --ignore-cr-at-eol.
  3. Write the persistent pin only after re-checking that the churn is gone.

A checkout that cannot be fully normalized — a git too old for --pathspec-from-file, an unwritable tree, a file locked by a running process — is left exactly as it was found. There is no rollback path because nothing is committed until it is known to be safe.

Two details worth calling out: files still dirty under --ignore-cr-at-eol are never touched, so a real edit survives even when it also got renormalized; and the restore takes its pathspec over stdin, because a fully renormalized checkout is thousands of paths, well past the Windows command-line limit.

It sits next to _discard_lockfile_churn at the same point in _cmd_update_impl, before any stash or branch logic, on the same reasoning: machine-generated dirt on a managed checkout should be cleared, not autostashed.

Verification

9 tests in tests/hermes_cli/test_update_eol_churn.py, driving real git repos in the broken state — LF index, CRLF worktree written by git's own checkout so the stat cache reproduces the churn-reads-clean condition, autocrlf=true still set.

Covered: churn invisible under autocrlf=true is still found and cleared; the pin is persisted; a real edit that also got renormalized survives; the pin is withheld when the restore cannot finish; autocrlf=input and already-pinned checkouts are left alone; 1,200 files normalize in one pass.

Mutation-checked, each caught by the intended test:

Mutation Test that fails
drop the withhold-pin guard test_pin_is_withheld_when_the_churn_cannot_be_cleared
restore with checkout -- . test_real_edits_survive_even_when_line_endings_also_flipped
scan without evaluating the tree as pinned test_churn_invisible_under_autocrlf_true_is_still_found + 4 others

288 tests pass across tests/hermes_cli/ -k 'update or install'.

The failure the restore-shim stands in for is exercised through a git wrapper that fails only on checkout, rather than a broken git_cmd. A broken git_cmd would fail the first config --get and return early, passing the test without ever reaching the guard.

Git for Windows ships core.autocrlf=true in its system config, which
renormalizes this repo's LF text files to CRLF in the working tree.
install.ps1 pins core.autocrlf=false on the managed clone for that reason
(#67730), but a checkout created before that landed never got the pin --
and cannot get it, because hermes-setup.exe resolves install.ps1 by an
immutable build-time commit pin and reuses the cached script forever. A
Windows install from May 2026 still runs the May install.ps1 no matter how
many times it updates. `hermes update` ships with the checkout itself, so
it is the only path left that reaches those installs.

The pin and the cleanup have to be one operation. Under autocrlf=true git
compares normalized content, so a CRLF working tree reads clean; pinning
alone would expose every tracked text file as modified and hand the very
next update an autostash and pop of the whole tree -- strictly worse than
the state it set out to fix. So the tree is evaluated as it would look
pinned (git -c, nothing persisted), the files whose only difference is the
line ending are restored, and the pin is written only once that is
verified clean. A checkout we cannot fully normalize is left exactly as it
was found.

Files still dirty under --ignore-cr-at-eol are never touched, so a real
edit survives even when it also got renormalized. The restore takes its
pathspec over stdin because a fully renormalized checkout is thousands of
paths, well past the Windows command-line limit.
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on e65ff96

ℹ️ Info

Desktop E2E visual evidence · View test artifacts · View job

1 visual diff.

inline evidence upload failed.

Failed to upload diff-665a0833239e-onboarding-overlay-diff.png with gh image (exit code 1): Error uploading /home/runner/work/_temp/e2e-evidence/diff-665a0833239e-onboarding-overlay-diff.png: step 0 (get upload token): uploadToken not found on repo page — do you have write access to NousResearch/hermes-agent? (or, if NousResearch enforces SAML SSO, authorize at https://github.com/orgs/NousResearch/sso)

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/cli CLI entry point, hermes_cli/, setup wizard platform/windows Native Windows-specific behavior or breakage area/install-update Installer, updater, packaging, wheels, doctor sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows labels Jul 30, 2026
@OutThisLife
OutThisLife merged commit 5e80739 into main Jul 30, 2026
41 checks passed
@OutThisLife
OutThisLife deleted the bb/update-eol-churn branch July 30, 2026 09:53
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…-churn

fix(update): repair managed checkouts still running core.autocrlf=true
33hodl pushed a commit to 33hodl/hermes-agent that referenced this pull request Aug 12, 2026
…-churn

fix(update): repair managed checkouts still running core.autocrlf=true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/install-update Installer, updater, packaging, wheels, doctor comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists platform/windows Native Windows-specific behavior or breakage sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants