Skip to content

Fix hosted gem block mixing CRLF Gemfile endings (#1831) - #1841

Merged
Mikola Lysenko (mikolalysenko) merged 4 commits into
mainfrom
agent/fix-hosted-gemfile-crlf-block
Nov 2, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 4 commits into
mainfrom
agent/fix-hosted-gemfile-crlf-block

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #1831

Summary

scan --mode hosted / get --mode hosted over a CRLF Gemfile (a Windows checkout, or core.autocrlf=true) wrote the Socket source "…" do … end block with bare LF endings, so the committed Gemfile ended up with mixed line endings. Now:

  • In-place rewrite of a gem line (plain, with options, parenthesized, inside the user's own source … do block): the block takes the ending of the line it replaces. The line's CR goes back at the end of the block, not inside it. An unterminated last line takes the file's ending, and a lone trailing CR at EOF is no longer read as a line ending.
  • Appended block for a transitive gem: takes the file's ending (line_endings::terminator, so the majority wins on a mixed file), for both the separator and the block.
  • Rollback / remove (upstream/gem.rs restore_manifest) now restores the declaration with the block's own ending. Before, it used CRLF whenever the file held any CRLF line. An LF block (the only spelling written before this PR) takes the ending of the lines around it, so existing redirected projects restore the same as before.

This is the hosted-mode twin of #1821 / #1822 (vendored mode).

Root cause

rewrite_gem in crates/socket-patch-core/src/patch/redirect/mod.rs built the block from a literal with hard-coded \n in both arms (in-place and append). The in-place arm's gem_line_re match also ran up to the line's CR, so the CR was dropped and the block ended in the original line's bare LF.

The recognizers already accept \r?\n: the re-run idempotency guard, gem_spelling_residue, the restore's socket_blocks, and VEX's parse_source_blocks (via str::lines). So a CRLF block is recognized everywhere, and the e2e test below confirms that against real bundler.

No wrapper changes: npm/, pypi/ and gem/ only dispatch to the binary.

Tests (red → green)

Issue facet Test Without fix With fix
#1831 single-line decl, decl with options + comment, gem(…), decl inside a user source … do block, unterminated last line, lone trailing CR, transitive append (terminated and not), plus a re-run no-op for each core patch::redirect::tests::gemfile_source_block_keeps_crlf_line_endings FAIL: mixed line endings: "…\r\nsource \"https://patch.test/…\" do\n gem …\nend\n" pass
LF Gemfile stays LF; an LF line in a CRLF file keeps its LF core gemfile_source_block_keeps_lf_line_endings pass (guard) pass
#1831 real bundler: CRLF Gemfile (both the issue's Driver::ScanVex and Driver::ScanVexSourceBlock shapes) stays CRLF after hosted scan, converging bundle install passes, re-scan is byte-identical, rollback gives back a CRLF Gemfile that declares the gem cli e2e_redirect_gem_build::gem_hosted_redirect_keeps_a_crlf_gemfile_crlf (--ignored, bundler 4.0.18 / Ruby 3.3.6 locally; runs in the gem e2e CI legs) FAIL: crlf-gemfile: the redirected Gemfile mixes line endings: "source \"http://127.0.0.1:…/upstream\"\r\nsource \"…/patch-registry/gem/…\" do\n gem \"vuln-gem\", \"1.0.0\"\nend\n" pass

One existing test changed its expectation: gemfile_single_line_declaration_lookalikes_still_rewrite has a CRLF declaration in an LF file (gem "vuln-gem", require: false\r). It pinned the old LF block and now expects that line's CRLF. That is the intended rule: the block takes the ending of the line it replaces.

Commands run locally:

  • cargo fmt --all -- --check: clean
  • cargo clippy --workspace --all-features -- -D warnings: clean
  • cargo test -p socket-patch-core --lib: 6992 passed, 4 failed. All 4 are the known permission-denial tests that can't fail a write as root in this sandbox (copy_tree::relax_loop_must_not_traverse_symlinked_root, vlt_heal::an_unremovable_hidden_lock_keeps_every_store_entry, pypi_poetry::wire_write_failure_…, pypi_requirements::wire_failure_rolls_back_…). None touch gem code.
  • cargo test -p socket-patch-cli --all-features --test e2e_redirect_gem_build --test e2e_vendor_gem_build (--ignored): 38 + 11 passed. Without --ignored: 21 + 35 passed.
  • I couldn't finish the full cargo test --workspace --all-features: the sandbox ran out of disk while linking the CLI test binaries. CI is the full-suite gate.

Per-issue checklist

/code-review high

Fixed:

  • Rollback chose its ending by "any CRLF in the file" while the forward pass now keeps the replaced line's ending. Rollback now follows the block's own ending.
  • The recorded FileEdit.new left out the CR that was actually written. It now records the exact spliced text.
  • A lone trailing CR at EOF was treated as a CRLF ending and left a stray CR. Now covered by a test.
  • Stale "the rewriter emits LF" comments are updated, and the unit test uses LineEndings::of instead of a hand-rolled counter.

Not changed:

https://claude.ai/code/session_011f9aeRvMuTRkgqjmDvPu3j


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
A hosted scan or get over a CRLF Gemfile (a Windows checkout, or
core.autocrlf=true) wrote the Socket `source ... do` block with bare
LF endings, so the committed Gemfile ended up with mixed line endings
and churned under editors and .gitattributes normalization.

The rewritten block now takes the ending of the gem line it replaces
(the file's ending for an unterminated last line), and the block
appended for a transitive gem takes the file's ending. This matches
what vendored mode does since #1822.

Fixes #1831

Assisted-by: Claude Code:claude-opus-5-5
Rollback restored a hosted gem declaration with CRLF whenever the
Gemfile held any CRLF line. It now takes the ending of the Socket block
it replaces, which the redirect writes in the ending of the original
line, so a mixed-ending Gemfile round-trips. A gem line that ends in a
lone CR at end of file no longer leaves a stray CR, and the recorded
edit now shows exactly what was written.

Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 29, 2026 17:09
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 210c955. Configure here.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 29, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Burn-down agent: labeled Ready for review at 210c955b (full: see head).

  • CI: 32/32 run checks green or neutral (50 skipped by path filters); mergeable, no conflicts. The one neutral row is a cancelled Bugbot run; the re-requested Bugbot review on this head reported no issues.
  • Bugbot: reviewed 210c955, no findings.
  • For the reviewer: rollback now picks the Socket block's line ending from the block itself; on mixed-ending Gemfiles it follows the surrounding lines (see "Not changed" in the description).
  • Slack announcement not sent this run (Slack send unavailable); the next run retries.

Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review brief (210c955)

What it does. A hosted gem redirect over a CRLF Gemfile (Windows checkout, core.autocrlf=true) wrote the Socket source "…" do … end block in bare LF, leaving a mixed-ending Gemfile. Now the block takes the ending of the gem line it replaces (or the file's ending when appended for a transitive gem), and rollback restores the declaration with the block's own ending. Hosted-mode twin of #1821/#1822. Fixes #1831.

Risk: low. Only the line endings of text the rewriter writes change. Every recognizer already accepted \r?\n, and an LF block written by an older release restores exactly as before.

Look here

  • redirect/mod.rs:8016: picking the ending for an in-place rewrite. The line's CR moves to the end of the block; an unterminated last line (a lone trailing CR included) takes the file's ending.
  • redirect/mod.rs:8111: splicing the block, and the recorded FileEdit.new now includes the CR.
  • redirect/mod.rs:8156: the append arm now uses the majority ending (line_endings::terminator). Before, any CRLF in the file picked CRLF. The two only differ on a mixed file.
  • upstream/gem.rs:1180:`` restore_manifest uses the block's ending, falling back to the surrounding text's.
  • Tests: mod.rs:18049 (8 CRLF shapes, each with a re-run no-op) and the real-bundler e2e e2e_redirect_gem_build.rs:4781.```

Verified

  • Read the full diff.
  • cargo test -p socket-patch-core --lib -- gemfile gem:: line_endings: 334/334 pass.
  • CI on 210c955: 82 check runs, 0 failing (31 success, 50 skipped, 1 neutral, which is the Bugbot check).
  • Cursor Bugbot reviewed 210c955 and found no issues; there are no review threads.
  • CHANGELOG.md is untouched.

Changes I made: none.

Open questions: none blocking. One small note: a lone CR at the very end of the file is dropped by the rewrite, and rollback doesn't put it back. That's intended and pinned by a test.

Auto-merge is armed, so approving sends this straight to the merge queue.


Generated by Claude Code

@mikolalysenko
Mikola Lysenko (mikolalysenko) added this pull request to the merge queue Nov 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[agent] Reviewed 210c955b3813d9b5c0d4a3916b0a9c66e3a819ea. Checked CRLF in-place/appended Gemfile blocks and restore line-ending handling. No major correctness issues found. Validation: source and regression-test review plus the available exact-head CI checks; no local test pass is claimed.

Merged via the queue into main with commit 3595c58 Nov 2, 2026
82 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the agent/fix-hosted-gemfile-crlf-block branch November 2, 2026 15:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hosted gem redirect writes the Socket source … do block with LF endings into a CRLF Gemfile, leaving it with mixed line endings

3 participants