use crate::dag::graph::{DagError, PatchDag};
use crate::patch::merge::{self, MergeResult};
use crate::patch::types::PatchId;
use std::collections::HashMap;
use suture_common::BranchName;
impl PatchDag {
pub fn merge_branches(
&self,
branch_a: &BranchName,
branch_b: &BranchName,
) -> Result<MergeResult, DagError> {
let target_a = self
.get_branch(branch_a)
.ok_or_else(|| DagError::BranchNotFound(branch_a.as_str().to_string()))?;
let target_b = self
.get_branch(branch_b)
.ok_or_else(|| DagError::BranchNotFound(branch_b.as_str().to_string()))?;
let lca_id = self
.lca(&target_a, &target_b)
.ok_or_else(|| DagError::Custom("no common ancestor found".to_string()))?;
let chain_a = self.patch_chain(&target_a);
let chain_b = self.patch_chain(&target_b);
let lca_pos_a = chain_a.iter().position(|id| *id == lca_id);
let lca_pos_b = chain_b.iter().position(|id| *id == lca_id);
let branch_a_patches: Vec<PatchId> = match lca_pos_a {
Some(pos) => chain_a[..pos].to_vec(), None => chain_a.clone(),
};
let branch_b_patches: Vec<PatchId> = match lca_pos_b {
Some(pos) => chain_b[..pos].to_vec(),
None => chain_b.clone(),
};
let all_patches: HashMap<PatchId, _> = self
.nodes
.iter()
.map(|(id, node)| (*id, node.patch.clone()))
.collect();
merge::merge(
&[lca_id],
&branch_a_patches,
&branch_b_patches,
&all_patches,
)
.map_err(|e| DagError::Custom(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use crate::dag::graph::PatchDag;
use crate::patch::types::{OperationType, Patch, TouchSet};
use suture_common::BranchName;
fn make_patch(addr: &str) -> Patch {
Patch::new(
OperationType::Modify,
TouchSet::single(addr),
Some(format!("file_{}", addr)),
vec![],
vec![],
"test".to_string(),
format!("edit {}", addr),
)
}
#[test]
fn test_merge_clean_divergent_branches() {
let mut dag = PatchDag::new();
let root = make_patch("root");
let root_id = dag.add_patch(root, vec![]).unwrap();
assert!(dag.has_patch(&root_id), "root should be in DAG");
let a1 = make_patch("A1");
let a1_id = dag.add_patch(a1, vec![root_id]).unwrap();
assert!(dag.has_patch(&a1_id), "a1 should be in DAG");
let b1 = make_patch("B1");
let b1_id = dag.add_patch(b1, vec![root_id]).unwrap();
assert!(dag.has_patch(&b1_id), "b1 should be in DAG");
let anc_a = dag.ancestors(&a1_id);
let anc_b = dag.ancestors(&b1_id);
assert!(anc_a.contains(&root_id), "root should be ancestor of a1");
assert!(anc_b.contains(&root_id), "root should be ancestor of b1");
let intersection: std::collections::HashSet<_> = anc_a.intersection(&anc_b).collect();
assert_eq!(intersection.len(), 1, "should have 1 common ancestor");
assert!(
intersection.contains(&root_id),
"common ancestor should be root_id"
);
let direct_lca = dag.lca(&a1_id, &b1_id);
assert_eq!(direct_lca, Some(root_id), "LCA of a1 and b1 should be root");
let branch_a = BranchName::new("branch_a").unwrap();
let branch_b = BranchName::new("branch_b").unwrap();
dag.create_branch(branch_a.clone(), a1_id).unwrap();
dag.create_branch(branch_b.clone(), b1_id).unwrap();
let result = dag.merge_branches(&branch_a, &branch_b).unwrap();
assert!(result.is_clean);
assert!(result.conflicts.is_empty());
}
#[test]
fn test_merge_conflicting_branches() {
let mut dag = PatchDag::new();
let root = make_patch("root");
let root_id = dag.add_patch(root, vec![]).unwrap();
let a1 = Patch::new(
OperationType::Modify,
TouchSet::from_addrs(["shared_addr"]),
Some("file_shared_addr".to_string()),
vec![1], vec![],
"alice".to_string(),
"edit shared_addr (version A)".to_string(),
);
let a1_id = dag.add_patch(a1, vec![root_id]).unwrap();
let b1 = Patch::new(
OperationType::Modify,
TouchSet::from_addrs(["shared_addr"]),
Some("file_shared_addr".to_string()),
vec![2], vec![],
"bob".to_string(),
"edit shared_addr (version B)".to_string(),
);
let b1_id = dag.add_patch(b1, vec![root_id]).unwrap();
let branch_a = BranchName::new("branch_a").unwrap();
let branch_b = BranchName::new("branch_b").unwrap();
dag.create_branch(branch_a.clone(), a1_id).unwrap();
dag.create_branch(branch_b.clone(), b1_id).unwrap();
let result = dag.merge_branches(&branch_a, &branch_b).unwrap();
assert!(
!result.is_clean,
"branches editing the same address should conflict"
);
assert_eq!(result.conflicts.len(), 1, "should have exactly 1 conflict");
}
}