Skip to content

fix: preserve trailing newlines in text-based 3-way merges - #2891

Merged
Erik Osterman (Cloud Posse) (osterman) merged 8 commits into
cloudposse:mainfrom
jorrite:fix-scaffold-update-text-newline
Aug 11, 2026
Merged

Erik Osterman (Cloud Posse) (osterman) merged 8 commits into
cloudposse:mainfrom
jorrite:fix-scaffold-update-text-newline

Conversation

@jorrite

@jorrite Jorrit Elfferich (jorrite) commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

what

  • Fix atmos scaffold generate --update unconditionally 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 one newlineSeparator ("\n") to each of ours/base/theirs before handing them to diff3.Merge, so diff3'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).
  • No change to conflict detection, threshold behavior, or ConflictStrategy handling — out of scope, and unaffected since the appended newline is identical across all three inputs.

why

  • TextMerger.Merge() delegates the actual 3-way merge to epiclabs-io/diff3, which reads each of base/ours/theirs line-by-line via bufio.Scanner (ScanLines, Go's standard-library default split function) and rejoins the merged lines with strings.Join(lines, "\n").
  • ScanLines strips 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 through GetLines + Join always 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 --update with nothing changed on the template side reproducibly comes back with 2.
  • Appending one newline to each input before the merge bumps every input's count to at least 1, so that guaranteed loss of exactly one cancels out and the original count is preserved — for both the no-op case and genuine changes, since whichever side's content ends up dominating a given region carries its own (now-restored) newline count through, independent of the others.
  • This must be applied to all three inputs, not just theirs: appending it only to theirs makes an otherwise-identical ours/theirs pair (a very common no-op shape) differ by one trailing newline as far as diff3 is concerned, which turns a no-op into a spurious detected change/conflict instead of fixing anything.

references

@jorrite
Jorrit Elfferich (jorrite) requested a review from a team as a code owner August 6, 2026 15:52
@atmos-pro

atmos-pro Bot commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

Tip

Atmos Pro  

No affected stacks workflow was detected for this pull request.
If this is expected, no action is needed.
Learn More. Ask AI.

@mergify mergify Bot added the triage Needs triage label Aug 6, 2026
@coderabbitai

coderabbitai Bot commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 03e7f89e-35e9-4740-98ba-2101e432875d

📥 Commits

Reviewing files that changed from the base of the PR and between 0a6b260 and 5451cea.

📒 Files selected for processing (1)
  • docs/fixes/2026-08-10-scaffold-update-trailing-newline.md
💤 Files with no reviewable changes (1)
  • docs/fixes/2026-08-10-scaffold-update-trailing-newline.md

📝 Walkthrough

Walkthrough

TextMerger.Merge now preserves trailing-newline counts by appending a newline to each diff3 input. Tests verify byte-exact output for unchanged and template-only merges. Documentation records the fix.

Changes

Trailing-newline preservation

Layer / File(s) Summary
Text merge and validation
pkg/generator/merge/text_merger.go, pkg/generator/merge/text_merger_test.go, docs/fixes/...
TextMerger.Merge appends a newline to each diff3 input. Tests cover trailing-newline and blank-line states. Documentation describes the updated merge behavior and validation.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preserving trailing newlines in text-based 3-way merges.
Linked Issues check ✅ Passed The implementation and regression tests address issue #2887 by preserving trailing newlines for unchanged and updated text merges.
Out of Scope Changes check ✅ Passed The implementation, tests, and fix documentation directly support issue #2887 and introduce no unrelated changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 3ce4349 and ecfc8db.

📒 Files selected for processing (2)
  • pkg/generator/merge/text_merger.go
  • pkg/generator/merge/text_merger_test.go

Comment thread pkg/generator/merge/text_merger_test.go
Comment thread pkg/generator/merge/text_merger_test.go
@github-actions github-actions Bot added the size/m Medium size PR label Aug 6, 2026
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 6, 2026
@mergify mergify Bot removed the triage Needs triage label Aug 6, 2026
@osterman Erik Osterman (Cloud Posse) (osterman) added the patch A minor, backward compatible change label Aug 7, 2026
@osterman

Copy link
Copy Markdown
Member

Please run the /fix-log to the update fix log (claude skill).

@codecov

codecov Bot commented Aug 8, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.82%. Comparing base (21c5899) to head (cb66a31).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #2891      +/-   ##
==========================================
- Coverage   82.83%   82.82%   -0.02%     
==========================================
  Files        1866     1866              
  Lines      181277   181277              
==========================================
- Hits       150160   150135      -25     
- Misses      23313    23329      +16     
- Partials     7804     7813       +9     
Flag Coverage Δ
unittests 82.82% <100.00%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
pkg/generator/merge/text_merger.go 89.38% <100.00%> (ø)

... and 12 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 10, 2026
@atmos-pro

atmos-pro Bot commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Tip

Atmos Pro  

No affected stacks workflow was detected for this pull request.
If this is expected, no action is needed.
Learn More. Ask AI.

Merged via the queue into cloudposse:main with commit f6587b0 Aug 11, 2026
82 checks passed
@atmos-pro

atmos-pro Bot commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Tip

Atmos Pro  

No affected stacks workflow was detected for this pull request.
If this is expected, no action is needed.
Learn More. Ask AI.

Igor Rodionov (goruha) added a commit that referenced this pull request Aug 11, 2026
…into 1199-pro-exec-metadata

* '1199-pro-exec-metadata' of github.com:cloudposse/atmos:
  Add task-runner dependencies, freshness checks, and preconditions to custom commands and workflows (#2882)
  feat(provisioner): Azure (azurerm) backend auto-provisioning (#2911)
  fix: preserve trailing newlines in text-based 3-way merges (#2891)
  fix(scaffold): preserve source in scaffold config (#2869)
  fix(deps): update github.com/epiclabs-io/diff3 digest to 3b16698 (#2917)
  fix(deps): update kubernetes monorepo to v0.36.3 (#2918)
@github-actions

Copy link
Copy Markdown

These changes were released in v1.226.0-rc.5.

@jorrite
Jorrit Elfferich (jorrite) deleted the fix-scaffold-update-text-newline branch September 12, 2026 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

patch A minor, backward compatible change size/m Medium size PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

scaffold generate --update strips trailing newlines (if any) from every merged file

2 participants