fix: stop a partial selection at a no-newline EOF from merging two lines - #28
Open
ashproto wants to merge 2 commits into
Open
fix: stop a partial selection at a no-newline EOF from merging two lines#28ashproto wants to merge 2 commits into
ashproto wants to merge 2 commits into
Conversation
When the last line of a file with no trailing newline changes, git emits a
`-`/`+` pair where BOTH sides carry `\ No newline at end of file`. The marker
claims the line before it ends the image it belongs to. `build_partial_hunk`
kept that marker whenever the preceding line had been emitted — including when
a demoted-to-context line was emitted after it, which makes the claim false.
`git apply` accepts the contradiction and resolves it by concatenating. With
base "a\nt" and worktree "a\nT2", discarding just the deletion produced
"a\ntT2" — the two lines merged onto one, Ok(()) returned, on the path with no
reflog and no backup bundle. Pre-existing: reproduces against HEAD.
The two directions are not symmetric, so they get different answers.
Reversing (discard/unstage) keeps the other half as context on the side that
still ends there, so dropping the stale marker expresses the intent exactly —
verified against real git, the file becomes "a\nt\nT2".
Going forward (stage) would need the demoted line newline-terminated on the new
side and not on the old. One context line cannot say that, so no patch
expresses it and it is now refused, in the same spirit as require_contiguous.
Whole-hunk ops replay git's own hunk verbatim and are unaffected, so staging
the whole hunk remains available.
Deciding the marker's fate needs to know what follows it, so the emitter now
tracks what the previous line was emitted AS rather than merely that it was,
and build_partial_hunk returns Result<Option<String>, String> to carry the
refusal. Callers keep their own "no lines selected" messages.
partial_hunk_no_newline_marker asserted the buggy output — its comment narrated
the mechanism ("last_emitted=true -> marker kept"). That exact patch shape was
confirmed against real git to stage "oldnew" instead of "old\nnew", so the
first half now expects the refusal.
cargo test -p git-core 224 passed; npm run check 530 files 0 errors;
npm test 386 passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#26 gained four commits while this sat stacked on it — the mode-change and intent-to-add discard fixes, the diff-prefix pin, and a merge of `next`. Two conflicts, both in ops_worktree.rs: - `discard_lines`: this branch made `build_partial_hunk` return a Result (`?`), #26 routed the header through `discard_header`. Kept both. - The tests block: both sides appended tests at the same point and shared one trailing brace. Kept every test from both, closing the first block explicitly. cargo test -p git-core 229 passed — all four no-newline-EOF tests and all five of #26's discard tests green together; clippy clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #26 — base is
feat/diff-hunk-affordance, so this shows only its own commit.discard_linesandrequire_contiguousdo not exist onnextyet; both arrive with #26. Merge order: #25 → #26 → this.Found by a review pass on #26 and deliberately kept out of it — this is pre-existing and reproduces byte-for-byte against
HEAD.The bug
When the last line of a file with no trailing newline changes, git emits a
-/+pair where both sides carry\ No newline at end of file:The marker claims the line before it ends the image it belongs to.
build_partial_hunkkept it whenever the preceding line had been emitted — including when a demoted-to-context line was emitted after it, which makes the claim false.git applyaccepts the contradiction and resolves it by concatenating.With base
"a\nt"and worktree"a\nT2", discarding just the deletion produced"a\ntT2"— the two lines merged onto one — returningOk(()), on the path with no reflog and no backup bundle.The two directions are not symmetric
Ok+"a\ntT2"Ok+"a\ntT2"Ok+"a\nt\nT2"— correctErr"patch does not apply"tas a-/+pairReversing keeps the other half as context on the side that still ends there, so dropping the stale marker expresses the intent exactly — verified against real git.
Going forward would need the demoted line newline-terminated on the new side and not on the old. One context line cannot say that, so no patch expresses it. It is now refused, in the same spirit as
require_contiguous. Whole-hunk ops replay git's own hunk verbatim and are unaffected, so staging the whole hunk remains available as the escape hatch.Implementation
Deciding a marker's fate needs to know what follows it, so the emitter now tracks what the previous line was emitted as (context /
-/+) rather than merely that it was, plus the first ordinal after it.build_partial_hunkreturnsResult<Option<String>, String>to carry the refusal; callers keep their own "no lines selected" messages.That is ~30 lines rather than the ~8 a marker-only tweak would need. The extra buys one source of truth: a separate guard function would have to re-simulate the emitter's drop/demote rules and could drift from them.
A test that asserted the bug
partial_hunk_no_newline_markerasserted the corrupt output as correct, and its comment narrated the buggy mechanism approvingly:It was written against the implementation rather than the behavior, so it passed happily while the code merged two lines. I reconstructed the exact patch that shape emits and ran it through real git before touching it — it stages
oldnewinstead ofold\nnew. Only then did I flip the assertion, with the proof recorded in the test comment.Tests (TDD — all four watched fail first)
discard_lines_at_no_newline_eof_keeps_lines_separateOk+"a\ntT2"Ok+"a\nt\nT2"unstage_lines_at_no_newline_eof_keeps_lines_separateOk+"a\ntT2"Ok+"a\nt\nT2"stage_lines_at_no_newline_eof_refuses_rather_than_corruptingOk+ corruptErr, index and worktree untouchedwhole_hunk_ops_at_no_newline_eof_are_unaffectedrequire_contiguousis untouched and remains the independent defence against a gapped selection reordering the file.Gates:
cargo test -p git-core224 passed ·npm run check530 files / 0 errors ·npm test386 passed.🤖 Generated with Claude Code