Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 15 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ config = { version = "0.15.15", features = ["toml"], default-features = false }
ctor = "0.2"
crossbeam = "0.8.4"
dashmap = "6.1.0"
diffy = "0.4.2"
diffy = "0.5.2"
directories = "6.0.0"
enum_dispatch = "0.3.13"
tracing-appender = "0.2.3"
Expand Down
6 changes: 5 additions & 1 deletion lore-revision/src/merge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ pub fn merge3_text(
mine_marker: Option<&str>,
theirs_marker: Option<&str>,
) -> Result<String, String> {
let merge_result = diffy::merge(base, mine, theirs);
// `Git`, not diffy's `Diff3` default: `Diff3` glues the next marker onto a
// final line that lacks a newline, which is unparsable.
let merge_result = diffy::MergeOptions::new()
.set_incomplete_hunk_style(diffy::IncompleteHunkStyle::Git)
.merge(base, mine, theirs);
let merge_conflicts = merge_result.is_err();
let mut merge_output = match merge_result {
Ok(str) | Err(str) => str,
Expand Down
30 changes: 30 additions & 0 deletions lore-revision/tests/merge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,36 @@ mod tests {
assert_eq!(result_string, expected_string);
}

/// Fails without `IncompleteHunkStyle::Git`: the default glues the next
/// marker onto the last line when it has no trailing newline.
#[test]
fn test_conflict_without_trailing_newlines() {
let base_string = "This is line 1.
This is line 2.";
let mine_string = "This is line 1.
This is line 2 as I wrote it.";
let theirs_string = "This is line 1.
This is line 2 as they wrote it.";

let result_string =
match merge3_text(base_string, mine_string, theirs_string, None, None, None) {
Err(str) | Ok(str) => str,
};

for marker in [
"<<<<<<< ours",
"||||||| original",
"=======",
">>>>>>> theirs",
] {
assert!(
result_string.lines().any(|line| line == marker),
"`{marker}` must occupy a line of its own, got:
{result_string}"
);
}
}

#[test]
fn test_markers() {
let base_string = "This is line 1.\nThis is line 2.\n";
Expand Down
69 changes: 69 additions & 0 deletions scripts/test/test_merge_resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,3 +584,72 @@ def test_merge_resolve_mine_no_paths(new_lore_repo):
)

repo.commit("Resolved all with mine (no paths)", offline=True)
# ---------------------------------------------------------------------------
# Test: a final line without a newline survives resolve unchanged
# ---------------------------------------------------------------------------

# `IncompleteHunkStyle::Git` inserts a newline after an incomplete final line so
# the following conflict marker starts at column 0. These pin that the newline
# belongs to the marker rendering only: resolving restores the side byte for
# byte, exactly as it was committed.

BASE_NO_EOL = b"line 1\nline 2"
MINE_NO_EOL = b"line 1\nline 2 mine"
THEIRS_NO_EOL = b"line 1\nline 2 theirs"


@pytest.mark.smoke
def test_merge_conflict_markers_own_line_without_trailing_newline(new_lore_repo):
"""Every marker starts a line even when the conflicting hunk has no EOL."""
repo: Lore = new_lore_repo()
setup_merge_conflict(
repo, {"a.txt": (BASE_NO_EOL, MINE_NO_EOL, THEIRS_NO_EOL)}
)

with repo.open_file("a.txt", "rb") as f:
conflicted = f.read().decode()

for marker in ["<<<<<<< ours", "||||||| original", "=======", ">>>>>>> theirs"]:
assert any(line == marker for line in conflicted.split("\n")), (
f"{marker!r} must occupy a whole line, got {conflicted!r}"
)

repo.branch_merge_abort(offline=True)


@pytest.mark.smoke
def test_merge_resolve_mine_restores_missing_trailing_newline(new_lore_repo):
"""resolve mine gives back the committed bytes, without the marker newline."""
repo: Lore = new_lore_repo()
setup_merge_conflict(
repo, {"a.txt": (BASE_NO_EOL, MINE_NO_EOL, THEIRS_NO_EOL)}
)

repo.branch_merge_resolve_mine(["a.txt"], offline=True, json=True)

with repo.open_file("a.txt", "rb") as f:
content = f.read()
assert content == MINE_NO_EOL, (
f"resolve mine must restore the exact bytes, got {content!r}"
)

repo.branch_merge_abort(offline=True)


@pytest.mark.smoke
def test_merge_resolve_theirs_restores_missing_trailing_newline(new_lore_repo):
"""resolve theirs gives back the committed bytes, without the marker newline."""
repo: Lore = new_lore_repo()
setup_merge_conflict(
repo, {"a.txt": (BASE_NO_EOL, MINE_NO_EOL, THEIRS_NO_EOL)}
)

repo.branch_merge_resolve_theirs(["a.txt"], offline=True, json=True)

with repo.open_file("a.txt", "rb") as f:
content = f.read()
assert content == THEIRS_NO_EOL, (
f"resolve theirs must restore the exact bytes, got {content!r}"
)

repo.branch_merge_abort(offline=True)
Loading