From 9b0debc30553d2d8d5c2966020bbc14e73305f0f Mon Sep 17 00:00:00 2001 From: Joao Pedro Henrique Date: Mon, 31 Jul 2023 23:11:39 -0300 Subject: [PATCH] feat: Add initial merge algorithm implementation --- Cargo.lock | 57 ++++++++++++++++ Cargo.toml | 1 + merge/Cargo.toml | 12 ++++ merge/src/lib.rs | 167 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 237 insertions(+) create mode 100644 merge/Cargo.toml create mode 100644 merge/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 2d3b800..bc1f00d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,6 +27,15 @@ version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +[[package]] +name = "diffy" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e616e59155c92257e84970156f506287853355f58cd4a6eb167385722c32b790" +dependencies = [ + "nu-ansi-term", +] + [[package]] name = "matching" version = "0.1.0" @@ -41,10 +50,36 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "merge" +version = "0.1.0" +dependencies = [ + "diffy", + "matching", + "model", + "utils", +] + [[package]] name = "model" version = "0.1.0" +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parsing" version = "0.1.0" @@ -94,3 +129,25 @@ dependencies = [ [[package]] name = "utils" version = "0.1.0" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index 9b473f0..5c918bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "bin", "matching", + "merge", "parsing", "model", "utils" diff --git a/merge/Cargo.toml b/merge/Cargo.toml new file mode 100644 index 0000000..dd2d04c --- /dev/null +++ b/merge/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "merge" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +model = { path = "../model" } +matching = { path = "../matching" } +utils = { path = "../utils" } +diffy = "0.3.0" \ No newline at end of file diff --git a/merge/src/lib.rs b/merge/src/lib.rs new file mode 100644 index 0000000..4fa1242 --- /dev/null +++ b/merge/src/lib.rs @@ -0,0 +1,167 @@ +use model::CSTNode; + +#[derive(Debug, Eq, PartialEq)] +pub enum MergeResultNode { + Conflict(CSTNode), + Success(CSTNode), +} + +pub fn merge(base: &CSTNode, left: &CSTNode, right: &CSTNode) -> MergeResultNode { + match (base, left, right) { + ( + CSTNode::Terminal { + kind, + value: value_base, + .. + }, + CSTNode::Terminal { + value: value_left, .. + }, + CSTNode::Terminal { + value: value_right, .. + }, + ) => { + // Unchanged + if value_left == value_base && value_right == value_base { + MergeResultNode::Success(base.to_owned()) + // Changed in both + } else if value_left != value_base && value_right != value_base { + match diffy::merge(&value_base, &value_left, &value_right) { + Ok(value) => MergeResultNode::Success(CSTNode::Terminal { + kind: kind.to_owned(), + value, + }), + Err(value) => MergeResultNode::Conflict(CSTNode::Terminal { + kind: kind.to_owned(), + value, + }), + } + // Only left changed + } else if value_left != value_base { + MergeResultNode::Success(left.to_owned()) + // Only right changed + } else { + MergeResultNode::Success(right.to_owned()) + } + } + (CSTNode::NonTerminal { .. }, CSTNode::NonTerminal { .. }, CSTNode::NonTerminal { .. }) => { + todo!() + } + (_, _, _) => panic!("Can not merge Terminal with Non-Terminal"), + } +} + +#[cfg(test)] +mod tests { + use model::CSTNode; + + use crate::{merge, MergeResultNode}; + + #[test] + fn if_i_am_merging_three_unchanged_nodes_it_is_a_success() { + let node = CSTNode::Terminal { + kind: "kind".into(), + value: "value".into(), + }; + assert_eq!(merge(&node, &node, &node), MergeResultNode::Success(node)) + } + + #[test] + fn returns_success_if_there_are_changes_in_both_left_and_right_and_they_are_not_conflicting() { + let base = CSTNode::Terminal { + kind: "kind".into(), + value: "\nvalue\n".into(), + }; + let left = CSTNode::Terminal { + kind: "kind".into(), + value: "left\nvalue\n".into(), + }; + let right = CSTNode::Terminal { + kind: "kind".into(), + value: "\nvalue\nright".into(), + }; + + assert_eq!( + merge(&base, &left, &right), + MergeResultNode::Success(CSTNode::Terminal { + kind: "kind".into(), + value: "left\nvalue\nright".into() + }) + ) + } + + #[test] + fn returns_conflict_if_there_are_changes_in_both_left_and_right_and_they_are_conflicting() { + let base = CSTNode::Terminal { + kind: "kind".into(), + value: "value".into(), + }; + let left = CSTNode::Terminal { + kind: "kind".into(), + value: "left_value".into(), + }; + let right = CSTNode::Terminal { + kind: "kind".into(), + value: "right_value".into(), + }; + + assert_eq!( + merge(&base, &left, &right), + MergeResultNode::Conflict(CSTNode::Terminal { + kind: "kind".into(), + value: "<<<<<<< ours\nleft_value||||||| original\nvalue=======\nright_value>>>>>>> theirs\n".into() + }) + ) + } + + #[test] + fn if_there_is_a_change_only_in_left_it_returns_the_changes_from_left() { + let base_and_right = CSTNode::Terminal { + kind: "kind".into(), + value: "value".into(), + }; + let left = CSTNode::Terminal { + kind: "kind".into(), + value: "value_left".into(), + }; + assert_eq!( + merge(&base_and_right, &left, &base_and_right), + MergeResultNode::Success(left) + ) + } + + #[test] + fn if_there_is_a_change_only_in_right_it_returns_the_changes_from_right() { + let base_and_left = CSTNode::Terminal { + kind: "kind".into(), + value: "value".into(), + }; + let right = CSTNode::Terminal { + kind: "kind".into(), + value: "value_right".into(), + }; + assert_eq!( + merge(&base_and_left, &base_and_left, &right), + MergeResultNode::Success(right), + ) + } + + #[test] + #[should_panic(expected = "Can not merge Terminal with Non-Terminal")] + fn test_can_not_merge_terminal_with_non_terminal() { + merge( + &CSTNode::Terminal { + kind: "kind".into(), + value: "value".into(), + }, + &CSTNode::Terminal { + kind: "kind".into(), + value: "value".into(), + }, + &CSTNode::NonTerminal { + kind: "kind".into(), + children: vec![], + }, + ); + } +}