Skip to content

lore-revision: Fix conflict markers glued to hunks lacking a trailing newline - #119

Closed
jochenhz wants to merge 1 commit into
EpicGames:mainfrom
Anchorpoint-Software:fix/diffy-conflict-marker-newlines
Closed

lore-revision: Fix conflict markers glued to hunks lacking a trailing newline#119
jochenhz wants to merge 1 commit into
EpicGames:mainfrom
Anchorpoint-Software:fix/diffy-conflict-marker-newlines

Conversation

@jochenhz

@jochenhz jochenhz commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

When both sides of a conflict end the file without a trailing newline, the merge glues
the conflict markers onto the content lines and the file becomes unparseable — markers
are only recognizable at the start of a line:

<<<<<<< ours
This is line 2 changed.||||||| original
This is line 2.=======
This is line 2 also changed.>>>>>>> theirs

git merge-file --diff3 puts every marker on its own line for the same inputs.

Repro is easy: commit a file written with no trailing newline on two branches
from a common base, then branch merge start. With trailing newlines the markers come
out fine.

The bug was in the diffy crate. It is fixed there now (bmwill/diffy#85), and 0.5.2 makes
the behaviour selectable (bmwill/diffy#88).

Updated per review

No vendoring. This is now a plain dependency bump to diffy = "0.5.2" plus
MergeOptions::set_incomplete_hunk_style(IncompleteHunkStyle::Git) in merge3_text.
The bump alone is not enough: 0.5.2 defaults to IncompleteHunkStyle::Diff3, which is
the old glued behaviour, so the setting is what does the work.

Tests that resolve restores the content unchanged. scripts/test/test_merge_resolve.py
gains three cases on a file whose last line has no trailing newline:

  • every conflict marker occupies a whole line;
  • merge resolve mine restores the committed bytes exactly;
  • merge resolve theirs restores the committed bytes exactly.

They compare bytes rather than strings, so an added newline fails the assertion instead of
passing unnoticed. The inserted newline belongs to the marker rendering only — resolving
through the Lore API reads the ~mine / ~theirs sidecars and returns the side as it was
committed.

lore-revision/tests/merge.rs keeps a unit-level regression test for the marker shape.

Testing

  • cargo test -p lore-revision — 4 merge tests, 364 lib tests
  • pytest test_merge_resolve.py test_merge.py test_conflict.py — 25 passed
  • pytest test_diff.py test_diff_git_baseline.py — 64 passed (PatchFormatter is the
    other diffy consumer, so the diff output is covered too)

Disclosure: I used Claude to investigate the root cause of this bug and to
implement the fix. I reviewed and tested the changes myself.

@jochenhz
jochenhz force-pushed the fix/diffy-conflict-marker-newlines branch from caaa1b4 to f80f8de Compare July 18, 2026 13:24
@mjansson

Copy link
Copy Markdown
Collaborator

Moving the markers to a new line is objectively wrong though, as that introduces a newline character in the content that is not actually there. If you then select mine or theirs hunk, you get a modification to the original mine or theirs file - an extra newline.

@bmwill

bmwill commented Jul 20, 2026

Copy link
Copy Markdown

I did accept the fix the @jochenhz submitted to diffy because he is correct in that the old behavior did not match the behavior of git merge-file --diff3. What i failed to check before accepting the fix is that the old behavior did correctly match the output of GNU diff3. So in this case it comes down to the fact that these two tools opted for different solutions, git likely optimizing for readability of the conflict.

Happy to take suggestions on how you'd like this addressed in diffy itself, it probably makes the most sense to provide a config toggle to select which behavior the user would like the output to match.

@jochenhz

Copy link
Copy Markdown
Contributor Author

I see the point that it creates a newline character that wasn't there before the merge. But Git is doing it like that and tools such as VSCode and Diffs (Conductor) cannot parse the diff created by Lore.
So at least a config value would be appreciated to match Git output.

@ajcarberry ajcarberry added the area:core Core library and its interfaces (lib, C API); revision, storage, transport, protocol internals label Aug 19, 2026
@jochenhz
jochenhz force-pushed the fix/diffy-conflict-marker-newlines branch from f80f8de to 4fa1796 Compare August 20, 2026 07:58
@mjansson

Copy link
Copy Markdown
Collaborator

Could this now be done with an update to diffy dependency instead of vendoring?

What happens in tools when selecting the mine/theirs/base that should have no newline? Do they introduce the newline and thereby generating the wrong result (i.e you're not getting the content you selected, but rather the content + a modification, the addition of the newline?)

@bmwill

bmwill commented Aug 31, 2026

Copy link
Copy Markdown

@mjansson Yes I just added this in diffy in bmwill/diffy#88. I'll get a release out shortly with this change which now lets the user select the merge behavior that they want via a config toggle, either the diff3 (don't add newlines) style or git style (adding of newlines)

@jochenhz

Copy link
Copy Markdown
Contributor Author

@mjansson I double-checked what the tools actually do.

Git itself always puts the conflict markers on their own line — it inserts that
newline when it writes the conflict. So the merge tools don't introduce anything,
they just carry through what Git already wrote. Resolving a hunk in VS Code gives
you the selected side plus Git's newline.

git checkout --ours/--theirs avoids it, but only per file; there's no per-hunk
equivalent.

Happy to drop the vendored copy and switch to the released crate with
IncompleteHunkStyle::Git as soon as @bmwill releases the new version. Ideally
Lore would default to the Git style and exposes a config value.

@mjansson

Copy link
Copy Markdown
Collaborator

Gotcha, so a tool that actually uses Lore API to do the resolve would get the correct content, a tool that does the edit itself (or if user manually edit resolve) would get the modified content.

I think I could live with that, as long as there are tests that verify that lore merge resolve restores the content without the added newline.

@bmwill

bmwill commented Aug 31, 2026

Copy link
Copy Markdown

@jochenhz 0.5.2 release is out, let me know if there are any other issues that crop up.

@jochenhz
jochenhz force-pushed the fix/diffy-conflict-marker-newlines branch from 4fa1796 to c3b735a Compare September 1, 2026 10:15
@mjansson mjansson changed the title Fix conflict markers glued to hunks lacking a trailing newline lore-revision: Fix conflict markers glued to hunks lacking a trailing newline Sep 1, 2026
@mjansson

mjansson commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Just a code spell listing issue, check https://github.com/EpicGames/lore/actions/runs/33496482388/job/99819750774?pr=119

A conflicting hunk whose last line has no trailing newline gets the next
marker glued onto it. Tools such as VSCode cannot parse this.

diffy 0.5.2 makes this selectable; `IncompleteHunkStyle::Git` inserts the
newline, as `git merge-file` does.

Signed-off-by: Jochen Hunz <j.hunz@anchorpoint.app>
@jochenhz
jochenhz force-pushed the fix/diffy-conflict-marker-newlines branch from c3b735a to 70ee9c0 Compare September 1, 2026 10:57
@jochenhz

jochenhz commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

I removed the vendoring and updated the PR to match latest of diffy with IncompleteHunkStyle::Git as the default.

@mjansson mjansson added the ready-to-import Approved by Epic staff for import into Lore label Sep 1, 2026
@epic-lore-bot epic-lore-bot Bot added imported Imported into Lore for internal review and removed ready-to-import Approved by Epic staff for import into Lore labels Sep 1, 2026
@epic-lore-bot

epic-lore-bot Bot commented Sep 1, 2026

Copy link
Copy Markdown

Imported as Lore CR-510.

epic-lore-bot Bot pushed a commit that referenced this pull request Sep 1, 2026
… newline

When both sides of a conflict end the file without a trailing newline, the merge glues
the conflict markers onto the content lines and the file becomes unparseable — markers
are only recognizable at the start of a line:

```
<<<<<<< ours
This is line 2 changed.||||||| original
This is line 2.=======
This is line 2 also changed.>>>>>>> theirs
```

git merge-file --diff3 puts every marker on its own line for the same inputs.

Repro is easy: commit a file written with no trailing newline on two branches
from a common base, then `branch merge start`. With trailing newlines the markers come
out fine.

The bug was in the diffy crate. It is fixed there now (bmwill/diffy#85), and 0.5.2 makes
the behaviour selectable (bmwill/diffy#88).

## Updated per review

**No vendoring.** This is now a plain dependency bump to `diffy = "0.5.2"` plus
`MergeOptions::set_incomplete_hunk_style(IncompleteHunkStyle::Git)` in `merge3_text`.
The bump alone is not enough: 0.5.2 defaults to `IncompleteHunkStyle::Diff3`, which is
the old glued behaviour, so the setting is what does the work.

**Tests that resolve restores the content unchanged.** `scripts/test/test_merge_resolve.py`
gains three cases on a file whose last line has no trailing newline:

- every conflict marker occupies a whole line;
- `merge resolve mine` restores the committed bytes exactly;
- `merge resolve theirs` restores the committed bytes exactly.

They compare bytes rather than strings, so an added newline fails the assertion instead of
passing unnoticed. The inserted newline belongs to the marker rendering only — resolving
through the Lore API reads the `~mine` / `~theirs` sidecars and returns the side as it was
committed.

`lore-revision/tests/merge.rs` keeps a unit-level regression test for the marker shape.

## Testing

- `cargo test -p lore-revision` — 4 merge tests, 364 lib tests
- `pytest test_merge_resolve.py test_merge.py test_conflict.py` — 25 passed
- `pytest test_diff.py test_diff_git_baseline.py` — 64 passed (`PatchFormatter` is the
  other diffy consumer, so the diff output is covered too)

---

Disclosure: I used Claude to investigate the root cause of this bug and to
implement the fix. I reviewed and tested the changes myself.

```
Imported-PR: #119
Imported-From: 70ee9c0
Imported-Base: 715645d
Imported-Merge: 627c14d
Imported-Merge-Strategy: verbatim
Imported-Merged-Paths: 0
Imported-Author: Jochen Hunz (jochenhz)
Signed-off-by: Jochen Hunz <j.hunz@anchorpoint.app>
GH-URL: #119
```

Lore-RevId: 864
Lore-Signature: 5fd0a5f2f53be5504b8f1c8c002116d01d80bf35c260225822e787e62478fbc3
@epic-lore-bot

epic-lore-bot Bot commented Sep 1, 2026

Copy link
Copy Markdown

Closed by mirrored commit cb6f575.

@epic-lore-bot epic-lore-bot Bot closed this Sep 1, 2026
@epic-lore-bot epic-lore-bot Bot added the merged Merged into Lore codebase label Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core Core library and its interfaces (lib, C API); revision, storage, transport, protocol internals imported Imported into Lore for internal review merged Merged into Lore codebase

Development

Successfully merging this pull request may close these issues.

4 participants