fix: preserve trailing newlines in text-based 3-way merges - #2891
fix: preserve trailing newlines in text-based 3-way merges#2891jorrite wants to merge 5 commits into
Conversation
TextMerger.Merge() delegates to epiclabs-io/diff3, whose line reader strips every line's trailing newline (via bufio.Scanner) and whose join step never restores one - so a genuinely unchanged file (and any real edit) lost its trailing newline unconditionally on `atmos scaffold generate --update`. matchTrailingNewline() now restores the reference's (theirs') exact trailing-newline count rather than a binary "append one \n" - this also correctly handles a blank line at EOF (2+ trailing newlines), and makes a separate no-op short-circuit unnecessary: when nothing meaningful changed, ours == theirs, so matching theirs' newlines already reconstructs ours byte-for-byte.
…fore passing to diff3 to let it cancel the separator we just added
|
Tip Atmos Pro
No affected stacks workflow was detected for this pull request. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthrough
ChangesTrailing-newline preservation
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related issues
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@pkg/generator/merge/text_merger_test.go`:
- Around line 820-839: Extend the merge test cases around the existing
genuine-change scenarios to cover template-only EOF newline changes: keep base
and ours identical, and verify theirs both adds and removes trailing newlines,
including “line 1\n” to “line 1\n\n” and “line 1”. Set each expected result to
preserve theirs’ newline count.
- Around line 756-761: Add a Go doc comment beginning with “Merge” immediately
before the exported TextMerger.Merge method, documenting its purpose and the
newline behavior referenced by TestTextMerger_TrailingNewlinePreservation. Keep
the existing test comment unchanged.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: a8cfbfe6-71e9-4064-8143-67c721c93c00
📒 Files selected for processing (2)
pkg/generator/merge/text_merger.gopkg/generator/merge/text_merger_test.go
what
atmos scaffold generate --updateunconditionally stripping the trailing newline from every file it 3-way-merges, whether or not the file actually changed.pkg/generator/merge/text_merger.go:TextMerger.Merge()now appends onenewlineSeparator("\n") to each ofours/base/theirsbefore handing them todiff3.Merge, sodiff3's guaranteed loss of exactly one trailing newline cancels out and the original count survives.pkg/generator/merge/text_merger_test.go: consolidated the trailing-newline regression coverage into a single table-driven test,TestTextMerger_TrailingNewlinePreservation, asserting exact byte-for-byte output across a no-op merge (0/1/2/3 trailing newlines, plus an internal blank line) and a genuine template change (theirs with 0/1/2 trailing newlines).ConflictStrategyhandling — out of scope, and unaffected since the appended newline is identical across all three inputs.why
TextMerger.Merge()delegates the actual 3-way merge toepiclabs-io/diff3, which reads each ofbase/ours/theirsline-by-line viabufio.Scanner(ScanLines, Go's standard-library default split function) and rejoins the merged lines withstrings.Join(lines, "\n").ScanLinesstrips every line's terminator — including the last — and gives no way to tell afterward whether the original input ended with a trailing newline or not. Concretely: for content ending in N trailing newlines, the round-trip throughGetLines+Joinalways reconstructs exactly N-1 (it loses exactly one, regardless of how many there were; for N = 0 there was nothing to lose in the first place). Verified directly: generating a file with 3 trailing newlines and running--updatewith nothing changed on the template side reproducibly comes back with 2.theirs: appending it only totheirsmakes an otherwise-identicalours/theirspair (a very common no-op shape) differ by one trailing newline as far asdiff3is concerned, which turns a no-op into a spurious detected change/conflict instead of fixing anything.references
Summary by CodeRabbit
Bug Fixes
Tests